fix(kernel): scope self:agent:list to the caller's own row - #848
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses an information disclosure vulnerability where the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. The roster was open to all, An agent could see every wall. But now it is tight, Filtered by right, And safety is back in the hall. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request addresses an information disclosure vulnerability where any ordinary agent could enumerate all other principals' profiles via the AgentList request. The kernel router's agent_list handler has been updated to check if the caller holds the global agent:list capability; if not, the returned list is filtered to only include the caller's own profile. Additionally, the test fixture was updated to seed the default admin profile, and a new integration test was added to verify the filtering behavior for non-admin callers. There are no review comments to address, and the implementation is clean and well-tested.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
13c534f to
130ba79
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes an information disclosure in the kernel’s AgentList admin handler by enforcing server-side filtering consistent with the gateway contract: callers with only self:agent:list can no longer enumerate other principals’ profiles, while callers with global agent:list (effectively admin via *) still see the full roster.
Changes:
- Pass the authenticated
callerintoagent_listand filter results to the caller’s own row unless the caller also holds globalagent:list. - Update state-test setup to mirror production by seeding the
defaultprincipal with theadmingroup, and add a regression test asserting self-only listing for non-admin callers. - Add a
### Securitychangelog entry describing the disclosure and the new behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| crates/astrid-kernel/src/kernel_router/admin/handlers.rs | Adds a caller-aware agent_list implementation with a global-capability gate to prevent principal roster disclosure. |
| crates/astrid-kernel/src/kernel_router/admin/state_tests.rs | Seeds default as admin in the fixture and adds a regression test proving non-admin callers only see themselves. |
| CHANGELOG.md | Documents the security fix and the resulting behavioral change for self:agent:list callers. |
Comments suppressed due to low confidence (1)
crates/astrid-kernel/src/kernel_router/admin/handlers.rs:473
- For callers without global
agent:list,agent_liststill scansetc/profiles/and resolves every principal profile beforeretain(...)narrows the output to a single row. That makes the self-scoped path more expensive than necessary and allows low-privilege agents to trigger repeated full-profile directory scans even though they can only ever see themselves.
Consider short-circuiting when the caller lacks global agent:list: resolve the caller profile and return a single AgentSummary without iterating the directory.
fn agent_list(kernel: &Arc<crate::Kernel>, caller: &PrincipalId) -> AdminResponseBody {
// Source of truth: `etc/profiles/{principal}.toml`. Iterating the
// home directory was the pre-#672 approach but stopped working
// when profiles moved out — and was always wrong in spirit since
// a principal's home dir can outlive its policy file (e.g. after
// `agent.delete`, where home stays as an ops concern but the
// profile is removed).
let profiles_dir = kernel.astrid_home.profiles_dir();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
AgentList always resolves to AuthorityScope::Self_, so the required cap is self:agent:list — which the agent builtin holds via self:*. That lowering is deliberate (an agent resolves its own group-inherited caps), but agent_list returned EVERY principal's profile regardless of the caller: any ordinary agent could enumerate every other principal's id, groups, grants and revokes via GET /api/sys/principals. The gateway already documented 'the kernel filters server-side' — the kernel never implemented it. agent_list now filters to the caller's own row unless the caller also holds the global agent:list capability (agent:list / agent:* / *). self:* does not match agent:list, so self-scoped callers are narrowed; an unresolvable profile fails secure to self-only. GroupList is left a full read (system config, needed for cap-inheritance resolution). The state-test fixture now admin-seeds the default principal to mirror production (Kernel::new seeds it), so dispatch-through-default keeps admin authority. CLI is unaffected. Behavioural change for API callers holding only self:agent:list. Surfaced by the CLI/API sysadmin parity audit. Closes #847
130ba79 to
5583ea6
Compare
## Linked Issue Closes #853 ## Summary `chore: release` — bumps all workspace crates **0.7.0 → 0.8.0** and rolls `CHANGELOG [Unreleased]` into `[0.8.0] - 2026-06-10`. Consolidates the ~50 PRs landed since v0.7.0: - **HTTP admin gateway** (#756 + follow-ups) — `astrid-gateway`: principals/caps/quotas/groups/invites/env over HTTP, audit SSE + historical queries, agent-prompt SSE, OpenAPI emission, bus-direct admin path (285× throughput), rustls TLS, CORS, metrics, invite + keypair CLI verbs. - **Runtime concurrency overhaul** (#813/#816/#819/#820) — routed per-(capsule, topic, principal) IPC with DRR fairness, async Wasmtime, dynamic instance pools, split blocking/IO semaphores, per-principal fuel + memory ledgers with enforcement and usage reporting. - **Host process + introspection surface** — `astrid:process` persistent tier (#866/#867/#871/#873), capability introspection (#868/#869), `astrid mcp serve` (#879/#880), `astrid-emit` (#814/#842). - **Security** — macOS 15+ sandbox no longer silently disabled (#855/#857), audit-feed principal scoping (#850/#851), failed-redeem audit rows (#846), `self:agent:list` roster leak (#848), pair-device redeem rate-limit (#844), bearer revocation + wire format v2 (#772). Breaking: `Capsule.toml` `[publish]`/`[subscribe]`-only IPC surface (#858/#864/#865, `[[interceptor]]` / `ipc_*` arrays / `[[topic]]` removed), bearer v2, MSRV 1.95, `astrid-openclaw` removed (#833). ## Changes - `[workspace.package].version` `0.7.0` → `0.8.0` + the 23 in-workspace path-dependency pins (now including `astrid-emit`; `axum`/`axum-server` stay on their external 0.7). - CHANGELOG: `[Unreleased]` → `[0.8.0] - 2026-06-10` with a release **synopsis** (extracted into the GitHub release body by release.yml on tag). Keeps the earlier roll's curation (canonical section order, bullets verbatim) and merges the 13 entries added to `[Unreleased]` by the 12 PRs that landed after the branch was first cut (#851–#885) — verified both directions that no bullet was lost. Branch history note: rebuilt on current `main` (was `CONFLICTING` and 12 commits behind); previous tip was `54c7294`. ## Release steps (maintainer) 1. Merge this PR. 2. Tag `v0.8.0` on the merge commit → release.yml creates the GitHub release from the CHANGELOG block. 3. Publish/distribute the new `astrid` binaries as usual. ## Test Plan Release PR — no code changes. `cargo check --workspace` passes on the rebuilt branch; full test/clippy/MSRV matrix runs in CI. The changelog merge was verified bullet-by-bullet in both directions: every `[Unreleased]` bullet on `main` appears in the rolled `[0.8.0]` section, and every bullet from the earlier curated roll is preserved verbatim.
Linked Issue
Closes #847
Summary
AgentListalways resolves toAuthorityScope::Self_, so the required cap isself:agent:list(held by theagentbuiltin viaself:*). That cap-lowering is intentional, butagent_listreturned every principal's profile regardless of the caller — so any ordinaryagentcould enumerate every other principal's id / groups / grants / revokes viaGET /api/sys/principals. The gateway already documents "the kernel filters server-side"; the kernel never implemented it. Found by the CLI/API sysadmin parity audit.Changes
handlers.rs:agent_listnow takes the caller and filters to the caller's own row unless the caller holds the globalagent:listcapability. New helpercaller_has_global_agent_list(fail-secure: unresolvable profile → self-only).GroupListintentionally left a full read — system config, needed for cap-inheritance resolution, not per-principal data.fixture()now admin-seeds thedefaultprincipal to mirror production (Kernel::new'sseed_default_principal_admin_profile).agent_list_filters_to_self_for_non_admin_caller; the existingagent_list_returns_every_home_dir_principalstill passes.### Securityentry.Precision note (from adversarial review)
The full roster requires both halves: the
AgentListpreamble independently requiresself:agent:list, and a bare globalagent:listgrant does not satisfy it (the grammar does not make a global cap imply itsself:-scoped form —capability_matches("agent:list","self:agent:list")is false). So in practice only theadmingroup's*(which matches both) sees everyone. This is pre-existing and fail-closed; the surprising 'a bareagent:listgrant can't list' interaction is filed separately as a design question in #849 (not addressed here).Test Plan
cargo test -p astrid-kernel --lib— 136/136 pass (sandbox off; under the sandbox the only 2 failures aresocket::testsUnixListener::bindPermissionDenied).cargo test -p astrid-kernel admin::— 68/68.cargo clippy --workspace --all-features -- -D warnings(CI-exact) — clean for the lib changes.cargo fmt -p astrid-kernel --check— clean.This narrows what an API caller holding only
self:agent:listsees onGET /api/sys/principals(and the client-sideGET /api/sys/principals/:id): the full roster → just their own row. Fail-secure, matches the gateway's documented contract, CLI unaffected (defaultis admin-seeded). Flagged because it changes a response shape for self-scoped callers.