@bevel-software/platform-core-backend 0.28.0 → 0.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 (143) hide show
  1. package/agent-guide/access-control.md +15 -0
  2. package/agent-guide/skills.md +8 -0
  3. package/dist/core/catalog-revision.d.ts.map +1 -1
  4. package/dist/core/catalog-revision.js +4 -2
  5. package/dist/core/catalog-revision.js.map +1 -1
  6. package/dist/core/compose-embed.d.ts +35 -0
  7. package/dist/core/compose-embed.d.ts.map +1 -0
  8. package/dist/core/compose-embed.js +14 -0
  9. package/dist/core/compose-embed.js.map +1 -0
  10. package/dist/core/core-ports.d.ts +10 -0
  11. package/dist/core/core-ports.d.ts.map +1 -1
  12. package/dist/core/core-ports.js.map +1 -1
  13. package/dist/core/create-core-server.d.ts.map +1 -1
  14. package/dist/core/create-core-server.js +7 -1
  15. package/dist/core/create-core-server.js.map +1 -1
  16. package/dist/core/create-core-services.d.ts +1 -1
  17. package/dist/core/create-core-services.d.ts.map +1 -1
  18. package/dist/core/create-core-services.js +25 -14
  19. package/dist/core/create-core-services.js.map +1 -1
  20. package/dist/core/lifecycle.d.ts +11 -6
  21. package/dist/core/lifecycle.d.ts.map +1 -1
  22. package/dist/core/lifecycle.js +13 -6
  23. package/dist/core/lifecycle.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +5 -2
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +36 -14
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +208 -85
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/auth/index.d.ts +15 -0
  31. package/dist/modules/auth/index.d.ts.map +1 -0
  32. package/dist/modules/auth/index.js +20 -0
  33. package/dist/modules/auth/index.js.map +1 -0
  34. package/dist/modules/embed/embed.interface.d.ts +22 -0
  35. package/dist/modules/embed/embed.interface.d.ts.map +1 -1
  36. package/dist/modules/embed/embed.service.d.ts +28 -14
  37. package/dist/modules/embed/embed.service.d.ts.map +1 -1
  38. package/dist/modules/embed/embed.service.js +38 -17
  39. package/dist/modules/embed/embed.service.js.map +1 -1
  40. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  41. package/dist/modules/embed/embed.tools.js +10 -2
  42. package/dist/modules/embed/embed.tools.js.map +1 -1
  43. package/dist/modules/kb-fs/index.d.ts +7 -0
  44. package/dist/modules/kb-fs/index.d.ts.map +1 -0
  45. package/dist/modules/kb-fs/index.js +2 -0
  46. package/dist/modules/kb-fs/index.js.map +1 -0
  47. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  48. package/dist/modules/plugins/join-request-jobs.service.js +43 -8
  49. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  50. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  51. package/dist/modules/skills/skills.tools.d.ts +8 -1
  52. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  53. package/dist/modules/skills/skills.tools.js +115 -11
  54. package/dist/modules/skills/skills.tools.js.map +1 -1
  55. package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
  56. package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
  57. package/dist/modules/tool-helpers/default-branch-write.js +63 -0
  58. package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
  59. package/dist/modules/tool-helpers/index.d.ts +1 -0
  60. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  61. package/dist/modules/tool-helpers/index.js +1 -0
  62. package/dist/modules/tool-helpers/index.js.map +1 -1
  63. package/dist/modules/workflow/file-lock.service.d.ts +27 -5
  64. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  65. package/dist/modules/workflow/file-lock.service.js +70 -18
  66. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  67. package/dist/modules/workflow/git/git.service.d.ts +13 -0
  68. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  69. package/dist/modules/workflow/git/git.service.js +24 -0
  70. package/dist/modules/workflow/git/git.service.js.map +1 -1
  71. package/dist/modules/workflow/index.d.ts +8 -0
  72. package/dist/modules/workflow/index.d.ts.map +1 -0
  73. package/dist/modules/workflow/index.js +8 -0
  74. package/dist/modules/workflow/index.js.map +1 -0
  75. package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
  76. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  77. package/dist/modules/workflow/pending-commits.service.js +4 -24
  78. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  79. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  80. package/dist/modules/workflow/workflow.routes.js +20 -1
  81. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  82. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  83. package/dist/modules/workflow/workflow.service.js +5 -3
  84. package/dist/modules/workflow/workflow.service.js.map +1 -1
  85. package/dist/modules/workspace/index.d.ts +10 -0
  86. package/dist/modules/workspace/index.d.ts.map +1 -0
  87. package/dist/modules/workspace/index.js +10 -0
  88. package/dist/modules/workspace/index.js.map +1 -0
  89. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  90. package/dist/modules/workspace/workspace.tools.js +106 -20
  91. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  92. package/dist/shared/workspace-id.d.ts +17 -0
  93. package/dist/shared/workspace-id.d.ts.map +1 -1
  94. package/dist/shared/workspace-id.js +24 -0
  95. package/dist/shared/workspace-id.js.map +1 -1
  96. package/kb-template/access.md +2 -1
  97. package/mcp-app/page.html +142 -8
  98. package/package.json +3 -3
  99. package/src/core/__tests__/compose-embed.test.ts +65 -0
  100. package/src/core/__tests__/lifecycle.test.ts +29 -26
  101. package/src/core/catalog-revision.ts +4 -2
  102. package/src/core/compose-embed.ts +50 -0
  103. package/src/core/core-ports.ts +10 -0
  104. package/src/core/create-core-server.ts +7 -1
  105. package/src/core/create-core-services.ts +27 -21
  106. package/src/core/lifecycle.ts +24 -12
  107. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +23 -3
  108. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +303 -25
  109. package/src/modules/access/__tests__/access-control.download-read.test.ts +3 -1
  110. package/src/modules/access/__tests__/access-control.service.test.ts +77 -16
  111. package/src/modules/access/__tests__/change-read-gate.test.ts +36 -5
  112. package/src/modules/access/access-control.interface.ts +5 -2
  113. package/src/modules/access/access-control.service.ts +236 -95
  114. package/src/modules/auth/index.ts +29 -0
  115. package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
  116. package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
  117. package/src/modules/embed/embed.interface.ts +24 -0
  118. package/src/modules/embed/embed.service.ts +44 -19
  119. package/src/modules/embed/embed.tools.ts +10 -2
  120. package/src/modules/kb-fs/index.ts +6 -0
  121. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
  122. package/src/modules/plugins/join-request-jobs.service.ts +45 -8
  123. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
  124. package/src/modules/skills/skills.contract.ts +4 -0
  125. package/src/modules/skills/skills.tools.ts +120 -10
  126. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
  127. package/src/modules/tool-helpers/default-branch-write.ts +90 -0
  128. package/src/modules/tool-helpers/index.ts +5 -0
  129. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +36 -2
  130. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
  131. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
  132. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
  133. package/src/modules/workflow/file-lock.service.ts +79 -18
  134. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
  135. package/src/modules/workflow/git/git.service.ts +23 -0
  136. package/src/modules/workflow/index.ts +7 -0
  137. package/src/modules/workflow/pending-commits.service.ts +4 -23
  138. package/src/modules/workflow/workflow.routes.ts +24 -1
  139. package/src/modules/workflow/workflow.service.ts +3 -2
  140. package/src/modules/workspace/__tests__/workspace.tools.test.ts +21 -11
  141. package/src/modules/workspace/index.ts +14 -0
  142. package/src/modules/workspace/workspace.tools.ts +120 -20
  143. package/src/shared/workspace-id.ts +24 -0
