@bevel-software/platform-core-backend 0.30.0 → 0.31.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 (156) hide show
  1. package/agent-guide/access-control.md +49 -4
  2. package/agent-guide/html-views.md +76 -2
  3. package/dist/core/catalog-revision.js +3 -3
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +2 -1
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts.map +1 -1
  8. package/dist/core/create-core-services.js +4 -2
  9. package/dist/core/create-core-services.js.map +1 -1
  10. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  11. package/dist/modules/access/access-control.service.js +1 -5
  12. package/dist/modules/access/access-control.service.js.map +1 -1
  13. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  14. package/dist/modules/audit/agent-audit.service.js +66 -6
  15. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  16. package/dist/modules/audit/audit.contract.d.ts +52 -8
  17. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  18. package/dist/modules/audit/audit.contract.js +37 -1
  19. package/dist/modules/audit/audit.contract.js.map +1 -1
  20. package/dist/modules/audit/audit.routes.d.ts +1 -1
  21. package/dist/modules/audit/audit.routes.d.ts.map +1 -1
  22. package/dist/modules/audit/audit.routes.js +15 -13
  23. package/dist/modules/audit/audit.routes.js.map +1 -1
  24. package/dist/modules/audit/request-audit.d.ts +21 -10
  25. package/dist/modules/audit/request-audit.d.ts.map +1 -1
  26. package/dist/modules/audit/request-audit.js +26 -10
  27. package/dist/modules/audit/request-audit.js.map +1 -1
  28. package/dist/modules/database/core-schema.d.ts +22 -3
  29. package/dist/modules/database/core-schema.d.ts.map +1 -1
  30. package/dist/modules/database/core-schema.js +15 -6
  31. package/dist/modules/database/core-schema.js.map +1 -1
  32. package/dist/modules/kb-fs/index.d.ts +1 -0
  33. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  34. package/dist/modules/kb-fs/index.js +1 -0
  35. package/dist/modules/kb-fs/index.js.map +1 -1
  36. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  37. package/dist/modules/kb-fs/locking-filesystem.js +3 -12
  38. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  39. package/dist/modules/kb-fs/mutex.d.ts +11 -0
  40. package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
  41. package/dist/modules/kb-fs/mutex.js +14 -0
  42. package/dist/modules/kb-fs/mutex.js.map +1 -1
  43. package/dist/modules/mcp/mcp-auth.middleware.d.ts +14 -0
  44. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  45. package/dist/modules/mcp/mcp-auth.middleware.js +4 -0
  46. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  47. package/dist/modules/mcp/mcp.routes.d.ts +5 -0
  48. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  49. package/dist/modules/mcp/mcp.routes.js +67 -0
  50. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  51. package/dist/modules/mcp/mcp.service.d.ts +107 -27
  52. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  53. package/dist/modules/mcp/mcp.service.js +257 -50
  54. package/dist/modules/mcp/mcp.service.js.map +1 -1
  55. package/dist/modules/mcp/request-context.d.ts +25 -0
  56. package/dist/modules/mcp/request-context.d.ts.map +1 -0
  57. package/dist/modules/mcp/request-context.js +11 -0
  58. package/dist/modules/mcp/request-context.js.map +1 -0
  59. package/dist/modules/mcp/shared-definitions.d.ts +56 -0
  60. package/dist/modules/mcp/shared-definitions.d.ts.map +1 -0
  61. package/dist/modules/mcp/shared-definitions.js +117 -0
  62. package/dist/modules/mcp/shared-definitions.js.map +1 -0
  63. package/dist/modules/mcp/surface-cache.d.ts +70 -0
  64. package/dist/modules/mcp/surface-cache.d.ts.map +1 -0
  65. package/dist/modules/mcp/surface-cache.js +104 -0
  66. package/dist/modules/mcp/surface-cache.js.map +1 -0
  67. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  68. package/dist/modules/tool-helpers/tool-context.js +33 -12
  69. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  70. package/dist/modules/tool-helpers/tool.contract.d.ts +8 -0
  71. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  72. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  73. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
  74. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +3 -14
  75. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
  76. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +18 -1
  77. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  78. package/dist/modules/workflow/agent-tools/workflow.tools.js +453 -2
  79. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  80. package/dist/modules/workflow/git/git.service.d.ts +113 -4
  81. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  82. package/dist/modules/workflow/git/git.service.js +470 -106
  83. package/dist/modules/workflow/git/git.service.js.map +1 -1
  84. package/dist/modules/workflow/read-on-branch.d.ts +35 -0
  85. package/dist/modules/workflow/read-on-branch.d.ts.map +1 -0
  86. package/dist/modules/workflow/read-on-branch.js +58 -0
  87. package/dist/modules/workflow/read-on-branch.js.map +1 -0
  88. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  89. package/dist/modules/workflow/workflow.routes.js +22 -41
  90. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  91. package/dist/modules/workflow/workflow.service.d.ts +48 -8
  92. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  93. package/dist/modules/workflow/workflow.service.js +163 -16
  94. package/dist/modules/workflow/workflow.service.js.map +1 -1
  95. package/dist/modules/workspace/index.d.ts +2 -0
  96. package/dist/modules/workspace/index.d.ts.map +1 -1
  97. package/dist/modules/workspace/index.js +4 -0
  98. package/dist/modules/workspace/index.js.map +1 -1
  99. package/dist/shared/git.contract.d.ts +7 -0
  100. package/dist/shared/git.contract.d.ts.map +1 -1
  101. package/dist/shared/git.contract.js +9 -0
  102. package/dist/shared/git.contract.js.map +1 -1
  103. package/dist/shared/no-link-below.d.ts +10 -0
  104. package/dist/shared/no-link-below.d.ts.map +1 -0
  105. package/dist/shared/no-link-below.js +30 -0
  106. package/dist/shared/no-link-below.js.map +1 -0
  107. package/migrations/0018_agent_events_page.sql +11 -0
  108. package/migrations/meta/0018_snapshot.json +2429 -0
  109. package/migrations/meta/_journal.json +8 -1
  110. package/package.json +3 -3
  111. package/src/core/catalog-revision.ts +3 -3
  112. package/src/core/create-core-server.ts +2 -1
  113. package/src/core/create-core-services.ts +4 -2
  114. package/src/modules/access/access-control.service.ts +1 -6
  115. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +22 -1
  116. package/src/modules/audit/__tests__/agent-audit.service.test.ts +78 -1
  117. package/src/modules/audit/__tests__/audit.routes.test.ts +43 -5
  118. package/src/modules/audit/agent-audit.service.ts +66 -6
  119. package/src/modules/audit/audit.contract.ts +62 -10
  120. package/src/modules/audit/audit.routes.ts +14 -12
  121. package/src/modules/audit/request-audit.ts +28 -13
  122. package/src/modules/database/core-schema.ts +15 -6
  123. package/src/modules/kb-fs/__tests__/mutex.canonical.test.ts +57 -0
  124. package/src/modules/kb-fs/index.ts +1 -0
  125. package/src/modules/kb-fs/locking-filesystem.ts +3 -12
  126. package/src/modules/kb-fs/mutex.ts +16 -0
  127. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +7 -1
  128. package/src/modules/mcp/__tests__/mcp.routes.page.test.ts +115 -0
  129. package/src/modules/mcp/__tests__/mcp.surface-cache.test.ts +521 -0
  130. package/src/modules/mcp/__tests__/shared-definitions.test.ts +28 -0
  131. package/src/modules/mcp/__tests__/surface-cache.test.ts +127 -0
  132. package/src/modules/mcp/mcp-auth.middleware.ts +21 -0
  133. package/src/modules/mcp/mcp.routes.ts +68 -0
  134. package/src/modules/mcp/mcp.service.ts +315 -51
  135. package/src/modules/mcp/request-context.ts +34 -0
  136. package/src/modules/mcp/shared-definitions.ts +121 -0
  137. package/src/modules/mcp/surface-cache.ts +154 -0
  138. package/src/modules/tool-helpers/tool-context.ts +37 -18
  139. package/src/modules/tool-helpers/tool.contract.ts +8 -0
  140. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +1 -1
  141. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +1 -1
  142. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +23 -12
  143. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +250 -4
  144. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +464 -4
  145. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +4 -22
  146. package/src/modules/workflow/agent-tools/workflow.tools.ts +462 -1
  147. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +76 -11
  148. package/src/modules/workflow/git/__tests__/git.service.mergeIntoBranch.test.ts +721 -0
  149. package/src/modules/workflow/git/git.service.ts +537 -106
  150. package/src/modules/workflow/read-on-branch.ts +79 -0
  151. package/src/modules/workflow/workflow.routes.ts +21 -39
  152. package/src/modules/workflow/workflow.service.ts +203 -16
  153. package/src/modules/workspace/index.ts +11 -0
  154. package/src/shared/__tests__/no-link-below.test.ts +45 -0
  155. package/src/shared/git.contract.ts +10 -0
  156. package/src/shared/no-link-below.ts +29 -0
