feat(supervise+orchestrator): slice 11 — per-bottle supervise queue + DLP safelist #367

Closed
didericis-claude wants to merge 1 commits from orchestrator-supervise-multitenant into orchestrator-gitgate-multitenant
Collaborator

Slice 11 of the docker consolidation program: make the supervise path multi-tenant in the shared gateway.

The gap this closes (a real cross-tenant leak)

Consolidated egress (slice 8) already served every bottle from one process, but supervise stayed single-tenant in two ways that are wrong under sharing:

  1. Proposal attribution — the token-allow proposal was keyed off a single SUPERVISE_BOTTLE_SLUG env, so every bottle's proposal landed under the same slug.
  2. DLP safelist leakself.safe_tokens was one global set. An operator approving bottle A's flagged token would add it to the set that bottle B's DLP scan consults — i.e. A's approved secret silently passes B's egress. A per-bottle-impossible bug that the shared process introduced.

Both are now keyed per bottle, resolved by source IP (same attribution invariant as egress/git-gate).

Changes

  • policy_resolver: resolve_policy_and_bottle_id — policy and bottle id from a single /resolve, so egress keys routing + the supervise queue/safelist in one round-trip. Fail-closed (403 → (None, None)).
  • egress_addon_core: resolve_client_context (+ ContextResolverLike) returns (Config, bottle_id), sharing the fail-closed parse with resolve_client_config via _config_from_policy.
  • egress_addon: _active_config → _resolve_flow returns (Config, slug); the global safe_tokens set → per-bottle _safe_tokens_for(slug); the token-allow write/await/archive + the approved-token add all use the resolved slug. Single-tenant unchanged (no resolver → slug = env SUPERVISE_BOTTLE_SLUG).

Tests

New coverage for the resolver, the fail-closed (Config, slug) matrix, and the cross-tenant isolation: an approval lands only in the calling bottle's safelist; the proposal is keyed by the source-IP-attributed bottle; unattributed IPs can't supervise.

Out of scope (follow-up slices)

  • The git-gate gitleaks-allow hook + supervise_server agent-proposal paths (also key off SUPERVISE_BOTTLE_SLUG).
  • WebSocket DLP still reads self.config only (inert in consolidated mode) — a separate slice, noted in-code.

Verification

pyright 0 errors, pylint 9.83/10, unit suite green (1700 tests; the 13 test_sidecar_init /bin/sleep errors are pre-existing NixOS-local noise).

Stacked on #365.

🤖 Generated with Claude Code

