@stigmer/server 3.27.2 → 3.29.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.
Files changed (227) hide show
  1. package/README.md +2 -0
  2. package/dist/authorization/authorizer.d.ts +3 -3
  3. package/dist/authorization/authorizer.js +3 -3
  4. package/dist/authorization/derived-tuples.js +6 -1
  5. package/dist/authorization/derived-tuples.js.map +1 -1
  6. package/dist/authorization/evaluator.d.ts +9 -10
  7. package/dist/authorization/evaluator.d.ts.map +1 -1
  8. package/dist/authorization/evaluator.js.map +1 -1
  9. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  10. package/dist/authorization/list-read-scope.js +29 -18
  11. package/dist/authorization/list-read-scope.js.map +1 -1
  12. package/dist/authorization/model/bindings.d.ts +41 -0
  13. package/dist/authorization/model/bindings.d.ts.map +1 -0
  14. package/dist/authorization/model/bindings.js +89 -0
  15. package/dist/authorization/model/bindings.js.map +1 -0
  16. package/dist/authorization/model/data/authorization-model.json +3141 -0
  17. package/dist/authorization/model/default-of.d.ts +2 -2
  18. package/dist/authorization/model/execution-viewer.d.ts +3 -0
  19. package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
  20. package/dist/authorization/model/execution-viewer.js +50 -0
  21. package/dist/authorization/model/execution-viewer.js.map +1 -0
  22. package/dist/authorization/model/index.d.ts +33 -21
  23. package/dist/authorization/model/index.d.ts.map +1 -1
  24. package/dist/authorization/model/index.js +74 -52
  25. package/dist/authorization/model/index.js.map +1 -1
  26. package/dist/authorization/model/openfga-json.d.ts +18 -0
  27. package/dist/authorization/model/openfga-json.d.ts.map +1 -0
  28. package/dist/authorization/model/openfga-json.js +266 -0
  29. package/dist/authorization/model/openfga-json.js.map +1 -0
  30. package/dist/authorization/model/rewrite.d.ts +39 -67
  31. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  32. package/dist/authorization/model/rewrite.js +1 -117
  33. package/dist/authorization/model/rewrite.js.map +1 -1
  34. package/dist/authorization/strict-json.d.ts +20 -0
  35. package/dist/authorization/strict-json.d.ts.map +1 -0
  36. package/dist/authorization/strict-json.js +45 -0
  37. package/dist/authorization/strict-json.js.map +1 -0
  38. package/dist/boot/compose.d.ts.map +1 -1
  39. package/dist/boot/compose.js +9 -7
  40. package/dist/boot/compose.js.map +1 -1
  41. package/dist/domain/workflowexecution/list-index.d.ts +1 -1
  42. package/dist/extensions/list-read-scope.d.ts +20 -3
  43. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  44. package/dist/extensions/list-read-scope.js +20 -1
  45. package/dist/extensions/list-read-scope.js.map +1 -1
  46. package/dist/index.d.ts +7 -8
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +15 -5
  49. package/dist/index.js.map +1 -1
  50. package/package.json +7 -6
  51. package/src/authorization/README.md +41 -34
  52. package/src/authorization/__tests__/authorizer.test.ts +5 -14
  53. package/src/authorization/__tests__/derived-tuples.test.ts +29 -27
  54. package/src/authorization/__tests__/evaluator.test.ts +53 -160
  55. package/src/authorization/__tests__/facts.test.ts +5 -11
  56. package/src/authorization/__tests__/list-read-scope.measure.test.ts +12 -20
  57. package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
  58. package/src/authorization/__tests__/organization-directory.test.ts +2 -2
  59. package/src/authorization/__tests__/store-test-kit.ts +246 -0
  60. package/src/authorization/__tests__/store-tests.test.ts +92 -223
  61. package/src/authorization/__tests__/support.ts +30 -2
  62. package/src/authorization/__tests__/throwaway-model.ts +75 -0
  63. package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
  64. package/src/authorization/authorizer.ts +3 -3
  65. package/src/authorization/derived-tuples.ts +7 -2
  66. package/src/authorization/evaluator.ts +8 -10
  67. package/src/authorization/list-read-scope.ts +37 -22
  68. package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
  69. package/src/authorization/model/__tests__/registry.test.ts +345 -277
  70. package/src/authorization/model/bindings.ts +131 -0
  71. package/src/authorization/model/data/authorization-model.json +3141 -0
  72. package/src/authorization/model/default-of.ts +2 -2
  73. package/src/authorization/model/execution-viewer.ts +56 -0
  74. package/src/authorization/model/index.ts +81 -72
  75. package/src/authorization/model/openfga-json.ts +310 -0
  76. package/src/authorization/model/rewrite.ts +39 -188
  77. package/src/authorization/strict-json.ts +52 -0
  78. package/src/boot/compose.ts +9 -7
  79. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
  80. package/src/extensions/list-read-scope.ts +32 -4
  81. package/src/index.ts +15 -44
  82. package/dist/authorization/model/agent.d.ts +0 -2
  83. package/dist/authorization/model/agent.d.ts.map +0 -1
  84. package/dist/authorization/model/agent.js +0 -43
  85. package/dist/authorization/model/agent.js.map +0 -1
  86. package/dist/authorization/model/agent_channel.d.ts +0 -2
  87. package/dist/authorization/model/agent_channel.d.ts.map +0 -1
  88. package/dist/authorization/model/agent_channel.js +0 -39
  89. package/dist/authorization/model/agent_channel.js.map +0 -1
  90. package/dist/authorization/model/agent_execution.d.ts +0 -2
  91. package/dist/authorization/model/agent_execution.d.ts.map +0 -1
  92. package/dist/authorization/model/agent_execution.js +0 -25
  93. package/dist/authorization/model/agent_execution.js.map +0 -1
  94. package/dist/authorization/model/agent_instance.d.ts +0 -2
  95. package/dist/authorization/model/agent_instance.d.ts.map +0 -1
  96. package/dist/authorization/model/agent_instance.js +0 -48
  97. package/dist/authorization/model/agent_instance.js.map +0 -1
  98. package/dist/authorization/model/agent_share.d.ts +0 -2
  99. package/dist/authorization/model/agent_share.d.ts.map +0 -1
  100. package/dist/authorization/model/agent_share.js +0 -29
  101. package/dist/authorization/model/agent_share.js.map +0 -1
  102. package/dist/authorization/model/api_key.d.ts +0 -2
  103. package/dist/authorization/model/api_key.d.ts.map +0 -1
  104. package/dist/authorization/model/api_key.js +0 -27
  105. package/dist/authorization/model/api_key.js.map +0 -1
  106. package/dist/authorization/model/artifact.d.ts +0 -2
  107. package/dist/authorization/model/artifact.d.ts.map +0 -1
  108. package/dist/authorization/model/artifact.js +0 -27
  109. package/dist/authorization/model/artifact.js.map +0 -1
  110. package/dist/authorization/model/channel_app.d.ts +0 -2
  111. package/dist/authorization/model/channel_app.d.ts.map +0 -1
  112. package/dist/authorization/model/channel_app.js +0 -28
  113. package/dist/authorization/model/channel_app.js.map +0 -1
  114. package/dist/authorization/model/environment.d.ts +0 -2
  115. package/dist/authorization/model/environment.d.ts.map +0 -1
  116. package/dist/authorization/model/environment.js +0 -33
  117. package/dist/authorization/model/environment.js.map +0 -1
  118. package/dist/authorization/model/execution_context.d.ts +0 -2
  119. package/dist/authorization/model/execution_context.d.ts.map +0 -1
  120. package/dist/authorization/model/execution_context.js +0 -19
  121. package/dist/authorization/model/execution_context.js.map +0 -1
  122. package/dist/authorization/model/iam_policy.d.ts +0 -2
  123. package/dist/authorization/model/iam_policy.d.ts.map +0 -1
  124. package/dist/authorization/model/iam_policy.js +0 -31
  125. package/dist/authorization/model/iam_policy.js.map +0 -1
  126. package/dist/authorization/model/identity_account.d.ts +0 -2
  127. package/dist/authorization/model/identity_account.d.ts.map +0 -1
  128. package/dist/authorization/model/identity_account.js +0 -25
  129. package/dist/authorization/model/identity_account.js.map +0 -1
  130. package/dist/authorization/model/mcp_server.d.ts +0 -2
  131. package/dist/authorization/model/mcp_server.d.ts.map +0 -1
  132. package/dist/authorization/model/mcp_server.js +0 -38
  133. package/dist/authorization/model/mcp_server.js.map +0 -1
  134. package/dist/authorization/model/memory.d.ts +0 -2
  135. package/dist/authorization/model/memory.d.ts.map +0 -1
  136. package/dist/authorization/model/memory.js +0 -26
  137. package/dist/authorization/model/memory.js.map +0 -1
  138. package/dist/authorization/model/oauth_app.d.ts +0 -2
  139. package/dist/authorization/model/oauth_app.d.ts.map +0 -1
  140. package/dist/authorization/model/oauth_app.js +0 -31
  141. package/dist/authorization/model/oauth_app.js.map +0 -1
  142. package/dist/authorization/model/organization.d.ts +0 -2
  143. package/dist/authorization/model/organization.d.ts.map +0 -1
  144. package/dist/authorization/model/organization.js +0 -58
  145. package/dist/authorization/model/organization.js.map +0 -1
  146. package/dist/authorization/model/platform_client.d.ts +0 -2
  147. package/dist/authorization/model/platform_client.d.ts.map +0 -1
  148. package/dist/authorization/model/platform_client.js +0 -31
  149. package/dist/authorization/model/platform_client.js.map +0 -1
  150. package/dist/authorization/model/plugin.d.ts +0 -2
  151. package/dist/authorization/model/plugin.d.ts.map +0 -1
  152. package/dist/authorization/model/plugin.js +0 -45
  153. package/dist/authorization/model/plugin.js.map +0 -1
  154. package/dist/authorization/model/schedule.d.ts +0 -2
  155. package/dist/authorization/model/schedule.d.ts.map +0 -1
  156. package/dist/authorization/model/schedule.js +0 -34
  157. package/dist/authorization/model/schedule.js.map +0 -1
  158. package/dist/authorization/model/session.d.ts +0 -2
  159. package/dist/authorization/model/session.d.ts.map +0 -1
  160. package/dist/authorization/model/session.js +0 -38
  161. package/dist/authorization/model/session.js.map +0 -1
  162. package/dist/authorization/model/skill.d.ts +0 -2
  163. package/dist/authorization/model/skill.d.ts.map +0 -1
  164. package/dist/authorization/model/skill.js +0 -39
  165. package/dist/authorization/model/skill.js.map +0 -1
  166. package/dist/authorization/model/workflow.d.ts +0 -2
  167. package/dist/authorization/model/workflow.d.ts.map +0 -1
  168. package/dist/authorization/model/workflow.js +0 -39
  169. package/dist/authorization/model/workflow.js.map +0 -1
  170. package/dist/authorization/model/workflow_execution.d.ts +0 -2
  171. package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
  172. package/dist/authorization/model/workflow_execution.js +0 -33
  173. package/dist/authorization/model/workflow_execution.js.map +0 -1
  174. package/dist/authorization/model/workflow_instance.d.ts +0 -2
  175. package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
  176. package/dist/authorization/model/workflow_instance.js +0 -96
  177. package/dist/authorization/model/workflow_instance.js.map +0 -1
  178. package/dist/authorization/store-test-kit.d.ts +0 -62
  179. package/dist/authorization/store-test-kit.d.ts.map +0 -1
  180. package/dist/authorization/store-test-kit.js +0 -265
  181. package/dist/authorization/store-test-kit.js.map +0 -1
  182. package/src/authorization/__tests__/enterprise-model.ts +0 -59
  183. package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
  184. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
  185. package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
  186. package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
  187. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
  188. package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
  189. package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
  190. package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
  191. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
  192. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
  193. package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
  194. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
  195. package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
  196. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
  197. package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
  198. package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
  199. package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
  200. package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
  201. package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
  202. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
  203. package/src/authorization/model/agent.ts +0 -64
  204. package/src/authorization/model/agent_channel.ts +0 -57
  205. package/src/authorization/model/agent_execution.ts +0 -33
  206. package/src/authorization/model/agent_instance.ts +0 -66
  207. package/src/authorization/model/agent_share.ts +0 -40
  208. package/src/authorization/model/api_key.ts +0 -28
  209. package/src/authorization/model/artifact.ts +0 -38
  210. package/src/authorization/model/channel_app.ts +0 -39
  211. package/src/authorization/model/environment.ts +0 -48
  212. package/src/authorization/model/execution_context.ts +0 -20
  213. package/src/authorization/model/iam_policy.ts +0 -42
  214. package/src/authorization/model/identity_account.ts +0 -26
  215. package/src/authorization/model/mcp_server.ts +0 -59
  216. package/src/authorization/model/memory.ts +0 -27
  217. package/src/authorization/model/oauth_app.ts +0 -43
  218. package/src/authorization/model/organization.ts +0 -59
  219. package/src/authorization/model/platform_client.ts +0 -43
  220. package/src/authorization/model/plugin.ts +0 -66
  221. package/src/authorization/model/schedule.ts +0 -49
  222. package/src/authorization/model/session.ts +0 -50
  223. package/src/authorization/model/skill.ts +0 -60
  224. package/src/authorization/model/workflow.ts +0 -60
  225. package/src/authorization/model/workflow_execution.ts +0 -46
  226. package/src/authorization/model/workflow_instance.ts +0 -125
  227. package/src/authorization/store-test-kit.ts +0 -395
