@stigmer/server 3.26.0 → 3.27.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/authorization/evaluator.d.ts +4 -1
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js +8 -0
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/model/agent.d.ts.map +1 -1
- package/dist/authorization/model/agent.js +4 -2
- package/dist/authorization/model/agent.js.map +1 -1
- package/dist/authorization/model/agent_channel.d.ts.map +1 -1
- package/dist/authorization/model/agent_channel.js +5 -4
- package/dist/authorization/model/agent_channel.js.map +1 -1
- package/dist/authorization/model/agent_instance.d.ts.map +1 -1
- package/dist/authorization/model/agent_instance.js +3 -2
- package/dist/authorization/model/agent_instance.js.map +1 -1
- package/dist/authorization/model/mcp_server.d.ts.map +1 -1
- package/dist/authorization/model/mcp_server.js +3 -2
- package/dist/authorization/model/mcp_server.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +3 -0
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/authorization/model/plugin.d.ts.map +1 -1
- package/dist/authorization/model/plugin.js +4 -2
- package/dist/authorization/model/plugin.js.map +1 -1
- package/dist/authorization/model/rewrite.d.ts +25 -7
- package/dist/authorization/model/rewrite.d.ts.map +1 -1
- package/dist/authorization/model/rewrite.js +40 -1
- package/dist/authorization/model/rewrite.js.map +1 -1
- package/dist/authorization/model/schedule.d.ts.map +1 -1
- package/dist/authorization/model/schedule.js +8 -3
- package/dist/authorization/model/schedule.js.map +1 -1
- package/dist/authorization/model/skill.d.ts.map +1 -1
- package/dist/authorization/model/skill.js +3 -2
- package/dist/authorization/model/skill.js.map +1 -1
- package/dist/authorization/model/workflow.d.ts.map +1 -1
- package/dist/authorization/model/workflow.js +3 -2
- package/dist/authorization/model/workflow.js.map +1 -1
- package/dist/authorization/model/workflow_execution.d.ts.map +1 -1
- package/dist/authorization/model/workflow_execution.js +4 -3
- package/dist/authorization/model/workflow_execution.js.map +1 -1
- package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
- package/dist/authorization/model/workflow_instance.js +5 -1
- package/dist/authorization/model/workflow_instance.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +2 -0
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/iampolicy/access-lists.d.ts +5 -3
- package/dist/domain/iampolicy/access-lists.d.ts.map +1 -1
- package/dist/domain/iampolicy/access-lists.js +72 -19
- package/dist/domain/iampolicy/access-lists.js.map +1 -1
- package/dist/domain/iampolicy/constants.d.ts +32 -6
- package/dist/domain/iampolicy/constants.d.ts.map +1 -1
- package/dist/domain/iampolicy/constants.js +41 -6
- package/dist/domain/iampolicy/constants.js.map +1 -1
- package/dist/domain/iampolicy/controller.d.ts +6 -0
- package/dist/domain/iampolicy/controller.d.ts.map +1 -1
- package/dist/domain/iampolicy/controller.js +16 -7
- package/dist/domain/iampolicy/controller.js.map +1 -1
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
- package/dist/domain/iampolicy/resource-store.js +2 -6
- package/dist/domain/iampolicy/resource-store.js.map +1 -1
- package/dist/domain/iampolicy/steps.d.ts.map +1 -1
- package/dist/domain/iampolicy/steps.js +49 -7
- package/dist/domain/iampolicy/steps.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -0
- package/dist/extensions/drivers.d.ts +12 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/gate-slots.d.ts +10 -1
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js +1 -0
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/extensions/principal-display.d.ts +34 -0
- package/dist/extensions/principal-display.d.ts.map +1 -0
- package/dist/extensions/principal-display.js +2 -0
- package/dist/extensions/principal-display.js.map +1 -0
- package/dist/extensions/registry.d.ts +6 -0
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +10 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +10 -0
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +16 -0
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/evaluator.test.ts +127 -2
- package/src/authorization/evaluator.ts +12 -1
- package/src/authorization/model/__tests__/registry.test.ts +64 -2
- package/src/authorization/model/agent.ts +4 -1
- package/src/authorization/model/agent_channel.ts +11 -3
- package/src/authorization/model/agent_instance.ts +3 -1
- package/src/authorization/model/mcp_server.ts +3 -1
- package/src/authorization/model/organization.ts +3 -0
- package/src/authorization/model/plugin.ts +4 -1
- package/src/authorization/model/rewrite.ts +65 -8
- package/src/authorization/model/schedule.ts +11 -2
- package/src/authorization/model/skill.ts +3 -1
- package/src/authorization/model/workflow.ts +3 -1
- package/src/authorization/model/workflow_execution.ts +4 -2
- package/src/authorization/model/workflow_instance.ts +5 -0
- package/src/boot/compose.ts +2 -0
- package/src/domain/iampolicy/__tests__/access-lists.test.ts +120 -6
- package/src/domain/iampolicy/__tests__/constants.test.ts +26 -4
- package/src/domain/iampolicy/__tests__/controller.test.ts +55 -1
- package/src/domain/iampolicy/__tests__/grant-path.test.ts +3 -3
- package/src/domain/iampolicy/__tests__/steps.test.ts +123 -1
- package/src/domain/iampolicy/access-lists.ts +98 -23
- package/src/domain/iampolicy/constants.ts +50 -6
- package/src/domain/iampolicy/controller.ts +30 -7
- package/src/domain/iampolicy/resource-store.ts +3 -6
- package/src/domain/iampolicy/steps.ts +89 -7
- package/src/domain/workflow/registry/data/task-kind-registry.json +1 -0
- package/src/extensions/__tests__/iam-policy-points.test.ts +30 -0
- package/src/extensions/__tests__/registry.test.ts +3 -0
- package/src/extensions/drivers.ts +12 -0
- package/src/extensions/gate-slots.ts +10 -0
- package/src/extensions/principal-display.ts +37 -0
- package/src/extensions/registry.ts +19 -0
- package/src/index.ts +26 -0
- package/src/pipeline/apiresource-meta.ts +21 -0
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* kind is reached by enum and by FGA type name alike, and that every
|
|
6
6
|
* permission the wire can ask about (the IamPermission vocabulary) is
|
|
7
7
|
* either a declared relation of the kind or absent from its `.fga` file —
|
|
8
|
-
* never a relation the transcript forgot
|
|
8
|
+
* never a relation the transcript forgot — and that the relations whose
|
|
9
|
+
* direct list admits a team (`team#member`) are exactly the roles the
|
|
10
|
+
* kind's `team_grantable_roles` names, a subset of what a person can be
|
|
11
|
+
* granted and never ownership, so the grant step and the model cannot
|
|
12
|
+
* disagree about where a team may be granted.
|
|
9
13
|
*
|
|
10
14
|
* The kind list is pinned twice on purpose: as the literal in registry
|
|
11
15
|
* order (a reordering or a dropped file is a visible diff) and as
|
|
@@ -20,13 +24,19 @@ import {
|
|
|
20
24
|
ApiResourceKindSchema,
|
|
21
25
|
ResourceTier,
|
|
22
26
|
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
23
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
IamPermissionSchema,
|
|
29
|
+
IamRole,
|
|
30
|
+
} from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
24
31
|
|
|
25
32
|
import {
|
|
26
33
|
getKindMeta,
|
|
34
|
+
grantableRolesFor,
|
|
27
35
|
kindEnumName,
|
|
36
|
+
teamGrantableRolesFor,
|
|
28
37
|
} from "../../../pipeline/apiresource-meta.js";
|
|
29
38
|
import { builtInModel, declarationFor } from "../index.js";
|
|
39
|
+
import type { Rewrite, SubjectType } from "../rewrite.js";
|
|
30
40
|
|
|
31
41
|
/** The open-source tier, in registry order (the `fga.mod` order of their files). */
|
|
32
42
|
const DECLARED_KINDS = [
|
|
@@ -121,6 +131,7 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
|
|
|
121
131
|
"can_create_execution_in",
|
|
122
132
|
"can_create_agent_share",
|
|
123
133
|
"can_create_agent_instance",
|
|
134
|
+
"can_create_team",
|
|
124
135
|
"can_grant_access",
|
|
125
136
|
"can_view_access",
|
|
126
137
|
"can_view_billing",
|
|
@@ -238,6 +249,32 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
|
|
|
238
249
|
],
|
|
239
250
|
};
|
|
240
251
|
|
|
252
|
+
/** The subject types a relation's direct lists name, wherever they sit in its rewrite. */
|
|
253
|
+
function directSubjects(rewrite: Rewrite): ReadonlyArray<SubjectType> {
|
|
254
|
+
switch (rewrite.node) {
|
|
255
|
+
case "this":
|
|
256
|
+
return rewrite.subjects;
|
|
257
|
+
case "computed":
|
|
258
|
+
case "from":
|
|
259
|
+
return [];
|
|
260
|
+
case "union":
|
|
261
|
+
case "intersection":
|
|
262
|
+
return rewrite.members.flatMap(directSubjects);
|
|
263
|
+
default: {
|
|
264
|
+
const exhaustive: never = rewrite;
|
|
265
|
+
throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function isTeamMembers(subject: SubjectType): boolean {
|
|
271
|
+
return (
|
|
272
|
+
subject.form === "userset" &&
|
|
273
|
+
subject.type === "team" &&
|
|
274
|
+
subject.relation === "member"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
241
278
|
/** Every kind whose `kind_meta.tier` is the open-source tier, by enum number. */
|
|
242
279
|
function openSourceTierKinds(): ReadonlySet<ApiResourceKind> {
|
|
243
280
|
const kinds = new Set<ApiResourceKind>();
|
|
@@ -271,6 +308,7 @@ describe("the built-in model's registry", () => {
|
|
|
271
308
|
ApiResourceKind.platform,
|
|
272
309
|
ApiResourceKind.identity_provider,
|
|
273
310
|
ApiResourceKind.invitation,
|
|
311
|
+
ApiResourceKind.team,
|
|
274
312
|
]) {
|
|
275
313
|
expect(declarationFor(unserved), kindEnumName(unserved)).toBeUndefined();
|
|
276
314
|
}
|
|
@@ -304,6 +342,30 @@ describe("the built-in model's registry", () => {
|
|
|
304
342
|
}
|
|
305
343
|
});
|
|
306
344
|
|
|
345
|
+
it("admits a team exactly on the roles each kind's team_grantable_roles lists — the transcript and the contract say one thing", () => {
|
|
346
|
+
for (const declaration of builtInModel.declarations) {
|
|
347
|
+
const admitsTeam = [...declaration.relations]
|
|
348
|
+
.filter(([, rewrite]) => directSubjects(rewrite).some(isTeamMembers))
|
|
349
|
+
.map(([relation]) => relation)
|
|
350
|
+
.sort();
|
|
351
|
+
const contract = teamGrantableRolesFor(declaration.kind)
|
|
352
|
+
.map((role) => IamRole[role])
|
|
353
|
+
.sort();
|
|
354
|
+
expect(admitsTeam, declaration.type).toEqual(contract);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("lets a team hold only roles a person can hold on the kind, and never ownership", () => {
|
|
359
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
360
|
+
const kind = value.number as ApiResourceKind;
|
|
361
|
+
const person = grantableRolesFor(kind);
|
|
362
|
+
for (const role of teamGrantableRolesFor(kind)) {
|
|
363
|
+
expect(person, `${value.name}: ${IamRole[role]}`).toContain(role);
|
|
364
|
+
expect(role, value.name).not.toBe(IamRole.owner);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
307
369
|
it("carries a derived rule only where kind_meta cannot derive the relation: default_of on the two instance kinds, execution_viewer on the workflow instance", () => {
|
|
308
370
|
const derived = builtInModel.declarations
|
|
309
371
|
.flatMap((d) => [...d.derived.keys()].map((r) => `${d.type}#${r}`))
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* and `organization#viewer` usersets on `viewer` are the org-visibility
|
|
6
6
|
* tuple's two shapes (the cloud writes `#viewer` since cloud#257; `#member`
|
|
7
7
|
* is the legacy shape it still honours), and `platform_viewer` is an
|
|
8
|
-
* identity-provider userset no open-source tuple ever names.
|
|
8
|
+
* identity-provider userset no open-source tuple ever names. `team#member`
|
|
9
|
+
* is the Enterprise team grant; open source serves no team, so no tuple
|
|
10
|
+
* here ever names one.
|
|
9
11
|
*/
|
|
10
12
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
11
13
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -44,6 +46,7 @@ export const agentDeclaration = declareKind({
|
|
|
44
46
|
objectOf("identity_account"),
|
|
45
47
|
usersetOf("organization", "member"),
|
|
46
48
|
usersetOf("organization", "viewer"),
|
|
49
|
+
usersetOf("team", "member"),
|
|
47
50
|
),
|
|
48
51
|
computed("owner"),
|
|
49
52
|
computed("platform_viewer"),
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* between owner and viewer: the conversation-participation role
|
|
5
5
|
* (reply and takeover without configuration authority), grantable as a
|
|
6
6
|
* role on this kind alone. Every owner participates; every participant
|
|
7
|
-
* views.
|
|
7
|
+
* views. Both grantable relations may name an Enterprise team
|
|
8
|
+
* (`team#member`), which no open-source tuple ever does.
|
|
8
9
|
*/
|
|
9
10
|
import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
10
11
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
from,
|
|
17
18
|
objectOf,
|
|
18
19
|
union,
|
|
20
|
+
usersetOf,
|
|
19
21
|
} from "./rewrite.js";
|
|
20
22
|
|
|
21
23
|
export const agentChannelDeclaration = declareKind({
|
|
@@ -33,11 +35,17 @@ export const agentChannelDeclaration = declareKind({
|
|
|
33
35
|
],
|
|
34
36
|
[
|
|
35
37
|
"participant",
|
|
36
|
-
union(
|
|
38
|
+
union(
|
|
39
|
+
direct(objectOf("identity_account"), usersetOf("team", "member")),
|
|
40
|
+
computed("owner"),
|
|
41
|
+
),
|
|
37
42
|
],
|
|
38
43
|
[
|
|
39
44
|
"viewer",
|
|
40
|
-
union(
|
|
45
|
+
union(
|
|
46
|
+
direct(objectOf("identity_account"), usersetOf("team", "member")),
|
|
47
|
+
computed("participant"),
|
|
48
|
+
),
|
|
41
49
|
],
|
|
42
50
|
["can_view", computed("viewer")],
|
|
43
51
|
["can_edit", computed("owner")],
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* viewer set (`viewer from default_of`; default-of.ts). `agent` is the
|
|
7
7
|
* parent link written on every instance for tracking and walked by
|
|
8
8
|
* nothing; `default_of` is the derived link written on the default
|
|
9
|
-
* instance alone.
|
|
9
|
+
* instance alone. `team#member` on `viewer` is the Enterprise team grant,
|
|
10
|
+
* which no open-source tuple ever names.
|
|
10
11
|
*/
|
|
11
12
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
12
13
|
import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
@@ -39,6 +40,7 @@ export const agentInstanceDeclaration = declareKind({
|
|
|
39
40
|
objectOf("identity_account"),
|
|
40
41
|
usersetOf("organization", "member"),
|
|
41
42
|
usersetOf("organization", "viewer"),
|
|
43
|
+
usersetOf("team", "member"),
|
|
42
44
|
),
|
|
43
45
|
computed("owner"),
|
|
44
46
|
from("viewer", "default_of"),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transcript of fga/model/agentic/mcp_server.fga — a blueprint with three
|
|
3
3
|
* read-side verbs: `can_use`, `can_clone` and `can_connect` (the OAuth
|
|
4
|
-
* connect lane's gate), all `viewer`.
|
|
4
|
+
* connect lane's gate), all `viewer`. `team#member` on `viewer` is the
|
|
5
|
+
* Enterprise team grant, which no open-source tuple ever names.
|
|
5
6
|
*/
|
|
6
7
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
7
8
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -40,6 +41,7 @@ export const mcpServerDeclaration = declareKind({
|
|
|
40
41
|
objectOf("identity_account"),
|
|
41
42
|
usersetOf("organization", "member"),
|
|
42
43
|
usersetOf("organization", "viewer"),
|
|
44
|
+
usersetOf("team", "member"),
|
|
43
45
|
),
|
|
44
46
|
computed("owner"),
|
|
45
47
|
computed("platform_viewer"),
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* share-link visitor; no open-source lane mints one (claim check C5), so
|
|
13
13
|
* the relation resolves to nothing and `can_create_session: member or
|
|
14
14
|
* guest` is `member` here — the transcript still carries the line.
|
|
15
|
+
* `can_create_team` is carried the same way: open source serves no Team
|
|
16
|
+
* kind, so nothing here asks it.
|
|
15
17
|
*/
|
|
16
18
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
17
19
|
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
@@ -48,6 +50,7 @@ export const organizationDeclaration = declareKind({
|
|
|
48
50
|
["can_create_execution_in", union(computed("member"), computed("guest"))],
|
|
49
51
|
["can_create_agent_share", computed("admin")],
|
|
50
52
|
["can_create_agent_instance", computed("member")],
|
|
53
|
+
["can_create_team", computed("admin")],
|
|
51
54
|
["can_grant_access", computed("admin")],
|
|
52
55
|
["can_view_access", computed("viewer")],
|
|
53
56
|
["can_view_billing", computed("viewer")],
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* the org's members and viewers and the platform viewer. Its verbs are the blueprint set: `can_view`, `can_edit`
|
|
6
6
|
* (visibility), `can_delete` (uninstall), `can_grant_access`,
|
|
7
7
|
* `can_view_access`; `can_use` is carried as the model's word for
|
|
8
|
-
* "install from" though no wire permission names it yet.
|
|
8
|
+
* "install from" though no wire permission names it yet. `team#member` on
|
|
9
|
+
* `viewer` is the Enterprise team grant, which no open-source tuple ever
|
|
10
|
+
* names.
|
|
9
11
|
*
|
|
10
12
|
* The members a plugin materialises are NOT reached through this type:
|
|
11
13
|
* each is an ordinary skill, MCP server, agent or workflow with its own
|
|
@@ -48,6 +50,7 @@ export const pluginDeclaration = declareKind({
|
|
|
48
50
|
objectOf("identity_account"),
|
|
49
51
|
usersetOf("organization", "member"),
|
|
50
52
|
usersetOf("organization", "viewer"),
|
|
53
|
+
usersetOf("team", "member"),
|
|
51
54
|
),
|
|
52
55
|
computed("owner"),
|
|
53
56
|
computed("platform_viewer"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The relation-rewrite vocabulary the model declarations are written in —
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
3
|
+
* the forms an OpenFGA `define` line takes in the cloud's `.fga` files,
|
|
4
|
+
* and nothing more:
|
|
5
5
|
*
|
|
6
6
|
* define organization: [organization] → direct(objectOf("organization"))
|
|
7
7
|
* define owner: [identity_account] or admin from organization
|
|
@@ -10,8 +10,20 @@
|
|
|
10
10
|
* or owner or platform_viewer
|
|
11
11
|
* → union(direct(objectOf(...), usersetOf(...), usersetOf(...)),
|
|
12
12
|
* computed("owner"), computed("platform_viewer"))
|
|
13
|
+
* define member: [identity_account] and viewer from organization
|
|
14
|
+
* → intersection(direct(objectOf("identity_account")), from("viewer", "organization"))
|
|
13
15
|
* define can_view: viewer → computed("viewer")
|
|
14
16
|
*
|
|
17
|
+
* `and` exists for one shape: a relation that is true only while another
|
|
18
|
+
* holds too — a team's member is a person granted the role AND still one
|
|
19
|
+
* of the organization's viewers, so leaving the organization ends every
|
|
20
|
+
* team-derived grant with no cleanup. A line is either a union or an
|
|
21
|
+
* intersection of leaves, never both: the DSL would need parentheses to
|
|
22
|
+
* mix them, the cloud's reader refuses parentheses, and `declareKind`
|
|
23
|
+
* refuses the nesting that would need them. `but not` has no node; the day
|
|
24
|
+
* a line needs it, the transcript cannot be written and the gap is a
|
|
25
|
+
* design act.
|
|
26
|
+
*
|
|
15
27
|
* A subject type is an object type or a userset; the model admits no
|
|
16
28
|
* wildcard (`type:*`) and declares no condition, so neither has a form
|
|
17
29
|
* here. The day a line needs one, the transcript cannot be written and
|
|
@@ -21,10 +33,7 @@
|
|
|
21
33
|
* file: the same relations, in the file's order, each line rewritten in
|
|
22
34
|
* these builders so a reader with the model open sees the same text, and
|
|
23
35
|
* so a structural compare against the file (the cloud's drift test at
|
|
24
|
-
* the re-pin) needs no translation table.
|
|
25
|
-
* (`and`, `but not`) do not appear in the model and have no node here;
|
|
26
|
-
* the day they do, the transcript cannot be written and the gap is a
|
|
27
|
-
* design act, not a silent approximation.
|
|
36
|
+
* the re-pin) needs no translation table.
|
|
28
37
|
*
|
|
29
38
|
* The direct form carries its type restrictions (`[identity_account,
|
|
30
39
|
* organization#viewer]`) because the evaluator ENFORCES them: a tuple
|
|
@@ -72,7 +81,12 @@ export type Rewrite =
|
|
|
72
81
|
readonly tupleset: string;
|
|
73
82
|
}
|
|
74
83
|
/** `a or b or c` — the first true member wins, in the line's order. */
|
|
75
|
-
| { readonly node: "union"; readonly members: ReadonlyArray<Rewrite> }
|
|
84
|
+
| { readonly node: "union"; readonly members: ReadonlyArray<Rewrite> }
|
|
85
|
+
/** `a and b` — true only when every member is, in the line's order; the first false one ends it. */
|
|
86
|
+
| {
|
|
87
|
+
readonly node: "intersection";
|
|
88
|
+
readonly members: ReadonlyArray<Rewrite>;
|
|
89
|
+
};
|
|
76
90
|
|
|
77
91
|
// ---------------------------------------------------------------------------
|
|
78
92
|
// Builders — named so a transcript reads like its `.fga` line.
|
|
@@ -102,6 +116,10 @@ export function union(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
|
102
116
|
return { node: "union", members };
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
export function intersection(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
120
|
+
return { node: "intersection", members };
|
|
121
|
+
}
|
|
122
|
+
|
|
105
123
|
// ---------------------------------------------------------------------------
|
|
106
124
|
// A kind's declaration.
|
|
107
125
|
// ---------------------------------------------------------------------------
|
|
@@ -148,7 +166,10 @@ export interface KindDeclarationInput {
|
|
|
148
166
|
* module load: a relation declared twice, a `computed` naming a relation
|
|
149
167
|
* the kind does not define, a `from` whose tupleset the kind does not
|
|
150
168
|
* define (its `relation` is the PARENT's and is resolved at evaluation),
|
|
151
|
-
* a derived rule for a relation the transcript does not declare
|
|
169
|
+
* a derived rule for a relation the transcript does not declare, and a
|
|
170
|
+
* line no `.fga` file could hold without parentheses (a union inside an
|
|
171
|
+
* intersection or the reverse, an operator nested in itself, an operator
|
|
172
|
+
* over fewer than two members).
|
|
152
173
|
*/
|
|
153
174
|
export function declareKind(input: KindDeclarationInput): KindDeclaration {
|
|
154
175
|
const type = kindEnumName(input.kind);
|
|
@@ -157,6 +178,10 @@ export function declareKind(input: KindDeclarationInput): KindDeclaration {
|
|
|
157
178
|
if (relations.has(name)) {
|
|
158
179
|
throw new Error(`${input.source}: relation '${name}' is declared twice`);
|
|
159
180
|
}
|
|
181
|
+
const shape = unwritableShape(rewrite);
|
|
182
|
+
if (shape !== undefined) {
|
|
183
|
+
throw new Error(`${input.source}: '${name}' ${shape}`);
|
|
184
|
+
}
|
|
160
185
|
relations.set(name, rewrite);
|
|
161
186
|
}
|
|
162
187
|
for (const [name, rewrite] of relations) {
|
|
@@ -199,6 +224,7 @@ function localReferences(
|
|
|
199
224
|
case "from":
|
|
200
225
|
return [{ role: "tupleset", relation: rewrite.tupleset }];
|
|
201
226
|
case "union":
|
|
227
|
+
case "intersection":
|
|
202
228
|
return rewrite.members.flatMap(localReferences);
|
|
203
229
|
default: {
|
|
204
230
|
const exhaustive: never = rewrite;
|
|
@@ -206,3 +232,34 @@ function localReferences(
|
|
|
206
232
|
}
|
|
207
233
|
}
|
|
208
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Why a line could not be written in a `.fga` file without parentheses,
|
|
238
|
+
* or undefined when it can: an operator's members are leaves (a direct
|
|
239
|
+
* list, a sibling relation, a tuple-to-userset), and there are at least
|
|
240
|
+
* two of them.
|
|
241
|
+
*/
|
|
242
|
+
function unwritableShape(rewrite: Rewrite): string | undefined {
|
|
243
|
+
switch (rewrite.node) {
|
|
244
|
+
case "this":
|
|
245
|
+
case "computed":
|
|
246
|
+
case "from":
|
|
247
|
+
return undefined;
|
|
248
|
+
case "union":
|
|
249
|
+
case "intersection": {
|
|
250
|
+
if (rewrite.members.length < 2) {
|
|
251
|
+
return `is ${rewrite.node === "union" ? "an or" : "an and"} over fewer than two members`;
|
|
252
|
+
}
|
|
253
|
+
const nested = rewrite.members.find(
|
|
254
|
+
(member) => member.node === "union" || member.node === "intersection",
|
|
255
|
+
);
|
|
256
|
+
return nested === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: `nests ${nested.node === "union" ? "or" : "and"} inside ${rewrite.node === "union" ? "or" : "and"}, which a .fga line cannot say without parentheses`;
|
|
259
|
+
}
|
|
260
|
+
default: {
|
|
261
|
+
const exhaustive: never = rewrite;
|
|
262
|
+
throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* definition, admin-owned like a blueprint and viewable by its owners and
|
|
4
4
|
* explicit grantees. The sessions its fires create name it back through
|
|
5
5
|
* `session#schedule`, a link the cloud writes and this edition does not
|
|
6
|
-
* (session.ts); the schedule's own relations are unaffected.
|
|
6
|
+
* (session.ts); the schedule's own relations are unaffected. An explicit
|
|
7
|
+
* grant may name an Enterprise team (`team#member`), which no open-source
|
|
8
|
+
* tuple ever does.
|
|
7
9
|
*/
|
|
8
10
|
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
9
11
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
from,
|
|
16
18
|
objectOf,
|
|
17
19
|
union,
|
|
20
|
+
usersetOf,
|
|
18
21
|
} from "./rewrite.js";
|
|
19
22
|
|
|
20
23
|
export const scheduleDeclaration = declareKind({
|
|
@@ -30,7 +33,13 @@ export const scheduleDeclaration = declareKind({
|
|
|
30
33
|
from("admin", "organization"),
|
|
31
34
|
),
|
|
32
35
|
],
|
|
33
|
-
[
|
|
36
|
+
[
|
|
37
|
+
"viewer",
|
|
38
|
+
union(
|
|
39
|
+
direct(objectOf("identity_account"), usersetOf("team", "member")),
|
|
40
|
+
computed("owner"),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
34
43
|
["can_view", computed("viewer")],
|
|
35
44
|
["can_edit", computed("owner")],
|
|
36
45
|
["can_delete", computed("owner")],
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* `can_use` and `can_clone` rather than `can_execute`. Only `can_view`,
|
|
4
4
|
* `can_edit`, `can_delete`, `can_grant_access` and `can_view_access` are
|
|
5
5
|
* in the wire's IamPermission vocabulary; the others are the model's and
|
|
6
|
-
* are carried so the transcript is the file.
|
|
6
|
+
* are carried so the transcript is the file. `team#member` on `viewer` is
|
|
7
|
+
* the Enterprise team grant, which no open-source tuple ever names.
|
|
7
8
|
*/
|
|
8
9
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
9
10
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -42,6 +43,7 @@ export const skillDeclaration = declareKind({
|
|
|
42
43
|
objectOf("identity_account"),
|
|
43
44
|
usersetOf("organization", "member"),
|
|
44
45
|
usersetOf("organization", "viewer"),
|
|
46
|
+
usersetOf("team", "member"),
|
|
45
47
|
),
|
|
46
48
|
computed("owner"),
|
|
47
49
|
computed("platform_viewer"),
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* without `can_create_instance` (a workflow instance is created through
|
|
4
4
|
* the workflow-instance lane, gated by the organization). Kept as its own
|
|
5
5
|
* transcript, not a shared "blueprint" helper, so the file-by-file drift
|
|
6
|
-
* compare sees exactly this file's lines.
|
|
6
|
+
* compare sees exactly this file's lines. `team#member` on `viewer` is the
|
|
7
|
+
* Enterprise team grant, which no open-source tuple ever names.
|
|
7
8
|
*/
|
|
8
9
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
9
10
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -42,6 +43,7 @@ export const workflowDeclaration = declareKind({
|
|
|
42
43
|
objectOf("identity_account"),
|
|
43
44
|
usersetOf("organization", "member"),
|
|
44
45
|
usersetOf("organization", "viewer"),
|
|
46
|
+
usersetOf("team", "member"),
|
|
45
47
|
),
|
|
46
48
|
computed("owner"),
|
|
47
49
|
computed("platform_viewer"),
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* audience through `execution_viewer from workflow_instance`, standing on
|
|
7
7
|
* the `workflow_instance` link the derivation writes from
|
|
8
8
|
* `spec.workflow_instance_id`. No `can_delete`: run history outlives its
|
|
9
|
-
* instance by design.
|
|
9
|
+
* instance by design. The per-run grant may name an Enterprise team
|
|
10
|
+
* (`team#member`), which no open-source tuple ever does.
|
|
10
11
|
*/
|
|
11
12
|
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
12
13
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
from,
|
|
19
20
|
objectOf,
|
|
20
21
|
union,
|
|
22
|
+
usersetOf,
|
|
21
23
|
} from "./rewrite.js";
|
|
22
24
|
|
|
23
25
|
export const workflowExecutionDeclaration = declareKind({
|
|
@@ -31,7 +33,7 @@ export const workflowExecutionDeclaration = declareKind({
|
|
|
31
33
|
[
|
|
32
34
|
"viewer",
|
|
33
35
|
union(
|
|
34
|
-
direct(objectOf("identity_account")),
|
|
36
|
+
direct(objectOf("identity_account"), usersetOf("team", "member")),
|
|
35
37
|
computed("owner"),
|
|
36
38
|
from("execution_viewer", "workflow_instance"),
|
|
37
39
|
),
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* admits — and `private` or unset derives nothing, so each run stays its
|
|
15
15
|
* triggerer's. The cloud has no writer for this tuple today; this
|
|
16
16
|
* edition enforces the field as the contract states it.
|
|
17
|
+
*
|
|
18
|
+
* `team#member` on `viewer` is the Enterprise team grant, which no
|
|
19
|
+
* open-source tuple ever names; `execution_viewer` is not a grantable role
|
|
20
|
+
* and admits no team.
|
|
17
21
|
*/
|
|
18
22
|
import { isMessage } from "@bufbuild/protobuf";
|
|
19
23
|
|
|
@@ -85,6 +89,7 @@ export const workflowInstanceDeclaration = declareKind({
|
|
|
85
89
|
objectOf("identity_account"),
|
|
86
90
|
usersetOf("organization", "member"),
|
|
87
91
|
usersetOf("organization", "viewer"),
|
|
92
|
+
usersetOf("team", "member"),
|
|
88
93
|
),
|
|
89
94
|
computed("owner"),
|
|
90
95
|
from("viewer", "default_of"),
|
package/src/boot/compose.ts
CHANGED
|
@@ -1309,6 +1309,8 @@ export async function composeServer(
|
|
|
1309
1309
|
grantScope:
|
|
1310
1310
|
extensions.drivers.policyGrantScope ?? newOrganizationOnlyGrantScope(),
|
|
1311
1311
|
queries: extensions.drivers.authorizationQueries,
|
|
1312
|
+
principalDisplay: extensions.drivers.principalDisplay,
|
|
1313
|
+
gateSteps: extensions.gateSteps,
|
|
1312
1314
|
edition: extensions.edition,
|
|
1313
1315
|
logger,
|
|
1314
1316
|
});
|