feat(trust): multi-agent workspaces — provenance chips, per-agent grant scoping, rate budgets (#59)#74
Conversation
…nt scoping, rate budgets (#59)
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 5 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (34)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
evaOS review status: closed or merged before reviewPR: #74 - feat(trust): multi-agent workspaces — provenance chips, per-agent grant scoping, rate budgets (#59) evaOS review stopped because the PR closed or merged before this queued head could be reviewed. Automation note: agents should wait for this comment to reach PR URL: #74 Details: state=closed |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98f4f228e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // this verb is the single source of truth for the operator-only fields. | ||
| autoConfirm: autoConfirmSubset, | ||
| expiresAt, | ||
| agents: agentsScope, |
There was a problem hiding this comment.
Preserve agent scopes on approval saves
When an operator uses the approvals widget to save changes on an already scoped grant, the UI displays item.agents for granted rows but only forwards tools, autoConfirm, and expiresAt through approveCapability; no agents option is ever sent. Because this assignment treats an omitted agents param as undefined, any such save silently clears the scope and widens the grant to all agents, defeating the new per-agent gating.
Useful? React with 👍 / 👎.
| grant.agents !== undefined && | ||
| (boundActor === undefined || !grant.agents.includes(boundActor)) | ||
| ) { | ||
| continue; |
There was a problem hiding this comment.
Allow scoped-out agents to request existing tools
When a connector grant already contains a tool but is scoped to another agent, this new filter hides it from the current agent, yet boardstate_tool_search.request still treats requested ids that are already in existing.tools as a no-op and returns before re-pending anything. In practice, if officecli:read_mail is granted only to agent:alice, agent:bob cannot get an approval card by requesting that same tool, so scoped grants can permanently block later agents from requesting access unless an operator edits the grant out-of-band.
Useful? React with 👍 / 👎.
| admit(rateWindows, connector, rateMax, `too many invocations for connector "${connector}"`); | ||
| if (perAgentRateMax !== undefined && actingAgent) { | ||
| admit( |
There was a problem hiding this comment.
Avoid charging connector limits for per-agent denials
With perAgentInvokeRateMax configured below the connector limit, this admits the connector window before checking the agent window. Once Alice has exhausted her per-agent budget, each refused Alice call still appends to rateWindows, so enough denied attempts can make Bob hit rate_limited at the connector level even though only Alice exceeded her budget. That breaks the advertised independent per-agent ceiling and lets one over-limit agent starve others.
Useful? React with 👍 / 👎.
Adversarial verify (narrow refute): sanitizeImportedWorkspace stripped grantedBy/autoConfirm/expiresAt but not agents — the one re-pend site missed when the others gained the scope strip. Currently inert (gateCall checks status before scope; approve overwrites), but a foreign doc's scope claims are as untrustworthy as a granted status: strip at the same seam. The exact toEqual in the import test makes the fixture's new agents field delta-sensitive.
Adversarial verify — verdict
Merging. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65358c1bd9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { connector, tool, args } = readInvokeParams(ctx.params); | ||
| checkRate(connector); | ||
| const { id, readOnly } = await gateCall(connector, tool); | ||
| const actingAgent = boundAgentActor(ctx); |
There was a problem hiding this comment.
Thread an agent identity into scoped MCP binding reads
When a scoped grant backs a live source:"mcp" widget, the normal binding path calls transport.request("dashboard.connector.read", params) without a request context (see packages/host/src/store.ts), so boundAgentActor(ctx) is always undefined here. That makes gateCall(..., actingAgent) reject every scoped read binding as capability_pending, even when the widget was authored by an allowed agent; only unscoped grants can render live data. The binding resolver needs to pass an authenticated/widget-author agent identity, or scoped grants should not be enforced on board-owned reads.
Useful? React with 👍 / 👎.
| !sameStringSet(grant.autoConfirm ?? [], currentGrant.autoConfirm ?? []) || | ||
| (grant.expiresAt ?? "") !== (currentGrant.expiresAt ?? "")); | ||
| (grant.expiresAt ?? "") !== (currentGrant.expiresAt ?? "") || | ||
| !sameStringSet(grant.agents ?? [], currentGrant.agents ?? [])); |
There was a problem hiding this comment.
Strip agent scopes from untrusted requested grants
This scope comparison only runs when the incoming grant is still status:"granted", so an untrusted dashboard.workspace.replace can write agents onto a requested or revoked capability and have it persist. That operator-only scope is then rendered in the approvals row as if it were operator-authored, violating the new single-writer invariant for agents; strip agents from non-granted/re-pended grants on the replace path as well.
Useful? React with 👍 / 👎.
| const invokeCtx = options.agentId !== undefined ? { agentId: options.agentId } : undefined; | ||
| const adapter = createBrokerAgentTools({ | ||
| broker: options.broker, | ||
| store: options.store, | ||
| invokeMutation: (input) => | ||
| host.request("dashboard.action.invoke", input) as Promise<InvokeMutationResult>, | ||
| host.request("dashboard.action.invoke", input, invokeCtx) as Promise<InvokeMutationResult>, |
There was a problem hiding this comment.
Bind broker tools to the calling agent
When one host serves multiple agent sessions, this install-time invokeCtx makes every session use the same scoped broker-tool identity. Since host.registerTool() and createAgentChatAgent({ host }) expose a global host.tools() set, passing agentId: "alice" here means Bob receives tools filtered for Alice, read-only broker tools can run under Alice's scoped grant, and mutations are parked with Alice's identity. The adapter needs a per-call/per-session agent context instead of a single captured identity.
Useful? React with 👍 / 👎.
| // This is the enforcement point for the direct `readOnly` execute path below, which | ||
| // does not go through the server-side invoke gate. |
There was a problem hiding this comment.
Rate-limit direct read-only broker tools
For the normal agent-tool path, granted read-only broker tools execute directly through broker.callTool() and never call dashboard.action.invoke or dashboard.connector.read; this added block even notes that assembly is their only gate. As a result, when perAgentInvokeRateMax is configured, an agent can exceed its per-agent budget simply by repeatedly calling an advertised read-only external tool, while the limit only applies to the RPC paths. Apply the same rate check to direct read-only agent tools or route them through the engine.
Useful? React with 👍 / 👎.
| const normalized = raw | ||
| .replace(/[^A-Za-z0-9._-]+/g, "-") | ||
| .replace(/^-+|-+$/g, "") | ||
| .slice(0, 64); | ||
| return normalized ? (`agent:${normalized}` as DashboardActor) : undefined; |
There was a problem hiding this comment.
Preserve already-normalized agent actors
If a caller threads RequestContext.agentId as the full actor string already used in grants, e.g. agent:assistant (several existing demos use that convention for host request context), this normalization turns the colon into a hyphen and returns agent:agent-assistant. A grant scoped to agent:assistant will then reject that same agent as out of scope, so boundAgentActor should accept/strip an existing agent: prefix before sanitizing.
Useful? React with 👍 / 👎.
| ...(toolsHash !== undefined ? { toolsHash } : {}), | ||
| ...(autoConfirm !== undefined ? { autoConfirm } : {}), | ||
| ...(expiresAt !== undefined ? { expiresAt } : {}), | ||
| ...(agents !== undefined ? { agents } : {}), |
There was a problem hiding this comment.
Reject scopes on un-enforced data grants
This now persists agents on any capability grant, including connector grants that only have methods or streams. Those data connectors are still served by installConnector, whose gate only checks status === "granted" before serving reads or broadcasting streams, so an operator can approve a grant that appears scoped to agent:alice while every client continues to receive the data once it is granted. Either reject agents on method/stream grants here or enforce the same agent gate in the data connector path.
Useful? React with 👍 / 👎.
Closes #59