@@ -1,255 +1,97 @@
1
1
  /**
2
- * Pins the built-in model's registry: which kinds are declared — every
3
- * kind of the open-source tier, in `fga.mod` order, and nothing else —
4
- * that each declaration names its `.fga` source by its own type, that a
5
- * kind is reached by enum and by FGA type name alike, and that every
6
- * permission the wire can ask about (the IamPermission vocabulary) is
7
- * either a declared relation of the kind or absent from its `.fga` file —
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.
2
+ * Pins the built-in model against its sources and the contract:
3
+ * - its types are the files `fga.mod` lists, in that order, one type per
4
+ * file named by the file, and every one is bound (bindings.ts);
5
+ * - every binding's schema is the message `kind_meta` spells for the
6
+ * kind (`ai.stigmer.<group>.<name>.<version>.<Name>`), `platform`
7
+ * excepted by name as the one rowless type, so the explicit table
8
+ * cannot bind a kind to the wrong message;
9
+ * - every declaration names a `.fga` file that exists under fga/model;
10
+ * - the model declares every open-source-tier kind, and outside the tier
11
+ * exactly the four kinds the wider editions serve, so a kind the
12
+ * contract moves between tiers is a visible diff here;
13
+ * - a kind is reached by enum and by FGA type name alike;
14
+ * - the relations whose direct list admits a team (`team#member`) are
15
+ * exactly the roles the kind's `team_grantable_roles` names, a subset
16
+ * of what a person can be granted and never ownership, so the grant
17
+ * step and the model cannot disagree about where a team may be
18
+ * granted;
19
+ * - the derived rules are exactly the three relations `kind_meta` cannot
20
+ * derive;
21
+ * - a kind whose authorization is its parent's whole
22
+ * (`inheritedAuthorizationParentOf`) holds nothing of its own: its one
23
+ * direct relation is the parent link, every other relation is built
24
+ * through it, and its `can_view` answers exactly what the parent's
25
+ * does, so the list read scope may ask the parent in the child's place
26
+ * (extensions/list-read-scope.ts) without hiding or showing a row the
27
+ * child's own check would not.
13
28
  *
14
- * The kind list is pinned twice on purpose: as the literal in registry
15
- * order (a reordering or a dropped file is a visible diff) and as
16
- * set-equal to the `kind_meta.tier` open-source members (a kind the
17
- * contract moves into or out of the tier is a visible diff here before
18
- * it is a silent gap in enforcement).
29
+ * Whether the model defines what the wire asks is the whole contract's
30
+ * question, pinned in __tests__/wire-permissions.test.ts.
19
31
  */
