Skip to content

fix(desktop): scope managed-agent roster per community (#7184) - #7204

Open
Kingvelazquez013 wants to merge 1 commit into
block:mainfrom
Kingvelazquez013:fix/tenant-isolation-7184
Open

Kingvelazquez013 wants to merge 1 commit into
block:mainfrom
Kingvelazquez013:fix/tenant-isolation-7184

Conversation

@Kingvelazquez013

Copy link
Copy Markdown

Security: Managed agents created in one community appear in all communities

Closes #7184

Problem

Managed agents are stored in a single agents/managed-agents.json under the app-data dir, read and written identically for every configured community. Creating an agent in Community 1 therefore makes it appear — with name, system prompt, env vars, and full configuration — in Communities 2 and 3. The agent list is also re-polled every 5 seconds from this one file, so the leak is continuous, not transient.

Why the fix goes here (and not at spawn)

The per-record relay_url pin is deliberately ignored at spawn (effective_agent_relay_url, #2122 "agents-everywhere") — every agent is eligible on every community, and the pair is keyed by the workspace relay. This PR does not reopen that decision. Spawn eligibility is untouched; what changes is roster visibility and storage scope, which is what #7184 reports.

What this PR does

1. Community shards in storage (managed_agents/storage.rs)

Keyed instances are partitioned by the host of their relay_url:

  • managed-agents-community.bookd.communities.buzz.xyz.json
  • managed-agents-community.av0.communities.buzz.xyz.json
  • …one shard per relay host

The legacy managed-agents.json continues to hold key-less definitions and unpinned records. Consequences:

  • No data migration required — existing stores parse as-is (they are the legacy file), and the first save partitions records forward shard-by-shard.
  • Rollback-safe — a pre-fix build reads the legacy file; definitions and unpinned records are still there. Records already moved to shards simply disappear from an old build's roster the same way they do from other communities today — no corruption, no invalid parse.
  • Host extraction (relay_host_of) is strict: scheme required, [a-z0-9.-] only, no empty/dotted labels. Unparseable relays fail open to the legacy store — a record is never dropped by a save.
  • Shard filenames are enumerated, never guessed, and the prefix managed-agents-community. is deliberately distinct from managed-agents.json so hand-made backup copies (.bak, Copilot-style per-community copies) are never mistaken for shards.

2. Community-scoped roster read (load_managed_agents_for_active_community)

list_managed_agents (the 5s-polled agents menu) now loads via a scoped reader that filters instances to the active workspace relay host (relay_ws_url_with_override — workspace override first, then env/build vars, then default, exactly matching how the rest of the app resolves the active community).

Fail-open rules, so the fix can never look like data loss:

  • Unresolvable active host → return everything (pre-fix behavior).
  • Record with empty/unparseable relay_url → visible everywhere (it cannot be attributed to a community).

3. Mint-time scoping (create_managed_agent)

An agent created with no explicit relay is stamped with the active workspace relay at creation. New agents are therefore born scoped to the community where they were created — the exact bug in #7184. Explicit relays still pin as before, and spawn still ignores the pin (#2122).

4. Boot reconcile covers shards (managed_agents/reconcile.rs)

The boot-time kind:30177 reconcile enumerates the legacy store plus every shard, so an agent's published identity head stays reconciled regardless of which community was active when its shard was last written. Per-file fail-loud parse contract is preserved (malformed shard → .invalid backup + loud error).

Testing

  • New unit tests: relay-host parsing (including wss://, ports, paths, traversal/unsafe-host rejection), partition routing (pinned → shard, unpinned → legacy), shard enumeration (ignores backups/hand copies/.invalid), shard filename shape.
  • Full desktop lib suite: 3059 passed, 0 failed; cargo clippy and cargo fmt --check clean.

Deliberately out of scope

@Kingvelazquez013
Kingvelazquez013 requested a review from a team as a code owner September 1, 2026 19:27
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is c045321a7fb3ca8939f28519ce7a555a6f597728...a4e1bf59b31f7eb8657b285c1d3d58bdafa50c53.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review a4e1bf59b31f7eb8657b285c1d3d58bdafa50c53 to authorize a new review.
Any previous review applies only to its recorded range.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd21a5bc8b

ℹ️ 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".

Comment on lines +599 to +603
for record in all {
if record.pubkey.is_empty() {
definitions_out.push(record);
} else {
legacy_out.push(record);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Repartition keyed records before writing the legacy store

Every scoped instance is first copied from shards into all, but this loop then puts every keyed record into legacy_out regardless of its relay. The same record is subsequently written to its shard, and load_agent_store concatenates both files, so creating the first scoped agent immediately returns duplicate roster entries; later saves persist and multiply those duplicates. Repartition the post-keyring records by relay instead of using pubkey alone.

Useful? React with 👍 / 👎.

// community that no longer has any records is rewritten as `[]` rather
// than deleted — deletion would race a concurrent reader and `[]` keeps
// the fail-loud parse contract uniform (a missing file is also valid).
for (host, mut shard_records) in shards {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear shards that become empty

When the last agent in a community is deleted, partition_by_community produces no entry for that host, so this loop never rewrites the existing shard as [] despite the preceding comment. Because future loads enumerate every old shard, the deleted record reappears after delete_managed_agent reports success, now after its key was deleted and its relay head tombstoned. Enumerate existing shards and explicitly empty or remove those absent from the new partition.

Useful? React with 👍 / 👎.

Comment on lines +41 to +44
let host = host_port
.rsplit_once(':')
.map(|(host, _)| host)
.unwrap_or(host_port);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Key shards by the complete relay identity

Discarding the scheme, port, path, and query collapses distinct valid community relays onto one key. For example, the UI permits both ws://localhost:3000 and ws://localhost:3030, while this function maps both to localhost; the active-community filter therefore exposes each community's agents in the other, leaving the reported isolation bug unfixed for these configurations. Use the repository's canonical full relay URL identity and encode or hash it for filenames.

AGENTS.md reference: AGENTS.md:L612-L617

Useful? React with 👍 / 👎.

// keyring-backed case (it is the user's own agent store) and closes the
// umask window a post-write `chmod` would leave open.
atomic_write_json_restricted(&path, &payload)
atomic_write_json_restricted(&legacy_path, &legacy_payload)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Commit the sharded store as one recoverable snapshot

This legacy-file commit is followed by independent commits for each shard, with no transaction marker or durable retry plan. If a later shard write fails—for example from disk exhaustion or a per-file permission problem—the command returns an error after earlier files already contain the update; a failed create can consequently persist an agent without running the remaining publication/setup steps, and retrying creates another identity. Persist one atomic snapshot or journal the remaining shard commits so every observable prefix is recoverable.

AGENTS.md reference: AGENTS.md:L204-L209

Useful? React with 👍 / 👎.

@Kingvelazquez013
Kingvelazquez013 force-pushed the fix/tenant-isolation-7184 branch 2 times, most recently from a77c5b7 to 046ce14 Compare September 7, 2026 17:24
@Kingvelazquez013

Copy link
Copy Markdown
Author

@wpfleger96 @jmecom @loganj — this PR has been rebased onto current main (046ce14) and the conflicts are resolved; cargo check passes clean. Could someone authorize the security review so this can move? Comment exactly:

@buzz-security-review 046ce14dd2066ae6946e16c1b6bde98dea18be97

Background: #7184 is a tenant-isolation breach — managed agents created in one community appear in all communities. This fix scopes the roster per community via per-relay shards (managed-agents-community.<host>.json) with fail-open reads so no existing store breaks. 404 insertions, 52 deletions. Ready for review.

Managed agents were stored in a single managed-agents.json shared by
every community, so an agent created in one community appeared in all
of them with full configuration — a tenant-isolation breach.

- Storage: split the keyed-instance store into per-community shards
  (managed-agents-community.<relay-host>.json) next to the legacy
  global store. Key-less definitions and unpinned records stay in the
  legacy file, so existing stores need no migration and a rollback
  build reads the same file.
- Load: load_managed_agents_for_active_community() filters instances
  to the active workspace relay host (fail-open: unresolvable host or
  unpinned record = visible, matching pre-fix behavior; never an
  empty-roster data-loss appearance).
- Create: an empty request relay is stamped with the active workspace
  relay at mint time, so new agents are born scoped to the community
  they were created in. Spawn eligibility is unchanged (block#2122
  agents-everywhere): the pin scopes roster visibility only.
- Reconcile: boot reconcile enumerates legacy store + all shards so
  kind:30177 heads for every community stay published.

Closes block#7184

Signed-off-by: Kingvelazquez013 <258349814+Kingvelazquez013@users.noreply.github.com>
@Kingvelazquez013
Kingvelazquez013 force-pushed the fix/tenant-isolation-7184 branch from 046ce14 to a4e1bf5 Compare September 9, 2026 11:48
@Kingvelazquez013

Copy link
Copy Markdown
Author

@wpfleger96 — this PR is now rebased onto current main and the updated head is a4e1bf59b31f7eb8657b285c1d3d58bdafa50c53.

Focused verification passed:

  • cargo check --manifest-path desktop/src-tauri/Cargo.toml
  • git diff --check origin/main...HEAD

Could you please authorize the security review for this exact head by commenting exactly:

@buzz-security-review a4e1bf59b31f7eb8657b285c1d3d58bdafa50c53

The tenant-isolation fix is ready for review. Thank you.

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.

Security: Managed agents created in one community appear in all communities (tenant isolation breach)

1 participant