revert(egress-proxy): drop Role + agent provisioner (keep git-push block)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s

Partial revert of fa06a3a. The role + agent-side provisioner felt
overengineered: anthropic-base-url + npm-registry's only realistic
host values match the tool defaults, so the role tags drove no-op
dotfile writes most of the time. If non-default npm registry / tea
config is needed in a future bottle, we can ship it through a more
direct mechanism then.

What stays from fa06a3a:
  - Universal HTTPS git-push block in the egress-proxy addon
    (`is_git_push_request` in egress_proxy_addon_core, called from
    the request hook before route matching; 403s git-receive-pack
    regardless of route). This is the security backstop so git-gate
    remains the only outbound write path; PR #29 keeps it.

What gets reverted:
  - `Role` field on EgressProxyRoute (manifest + runtime).
  - `EGRESS_PROXY_ROLES` + `EGRESS_PROXY_SINGLETON_ROLES` constants
    and singleton-role validation.
  - `backend/docker/provision/egress_proxy.py` (npmrc + tea config).
  - `provision_egress_proxy` slot in `BottleBackend.provision`.
  - `prepare.py`'s role-based ANTHROPIC_BASE_URL detection (back to
    the token_ref="CLAUDE_CODE_OAUTH_TOKEN" auto-detect).
  - Manifest + provisioner tests for the above.

