Skip to content

Record who a coworker was opened to, not only where it was pointed - #356

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:bot-audit-records-visibility
Sep 4, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:bot-audit-records-visibility

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

visibility is not a display preference.

// server/src/agents/profile-policy.ts
export function canAccessAgent(actor, agent) {
  return agent.visibility === "public" || agent.ownerUserId === actor.id || actor.role === "admin";
}
export const canRunAgent = canAccessAgent;

So public admits every signed-in person to a coworker, and being admitted to a coworker is being
allowed to act as it — with the connectors, the tools and the browser it was granted
(server/src/app.ts routes "may this person act as this Bot" through the same read). Since the
coworker dialogs landed in #317 it is one click, saved on pick
(app/src/components/agents/agent-dialog.tsx:299).

bot.created and bot.updated record the name, the endpoint and whether a key was set. They say
nothing about this. An edit that opened a coworker to the whole deployment was byte-identical on the
audit page to one that corrected its title, and the trail could not answer "who made this Bot
available to everybody, and when".

server/src/audit.ts:322-333 already gives the reason these event types exist, and it applies here
word for word:

The trail recorded every mouse movement a Bot made and could not answer "who pointed this Bot at
that host, and when", which is the first question asked in an incident.

Both rows now carry visibility.

Why on every row and not only on the row that moved it. The route has no before to compare
against — store.update is handed the new input and returns the result — so recording only a change
would mean an extra read purely to shape a log line. Recording the value on each row is also the more
useful trail: read forward, it says what was reachable at every point, which is the question an
incident actually asks. name is already carried this way, so the shape is not new.

Nothing else moves. The endpoint and the key are still recorded as what changed rather than as new
values, and the key itself is still never recorded.

Where it runs

  • New state that outlives a request? None. One extra field on a payload the route already
    writes.
  • What happens on the second replica? Identical. record() writes through the same
    recordAuditEvent into the same Postgres table from whichever replica served the edit.
  • Anything serialised? Nothing. No new read, no new write, no ordering requirement.
  • Anything fanned out to a browser? No. The route's response is unchanged.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: no acting path is touched.
  • New refusals and new failures each write a row: no new refusal. This makes an existing row able
    to reconstruct a decision it could not.
  • Nothing new is trusted from the client. parsed.value.visibility is the value the parser
    already validated to "public" | "private" before the store was allowed to write it, and it is
    the same value the store applied — not a separate claim from the body.
  • Nothing sensitive added. visibility is one of two enum words; it is not a secret and it is
    already returned to every browser in the agent DTO.

Proof

Three tests in server/tests/bot-lifecycle-audit.test.ts, which runs without a database. Fail-before
confirmed by removing the two visibility: lines from the payloads:

(fail) what a Bot is, on the trail > creating one records who may reach it
(fail) what a Bot is, on the trail > opening one to the whole deployment says so
(fail) what a Bot is, on the trail > an edit that leaves it private says that too

 16 pass
 3 fail

With the fix:

$ bun test server/tests/bot-lifecycle-audit.test.ts server/tests/agent-routes.test.ts
 68 pass
 0 fail
 123 expect() calls

Gates:

$ bun run format:check   Checked 515 files. No fixes applied.
$ bun run lint           Checked 518 files. No fixes applied.
$ bun run typecheck      app / server / worker: Exited with code 0

Changelog

  • The trail says who a coworker was opened to, under Unreleased.

`visibility` is not a display preference. `accessFilter` admits a public
coworker to every signed-in person, and `canRunAgent` is `canAccessAgent`,
so public hands everybody in the deployment the right to act as that Bot
and spend the connector grants, the tools and the browser it holds. Since
the coworker dialogs landed it is one click, written on pick.

bot.created and bot.updated recorded the name, the endpoint and whether a
key was set, and said nothing about this. So an edit that opened a coworker
to the whole deployment was byte-identical on the audit page to one that
corrected its title, and "who made this Bot available to everybody, and
when" had the same answer the endpoint question used to have: nothing.
audit.ts already gives the reason for the endpoint - "the first question
asked in an incident" - and it applies here word for word.

Carried on every row rather than only the row that moved it, the way `name`
already is. The route has no before to compare against, and recording the
value on each row is what lets somebody read the trail forward and say what
was reachable at any point, which is the question an incident actually
asks.

Nothing else changes: the payload is still what changed rather than the new
values for the endpoint and the key, and the key is still never recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed diff against current main; resolved CHANGELOG keep-both; validated locally (format + composed test run). CI green.

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.

2 participants