Slice 11 of the docker consolidation program: make the **supervise** path multi-tenant in the shared gateway. ### The gap this closes (a real cross-tenant leak) Consolidated egress (slice 8) already served every bottle from one process, but supervise stayed single-tenant in two ways that are wrong under sharing: 1. **Proposal attribution** — the token-allow proposal was keyed off a single `SUPERVISE_BOTTLE_SLUG` env, so every bottle's proposal landed under the same slug. 2. **DLP safelist leak** — `self.safe_tokens` was one *global* set. An operator approving bottle A's flagged token would add it to the set that **bottle B's** DLP scan consults — i.e. A's approved secret silently passes B's egress. A per-bottle-impossible bug that the shared process introduced. Both are now keyed per bottle, resolved by **source IP** (same attribution invariant as egress/git-gate). ### Changes - **`policy_resolver`**: `resolve_policy_and_bottle_id` — policy **and** bottle id from a single `/resolve`, so egress keys routing + the supervise queue/safelist in one round-trip. Fail-closed (`403 → (None, None)`). - **`egress_addon_core`**: `resolve_client_context` (+ `ContextResolverLike`) returns `(Config, bottle_id)`, sharing the fail-closed parse with `resolve_client_config` via `_config_from_policy`. - **`egress_addon`**: `_active_config → _resolve_flow` returns `(Config, slug)`; the global `safe_tokens` set → per-bottle `_safe_tokens_for(slug)`; the token-allow write/await/archive + the approved-token add all use the resolved slug. **Single-tenant unchanged** (no resolver → slug = env `SUPERVISE_BOTTLE_SLUG`). ### Tests New coverage for the resolver, the fail-closed `(Config, slug)` matrix, and the **cross-tenant isolation**: an approval lands only in the calling bottle's safelist; the proposal is keyed by the source-IP-attributed bottle; unattributed IPs can't supervise. ### Out of scope (follow-up slices) - The git-gate `gitleaks-allow` hook + `supervise_server` agent-proposal paths (also key off `SUPERVISE_BOTTLE_SLUG`). - WebSocket DLP still reads `self.config` only (inert in consolidated mode) — a separate slice, noted in-code. ### Verification pyright **0 errors**, pylint **9.83/10**, unit suite green (1700 tests; the 13 `test_sidecar_init` `/bin/sleep` errors are pre-existing NixOS-local noise). Stacked on #365. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
didericis-claude added 1 commit 2026-07-13 19:48:50 -04:00
feat(supervise+orchestrator): slice 11 — per-bottle supervise queue + DLP safelist
lint / lint (push) Successful in 2m4s
test / unit (pull_request) Successful in 1m6s
test / integration (pull_request) Successful in 23s
test / coverage (pull_request) Successful in 1m15s
8827c64a83
Consolidated egress ran every bottle through one process but keyed the
supervise proposal queue off a single SUPERVISE_BOTTLE_SLUG env and kept
one *global* DLP safelist — so in the shared gateway an operator's token
approval for bottle A would (a) be attributed to the wrong bottle and
(b) leak into bottle B's DLP scan (A's approved secret passes B's egress).
This slice keys both per bottle, resolved by source IP.

- policy_resolver: add `resolve_policy_and_bottle_id` — policy + bottle id
  in one `/resolve`, so egress keys routing *and* the supervise
  queue/safelist from a single round-trip. Fail-closed (403 -> (None,None)).
- egress_addon_core: add `resolve_client_context` (+ `ContextResolverLike`)
  returning `(Config, bottle_id)`, sharing the fail-closed parse with
  `resolve_client_config` via `_config_from_policy`.
- egress_addon: `_active_config` -> `_resolve_flow` returns `(Config, slug)`;
  `safe_tokens` set -> per-bottle `_safe_tokens_for(slug)`; the token-allow
  write/await/archive + the approved-token add all use the resolved slug.
  Single-tenant (no resolver) unchanged — slug = the env SUPERVISE_BOTTLE_SLUG.

New tests cover the resolver, the fail-closed context matrix, and the
cross-tenant isolation (an approval lands only in the calling bottle's
safelist; the proposal is keyed by the source-IP-attributed bottle;
unattributed IPs can't supervise).

Out of scope (noted): the git-gate gitleaks-allow hook + supervise_server
agent-proposal paths, and websocket DLP (still self.config-only, inert in
consolidated mode) — follow-up slices.

pyright 0 errors; pylint 9.83/10; unit suite green (1700 tests; the 13
test_sidecar_init /bin/sleep errors are pre-existing NixOS-local noise).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
didericis approved these changes 2026-07-13 19:55:02 -04:00
Author
Collaborator

Collapsed into the single stack PR #380 (per-host orchestrator, PRD 0070). Closing this slice PR; review + merge happens on #380.

Collapsed into the single stack PR #380 (per-host orchestrator, PRD 0070). Closing this slice PR; review + merge happens on #380.
didericis-claude closed this pull request 2026-07-14 00:57:28 -04:00
Some checks are pending
lint / lint (push) Successful in 2m4s
test / unit (pull_request) Successful in 1m6s
test / integration (pull_request) Successful in 23s
test / coverage (pull_request) Successful in 1m15s

Pull request closed

Sign in to join this conversation.