@@ -943,16 +943,24 @@ describe('AccessControlService', () => {
943
943
  expect(await svc.canRead(workspaceId, 'sara@example.com', 'Knowledge/Foo.md')).toBe(true);
944
944
  });
945
945
 
946
- it('no admin rescue for read — admins read a restricted node only if listed', async () => {
946
+ it('the root floor gives admins read (write implies it) whatever the root says; a nearer scope can still exclude them', async () => {
947
947
  const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
948
948
  await writeFile(repo, 'roles.yaml', ROLES_YAML);
949
- // Write/read restricted to Product Manager — Admin is neither listed nor a
950
- // writer here, so there's no rescue path to read (unlike write on access.md).
949
+ // Write/read restricted to Product Manager — Admin is not named at the
950
+ // root, but the floor gives Admin write there and write implies read, so
951
+ // an admin who could create anything under the root can also see it.
951
952
  await writeFile(repo, 'access.md', '---\nwrite:\n - Product Manager\nread:\n - Product Manager\n---\n');
953
+ // A subfolder is an ordinary scope: it takes Admin's read away as it would anyone's.
954
+ await writeFile(repo, 'Knowledge/Board/access.md', '---\nread:\n - deny Admin\n---\n');
952
955
 
953
956
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR, new NodeFs());
954
- // razvan is Admin but not a Product Manager — restricted read denies him.
955
- expect(await svc.canRead(workspaceId, 'razvan@bevel.software', 'Knowledge/Foo.md')).toBe(false);
957
+ // razvan is Admin but not a Product Manager.
958
+ expect(await svc.canRead(workspaceId, 'razvan@bevel.software', 'Knowledge/Foo.md')).toBe(true);
959
+ expect(await svc.canRead(workspaceId, 'razvan@bevel.software', 'Knowledge/Board/Minutes.md')).toBe(false);
960
+ // The floor is write and read, not download.
961
+ expect(await svc.canDownload(workspaceId, 'razvan@bevel.software', 'Knowledge/Foo.md')).toBe(false);
962
+ // Nobody else gains anything from it.
963
+ expect(await svc.canRead(workspaceId, 'ali@bevel.software', 'Knowledge/Foo.md')).toBe(false);
956
964
  });
957
965
 
958
966
  it("a node's own read: frontmatter grants just that file", async () => {
@@ -1002,17 +1010,24 @@ describe('AccessControlService', () => {
1002
1010
  });
1003
1011
 
1004
1012
  describe('eligibleReaders', () => {
1005
- it('reports restricted=true with no readers for a default-denied node', async () => {
1013
+ it('reports restricted=true, with Admin as the one reader, for a default-denied node', async () => {
1006
1014
  const { workspaceDir, repo } = await seedWorkspace(root, workspaceId);
1007
1015
  await writeFile(repo, 'roles.yaml', ROLES_YAML);
1008
1016
  // The only line in the tree is a DENIAL — a denial grants nobody, and
1009
1017
  // no verb that folds into read grants anyone either, so the node is
1010
- // default-denied with no readers to name.
1018
+ // default-denied. Admin still reads it through the root floor, and the
1019
+ // list says so: the banner must name who can actually open it.
1011
1020
  await writeFile(repo, 'access.md', '---\nread:\n - deny everyone\n---\n');
1012
1021
 
1013
1022
  const svc = new AccessControlService(stubWorkspaceService(workspaceId, workspaceDir), PROCESS_MAP_DIR, new NodeFs());
1014
1023
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Foo.md');
1015
- expect(e).toEqual({ restricted: true, principals: [], roles: [], users: [], publicVia: [] });
1024
+ expect(e).toEqual({
1025
+ restricted: true,
1026
+ principals: [{ name: 'Admin', kind: 'role' }],
1027
+ roles: ['Admin'],
1028
+ users: [],
1029
+ publicVia: [],
1030
+ });
1016
1031
  });
1017
1032
 
1018
1033
  it('reports restricted=false when read: everyone applies', async () => {
@@ -1047,10 +1062,15 @@ describe('AccessControlService', () => {
1047
1062
  // node really is readable by everyone — not restricted.
1048
1063
  expect(await svc.canRead(workspaceId, 'felix@example.com', 'Knowledge/Open/Foo.md')).toBe(true);
1049
1064
  const e = await svc.eligibleReaders(workspaceId, 'Knowledge/Open/Foo.md');
1065
+ // Admin is listed beside everyone, as it is for write: the floor holds
1066
+ // wherever no nearer scope excludes it.
1050
1067
  expect(e).toEqual({
1051
1068
  restricted: false,
1052
- principals: [{ name: 'everyone', kind: 'role' }],
1053
- roles: ['everyone'],
1069
+ principals: [
1070
+ { name: 'Admin', kind: 'role' },
1071
+ { name: 'everyone', kind: 'role' },
1072
+ ],
1073
+ roles: ['Admin', 'everyone'],
1054
1074
  users: [],
1055
1075
  publicVia: [],
1056
1076
  });
@@ -1133,19 +1153,60 @@ describe('AccessControlService', () => {
1133
1153
  });
1134
1154
 
1135
1155
  /**
1136
- * Beyond the two rescue files, the owner gets exactly what the Admin role
1137
- * gets: the root write floor, and nothing a nearer rule takes away. Read
1138
- * and download still answer to the access tree.
1156
+ * Beyond the two rescue files, the owner IS the Admin role: the root floor
1157
+ * (write, and the read it implies), every grant and denial that names
1158
+ * Admin along the way, and nothing more. An owner the file does not name
1159
+ * used to hold write everywhere and read nowhere, so the read-before-write
1160
+ * gate refused them the very file the floor let them create.
1139
1161
  */
1140
- it('gets write only through the root floor, never past a subfolder that excludes Admin', async () => {
1162
+ it('is bound by the Admin rules for every verb, as a member of the role would be', async () => {
1141
1163
  const ws = await seeded();
1142
1164
  const { repo } = await seedWorkspace(root, workspaceId);
1143
1165
  await writeFile(repo, 'HR/access.md', '---\nwrite:\n - deny Admin\n---\n');
1166
+ await writeFile(repo, 'Board/access.md', '---\nread:\n - deny Admin\n---\n');
1167
+ await writeFile(repo, 'Vault/access.md', '---\ndownload:\n - Admin\n---\n');
1144
1168
  const svc = new AccessControlService(ws, PROCESS_MAP_DIR, new NodeFs(), [OWNER]);
1169
+ // The floor: write and read under the root, not download.
1145
1170
  expect(await svc.canWrite(workspaceId, OWNER, 'Knowledge/Foo.md')).toBe(true);
1146
- expect(await svc.canWrite(workspaceId, OWNER, 'HR/Salaries.md')).toBe(false);
1147
- expect(await svc.canRead(workspaceId, OWNER, 'Knowledge/Foo.md')).toBe(false);
1171
+ expect(await svc.canRead(workspaceId, OWNER, 'Knowledge/Foo.md')).toBe(true);
1148
1172
  expect(await svc.canDownload(workspaceId, OWNER, 'Knowledge/Foo.md')).toBe(false);
1173
+ // A nearer rule naming Admin binds the owner too, each verb on its own.
1174
+ expect(await svc.canWrite(workspaceId, OWNER, 'HR/Salaries.md')).toBe(false);
1175
+ expect(await svc.canRead(workspaceId, OWNER, 'HR/Salaries.md')).toBe(true);
1176
+ expect(await svc.canRead(workspaceId, OWNER, 'Board/Minutes.md')).toBe(false);
1177
+ expect(await svc.canWrite(workspaceId, OWNER, 'Board/Minutes.md')).toBe(false);
1178
+ // A grant to Admin is a grant to the owner — the role's keys are theirs.
1179
+ expect(await svc.canDownload(workspaceId, OWNER, 'Vault/Keys.md')).toBe(true);
1180
+ // The explorer's batch asks the same question and gets the same answer.
1181
+ const batch = await svc.canReadBatch(workspaceId, OWNER, ['Knowledge/Foo.md', 'Board/Minutes.md']);
1182
+ expect([...batch.entries()]).toEqual([
1183
+ ['Knowledge/Foo.md', true],
1184
+ ['Board/Minutes.md', false],
1185
+ ]);
1186
+ });
1187
+
1188
+ it('is counted when the share dialog asks whether everyone can read: a public folder that denies Admin is restricted for them', async () => {
1189
+ const ws = await seeded();
1190
+ const { repo } = await seedWorkspace(root, workspaceId);
1191
+ // Everyone reads, Admin is carved out, and the role's one member is let
1192
+ // back in by name (a direct entry outranks the role's denial). The only
1193
+ // person the denial still reaches is the owner, whom roles.yaml does not
1194
+ // name — so a check that enumerates roles.yaml alone sees nobody denied.
1195
+ await writeFile(
1196
+ repo,
1197
+ 'Open/access.md',
1198
+ '---\nread:\n - everyone\n - deny Admin\n - Someone Else <someone-else@example.com>\n---\n',
1199
+ );
1200
+ await writeFile(repo, 'Public/access.md', '---\nread:\n - everyone\n---\n');
1201
+ const svc = new AccessControlService(ws, PROCESS_MAP_DIR, new NodeFs(), [OWNER]);
1202
+ expect(await svc.canRead(workspaceId, 'someone-else@example.com', 'Open/Notes.md')).toBe(true);
1203
+ // The denial reaches the owner, so the folder is not readable by
1204
+ // everyone and the dialog must not say it is.
1205
+ expect(await svc.canRead(workspaceId, OWNER, 'Open/Notes.md')).toBe(false);
1206
+ expect((await svc.eligibleReaders(workspaceId, 'Open/Notes.md')).restricted).toBe(true);
1207
+ // Without the carve-out the same owner reads, and the folder is public.
1208
+ expect(await svc.canRead(workspaceId, OWNER, 'Public/Notes.md')).toBe(true);
1209
+ expect((await svc.eligibleReaders(workspaceId, 'Public/Notes.md')).restricted).toBe(false);
1149
1210
  });
1150
1211
 
1151
1212
  it('is matched case-insensitively, like every other email here', async () => {
@@ -185,13 +185,44 @@ describe('ChangeReadGate', () => {
185
185
 
186
186
  it('the rescues name FILES: a folder target at the root is judged as a folder, even for an admin', async () => {
187
187
  // The KB clone's own folder, asked about as an extraction destination:
188
- // the root scope, which here grants nobody — an admin included.
189
- expect(await judge(ADMIN, KB, 'dir')).toEqual({ allowed: false, unreadable: '' });
190
- expect(await judge(ADMIN, `${KB}/KnowledgeBase`, 'dir')).toEqual({ allowed: false, unreadable: 'KnowledgeBase' });
191
- // With a root that lets the admin read (write folds into read), the same folders open.
188
+ // the root scope. It grants nobody here, so a non-admin is refused; an
189
+ // admin reads it through the root floor (write implies read), which is
190
+ // the access tree's own answer, not a rescue.
191
+ expect(await judge(ALICE, KB, 'dir')).toEqual({ allowed: false, unreadable: '' });
192
+ expect(await judge(ALICE, `${KB}/KnowledgeBase`, 'dir')).toEqual({ allowed: false, unreadable: 'KnowledgeBase' });
193
+ expect(await judge(ADMIN, KB, 'dir')).toEqual({ allowed: true, via: 'readable' });
194
+ expect(await judge(ADMIN, `${KB}/KnowledgeBase`, 'dir')).toEqual({ allowed: true, via: 'readable' });
195
+ });
196
+
197
+ it('an admin may add a loose file directly in the knowledge folder, with or without a root access.md', async () => {
198
+ // The reported case: an admin could make a folder under the knowledge
199
+ // root (the new-folder exception) and write inside it, but not a page
200
+ // directly in the root — the floor gave them write there and nothing
201
+ // gave them read. Write implies read, so the floor gives both.
202
+ expect(await judge(ADMIN, `${KB}/KnowledgeBase/notes.md`)).toEqual({ allowed: true, via: 'readable' });
192
203
  await write(repo, 'access.md', `---\nowner:\n - Admin\n---\nwrite:\n - Admin\n`);
193
204
  access.invalidate(WS);
194
- expect(await judge(ADMIN, KB, 'dir')).toEqual({ allowed: true, via: 'readable' });
205
+ expect(await judge(ADMIN, `${KB}/KnowledgeBase/notes.md`)).toEqual({ allowed: true, via: 'readable' });
206
+ });
207
+
208
+ it('the deployment owner is an admin for this gate too, whether or not roles.yaml names them', async () => {
209
+ const OWNER = 'owner@bevel.software';
210
+ const ws = stubWorkspaceService(path.join(root, WS));
211
+ const owned = new ChangeReadGate(
212
+ ws,
213
+ new AccessControlService(ws, KB, new NodeFs(), [OWNER]),
214
+ testKbContext({ kbDirName: KB }),
215
+ new NodeFs(),
216
+ );
217
+ await write(repo, 'KnowledgeBase/Board/access.md', '---\nread:\n - deny Admin\n---\n');
218
+ expect(await owned.judge(WS, OWNER, `${KB}/KnowledgeBase/notes.md`, 'file')).toEqual({ allowed: true, via: 'readable' });
219
+ // Excluded exactly where the Admin role is.
220
+ expect(await owned.judge(WS, OWNER, `${KB}/KnowledgeBase/Board/memo.md`, 'file')).toEqual({
221
+ allowed: false,
222
+ unreadable: 'KnowledgeBase/Board',
223
+ });
224
+ // Not an owner on the gate's own service: no floor, no read.
225
+ expect(await judge(OWNER, `${KB}/KnowledgeBase/notes.md`)).toEqual({ allowed: false, unreadable: 'KnowledgeBase' });
195
226
  });
196
227
 
197
228
  it('a roles.yaml that is there but unusable is not an open door', async () => {
@@ -149,8 +149,11 @@ export type AccessDecisionSource = {
149
149
  * `via` names the tier that matched and `principal` the display name of the
150
150
  * group, role or plugin principal it matched on (the caller's own email for
151
151
  * `person`, `everyone` for the built-in). `admin-floor` is Admin's write at
152
- * the repository root, which no rule there can take away; its source is the
153
- * root's rules, or null when the root has none.
152
+ * the repository root, and the read that write implies, which no rule there
153
+ * can take away; its source is the scope the floor decided at — the root's
154
+ * rules (`folder`), or the own `frontmatter` of a file directly in the root,
155
+ * whose entries are part of the root and cannot take the floor away either —
156
+ * or null when the walk found no scope at all.
154
157
  */
155
158
  export interface AccessDecision {
156
159
  allowed: boolean;