feat(orchestrator): slice 7 — sidecar-side PolicyResolver #362

Open
didericis-claude wants to merge 2 commits from orchestrator-slice7 into orchestrator-slice6
Collaborator

Slice 7 of PRD 0070, stacked on #361. The data-plane bridge that lets the consolidated sidecar apply each bottle's policy per request.

  • policy_resolver.pyPolicyResolver resolves a client's policy from the orchestrator's POST /resolve keyed on (source_ip, identity_token), cached briefly (short TTL) so it isn't a round-trip per request; invalidate() drops an entry on teardown / live reload.
  • Fail-closed: unattributed client (orchestrator 403) → None so the caller denies; unreachable / unexpected status raises so the caller can fail closed too. Stdlib-only and free of bot-bottle imports → COPYable flat into the sidecar bundle.

Scope note (honest): this is the sidecar-side client. Wiring it into the live egress mitmproxy addon (select Config per client IP in the request path) and git-gate, plus routing all bottles' egress to the one shared sidecar, are the remaining data-plane pieces — a heavier change to the bundle's adversarial-input code, taken next.

Re the slice-6 review note ("policy should be a structured type"): the resolved policy blob is exactly the egress Config schema already defined in egress_addon_core (load_config), so it is structured at the point of consumption; the resolver stays format-agnostic (returns the blob) so git-gate/others can share it.

Tests: resolve returns / caches / expires / invalidates; 403 → None (fail closed); other status + unreachable raise; missing policy → empty; posts source_ip + token. Full suite green.

Merge order: #352#356#357#358#360#361 → this.

🤖 Generated with Claude Code

Slice 7 of PRD 0070, **stacked on #361**. The data-plane bridge that lets the consolidated sidecar apply each bottle's policy per request. - **`policy_resolver.py`** — `PolicyResolver` resolves a client's policy from the orchestrator's `POST /resolve` keyed on `(source_ip, identity_token)`, cached briefly (short TTL) so it isn't a round-trip per request; `invalidate()` drops an entry on teardown / live reload. - **Fail-closed:** unattributed client (orchestrator `403`) → `None` so the caller denies; unreachable / unexpected status **raises** so the caller can fail closed too. Stdlib-only and free of bot-bottle imports → COPYable flat into the sidecar bundle. **Scope note (honest):** this is the sidecar-side *client*. Wiring it into the **live egress mitmproxy addon** (select `Config` per client IP in the request path) and **git-gate**, plus **routing** all bottles' egress to the one shared sidecar, are the remaining data-plane pieces — a heavier change to the bundle's adversarial-input code, taken next. Re the slice-6 review note ("policy should be a structured type"): the resolved policy blob is exactly the egress **`Config`** schema already defined in `egress_addon_core` (`load_config`), so it *is* structured at the point of consumption; the resolver stays format-agnostic (returns the blob) so git-gate/others can share it. **Tests:** resolve returns / caches / expires / invalidates; `403` → None (fail closed); other status + unreachable raise; missing policy → empty; posts source_ip + token. Full suite green. Merge order: #352 → #356 → #357 → #358 → #360 → #361 → this. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
didericis-claude added 1 commit 2026-07-13 17:21:47 -04:00
feat(orchestrator): slice 7 — sidecar-side PolicyResolver (#352)
lint / lint (push) Successful in 2m2s
test / unit (pull_request) Successful in 1m3s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m9s
72ea500342
The data-plane bridge that lets the consolidated sidecar apply each
bottle's policy per request. `PolicyResolver` resolves a client's policy
from the orchestrator's `POST /resolve` keyed on (source_ip, identity
token) and caches it briefly (short TTL) so it isn't a round-trip per
request; `invalidate()` drops an entry on teardown / live reload.

Fail-closed: an unattributed client (orchestrator answers 403) resolves to
None so the caller denies; unreachable / unexpected status raises so the
caller can fail closed too rather than serve stale/empty policy. Stdlib
only and free of bot-bottle imports, so it can be COPYed flat into the
sidecar bundle.

Scope note: this is the sidecar-side *client*. Wiring it into the live
egress mitmproxy addon (select `Config` per client IP in the request path)
and git-gate, plus routing all bottles' egress to the one shared sidecar,
are the remaining data-plane pieces — a heavier change to the sidecar
bundle's adversarial-input code, taken next.

Tests: resolve returns/caches/expires/invalidates; 403 -> None (fail
closed); other HTTP status + unreachable raise; missing policy -> empty;
posts source_ip + identity_token. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
didericis reviewed 2026-07-13 17:29:13 -04:00
@@ -0,0 +49,4 @@
self._ttl = ttl
self._timeout = timeout
# source_ip -> (fetched_at_monotonic, policy | None)
self._cache: dict[str, tuple[float, str | None]] = {}
Owner

@didericis-claude Should probably ignore the cache... think the policy resolver will be called infrequently enough and the speed is not important enough of a concern that the cache value here isn't really warranted. It's more important that we honor any revocations or agent changes/shutdowns that the orchestrator is aware of.

@didericis-claude Should probably ignore the cache... think the policy resolver will be called infrequently enough and the speed is not important enough of a concern that the cache value here isn't really warranted. It's more important that we honor any revocations or agent changes/shutdowns that the orchestrator is aware of.
didericis added 1 commit 2026-07-13 17:32:09 -04:00
refactor(orchestrator): drop the PolicyResolver cache (#352)
lint / lint (push) Successful in 2m0s
test / unit (pull_request) Successful in 1m1s
test / integration (pull_request) Successful in 20s
test / coverage (pull_request) Successful in 1m12s
de9da29027
Review: the resolver is called rarely enough that a round-trip doesn't
matter, and correctness beats speed — always fetching means a revocation,
policy change, or teardown the orchestrator knows about is honored
immediately instead of lingering for a cache TTL. Remove the TTL cache
(and `invalidate`); `resolve` now hits the orchestrator every call. Noted
in the docstring that any future caching should use orchestrator-driven
invalidation, not a blind TTL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
Author
Collaborator

Agreed — dropped the cache in de9da29. resolve now hits the orchestrator every call, so a revocation / policy change / teardown the orchestrator knows about is honored immediately (no TTL staleness window on a security-relevant path). Also removed invalidate (moot without a cache).

Left a note in the docstring: if this ever becomes a hot path, add caching with orchestrator-driven invalidation (push on change), not a blind TTL — precisely so we never serve stale-permissive policy after a revocation.

Agreed — dropped the cache in `de9da29`. `resolve` now hits the orchestrator every call, so a revocation / policy change / teardown the orchestrator knows about is honored immediately (no TTL staleness window on a security-relevant path). Also removed `invalidate` (moot without a cache). Left a note in the docstring: if this ever becomes a hot path, add caching with **orchestrator-driven invalidation** (push on change), not a blind TTL — precisely so we never serve stale-permissive policy after a revocation.
didericis approved these changes 2026-07-13 17:34:36 -04:00
Some checks are pending
lint / lint (push) Successful in 2m0s
test / unit (pull_request) Successful in 1m1s
test / integration (pull_request) Successful in 20s
test / coverage (pull_request) Successful in 1m12s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin orchestrator-slice7:orchestrator-slice7
git checkout orchestrator-slice7
Sign in to join this conversation.