@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.
Files changed (121) hide show
  1. package/dist/authorization/evaluator.d.ts +4 -1
  2. package/dist/authorization/evaluator.d.ts.map +1 -1
  3. package/dist/authorization/evaluator.js +8 -0
  4. package/dist/authorization/evaluator.js.map +1 -1
  5. package/dist/authorization/model/agent.d.ts.map +1 -1
  6. package/dist/authorization/model/agent.js +4 -2
  7. package/dist/authorization/model/agent.js.map +1 -1
  8. package/dist/authorization/model/agent_channel.d.ts.map +1 -1
  9. package/dist/authorization/model/agent_channel.js +5 -4
  10. package/dist/authorization/model/agent_channel.js.map +1 -1
  11. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  12. package/dist/authorization/model/agent_instance.js +3 -2
  13. package/dist/authorization/model/agent_instance.js.map +1 -1
  14. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  15. package/dist/authorization/model/mcp_server.js +3 -2
  16. package/dist/authorization/model/mcp_server.js.map +1 -1
  17. package/dist/authorization/model/organization.d.ts.map +1 -1
  18. package/dist/authorization/model/organization.js +3 -0
  19. package/dist/authorization/model/organization.js.map +1 -1
  20. package/dist/authorization/model/plugin.d.ts.map +1 -1
  21. package/dist/authorization/model/plugin.js +4 -2
  22. package/dist/authorization/model/plugin.js.map +1 -1
  23. package/dist/authorization/model/rewrite.d.ts +25 -7
  24. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  25. package/dist/authorization/model/rewrite.js +40 -1
  26. package/dist/authorization/model/rewrite.js.map +1 -1
  27. package/dist/authorization/model/schedule.d.ts.map +1 -1
  28. package/dist/authorization/model/schedule.js +8 -3
  29. package/dist/authorization/model/schedule.js.map +1 -1
  30. package/dist/authorization/model/skill.d.ts.map +1 -1
  31. package/dist/authorization/model/skill.js +3 -2
  32. package/dist/authorization/model/skill.js.map +1 -1
  33. package/dist/authorization/model/workflow.d.ts.map +1 -1
  34. package/dist/authorization/model/workflow.js +3 -2
  35. package/dist/authorization/model/workflow.js.map +1 -1
  36. package/dist/authorization/model/workflow_execution.d.ts.map +1 -1
  37. package/dist/authorization/model/workflow_execution.js +4 -3
  38. package/dist/authorization/model/workflow_execution.js.map +1 -1
  39. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  40. package/dist/authorization/model/workflow_instance.js +5 -1
  41. package/dist/authorization/model/workflow_instance.js.map +1 -1
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +2 -0
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/domain/iampolicy/access-lists.d.ts +5 -3
  46. package/dist/domain/iampolicy/access-lists.d.ts.map +1 -1
  47. package/dist/domain/iampolicy/access-lists.js +72 -19
  48. package/dist/domain/iampolicy/access-lists.js.map +1 -1
  49. package/dist/domain/iampolicy/constants.d.ts +32 -6
  50. package/dist/domain/iampolicy/constants.d.ts.map +1 -1
  51. package/dist/domain/iampolicy/constants.js +41 -6
  52. package/dist/domain/iampolicy/constants.js.map +1 -1
  53. package/dist/domain/iampolicy/controller.d.ts +6 -0
  54. package/dist/domain/iampolicy/controller.d.ts.map +1 -1
  55. package/dist/domain/iampolicy/controller.js +16 -7
  56. package/dist/domain/iampolicy/controller.js.map +1 -1
  57. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  58. package/dist/domain/iampolicy/resource-store.js +2 -6
  59. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  60. package/dist/domain/iampolicy/steps.d.ts.map +1 -1
  61. package/dist/domain/iampolicy/steps.js +49 -7
  62. package/dist/domain/iampolicy/steps.js.map +1 -1
  63. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -0
  64. package/dist/extensions/drivers.d.ts +12 -0
  65. package/dist/extensions/drivers.d.ts.map +1 -1
  66. package/dist/extensions/gate-slots.d.ts +10 -1
  67. package/dist/extensions/gate-slots.d.ts.map +1 -1
  68. package/dist/extensions/gate-slots.js +1 -0
  69. package/dist/extensions/gate-slots.js.map +1 -1
  70. package/dist/extensions/principal-display.d.ts +34 -0
  71. package/dist/extensions/principal-display.d.ts.map +1 -0
  72. package/dist/extensions/principal-display.js +2 -0
  73. package/dist/extensions/principal-display.js.map +1 -0
  74. package/dist/extensions/registry.d.ts +6 -0
  75. package/dist/extensions/registry.d.ts.map +1 -1
  76. package/dist/extensions/registry.js +10 -0
  77. package/dist/extensions/registry.js.map +1 -1
  78. package/dist/index.d.ts +6 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +20 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/pipeline/apiresource-meta.d.ts +10 -0
  83. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  84. package/dist/pipeline/apiresource-meta.js +16 -0
  85. package/dist/pipeline/apiresource-meta.js.map +1 -1
  86. package/package.json +6 -6
  87. package/src/authorization/__tests__/evaluator.test.ts +127 -2
  88. package/src/authorization/evaluator.ts +12 -1
  89. package/src/authorization/model/__tests__/registry.test.ts +64 -2
  90. package/src/authorization/model/agent.ts +4 -1
  91. package/src/authorization/model/agent_channel.ts +11 -3
  92. package/src/authorization/model/agent_instance.ts +3 -1
  93. package/src/authorization/model/mcp_server.ts +3 -1
  94. package/src/authorization/model/organization.ts +3 -0
  95. package/src/authorization/model/plugin.ts +4 -1
  96. package/src/authorization/model/rewrite.ts +65 -8
  97. package/src/authorization/model/schedule.ts +11 -2
  98. package/src/authorization/model/skill.ts +3 -1
  99. package/src/authorization/model/workflow.ts +3 -1
  100. package/src/authorization/model/workflow_execution.ts +4 -2
  101. package/src/authorization/model/workflow_instance.ts +5 -0
  102. package/src/boot/compose.ts +2 -0
  103. package/src/domain/iampolicy/__tests__/access-lists.test.ts +120 -6
  104. package/src/domain/iampolicy/__tests__/constants.test.ts +26 -4
  105. package/src/domain/iampolicy/__tests__/controller.test.ts +55 -1
  106. package/src/domain/iampolicy/__tests__/grant-path.test.ts +3 -3
  107. package/src/domain/iampolicy/__tests__/steps.test.ts +123 -1
  108. package/src/domain/iampolicy/access-lists.ts +98 -23
  109. package/src/domain/iampolicy/constants.ts +50 -6
  110. package/src/domain/iampolicy/controller.ts +30 -7
  111. package/src/domain/iampolicy/resource-store.ts +3 -6
  112. package/src/domain/iampolicy/steps.ts +89 -7
  113. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -0
  114. package/src/extensions/__tests__/iam-policy-points.test.ts +30 -0
  115. package/src/extensions/__tests__/registry.test.ts +3 -0
  116. package/src/extensions/drivers.ts +12 -0
  117. package/src/extensions/gate-slots.ts +10 -0
  118. package/src/extensions/principal-display.ts +37 -0
  119. package/src/extensions/registry.ts +19 -0
  120. package/src/index.ts +26 -0
  121. 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 { IamPermissionSchema } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
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(direct(objectOf("identity_account")), computed("owner")),
38
+ union(
39
+ direct(objectOf("identity_account"), usersetOf("team", "member")),
40
+ computed("owner"),
41
+ ),
37
42
  ],
38
43
  [
39
44
  "viewer",
40
- union(direct(objectOf("identity_account")), computed("participant")),
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 four forms an OpenFGA `define` line takes in the cloud's 27 `.fga`
4
- * files, and nothing more:
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. Intersection and exclusion
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
- ["viewer", union(direct(objectOf("identity_account")), computed("owner"))],
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"),
@@ -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
  });