32
+ import { existsSync, readFileSync } from "node:fs";
33
+ import { fileURLToPath } from "node:url";
34
+
20
35
  import { describe, expect, it } from "vitest";
21
36
 
37
+ import { ApiResourceGroup } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_group_pb";
22
38
  import {
23
39
  ApiResourceKind,
24
40
  ApiResourceKindSchema,
41
+ ApiResourceVersion,
25
42
  ResourceTier,
26
43
  } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
- import {
28
- IamPermissionSchema,
29
- IamRole,
30
- } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
44
+ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
31
45
 
32
46
  import {
33
47
  getKindMeta,
34
48
  grantableRolesFor,
49
+ inheritedAuthorizationParentOf,
35
50
  kindEnumName,
36
51
  teamGrantableRolesFor,
37
52
  } from "../../../pipeline/apiresource-meta.js";
53
+ import {
54
+ computed,
55
+ direct,
56
+ from,
57
+ objectOf,
58
+ throwawayDeclaration,
59
+ union,
60
+ } from "../../__tests__/throwaway-model.js";
61
+ import { KIND_BINDINGS, ROWLESS } from "../bindings.js";
38
62
  import { builtInModel, declarationFor } from "../index.js";
39
- import type { Rewrite, SubjectType } from "../rewrite.js";
63
+ import type { KindDeclaration, Rewrite, SubjectType } from "../rewrite.js";
40
64
 