355 unit + 24 integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:02:01 -04:00
parent fa06a3a0ab
commit 4abea282e0
9 changed files with 24 additions and 453 deletions
+9 -18
View File
@@ -219,23 +219,20 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
argv.
Default orchestration: ca → prompt → skills → git →
egress_proxy → supervise. CA install runs first so the
agent's trust store is rebuilt before anything inside the
agent makes a TLS call. egress_proxy runs after git because
its provisioner may layer on top of `~/.gitconfig` entries
provision_git writes. Subclasses typically don't override
this; they implement the sub-methods below.
supervise. CA install runs first so the agent's trust store
is rebuilt before anything inside the agent makes a TLS call.
Subclasses typically don't override this; they implement the
sub-methods below.
PRD 0017: most agent-side rewrites (HTTPS routing) are
obsolete because egress-proxy sits on the HTTPS_PROXY path.
The remaining rewrites — npm registry, tea config,
ANTHROPIC_BASE_URL — exist for tools that need an explicit
URL config rather than just respecting HTTPS_PROXY."""
PRD 0017: cred-proxy's agent-side dotfile rewrites (~/.npmrc,
~/.gitconfig insteadOf, tea config) are gone. Egress-proxy is
on the agent's HTTP_PROXY path so every tool that respects
HTTPS_PROXY (claude-code, git over HTTPS, npm, curl) is
intercepted without per-tool reconfiguration."""
self.provision_ca(plan, target)
prompt_path = self.provision_prompt(plan, target)
self.provision_skills(plan, target)
self.provision_git(plan, target)
self.provision_egress_proxy(plan, target)
self.provision_supervise(plan, target)
return prompt_path
@@ -265,12 +262,6 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
"""Copy the host's cwd `.git` directory into the running
bottle if the user requested --cwd. No-op otherwise."""
def provision_egress_proxy(self, plan: PlanT, target: str) -> None:
"""Drop the agent-side dotfiles driven by `egress_proxy.routes[].role`
per PRD 0017 (~/.npmrc, ~/.config/tea/config.yml). Default
impl is a no-op for backends that don't yet support the
egress-proxy sidecar; the Docker backend overrides."""
def provision_supervise(self, plan: PlanT, target: str) -> None:
"""Write the in-bottle Claude Code MCP config so the agent
discovers the per-bottle supervise sidecar (PRD 0013).
-4
View File
@@ -27,7 +27,6 @@ from .egress_proxy import DockerEgressProxy
from .git_gate import DockerGitGate
from .pipelock import DockerPipelockProxy
from .provision import ca as _ca
from .provision import egress_proxy as _egress_proxy_prov
from .provision import git as _git
from .provision import prompt as _prompt
from .provision import skills as _skills
@@ -81,9 +80,6 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
def provision_git(self, plan: DockerBottlePlan, target: str) -> None:
_git.provision_git(plan, target)
def provision_egress_proxy(self, plan: DockerBottlePlan, target: str) -> None:
_egress_proxy_prov.provision_egress_proxy(plan, target)
def provision_supervise(self, plan: DockerBottlePlan, target: str) -> None:
_supervise_prov.provision_supervise(plan, target)
+12 -22
View File
@@ -175,31 +175,21 @@ def resolve_plan(
# never lands on argv or in env_file) goes into one dict. Nothing
# mutates the host os.environ.
forwarded_env: dict[str, str] = dict(resolved.forwarded)
# Find the (at most one) egress-proxy route claiming the
# anthropic-base-url role. Manifest validation enforces the
# singleton constraint. The role flips on claude-code's
# placeholder OAuth token + telemetry-off env vars and pins
# ANTHROPIC_BASE_URL at the route's host. Egress-proxy then
# strips inbound Authorization on every request and injects
# the real one from the route's `auth.token_ref` env var.
anthropic_route = next(
(r for r in egress_proxy_plan.routes if "anthropic-base-url" in r.roles),
None,
# When the bottle declares an egress-proxy route for the Anthropic
# OAuth flow, claude-code's outbound Authorization gets stripped +
# re-injected by egress-proxy. The agent's environ still needs
# *something* claude-code recognises as a credential or it refuses
# to start; ship a non-secret placeholder. The placeholder is not
# any real `auth.token_ref` value, so leaking it would tell an
# attacker only that egress-proxy is in front.
has_anthropic_auth = any(
r.token_ref == "CLAUDE_CODE_OAUTH_TOKEN"
for r in egress_proxy_plan.routes
)
if anthropic_route is not None:
# Point claude-code at the canonical Anthropic URL. HTTPS_PROXY
# routes the request through egress-proxy, which injects the
# real OAuth header from the host env named by the route's
# auth.token_ref.
forwarded_env["ANTHROPIC_BASE_URL"] = f"https://{anthropic_route.host}"
# claude-code refuses to start without *some* credential in
# its env. The proxy strips inbound Authorization on every
# request and injects the real one — so a non-secret
# placeholder is sufficient. The agent cannot exfiltrate
# this string because it carries no meaning to upstream.
if has_anthropic_auth:
forwarded_env["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-proxy-placeholder"
# Belt-and-braces: turn off telemetry endpoints (statsig,
# error reporting) that don't route through ANTHROPIC_BASE_URL.
# error reporting) that egress-proxy can't gate by auth.
forwarded_env.setdefault("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1")
forwarded_env.setdefault("DISABLE_ERROR_REPORTING", "1")
_write_env_file(resolved, env_file)
@@ -1,142 +0,0 @@
"""Egress-proxy agent-side provisioning (PRD 0017).
Writes the dotfiles / env-var nudges that point tools needing an
explicit URL config at the canonical upstream. The agent's
`HTTPS_PROXY=egress-proxy` already catches HTTPS traffic — these
provisioners exist for tools that:
- need an explicit base-URL setting beyond the proxy (claude-code
via `ANTHROPIC_BASE_URL`), or
- read a per-tool config to know which upstream to talk to (npm's
`~/.npmrc`, tea's `~/.config/tea/config.yml`).
The `ANTHROPIC_BASE_URL` env is set at `docker run -e` time by the
backend's launch step, not here — it has to be in the agent's environ
before claude starts, and there is no point in writing it to a
dotfile the agent would have to source. See `prepare.py` for that.
This module handles the rest: ~/.npmrc and ~/.config/tea/config.yml.
"""
from __future__ import annotations
import os
import subprocess
from pathlib import Path
from ....egress_proxy import EgressProxyRoute
from ....log import info
from .. import util as docker_mod
from ..bottle_plan import DockerBottlePlan
def provision_egress_proxy(plan: DockerBottlePlan, target: str) -> None:
"""Drop the agent-side dotfiles for each declared egress-proxy
route role. No-op when the bottle has no roles to provision."""
routes = plan.egress_proxy_plan.routes
if not routes:
return
_provision_npmrc(plan, target, routes)
_provision_tea_config(plan, target, routes)
# --- npm --------------------------------------------------------------------
def render_npmrc(routes: tuple[EgressProxyRoute, ...]) -> str:
"""Render `~/.npmrc` content. Driven by the `npm-registry` role:
finds the (single) route that claims it and writes a registry=
line pointing at the canonical upstream URL. Empty string when
no such route exists, so callers can branch on emptiness.
Egress-proxy is on the agent's HTTPS_PROXY, so the canonical
URL routes through the proxy automatically and gets DLP +
path_allowlist + auth-injection. The npmrc deliberately carries
no `_authToken` — the proxy strips inbound Authorization and
injects the upstream one from the bottle's egress-proxy auth
config. Manifest validation enforces that the role is a
singleton, so the first match is the only match."""
for r in routes:
if "npm-registry" in r.roles:
return f"registry=https://{r.host}/\n"
return ""
def _provision_npmrc(
plan: DockerBottlePlan,
target: str,
routes: tuple[EgressProxyRoute, ...],
) -> None:
content = render_npmrc(routes)
if not content:
return
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
container_npmrc = f"{container_home}/.npmrc"
npmrc = plan.stage_dir / "agent_npmrc"
npmrc.write_text(content)
npmrc.chmod(0o600)
info(f"writing {container_npmrc} (egress-proxy npm registry)")
subprocess.run(
["docker", "cp", str(npmrc), f"{target}:{container_npmrc}"],
stdout=subprocess.DEVNULL,
check=True,
)
docker_mod.docker_exec_root(target, ["chown", "node:node", container_npmrc])
docker_mod.docker_exec_root(target, ["chmod", "644", container_npmrc])
# --- tea --------------------------------------------------------------------
def render_tea_config(routes: tuple[EgressProxyRoute, ...]) -> str:
"""Render `~/.config/tea/config.yml`. Driven by the `tea-login`
role: each route that claims it produces one `logins:` entry
pointing at the canonical Gitea URL. The egress-proxy injects
the real token at request time; the value in `token:` here is a
placeholder. `tea` refuses to make calls without a non-empty
token field, so the placeholder is necessary."""
tea_routes = [r for r in routes if "tea-login" in r.roles]
if not tea_routes:
return ""
lines = ["logins:"]
for r in tea_routes:
lines.extend([
f"- name: {r.host}",
f" url: https://{r.host}",
" token: egress-proxy-placeholder",
" default: false",
" ssh_host: \"\"",
" ssh_key: \"\"",
" insecure: false",
])
return "\n".join(lines) + "\n"
def _provision_tea_config(
plan: DockerBottlePlan,
target: str,
routes: tuple[EgressProxyRoute, ...],
) -> None:
content = render_tea_config(routes)
if not content:
return
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
container_tea = f"{container_home}/.config/tea/config.yml"
cfg = plan.stage_dir / "agent_tea_config.yml"
cfg.write_text(content)
cfg.chmod(0o600)
info(
f"writing {container_tea} "
f"({len([r for r in routes if 'tea-login' in r.roles])} tea login(s))"
)
docker_mod.docker_exec_root(
target, ["mkdir", "-p", str(Path(container_tea).parent)]
)
subprocess.run(
["docker", "cp", str(cfg), f"{target}:{container_tea}"],
stdout=subprocess.DEVNULL,
check=True,
)
docker_mod.docker_exec_root(target, [
"chown", "-R", "node:node", str(Path(container_tea).parent),
])
docker_mod.docker_exec_root(target, ["chmod", "600", container_tea])
+1 -9
View File
@@ -62,19 +62,13 @@ class EgressProxyRoute:
(e.g. `EGRESS_PROXY_TOKEN_0`); `token_ref` is the host env var
the CLI reads at launch and forwards into the container's environ
under `token_env`. Routes that share a `token_ref` coalesce to
one `token_env` slot.
`roles` are the provisioner tags from the manifest route (see
`manifest.EGRESS_PROXY_ROLES`). Each tag drives one agent-side
dotfile / env rewrite at bottle bring-up. The addon never reads
these — they flow through the plan to the host-side provisioner."""
one `token_env` slot."""
host: str
path_allowlist: tuple[str, ...] = ()
auth_scheme: str = ""
token_env: str = ""
token_ref: str = ""
roles: tuple[str, ...] = ()
@dataclass(frozen=True)
@@ -154,13 +148,11 @@ def egress_proxy_routes_for_bottle(
auth_scheme=r.AuthScheme,
token_env=token_env,
token_ref=r.TokenRef,
roles=r.Role,
))
else:
out.append(EgressProxyRoute(
host=r.Host,
path_allowlist=r.PathAllowlist,
roles=r.Role,
))
return tuple(out)
+2 -89
View File
@@ -129,44 +129,6 @@ class GitEntry:
# token-not-Bearer quirk (go-gitea/gitea#16734).
EGRESS_PROXY_AUTH_SCHEMES = ("Bearer", "token")
# Agent-side provisioner role tags a route may carry. Each tag drives
# one dotfile / env rewrite at bottle bring-up so tools that need an
# explicit URL config (rather than just respecting HTTPS_PROXY) point
# at the canonical upstream. Egress-proxy is on the agent's HTTP_PROXY
# path, so the canonical URL routes through the proxy automatically —
# the dotfile values are upstream URLs, not proxy URLs.
#
# anthropic-base-url: set ANTHROPIC_BASE_URL=https://<host> in the
# agent's environ (signals claude-code to use
# a non-default Anthropic endpoint; in practice
# the host is api.anthropic.com, so the value
# matches claude-code's default — the marker
# is what drives the placeholder-token +
# telemetry-off env vars).
# npm-registry: write ~/.npmrc `registry=https://<host>/`.
# tea-login: add an entry to ~/.config/tea/config.yml
# (url = https://<host>) so `tea` knows which
# Gitea host to talk to.
#
# Routes without a `role` are pure proxy entries: egress-proxy
# enforces path_allowlist + injects auth, but no agent-side dotfile
# is written. (`git-insteadof` is intentionally absent — egress-proxy
# already 403s HTTPS git push universally; PRD 0017's git story is
# `bottle.git` + git-gate for SSH push.)
EGRESS_PROXY_ROLES = frozenset({
"anthropic-base-url",
"npm-registry",
"tea-login",
})
# Roles whose semantics imply a single route can carry them. A second
# route claiming the same role would make the provisioner's choice
# ambiguous (which host goes into ANTHROPIC_BASE_URL?).
EGRESS_PROXY_SINGLETON_ROLES = frozenset({
"anthropic-base-url",
"npm-registry",
})
@dataclass(frozen=True)
class EgressProxyRoute:
@@ -181,10 +143,6 @@ class EgressProxyRoute:
manifest's `auth` block is omitted both fields are empty strings —
no Authorization is written, no token forwarded.
`Role` carries optional provisioner tags (see EGRESS_PROXY_ROLES).
Each tag drives one agent-side dotfile / env rewrite when the
sidecar comes up.
Validation rules (enforced in `from_dict`):
- `host` required, non-empty.
- `path_allowlist` optional, list of absolute path prefixes.
@@ -192,17 +150,12 @@ class EgressProxyRoute:
`token_ref` as non-empty strings; an empty `auth: {}` is an
error rather than a synonym for "no auth" (omit `auth` for
that case).
- `role` optional. String or list of strings drawn from
EGRESS_PROXY_ROLES. Singleton roles (see
EGRESS_PROXY_SINGLETON_ROLES) may appear on at most one
route per bottle.
"""
Host: str
PathAllowlist: tuple[str, ...] = ()
AuthScheme: str = ""
TokenRef: str = ""
Role: tuple[str, ...] = ()
@classmethod
def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "EgressProxyRoute":
@@ -273,37 +226,11 @@ class EgressProxyRoute:
auth_scheme = auth_scheme_raw
token_ref = token_ref_raw
role_raw = d.get("role")
roles: tuple[str, ...] = ()
if role_raw is None:
roles = ()
elif isinstance(role_raw, str):
roles = (role_raw,)
elif isinstance(role_raw, list):
role_list = cast(list[object], role_raw)
collected_roles: list[str] = []
for r in role_list:
if not isinstance(r, str):
die(f"{label} role items must be strings (got {type(r).__name__})")
collected_roles.append(r)
roles = tuple(collected_roles)
else:
die(
f"{label} role must be a string or a list of strings "
f"(was {type(role_raw).__name__})"
)
for r in roles:
if r not in EGRESS_PROXY_ROLES:
die(
f"{label} role {r!r} is not one of "
f"{', '.join(sorted(EGRESS_PROXY_ROLES))}"
)
for k in d:
if k not in ("host", "path_allowlist", "auth", "role"):
if k not in ("host", "path_allowlist", "auth"):
die(
f"{label} has unknown key {k!r}; accepted keys are "
f"'host', 'path_allowlist', 'auth', 'role'"
f"'host', 'path_allowlist', 'auth'"
)
return cls(
@@ -311,7 +238,6 @@ class EgressProxyRoute:
PathAllowlist=prefixes,
AuthScheme=auth_scheme,
TokenRef=token_ref,
Role=roles,
)
@@ -789,10 +715,6 @@ def _validate_egress_proxy_routes(
- Hosts must be unique within the bottle. The proxy matches by
exact-host (v1, prefix matching is on path_allowlist only);
duplicate hosts leave the route choice ambiguous.
- Singleton roles (`anthropic-base-url`, `npm-registry`) may
appear on at most one route — each drives a single agent-side
dotfile/env entry, so two routes claiming the role would make
the choice ambiguous.
No cross-validation against `bottle.git` is performed. git-gate
(SSH push/fetch) and egress-proxy (HTTPS) broker different
@@ -807,15 +729,6 @@ def _validate_egress_proxy_routes(
f"{r.Host!r}; each host must be unique on the proxy."
)
seen_hosts[key] = None
for role in EGRESS_PROXY_SINGLETON_ROLES:
with_role = [r for r in routes if role in r.Role]
if len(with_role) > 1:
hosts = ", ".join(r.Host for r in with_role)
die(
f"bottle '{bottle_name}' egress_proxy.routes has {len(with_role)} "
f"routes with role {role!r} (hosts: {hosts}); this role drives a "
f"single agent-side rewrite — pick one."
)
def _validate_unique_git_names(bottle_name: str, git: tuple[GitEntry, ...]) -> None:
-58
View File
@@ -128,64 +128,6 @@ class TestAuth(unittest.TestCase):
}])
class TestRole(unittest.TestCase):
def test_omitted_means_no_roles(self):
b = _bottle([{"host": "x.example"}])
self.assertEqual((), b.egress_proxy.routes[0].Role)
def test_string_normalizes_to_tuple(self):
b = _bottle([{"host": "api.anthropic.com",
"role": "anthropic-base-url",
"auth": {"scheme": "Bearer", "token_ref": "T"}}])
self.assertEqual(("anthropic-base-url",),
b.egress_proxy.routes[0].Role)
def test_list_supported(self):
b = _bottle([{"host": "registry.npmjs.org",
"role": ["npm-registry"]}])
self.assertEqual(("npm-registry",), b.egress_proxy.routes[0].Role)
def test_unknown_role_rejected(self):
with self.assertRaises(Die):
_bottle([{"host": "x.example", "role": "git-insteadof"}])
def test_non_string_role_rejected(self):
with self.assertRaises(Die):
_bottle([{"host": "x.example", "role": 42}])
def test_list_with_non_string_item_rejected(self):
with self.assertRaises(Die):
_bottle([{"host": "x.example", "role": ["npm-registry", 42]}])
def test_singleton_anthropic_base_url_enforced(self):
with self.assertRaises(Die):
_bottle([
{"host": "api.anthropic.com", "role": "anthropic-base-url",
"auth": {"scheme": "Bearer", "token_ref": "T1"}},
{"host": "api2.anthropic.example",
"role": "anthropic-base-url",
"auth": {"scheme": "Bearer", "token_ref": "T2"}},
])
def test_singleton_npm_registry_enforced(self):
with self.assertRaises(Die):
_bottle([
{"host": "registry.npmjs.org", "role": "npm-registry"},
{"host": "npm.example", "role": "npm-registry"},
])
def test_tea_login_is_not_singleton(self):
# Multiple Gitea instances on one bottle is a legitimate
# dev setup; tea-login lays one logins[] entry per route.
b = _bottle([
{"host": "gitea.example", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "T1"}},
{"host": "other-gitea.example", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "T2"}},
])
self.assertEqual(2, len(b.egress_proxy.routes))
class TestRouteValidation(unittest.TestCase):
def test_duplicate_hosts_rejected(self):
# Routes match by exact host; duplicates leave the choice
-2
View File
@@ -25,7 +25,6 @@ _BOTTLE_DEV = """
egress_proxy:
routes:
- host: api.anthropic.com
role: anthropic-base-url
auth:
scheme: Bearer
token_ref: CLAUDE_CODE_OAUTH_TOKEN
@@ -93,7 +92,6 @@ class TestBottleFileParses(_ResolveCase):
self.assertEqual("api.anthropic.com", routes[0].Host)
self.assertEqual("Bearer", routes[0].AuthScheme)
self.assertEqual("CLAUDE_CODE_OAUTH_TOKEN", routes[0].TokenRef)
self.assertEqual(("anthropic-base-url",), routes[0].Role)
self.assertEqual(["example.com"], list(m.bottles["dev"].egress.allowlist))
-109
View File
@@ -1,109 +0,0 @@
"""Unit: agent-side provisioning for egress-proxy roles (PRD 0017).
Each role drives one dotfile / env-var rewrite at bottle bring-up.
HTTPS_PROXY routes the canonical URL through egress-proxy, which
injects auth and DLP-scans on the upstream leg."""
import unittest
from claude_bottle.backend.docker.provision.egress_proxy import (
render_npmrc,
render_tea_config,
)
from claude_bottle.egress_proxy import egress_proxy_routes_for_bottle
from claude_bottle.manifest import Manifest
def _routes(manifest_routes):
m = Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"routes": manifest_routes}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
return egress_proxy_routes_for_bottle(m.bottles["dev"])
# --- npmrc -----------------------------------------------------------
class TestRenderNpmrc(unittest.TestCase):
def test_canonical_upstream_url(self):
routes = _routes([
{"host": "registry.npmjs.org", "role": "npm-registry",
"auth": {"scheme": "Bearer", "token_ref": "NPM_TOKEN"}},
])
self.assertEqual(
"registry=https://registry.npmjs.org/\n",
render_npmrc(routes),
)
def test_empty_when_no_npm_role(self):
routes = _routes([
{"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "GH"}},
])
self.assertEqual("", render_npmrc(routes))
def test_no_routes_empty(self):
self.assertEqual("", render_npmrc(()))
def test_no_auth_token_in_npmrc(self):
# The proxy injects auth; the npmrc must carry no secret —
# not even `:always-auth=true` lines that would prompt npm
# to wait for credentials. Just the registry URL.
routes = _routes([
{"host": "registry.npmjs.org", "role": "npm-registry",
"auth": {"scheme": "Bearer", "token_ref": "NPM_TOKEN"}},
])
out = render_npmrc(routes)
self.assertNotIn("_authToken", out)
self.assertNotIn("NPM_TOKEN", out)
# --- tea config ------------------------------------------------------
class TestRenderTeaConfig(unittest.TestCase):
def test_single_login(self):
routes = _routes([
{"host": "gitea.dideric.is", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "GITEA_TOKEN"}},
])
out = render_tea_config(routes)
self.assertIn("- name: gitea.dideric.is", out)
self.assertIn("url: https://gitea.dideric.is", out)
self.assertIn("token: egress-proxy-placeholder", out)
def test_multiple_logins_each_get_own_entry(self):
routes = _routes([
{"host": "gitea.a.example", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "T_A"}},
{"host": "gitea.b.example", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "T_B"}},
])
out = render_tea_config(routes)
self.assertIn("- name: gitea.a.example", out)
self.assertIn("- name: gitea.b.example", out)
def test_empty_when_no_tea_role(self):
routes = _routes([
{"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "GH"}},
])
self.assertEqual("", render_tea_config(routes))
def test_no_routes_empty(self):
self.assertEqual("", render_tea_config(()))
def test_no_real_token_in_config(self):
routes = _routes([
{"host": "gitea.dideric.is", "role": "tea-login",
"auth": {"scheme": "token", "token_ref": "GITEA_TOKEN"}},
])
out = render_tea_config(routes)
# GITEA_TOKEN is just the env var name, not the value —
# placeholder-only is the SC.
self.assertIn("egress-proxy-placeholder", out)
if __name__ == "__main__":
unittest.main()