@@ -12,6 +12,23 @@ import type { BevelOAuthProvider } from './oauth/bevel-oauth-provider.js';
12
12
  import '../tool-auth/external-api-key.interface.js'; // Express Request augmentation (req.externalApiKeyId)
13
13
  import '../audit/audit.contract.js'; // Express Request augmentation (req.agentConnectionId)
14
14
 
15
+ /** Which of the four credentials below an MCP request was authenticated with. */
16
+ export type McpCallerKind = 'key' | 'oauth' | 'internal' | 'session';
17
+
18
+ declare global {
19
+ // eslint-disable-next-line @typescript-eslint/no-namespace
20
+ namespace Express {
21
+ interface Request {
22
+ /**
23
+ * The kind of credential the MCP auth middleware admitted the request
24
+ * with. Only a `session` — the person signed in to the app — may name
25
+ * the page its call comes from (`X-Bevel-Page`).
26
+ */
27
+ mcpCallerKind?: McpCallerKind;
28
+ }
29
+ }
30
+ }
31
+
15
32
  /**
16
33
  * Auth middleware for the MCP endpoint. Accepts any of:
17
34
  *
@@ -94,6 +111,7 @@ export function createMcpAuthMiddleware(
94
111
  req.userId = resolved.user.id;
95
112
  req.userEmail = resolved.user.email;
96
113
  req.externalApiKeyId = resolved.tokenId;
114
+ req.mcpCallerKind = 'key';
97
115
  next();
98
116
  return;
99
117
  } catch (err) {
@@ -136,6 +154,7 @@ export function createMcpAuthMiddleware(
136
154
  // Outside the try so a synchronous throw from a downstream handler isn't
137
155
  // misclassified as a token-verification failure (and doesn't try to write
138
156
  // a second response).
157
+ req.mcpCallerKind = 'oauth';
139
158
  next();
140
159
  return;
141
160
  }
@@ -215,6 +234,7 @@ export function createMcpAuthMiddleware(
215
234
  oauthProvider.noteConnectionUse(claim.connectionId);
216
235
  req.agentConnectionId = claim.connectionId;
217
236
  }
237
+ req.mcpCallerKind = 'internal';
218
238
  next();
219
239
  return;
220
240
  }
@@ -236,6 +256,7 @@ export function createMcpAuthMiddleware(
236
256
  }
237
257
  req.userId = session.userId;
238
258
  req.userEmail = session.email;
259
+ req.mcpCallerKind = 'session';
239
260
  next();
240
261
  };
241
262
  }
@@ -16,6 +16,8 @@ import { MCP_LOOPBACK_TOKEN_TTL_MS, type McpService } from './mcp.service.js';
16
16
  import type { BevelOAuthProvider } from './oauth/bevel-oauth-provider.js';
17
17
  import type { ILlmUsageMeter } from '../tool-auth/llm-usage-meter.js';
18
18
  import '../tool-auth/external-api-key.interface.js'; // req.externalApiKeyId augmentation
19
+ import './mcp-auth.middleware.js'; // req.mcpCallerKind augmentation
20
+ import { isPagePath } from '../audit/index.js';
19
21
 
20
22
  /** Pull the raw bearer token off an already-authenticated request. */