41
- /** The open-source tier, in registry order (the `fga.mod` order of their files). */
42
- const DECLARED_KINDS = [
43
- ApiResourceKind.identity_account,
44
- ApiResourceKind.iam_policy,
45
- ApiResourceKind.api_key,
46
- ApiResourceKind.oauth_app,
47
- ApiResourceKind.platform_client,
48
- ApiResourceKind.organization,
49
- ApiResourceKind.agent,
50
- ApiResourceKind.agent_channel,
51
- ApiResourceKind.agent_share,
52
- ApiResourceKind.channel_app,
53
- ApiResourceKind.agent_instance,
54
- ApiResourceKind.agent_execution,
55
- ApiResourceKind.artifact,
56
- ApiResourceKind.environment,
57
- ApiResourceKind.execution_context,
58
- ApiResourceKind.mcp_server,
59
- ApiResourceKind.memory,
60
- ApiResourceKind.schedule,
61
- ApiResourceKind.session,
62
- ApiResourceKind.skill,
63
- ApiResourceKind.workflow,
64
- ApiResourceKind.workflow_instance,
65
- ApiResourceKind.workflow_execution,
66
- ApiResourceKind.plugin,
67
- ] as const;
65
+ /** The server package root, where fga/model lives. */
66
+ const PACKAGE_ROOT = new URL("../../../../", import.meta.url);
68
67
 
