Skip to content

feat(api): read-only activity API for agents and automations - #477

Open
biztex wants to merge 1 commit into
onecli:mainfrom
biztex:feat/activity-api
Open

feat(api): read-only activity API for agents and automations#477
biztex wants to merge 1 commit into
onecli:mainfrom
biztex:feat/activity-api

Conversation

@biztex

@biztex biztex commented Aug 3, 2026

Copy link
Copy Markdown

I have read the CONTRIBUTING.md file.

YES

What kind of change does this PR introduce?

Feature — a read-only HTTP surface over data that already exists. Closes #411.

What is the current behavior?

Activity verification is dashboard-only. request-log-service is reachable from a Next.js server action (lib/actions/request-logs.ts), but there is no API route for it, so an agent cannot confirm its own calls actually flowed through OneCLI without driving a browser. As @fireharp put it, for an agent-first product the agent should be able to check "did my request land, with the expected agent/credential mapping?" itself.

What is the new behavior?

GET /v1/activity — keyset-paginated feed, and GET /v1/activity/:id — one event. Both project-scoped, both read-only.

On top of the existing all / hide-llm / blocked filter, the field-level narrowing an automation actually needs:

param match
agentId exact
host, provider case-insensitive substring (api.github.com or just github)
method exact, upper-cased
status exact
since / until inclusive lower / exclusive upper bound
limit (≤200), cursorCreatedAt + cursorId paging
curl -H "Authorization: Bearer $ONECLI_KEY" \
  "$ONECLI_URL/v1/activity?agentId=$AGENT&host=api.github.com&since=$T0&limit=10"

Two properties this had to get right

1. It's a second surface over one service, never a way around it. Both reads pass the caller as the viewer, so the admin-only org-rule redaction the Activity page applies is applied here identically — a non-admin cannot use the API to read org rule names the dashboard hides from them. The single-event read scopes the lookup in its where, so a foreign id is indistinguishable from a missing one and ids can't be probed. Both are pinned by tests, including a serialize-the-whole-payload check that no org rule identifier leaks.

2. Invalid input fails loudly. An unparseable timestamp, an inverted window, an out-of-range limit, an unknown filter, or a half-supplied cursor all 400. A silently dropped bound would answer "nothing since X" with everything — which reads as "OneCLI isn't logging", the exact confusion this endpoint exists to remove. A half-cursor would restart at page one and loop a paging automation forever.

One implementation note worth flagging for review: query conditions are ANDed rather than assigned onto the where root, because filter=blocked already owns status and the cursor owns OR — a root assignment would silently drop one of them (filter=blocked&status=200 would return every 200 instead of nothing). There's a regression test for exactly that.

Tests

  • routes/activity.test.ts (12): auth required; project scoping; viewer threading; all filters parsed through to the service; cursor forwarding; and each 400 path asserting the service was not called.
  • services/request-log-service.test.ts (+10): ActivityQuery → Prisma where (including the AND-vs-root regression and the cursor/filter coexistence), plus getRequestLogById for hit, unknown id, foreign project id, redaction parity with the feed, and fail-safe with no viewer.
  • Full packages/api suite: 835 passed. apps/web suite: 13 passed. turbo run lint check-types green for api/web/db/ui; prettier clean.

Deliberately out of scope

  • The MCP/skill surface the issue mentions as optional — that's a product-shaped decision (which tools, what naming) I'd rather have your direction on. This PR is the minimal read-only API the issue specifies; the MCP layer can wrap it without further backend changes.
  • extraData shaping. The payload is returned exactly as the dashboard receives it, so this endpoint exposes nothing new. Worth noting for the issue's "no sensitive credential values" requirement: that guarantee comes from what the gateway writes into extra_data, not from this route — if you'd like an explicit allow-list projection for the API, happy to add it in a follow-up.
  • Org-scoped activity (/v1/activity across a whole org) — the underlying service is project-scoped today; say the word if you want that axis too.

Activity verification was dashboard-only: the request-log service was
reachable from a Next.js server action but had no HTTP surface, so an
agent could not confirm its own calls actually flowed through OneCLI
without driving a browser.

Add GET /v1/activity (keyset-paginated feed) and GET /v1/activity/:id,
both project-scoped, plus the field-level narrowing an automation needs
to answer "did MY call land?": agentId, host, provider, method, status,
since/until, on top of the existing all/hide-llm/blocked filter.

This is a second surface over the SAME service, not a way around it.
Both reads pass the caller as the viewer, so the admin-only org-rule
redaction the Activity page applies is applied here too, and the
single-event read is scoped in its where clause - a foreign id is
indistinguishable from a missing one, so ids cannot be probed.

Invalid input fails loudly rather than silently widening a query: an
unparseable timestamp, an inverted window, an out-of-range limit, an
unknown filter, or a half-supplied cursor all 400. A silently dropped
bound would answer "nothing since X" with everything, which reads as
"OneCLI is not logging" - the exact confusion this endpoint exists to
remove.

Query conditions are ANDed rather than assigned onto the where root:
filter=blocked already owns `status` and the cursor owns `OR`, so a
root assignment would drop one of them.

Closes onecli#411
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose Activity data as an API and make it agent friendly (skill/MCP ready)

1 participant