21
23
  function extractBearer(req: Request): string {
@@ -43,6 +45,33 @@ function jsonRpcError(res: Response, status: number, code: number, message: stri
43
45
  res.status(status).json({ jsonrpc: '2.0', error: { code, message }, id: null });
44
46
  }
45
47
 
48
+ /** The header a page's calls carry: the page's knowledge-base path, URI-encoded. */
49
+ export const PAGE_HEADER = 'x-bevel-page';
50
+
51
+ /**
52
+ * The page a request names, decoded: null when it names none, an error
53
+ * message when it names one it may not.
54
+ *
55
+ * Only a session — the person signed in to the app, whose app is relaying a
56
+ * page's calls — may name one. A connection key, an OAuth token or an
57
+ * internal token belongs to an agent, which has an Audit log row of its own;
58
+ * letting it name a page would let it write its calls into a page's history.
59
+ */
60
+ function pageOf(req: Request): { page: string | null } | { error: string } {
61
+ const raw = req.headers[PAGE_HEADER];
62
+ if (raw === undefined) return { page: null };
63
+ if (req.mcpCallerKind !== 'session') {
64
+ return { error: 'X-Bevel-Page names the page a signed-in person\'s call comes from; it is not accepted with this credential.' };
65
+ }
66
+ let page: string;
67
+ try {
68
+ page = decodeURIComponent(Array.isArray(raw) ? (raw[0] ?? '') : raw);
69
+ } catch {
70
+ return { error: 'X-Bevel-Page is not a URI-encoded path.' };
71
+ }
72
+ return isPagePath(page) ? { page } : { error: 'X-Bevel-Page is not a knowledge-base path.' };
73
+ }
74
+
46
75
  /**
47
76
  * Routes for the remote MCP server + the connection-key management endpoints.
48
77
  *
@@ -57,6 +86,8 @@ function jsonRpcError(res: Response, status: number, code: number, message: stri
57
86
  *
58
87
  * POST /mcp/local-token — exchange an MCP OAuth access token
59
88
  * for a loopback internal token
89
+ * POST /mcp/page-refusals — record a page's call its viewer
90
+ * refused (session only)
60
91
  *
61
92
  * The `/mcp` POST accepts either a connection key or a JWT/OAuth token (see
62
93
  * McpAuthMiddleware). The `/mcp/external-api-keys/*` endpoints accept only the JWT —
@@ -100,6 +131,11 @@ export function createMcpRoutes(
100
131
  * closes — finished, or abandoned by the client mid-call.
101
132
  */
102
133
  router.post('/mcp', mcpAuthMiddleware, async (req, res) => {
134
+ const named = pageOf(req);
135
+ if ('error' in named) {
136
+ jsonRpcError(res, 400, TRANSPORT_ERROR, named.error);
137
+ return;
138
+ }
103
139
  let server: Server | undefined;
104
140
  res.on('close', () => {
105
141
  // Closing the server closes its transport too.
@@ -122,6 +158,9 @@ export function createMcpRoutes(
122
158
  // The proxy authenticates its loopback calls with the SAME bearer the
123
159
  // client used here, so it acts on the request exactly as the caller would.
124
160
  bearer: extractBearer(req),
161
+ // The page whose script made the call, when the app relays one —
162
+ // its calls are audited under the page (see pageOf).
163
+ ...(named.page ? { page: named.page } : {}),
125
164
  },
126
165
  req.body,
127
166
  );
@@ -165,6 +204,35 @@ export function createMcpRoutes(
165
204
  router.get('/mcp', methodNotAllowed);
166
205
  router.delete('/mcp', methodNotAllowed);
167
206
 
207
+ /**
208
+ * A call a page asked to make and its viewer refused. It never ran, so it
209
+ * never reached `/mcp`; the app reports it so the Audit log lists it
210
+ * under the page, as refused, beside the page's allowed calls.
211
+ *
212
+ * Body: `{ page, tool, args? }` — the page's knowledge-base path and the
213
+ * tool's name as the page called it. Session only, like the page header.
214
+ */
215
+ router.post('/mcp/page-refusals', mcpAuthMiddleware, async (req, res) => {
216
+ if (req.mcpCallerKind !== 'session') {
217
+ res.status(403).json({ error: 'Only the app records a page\'s refused calls.' });
218
+ return;
219
+ }
220
+ const body = (req.body ?? {}) as { page?: unknown; tool?: unknown; args?: unknown };
221
+ if (typeof body.page !== 'string' || !isPagePath(body.page) || typeof body.tool !== 'string' || !body.tool) {
222
+ res.status(400).json({ error: 'page (a knowledge-base path) and tool are required' });
223
+ return;
224
+ }
225
+ const args =
226
+ body.args && typeof body.args === 'object' && !Array.isArray(body.args) ? (body.args as Record<string, unknown>) : {};
227
+ try {
228
+ await mcpService.recordPageRefusal({ userId: req.userId!, bearer: extractBearer(req) }, body.page, body.tool, args);
229
+ res.status(204).end();
230
+ } catch (err) {
231
+ log.error('recording a page refusal failed:', { err });
232
+ res.status(500).json({ error: 'Failed to record the refusal' });
233
+ }
234
+ });
235
+
168
236
  // ── Connection-key management (JWT-only) ───────────────────────────────
169
237
 
170
238
  router.get('/mcp/external-api-keys', jwtAuthMiddleware, async (req, res) => {