69
- /**
70
- * The permissions each `.fga` file defines that the wire vocabulary also
71
- * names, in the file's order — derived by hand from the files at the
72
- * pinned commit. A relation in the file and not here, or here and not in
73
- * the transcript, fails. Verbs the files define outside the vocabulary
74
- * (`can_use`, `can_clone`, `can_rotate`, `can_revoke`) are the model's
75
- * own and are not listed.
76
- */
77
- const WIRE_PERMISSIONS_BY_TYPE: Readonly<
78
- Record<string, ReadonlyArray<string>>
79
- > = {
80
- identity_account: [
81
- "can_view",
82
- "can_edit",
83
- "can_delete",
84
- "can_grant_access",
85
- "can_view_access",
86
- ],
87
- iam_policy: [
88
- "can_view",
89
- "can_edit",
90
- "can_delete",
91
- "can_grant_access",
92
- "can_view_access",
93
- ],
94
- api_key: [
95
- "can_view",
96
- "can_edit",
97
- "can_delete",
98
- "can_grant_access",
99
- "can_view_access",
100
- ],
101
- oauth_app: [
102
- "can_view",
103
- "can_edit",
104
- "can_delete",
105
- "can_grant_access",
106
- "can_view_access",
107
- ],
108
- platform_client: [
109
- "can_view",
110
- "can_edit",
111
- "can_delete",
112
- "can_grant_access",
113
- "can_view_access",
114
- ],
115
- organization: [
116
- "can_view",
117
- "can_edit",
118
- "can_delete",
119
- "can_create_agent",
120
- "can_create_workflow",
121
- "can_create_session",
122
- "can_create_environment",
123
- "can_create_skill",
124
- "can_create_plugin",
125
- "can_create_mcp_server",
126
- "can_create_idp",
127
- "can_create_identity_account",
128
- "can_create_oauth_app",
129
- "can_create_platform_client",
130
- "can_create_channel_app",
131
- "can_create_execution_in",
132
- "can_create_agent_share",
133
- "can_create_agent_instance",
134
- "can_create_team",
135
- "can_grant_access",
136
- "can_view_access",
137
- "can_view_billing",
138
- "can_manage_billing",
139
- ],
140
- agent: [
141
- "can_view",
142
- "can_edit",
143
- "can_delete",
144
- "can_execute",
145
- "can_create_instance",
146
- "can_grant_access",
147
- "can_view_access",
148
- ],
149
- agent_channel: [
150
- "can_view",
151
- "can_edit",
152
- "can_delete",
153
- "can_participate",
154
- "can_grant_access",
155
- "can_view_access",
156
- ],
157
- agent_share: [
158
- "can_view",
159
- "can_edit",
160
- "can_delete",
161
- "can_grant_access",
162
- "can_view_access",
163
- ],
164
- channel_app: [
165
- "can_view",
166
- "can_edit",
167
- "can_delete",
168
- "can_grant_access",
169
- "can_view_access",
170
- ],
171
- agent_instance: [
172
- "can_view",
173
- "can_execute",
174
- "can_edit",
175
- "can_delete",
176
- "can_grant_access",
177
- "can_view_access",
178
- ],
179
- agent_execution: ["can_view", "can_edit"],
180
- artifact: ["can_view", "can_edit"],
181
- environment: [
182
- "can_view",
183
- "can_edit",
184
- "can_delete",
185
- "can_read_secrets",
186
- "can_grant_access",
187
- "can_view_access",
188
- ],
189
- execution_context: ["can_view", "can_edit"],
190
- mcp_server: [
191
- "can_view",
192
- "can_edit",
193
- "can_delete",
194
- "can_connect",
195
- "can_grant_access",
196
- "can_view_access",
197
- ],
198
- memory: ["can_view", "can_edit", "can_delete"],
199
- schedule: [
200
- "can_view",
201
- "can_edit",
202
- "can_delete",
203
- "can_grant_access",
204
- "can_view_access",
205
- ],
206
- session: [
207
- "can_view",
208
- "can_edit",
209
- "can_delete",
210
- "can_create_execution_in",
211
- "can_grant_access",
212
- "can_view_access",
213
- ],
214
- skill: [
215
- "can_view",
216
- "can_edit",
217
- "can_delete",
218
- "can_grant_access",
219
- "can_view_access",
220
- ],
221
- workflow: [
222
- "can_view",
223
- "can_edit",
224
- "can_delete",
225
- "can_execute",
226
- "can_grant_access",
227
- "can_view_access",
228
- ],
229
- plugin: [
230
- "can_view",
231
- "can_edit",
232
- "can_delete",
233
- "can_grant_access",
234
- "can_view_access",
235
- ],
236
- workflow_instance: [
237
- "can_view",
238
- "can_execute",
239
- "can_edit",
240
- "can_delete",
241
- "can_grant_access",
242
- "can_view_access",
243
- ],
244
- workflow_execution: [
245
- "can_view",
246
- "can_edit",
247
- "can_grant_access",
248
- "can_view_access",
249
- ],
250
- };
68
+ /** The kinds the model defines outside the open-source tier: the ones the wider editions serve. */
69
+ const WIDER_EDITION_KINDS: ReadonlyArray<ApiResourceKind> = [
70
+ ApiResourceKind.platform,
71
+ ApiResourceKind.identity_provider,
72
+ ApiResourceKind.invitation,
73
+ ApiResourceKind.team,
74
+ ];
75
+
76
+ /** The files `fga.mod` lists, in its order. */
77
+ function modFiles(): ReadonlyArray<string> {
78
+ const text = readFileSync(fileURLToPath(new URL("fga/model/fga.mod", PACKAGE_ROOT)), "utf8");
79
+ return [...text.matchAll(/^\s*-\s*(\S+\.fga)\s*$/gm)].map((match) => match[1] ?? "");
80
+ }
81
+
82
+ /** The message `kind_meta` spells for a kind. */
83
+ function kindMetaMessageName(kind: ApiResourceKind): string {
84
+ const meta = getKindMeta(kind);
85
+ return [
86
+ "ai",
87
+ "stigmer",
88
+ ApiResourceGroup[meta.group],
89
+ meta.name.toLowerCase(),
90
+ ApiResourceVersion[meta.version],
91
+ meta.name,
92
+ ].join(".");
93
+ }
251
94
 
