feat(agent-bff): audit reads and action executions - #1885
Conversation
The BFF wrote no activity log at all, so a user fetching data or triggering an action through it left no audit trail. Wrap list, relation list and action execute with the mcp-server pattern: a pending log awaited before the operation, a fire-and-forget status transition after it, blocking a write whose log cannot be created and proceeding on a read. A lazy resolver lands the Forest server bearer for both auth modes in one place, and the drain reachable through stop() keeps a status transition from dying with the process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 new issues
|
| store: session.store, | ||
| serverClient: session.serverClient, | ||
| }); | ||
| } catch { |
There was a problem hiding this comment.
🟠 High auth/forest-server-token-middleware.ts:45
When ensureFreshServerAccess fails with an OAuthRequestError (status 502) because the Forest server is unreachable, this catch-all converts it to sessionExpired (401). Valid OAuth users therefore receive a session-expired response and action executions are blocked until re-authentication instead of returning a retryable upstream error. Preserve and rethrow the OAuth request failure, mapping only genuine session-expiration errors to sessionExpired.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/auth/forest-server-token-middleware.ts around line 45:
When `ensureFreshServerAccess` fails with an `OAuthRequestError` (status 502) because the Forest server is unreachable, this catch-all converts it to `sessionExpired` (401). Valid OAuth users therefore receive a session-expired response and action executions are blocked until re-authentication instead of returning a retryable upstream error. Preserve and rethrow the OAuth request failure, mapping only genuine session-expiration errors to `sessionExpired`.
| } catch (error) { | ||
| // The document may not exist yet when the transition lands, and only then is a retry worth | ||
| // anything: a network failure loses the transition permanently. | ||
| if (error instanceof NotFoundError && attempt < MAX_STATUS_ATTEMPTS) { |
There was a problem hiding this comment.
🟡 Medium activity-log/activity-logs-creator.ts:183
When updateActivityLogStatus encounters a transient transport or 5xx failure, the activity log remains permanently pending even though the audited operation has finished, corrupting audit status and action-failure statistics. updateStatus retries only NotFoundError, so these recoverable failures reach the fire-and-forget catcher immediately; add bounded retries for transient status-update failures while preserving non-retryable errors.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/activity-log/activity-logs-creator.ts around line 183:
When `updateActivityLogStatus` encounters a transient transport or 5xx failure, the activity log remains permanently `pending` even though the audited operation has finished, corrupting audit status and action-failure statistics. `updateStatus` retries only `NotFoundError`, so these recoverable failures reach the fire-and-forget catcher immediately; add bounded retries for transient status-update failures while preserving non-retryable errors.
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (15) 🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|

agent-bffwrote no activity log at all — zero occurrences ofactivity/ActivityLog/activityLogsServicein its source. A user who fetched data or triggered an action through the BFF left no audit trail, and the agent it proxies to writes none either (routes/access/audit-trail.tsonly reads the trail). Onlymcp-serverandworkflow-executorwere writing logs.Fixes PRD-1150
Depends on ForestAdmin/forestadmin-server#8483, which supplies the credential and the
BFFsource value. Implemented against that contract and mocked in tests, so this branch is reviewable now but must not ship before it.Audited surface
Strict parity with mcp-server: three routes.
actionlistsearchif the body carries one, elsefilter, elseindexrelations/:rel/listlistRelatedData, parent id as the record, label naming the relation and its refinementsactions/:name/executeaction, record ids, label naming the actioncount,relations/:rel/countandactions/:name/formwrite nothing, deliberately. mcp-server has no standalone count tool — count is folded inside the list tool's single log — and does not auditget-action-formeither. The accepted consequence is that a filteredcountcalled on its own stays unaudited, which is an information oracle the MCP surface does not expose; auditing it would double the audit volume, since a table page-load fireslistandcountas two separate requests and every pagination click replays both.How it is wired
New
src/activity-log/: the service (its own instance, becauseForestAdminClientOptionshas noheadersfield and the source must travel as one), the creator holding the action→type map and the fail policy, the wrapper, the drainer, and a composition root so the route middlewares take a writer and never see the service or the token plumbing.src/auth/forest-server-token-middleware.tslands a lazyctx.state.resolveForestServerToken, memoised per request. API-key mode returns the token that came with the resolve response; OAuth mode callsensureFreshServerAccess. Lazy because/health, permissions, context, the OpenAPI document and the docs route audit nothing and must not pay a session lookup — and permissions is hit on every page load.Fail policy, verbatim from mcp-server: a write whose pending log cannot be created is blocked (503
audit_unavailable, new, mirroringpermissions_unavailable); a read proceeds with a warning; an authorization refusal propagates even for a read (403audit_not_authorized, new). Both statuses were already declared on these routes andRetry-Afterwas already wired for 503, so only the OpenAPI descriptions needed extending.Points worth a reviewer's attention
Approval needed an explicit special case. mcp-server treats
approvalRequestedas a success, so its entry endscompleted. The BFF throwsactionRequiresApproval, which verbatim wrapping would record asfailed— the same business event recorded differently depending on the channel, making action-failure statistics unusable. The log is markedcompletedbefore the rethrow.activityLogsis a required option on both route middlewares rather than reached fromctx.state, so a wiring mistake cannot silently disable auditing. That is why the existing construction sites in the test suite now pass an explicit passthrough. The token resolver is what stays off their dependency lists.Execute wraps the whole sequence,
loadActionandsetFieldsincluded, matchingexecute-action.ts. So an unknown action, or an agent down at form load, produces afailedentry for an attempt that never touched data. That is the intent — capture the attempt.installShutdownHandlersreplaces the previously installed pair instead of adding one.runCliis called many times in a single test file and would otherwise accumulate signal listeners; one process runs one BFF, so replacement is also the right production semantics.An API-key write with no token answers 503, not 401. There is no session to have expired — the resolve response simply predates the server change.
Known follow-ups, not addressed here
Metricsport hasincrementandgaugebut no duration.cli-core.tsintobuild-bff.tsand is already in review. Land it first and rebase this.Verification
tsc --noEmitclean;yarn workspace @forestadmin/agent-bff test→ 91 suites, 1698 tests passing; targeted eslint clean.🤖 Generated with Claude Code
Note
Add activity-log auditing for
agent-bffreads and action executionssearch,filter,index, orlistRelatedDataactions.createForestServerTokenMiddlewareto resolve Forest server tokens for API-key and OAuth requests, providing credentials for audit writes.BFFHttpServer.stopandinstallShutdownHandlersnow await pending activity-log status updates viaActivityLogDrainerduring SIGTERM/SIGINT.auditNotAuthorizedor 503auditUnavailableerror. Read requests proceed without a log when the audit store is unavailable.📊 Macroscope summarized 8493042. 16 files reviewed, 2 issues evaluated, 0 issues filtered, 2 comments posted
🗂️ Filtered Issues