252
- /** The subject types a relation's direct lists name, wherever they sit in its rewrite. */
253
95
  function directSubjects(rewrite: Rewrite): ReadonlyArray<SubjectType> {
254
96
  switch (rewrite.node) {
255
97
  case "this":
@@ -267,15 +109,185 @@ function directSubjects(rewrite: Rewrite): ReadonlyArray<SubjectType> {
267
109
  }
268
110
  }
269
111
 
112
+ /** The relation every list lane reads through, on the child and on its parent. */
113
+ const LIST_RELATION = "can_view";
114
+
115
+ /** The `this`, `computed` and `from` nodes a rewrite is built from, in the line's order. */
116
+ function leavesOf(rewrite: Rewrite): ReadonlyArray<Rewrite> {
117
+ switch (rewrite.node) {
118
+ case "this":
119
+ case "computed":
120
+ case "from":
121
+ return [rewrite];
122
+ case "union":
123
+ case "intersection":
124
+ return rewrite.members.flatMap(leavesOf);
125
+ default: {
126
+ const exhaustive: never = rewrite;
127
+ throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The relations of `declaration` that `relation` includes whole: itself,
134
+ * and every sibling a union member or a computed node names, transitively.
135
+ * An intersection includes none of its members whole, so it stops here.
136
+ */
137
+ function includedWhole(declaration: KindDeclaration, relation: string): ReadonlySet<string> {
138
+ const included = new Set<string>();
139
+ const visit = (name: string): void => {
140
+ if (included.has(name)) {
141
+ return;
142
+ }
143
+ included.add(name);
144
+ const walk = (rewrite: Rewrite | undefined): void => {
145
+ if (rewrite?.node === "computed") {
146
+ visit(rewrite.relation);
147
+ } else if (rewrite?.node === "union") {
148
+ rewrite.members.forEach(walk);
149
+ }
150
+ };
151
+ walk(declaration.relations.get(name));
152
+ };
153
+ visit(relation);
154
+ return included;
155
+ }
156
+
157
+ /**
158
+ * The parent relations `relation` of `child` reaches through `link`,
159
+ * following the child's own computed relations to their leaves.
160
+ */
161
+ function parentRelationsReached(
162
+ child: KindDeclaration,
163
+ relation: string,
164
+ link: string,
165
+ ): ReadonlySet<string> {
166
+ const reached = new Set<string>();
167
+ const seen = new Set<string>();
168
+ const visit = (name: string): void => {
169
+ if (seen.has(name)) {
170
+ return;
171
+ }
172
+ seen.add(name);
173
+ const rewrite = child.relations.get(name);
174
+ for (const leaf of rewrite === undefined ? [] : leavesOf(rewrite)) {
175
+ if (leaf.node === "computed") {
176
+ visit(leaf.relation);
177
+ } else if (leaf.node === "from" && leaf.tupleset === link) {
178
+ reached.add(leaf.relation);
179
+ }
180
+ }
181
+ };
182
+ visit(relation);
183
+ return reached;
184
+ }
185
+
186
+ /**
187
+ * Every way `child` breaks the promise its `kind_meta` makes when it says
188
+ * the kind's authorization is its parent's whole (PARENT scope, INHERITED
189
+ * owner); empty when the model keeps it. The list read scope reads that
190
+ * promise to let a driver ask the parent's `can_view` in the child's
191
+ * place, which gives the child's answer only while:
192
+ * - the child's one direct relation is the link, to the parent type
193
+ * alone, so nothing is granted on the child that the parent does not
194
+ * hold;
195
+ * - every other relation is built through the link or from the child's
196
+ * own relations, never from another object;
197
+ * - the child's `can_view` includes the parent's (`can_view from <link>`
198
+ * as a union member), so asking the parent hides nothing;
199
+ * - every parent relation the child's `can_view` reaches is one the
200
+ * parent's `can_view` includes whole, so asking the parent shows
201
+ * nothing more.
202
+ * The last reads inclusion structurally (union and computed), so it errs
203
+ * toward a violation: a shape it cannot prove is a design review, never a
204
+ * pass.
205
+ */
206
+ function inheritedWholeViolations(
207
+ child: KindDeclaration,
208
+ parent: KindDeclaration,
209
+ link: string,
210
+ ): ReadonlyArray<string> {
211
+ const violations: string[] = [];
212
+ const linkRewrite = child.relations.get(link);
213
+ const [linked, ...more] = linkRewrite?.node === "this" ? linkRewrite.subjects : [];
214
+ if (linked?.form !== "object" || linked.type !== parent.type || more.length > 0) {
215
+ violations.push(`${child.type}#${link} is not the direct link [${parent.type}] alone`);
216
+ }
217
+ for (const [name, rewrite] of child.relations) {
218
+ if (name === link) {
219
+ continue;
220
+ }
221
+ for (const leaf of leavesOf(rewrite)) {
222
+ if (leaf.node === "this") {
223
+ violations.push(`${child.type}#${name} admits a tuple on the child itself`);
224
+ } else if (leaf.node === "from" && leaf.tupleset !== link) {
225
+ violations.push(
226
+ `${child.type}#${name} reads ${leaf.relation} from ${leaf.tupleset}, not from ${link}`,
227
+ );
228
+ } else if (leaf.node === "computed" && !child.relations.has(leaf.relation)) {
229
+ violations.push(
230
+ `${child.type}#${name} names ${leaf.relation}, which the child does not declare`,
231
+ );
232
+ }
233
+ }
234
+ }
235
+ const childView = child.relations.get(LIST_RELATION);
236
+ if (childView === undefined || !parent.relations.has(LIST_RELATION)) {
237
+ violations.push(`${child.type} and ${parent.type} must both declare ${LIST_RELATION}`);
238
+ return violations;
239
+ }
240
+ const members = childView.node === "union" ? childView.members : [childView];
241
+ if (
242
+ !members.some(
243
+ (member) =>
244
+ member.node === "from" && member.relation === LIST_RELATION && member.tupleset === link,
245
+ )
246
+ ) {
247
+ violations.push(
248
+ `${child.type}#${LIST_RELATION} does not include ${LIST_RELATION} from ${link}`,
249
+ );
250
+ }
251
+ const parentView = includedWhole(parent, LIST_RELATION);
252
+ for (const relation of parentRelationsReached(child, LIST_RELATION, link)) {
253
+ if (!parentView.has(relation)) {
254
+ violations.push(
255
+ `${child.type}#${LIST_RELATION} reaches ${parent.type}#${relation}, which ${parent.type}#${LIST_RELATION} does not include`,
256
+ );
257
+ }
258
+ }
259
+ return violations;
260
+ }
261
+
262
+ /** A declaration the model must hold, or a loud failure naming it. */
263
+ function declared(declaration: KindDeclaration | undefined, type: string): KindDeclaration {
264
+ if (declaration === undefined) {
265
+ throw new Error(`${type} is not in the built-in model`);
266
+ }
267
+ return declaration;
268
+ }
269
+
270
+ /** The kinds whose `kind_meta` makes their authorization their parent's whole, with that parent. */
271
+ function inheritedWholeKinds(): ReadonlyArray<{
272
+ readonly kind: ApiResourceKind;
273
+ readonly parentType: string;
274
+ readonly link: string;
275
+ }> {
276
+ return ApiResourceKindSchema.values.flatMap((value) => {
277
+ const kind = value.number as ApiResourceKind;
278
+ if (kind === ApiResourceKind.api_resource_kind_unknown) {
279
+ return [];
280
+ }
281
+ const parent = inheritedAuthorizationParentOf(kind);
282
+ return parent === undefined ? [] : [{ kind, parentType: parent.kind, link: parent.relation }];
283
+ });
284
+ }
285
+
270
286
  function isTeamMembers(subject: SubjectType): boolean {
271
- return (
272
- subject.form === "userset" &&
273
- subject.type === "team" &&
274
- subject.relation === "member"
275
- );
287
+ return subject.form === "userset" && subject.type === "team" && subject.relation === "member";
276
288
  }
277
289
 
278
- /** Every kind whose `kind_meta.tier` is the open-source tier, by enum number. */
290
+ /** Every kind whose `kind_meta.tier` is the open-source tier. */
279
291
  function openSourceTierKinds(): ReadonlySet<ApiResourceKind> {
280
292
  const kinds = new Set<ApiResourceKind>();
281
293
  for (const value of ApiResourceKindSchema.values) {
@@ -290,59 +302,63 @@ function openSourceTierKinds(): ReadonlySet<ApiResourceKind> {
290
302
  return kinds;
291
303
  }
292
304
 
293
- describe("the built-in model's registry", () => {
294
- it("declares exactly the open-source tier's kinds, in fga.mod order", () => {
295
- expect(builtInModel.declarations.map((d) => d.kind)).toEqual([
296
- ...DECLARED_KINDS,
297
- ]);
298
- expect(new Set(DECLARED_KINDS)).toEqual(openSourceTierKinds());
305
+ describe("the built-in model", () => {
306
+ it("defines one type per file fga.mod lists, named by the file, in fga.mod order, every one bound", () => {
307
+ const files = modFiles();
308
+ expect(files.length).toBeGreaterThan(0);
309
+ expect(builtInModel.declarations.map((d) => d.type)).toEqual(
310
+ files.map((file) => file.replace(/^.*\//, "").replace(/\.fga$/, "")),
311
+ );
312
+ expect(builtInModel.declarations.map((d) => d.source)).toEqual(
313
+ files.map((file) => `fga/model/${file}`),
314
+ );
315
+ expect([...KIND_BINDINGS.keys()].map(kindEnumName)).toEqual(
316
+ builtInModel.declarations.map((d) => d.type),
317
+ );
299
318
  });
300
319
 
301
- it("reaches a declaration by kind and by FGA type name, and answers undefined for the kinds this edition does not serve", () => {
302
- for (const kind of DECLARED_KINDS) {
303
- const byKind = declarationFor(kind);
304
- expect(byKind?.kind).toBe(kind);
305
- expect(builtInModel.byType(kindEnumName(kind))).toBe(byKind);
306
- }
307
- for (const unserved of [
308
- ApiResourceKind.platform,
309
- ApiResourceKind.identity_provider,
310
- ApiResourceKind.invitation,
311
- ApiResourceKind.team,
312
- ]) {
313
- expect(declarationFor(unserved), kindEnumName(unserved)).toBeUndefined();
320
+ it("binds every kind to the message kind_meta spells, and platform alone to ROWLESS", () => {
321
+ for (const declaration of builtInModel.declarations) {
322
+ if (declaration.kind === ApiResourceKind.platform) {
323
+ expect(declaration.schema).toBe(ROWLESS);
324
+ continue;
325
+ }
326
+ expect(declaration.schema?.typeName, declaration.type).toBe(
327
+ kindMetaMessageName(declaration.kind),
328
+ );
314
329
  }
315
- expect(builtInModel.byType("")).toBeUndefined();
316
330
  });
317
331
 
318
- it("names its `.fga` source by the kind's own type, under the module the fga.mod files it in", () => {
332
+ it("names a .fga source that exists", () => {
319
333
  for (const declaration of builtInModel.declarations) {
320
- expect(declaration.source).toMatch(
321
- new RegExp(
322
- `^fga/model/(tenancy|agentic|iam)/${declaration.type}\\.fga$`,
323
- ),
324
- );
334
+ expect(
335
+ existsSync(fileURLToPath(new URL(declaration.source, PACKAGE_ROOT))),
336
+ declaration.source,
337
+ ).toBe(true);
325
338
  }
326
339
  });
327
340
 
328
- it("declares, for every kind, exactly the wire permissions its `.fga` file defines", () => {
329
- const wireVocabulary = new Set(
330
- IamPermissionSchema.values.map((value) => value.name),
331
- );
332
- expect(Object.keys(WIRE_PERMISSIONS_BY_TYPE).sort()).toEqual(
333
- builtInModel.declarations.map((d) => d.type).sort(),
334
- );
341
+ it("defines every open-source-tier kind, and outside the tier exactly the kinds the wider editions serve", () => {
342
+ const declared = new Set(builtInModel.declarations.map((d) => d.kind));
343
+ const tier = openSourceTierKinds();
344
+ for (const kind of tier) {
345
+ expect(declared.has(kind), kindEnumName(kind)).toBe(true);
346
+ }
347
+ expect(
348
+ [...declared].filter((kind) => !tier.has(kind)).map(kindEnumName).sort(),
349
+ ).toEqual(WIDER_EDITION_KINDS.map(kindEnumName).sort());
350
+ });
351
+
352
+ it("reaches a declaration by kind and by FGA type name", () => {
335
353
  for (const declaration of builtInModel.declarations) {
336
- const declaredWirePermissions = [...declaration.relations.keys()].filter(
337
- (relation) => wireVocabulary.has(relation),
338
- );
339
- expect(declaredWirePermissions, declaration.type).toEqual(
340
- WIRE_PERMISSIONS_BY_TYPE[declaration.type],
341
- );
354
+ expect(declarationFor(declaration.kind)).toBe(declaration);
355
+ expect(builtInModel.byType(kindEnumName(declaration.kind))).toBe(declaration);
342
356
  }
357
+ expect(declarationFor(ApiResourceKind.api_resource_kind_unknown)).toBeUndefined();
358
+ expect(builtInModel.byType("")).toBeUndefined();
343
359
  });
344
360
 
345
- it("admits a team exactly on the roles each kind's team_grantable_roles lists — the transcript and the contract say one thing", () => {
361
+ it("admits a team exactly on the roles each kind's team_grantable_roles lists — the model and the contract say one thing", () => {
346
362
  for (const declaration of builtInModel.declarations) {
347
363
  const admitsTeam = [...declaration.relations]
348
364
  .filter(([, rewrite]) => directSubjects(rewrite).some(isTeamMembers))
@@ -376,4 +392,56 @@ describe("the built-in model's registry", () => {
376
392
  "workflow_instance#execution_viewer",
377
393
  ]);
378
394
  });
395
+
396
+ it("gives a kind whose authorization is its parent's nothing of its own, so asking the parent answers for it", () => {
397
+ const inherited = inheritedWholeKinds();
398
+ expect(inherited.length, "kinds whose authorization is their parent's").toBeGreaterThan(0);
399
+ for (const { kind, parentType, link } of inherited) {
400
+ const child = declared(declarationFor(kind), kindEnumName(kind));
401
+ const parent = declared(builtInModel.byType(parentType), parentType);
402
+ expect(inheritedWholeViolations(child, parent, link), kindEnumName(kind)).toEqual([]);
403
+ }
404
+ });
405
+ });
406
+
407
+ describe("the parent-inheritance walker refuses what would make asking the parent wrong", () => {
408
+ const execution = declared(declarationFor(ApiResourceKind.agent_execution), "agent_execution");
409
+ const session = declared(builtInModel.byType("session"), "session");
410
+
411
+ /** agent_execution as the model has it, with one line rewritten. */
412
+ function executionWith(relation: string, rewrite: Rewrite): KindDeclaration {
413
+ return throwawayDeclaration({
414
+ kind: ApiResourceKind.agent_execution,
415
+ relations: [...execution.relations].map(([name, current]) =>
416
+ name === relation ? ([name, rewrite] as const) : ([name, current] as const),
417
+ ),
418
+ });
419
+ }
420
+
421
+ it.each([
422
+ {
423
+ shape: "a viewer granted on the execution itself",
424
+ child: executionWith(
425
+ "viewer",
426
+ union(direct(objectOf("identity_account")), from("viewer", "session"), computed("owner")),
427
+ ),
428
+ violation: "agent_execution#viewer admits a tuple on the child itself",
429
+ },
430
+ {
431
+ shape: "a can_view that no longer includes the session's",
432
+ child: executionWith("can_view", computed("viewer")),
433
+ violation: "agent_execution#can_view does not include can_view from session",
434
+ },
435
+ {
436
+ shape: "a can_view reaching a session relation its can_view does not include",
437
+ child: executionWith(
438
+ "can_view",
439
+ union(computed("viewer"), from("can_view", "session"), from("can_delete", "session")),
440
+ ),
441
+ violation:
442
+ "agent_execution#can_view reaches session#can_delete, which session#can_view does not include",
443
+ },
444
+ ])("$shape", ({ child, violation }) => {
445
+ expect(inheritedWholeViolations(child, session, "session")).toEqual([violation]);
446
+ });
379
447
  });