Skip to content

feat(operator): the egress proxy of run pods, and their network deny by default (D15c) - #42

Merged
stephane-segning merged 25 commits into
mainfrom
claude/compassionate-faraday-hxtil6
Oct 27, 2026
Merged

stephane-segning merged 25 commits into
mainfrom
claude/compassionate-faraday-hxtil6

Conversation

@stephane-segning

@stephane-segning stephane-segning commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

1. Summary

This PR changes:

  • The operator renders an egress proxy for each coder that has run pods. The run pods' network becomes deny by default, and the run-pod template carries the proxy's variables. This matches what the adam-rs chart renders since feat(egress): an HTTPS-only egress proxy for run pods another-adam-rs#134 and feat(chart): run pods reach the internet only through the egress proxy another-adam-rs#135.

    • The proxy is adam-egress from the agent's coder image: a Deployment, Service, ServiceAccount, allow-list ConfigMap, PodDisruptionBudget and its own NetworkPolicy.
    • Run pods may reach DNS, the proxy's pods on 3128, and extraEgress.
  • Parity moves from adam-rs 3f714d0 to 7598255. Two new parity cases move the proxy off its defaults, and the proxy's six objects are compared object by object.

  • The operator chart gets runPods.egressProxy.{allow, extraAllow, extraNoProxy, replicas, ports, resources}, carried in RUN_PODS_FILE. It refuses what the coder chart refuses at render, and networkPolicy.enabled: false is refused.

  • Apply and clear order. The proxy is applied before the policy that names it. On clear:

    1. the binding, the run pods, the proxy and the proxy's policy go first;
    2. the run-pods policy is removed last of the network objects, so run pods never sit with an open policy;
    3. then the quota, Service, files and template;
    4. the accounts last.
  • run-pods-e2e.sh gains step 7. From a run pod the operator made:

    • a direct connection to the internet, to the API server and to another pod is refused, after controls;
    • curl and npm work through the proxy;
    • example.com gets 403;
    • no secret appears in the proxy's log;
    • no policy names an IPv6 range.

    The coder pin moves to sha-7598255@sha256:6e74b292…, which has adam-egress (verified 2026-10-27, anonymous manifest request).

  • Docs:

    • a D15c section in §60a and P-021, with ten decisions for the owner;
    • the §59a field row, Risks, a §93 question and the revision history;
    • CLAUDE.md and README.md. They also record that D15b's job ran green (verified 2026-10-27, run 40435825771).

It solves:


2. Intent

The intent of this PR is:

A run pod is where a person's commands run. Whichever way it is made (the coder's own chart or the platform's operator), it reaches the internet only through the allow-listed proxy. The parity goldens keep the two the same.


3. Scope

In Scope

  • The domain, the runtime, the controller and the operator binary's settings.
  • The operator chart and render-check.sh.
  • The parity tools and goldens.
  • The kind script and its pin.
  • The docs.

Out of Scope

These are the decisions in §60a, P-021:

  • An AgentConfig that extends the allow-list. The list is deployment-level only.
  • Refusing an agent image without adam-egress: it is documented, not refused. Note that defaultAgentImage is still such an image; moving it is a decision for the owner.
  • Scheduling settings for the proxy.

4. Verification

I verified this change by:

  • Running automated tests
  • Running manual tests
  • Checking logs
  • Checking metrics
  • Testing error cases
  • Testing permissions/security behavior
  • Testing rollback or failure behavior, if relevant

Commands run:

cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
sh deploy/operator/tests/render-check.sh
sh deploy/operator/tests/bump-tag-test.sh
sh tools/adam-parity/regen.sh --check
node tools/docs-check/check-docs.mjs
sh deploy/operator/tests/run-pods-admission.sh   # against a bare kube-apiserver v1.35.8
shellcheck 0.9.0 / dash -n deploy/operator/tests/e2e/run-pods-e2e.sh

Results:

fmt 0, clippy 0, test 0 (1280 passed), render-check 0 (411 checks; the four off-by-default goldens byte-identical), bump-tag-test 0, parity --check 0, docs-check 0
run-pods-admission.sh: the 25 refusals hold with the new template; the proxy's objects applied by server-side apply on the same server
24 deliberate breakages of the new render checks, each caught by its own check

Unverified until CI runs run-pods-e2e:

  • a kubelet starting the proxy from the pinned image;
  • kindnet dropping the direct connections;
  • curl and npm through the proxy;
  • the proxy's log.

No cluster or container daemon exists where this was written. D15 stays open until that job is green.


5. Screenshots / Evidence

  • Logs: the commands above. The same checks passed on adam-rs's own kind job on its first real run (run 40471872938, job 122080662548).

6. Risk Assessment

Risk level:

  • Low
  • Medium
  • High

Potential risks:

  • After the upgrade, run pods made before it lose their way out until they are made again.
  • An agent whose image has no adam-egress gets a proxy that cannot start, so its run pods have no internet.
  • RunPods::network is now required in the neutral spec, which breaks every RunPods literal.

Mitigation:

  • Run pods are off by default, and nothing changes in any render with them off.
  • The upgrade note and the image requirement are in §60a and the chart README.

7. AI Usage Declaration

AI was used for:

  • Understanding existing code
  • Generating code
  • Refactoring
  • Generating tests
  • Drafting documentation
  • Reviewing the diff
  • Not used

Human verification:

  • I understand every meaningful change in this PR
  • I checked generated code manually
  • I checked generated tests manually
  • I removed unsupported AI assumptions
  • I accept responsibility for this PR

8. Reviewer Focus

Please focus your review on:

  • Correctness
  • Architecture
  • Security
  • Performance
  • Tests
  • Maintainability
  • Product intent
  • Edge cases

Generated by Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

claude added 24 commits October 26, 2026 21:23
… run pods

The goldens were rendered at 0391809. adam-rs main is at 3f714d0, where the coder
refuses to start with a GitHub App key file beside local commands (exit 78) unless
ALLOW_COMMANDS_NEXT_TO_SECRETS is accepted, and the chart refuses to render
github.auth=app with runPods.enabled=false unless runPods.allowCommandsNextToSecrets
is true (adam-rs#131).

The two App cases (coder, coder-split) are about the pods of the agent, so they keep
their meaning and accept the risk by name; extract.py does not project the variable
(an AgentConfig takes it in extraEnv). The token case needs nothing. The renders at
the two revisions differ only by that variable: the env contract of a coder with
run pods off did not move. extract.py now also projects each pod's service account and
token, and, when the chart renders run pods, the pod template, the quota, the
priority class, the network policy, the service account, the role and its binding and
the admission policy with its binding, under runPods. The README says what was
decided and why.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
A coder that runs its commands in pods of their own (adam-rs#131) needs, beside its
workers, a pod template, a quota, a network policy, an account with a bound Role and
an admission policy. The coder chart renders them; the operator now does too, for an
AgentConfig that sets coder.runPods.sizeClass, and renders nothing and changes no
digest for one that does not.

- aap-api: Coder.runPods { sizeClass } (the CRDs regenerated and copied).
- aap-ports: RuntimeSpec.run_pods and Workload.service_account, absent from the JSON
  when absent; EnvValue::PodNamespace; the invariants in check().
- aap-domain: the settings the deployment gives (RunPodSettings, read from a file the
  chart renders and validated at start), the template as a file set of the workers,
  the variables of adam's contract; what is only applied is left out of the digest. The only home of
  the contract stays aap-domain.
- aap-runtime-kubernetes: the account, the quota, the network policy, the binding and
  the ConfigMap of the policy's parameters; removed when the service stops asking or
  goes, before anything the binding was holding up; no object of another owner touched.
- aap-controller, operator: RUN_PODS_FILE.
- deploy/operator: runPods and runPodClasses (off by default, and then not a trace in
  any render: the four goldens are byte for byte what they were), a PriorityClass, the
  Role of run pods, one admission policy for every agent with its parameters in a
  ConfigMap per agent, and the operator's Role gaining serviceaccounts, resourcequotas,
  rolebindings, pods/delete and the right to BIND that one Role by name (it holds no
  right to make a pod or exec). A validator refuses what the operator would refuse at
  start. render-check.sh holds all of it, and api --check reads the shape with run
  pods on.
- Forms and the Platform API: coder.runPods.sizeClass as agent.runPodSizeClass, and
  GET /v1/info offering the classes the chart writes into the API's file.
- Parity at adam-rs 3f714d0: four more cases of the coder chart with run pods on, the
  template compared in aap-domain, the quota, network policy, binding, Role,
  PriorityClass and the admission policy (with the parameters substituted) compared in
  aap-runtime-kubernetes. tests/run-pods-admission.sh asks a real API server.

Refs #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
…erator may

The previous commit made `ensure` of a spec without run pods, and `delete`, list
serviceaccounts, resourcequotas and rolebindings to clear what a service that stopped
asking had left. The operator's Role has no right on those kinds unless the chart has
runPods.enabled (the default render must stay what it was), so with run pods off every
reconcile and every delete of every agent would have failed with Forbidden. The fake
API server enforces no RBAC and the tests did not see it; a review of the diff against
the default Role did.

The look is now a choice of the composition root: KubernetesRuntime::clearing_run_pods,
off by default, turned on by `operator run` when it has settings for run pods (the same
condition under which the chart gives the rights). A spec that has run pods is applied
whatever it says. The cost is stated in the READMEs and in section 60a: turning
runPods.enabled off clears nothing, so remove coder.runPods from the configs first.

Two tests hold it: a default provider makes not one request that names the three kinds
nor deletes a pod, through an ensure, a second ensure, a status and a delete; and a
provider that is not looking applies the objects of a spec that has run pods and leaves
them when a later spec has none.

Refs #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
…nly with settings

The composition root decides whether the Kubernetes provider looks for run pod objects
to clear, and a mutation of that one line (never told to look) survived every test: no
test reads the provider's flag, and the cluster tests skip without a cluster. The
decision is now a function of its own, kubernetes_runtime, and the provider reports the
flag (is_clearing_run_pods), so a unit test holds both ways.

Refs #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
Section 60a gains the "D15a built" note: what is built and where, how it is proven, the
parity with the coder chart at adam-rs 3f714d0, what a bare kube-apiserver v1.35.8 was
asked and answered, what a person must know (the operator can bind the one Role of run
pods, the guard fails open for an account whose parameters are deleted, the agents of a
namespace share the tighter quota, the account may exec in any pod of its namespace),
eleven decisions for the owner to confirm, what only the kind end-to-end (D15b) can prove,
and what is still unverified. P-020 proposes the split between the namespaced operator and
the chart's cluster-scoped guard with its costs. The D15 row is split in two, the risks
gain two rows, section 59a's field table the new row, the index its revision.

The chart's README gains "Run pods" (a sequence and a state diagram, every value, who
makes what, the five things to know, how to check a cluster) and the Rights table the
rows the chart adds; the crates' READMEs and the goldens' README say what changed in each;
CLAUDE.md and the root README carry the status. The chart's CI also lints and runs
kubeconform over the render with run pods on.

Refs #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
The workspace denies expect_used outside tests by lint, and a unit test inside src/ is
not a test target the allow of the integration tests covers; clippy -D warnings said so
only after the test was written and the gates had been run once.

Refs #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
shellcheck 0.9.0, the version CI runs, reads `A && B || C` as SC2015;
0.11.0 does not. The check refuses the same parameters as before.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
An OpenAI-compatible endpoint that plays a coder's model with no network and
no key, for the kind end-to-end of run pods (D15b). It answers every task with
the same three turns: a scratch project (or prepare_workspace with a
repository), run_command with a probe written for the task, and the answer,
which holds the probe's output. The probe prints one key=value line per fact
(host, user, directory, what /work and /work/workspaces hold, the NAMES of the
environment variables and never a value, whether a peer's path is visible, and
with a repository whether the mirror can be written and what git says), writes
the same lines to /tmp/probe-<name>.out, and can wait for a release file.

The journal keeps what the model was asked (run, turn, tool results) and no
header, so the key the coder sends is not in it. The coder's own image runs it
from a ConfigMap, so nothing else is pulled. Seven node:test tests run the
probe for real and cover both shapes of answer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
The two coders the end-to-end runs, both the real adam-rs coder image
(ghcr.io/vymalo/another-adam-rs/coder:sha-3f714d0, adam-rs main at #131, which
contains #117) pinned by tag and digest on two ref: lines of run-pods.yaml and
nowhere else: e2e-plain, a token coder with no run pods, and e2e-coder, a
GitHub App coder with coder.runPods. defaultAgentImage is not moved. The pin
was read from ghcr.io anonymously on 2026-10-26 and again on 2026-10-27 (a
Docker v2 image manifest whose digest is the one in the file; six of the
workspace run image's seven layers are the coder's).

run-pods.values.yaml is laid over the chart: run pods on, a quota of two pods
so the script can fill it. run-pods-support.yaml is a throwaway Postgres with
two databases and the scripted model as a Deployment of the coder's own image.

bin/operator/tests/e2e_run_pods_fixture.rs needs no cluster: both coders pass
aap_domain::resolve (the plain one the same on an operator with and without run
pods; the other refused by one without and resolved by one with), the claim is
work-e2e-coder-0, the pin is one pin, and the fixtures name the Secrets,
Services and files the script makes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
run-pods-e2e.sh installs the operator chart in kind twice, first with
runPods.enabled=false and then without the flag (the upgrade), and runs the
real coder as a token coder with no run pods and a GitHub App coder with
coder.runPods, on the scripted model. It shows, step by step: the cluster-scoped
objects absent, then present, and the operator's can-i answers under both Roles;
the plain agent unchanged by the upgrade (objects, pod uid, start and restarts,
StatefulSet generation, config digest, asked again minutes later); the
operator's objects for the coder and none for the plain one; run-pods-admission.sh
run in the cluster as the agent's account; two runs, each in a run pod made from
the operator's template and started by the kubelet, the command run in it, each
pod mounting its own run only; the quota counting two pods, a third refused with
exceeded quota, a run that waits for a slot with no pod; the network policy (DNS
and a public address answer, a private address does not, while the coder's pod
reaches it); a run in a repository with a read-only mirror and a worktree; the
release; the plain agent asking for run pods and stopping; a deleted service
leaving nothing, run pods included, only the claim; no secret and no panic in
any answer or log.

House style of api-e2e.sh and chain-e2e.sh: only a kind-* context, a namespace of
its own that it labels and refuses without the label, undoes only what it made,
printf for bodies, logs from a successful kubectl logs holding a line of their
component, HELM_KUBECONTEXT, diagnostics without any line that holds a secret.

Walked under dash against a bare kube-apiserver v1.35.8 (RBAC, quota, admission
policy), the real operator running as its own service account, the real coder
(adam-rs main, the code of 3f714d0) and a Postgres, with a stand-in for the
kubelet: all steps pass (44 ok). Seven assertions were broken on purpose, each
in a fresh world, and each failed in the step it should. A real kubelet, kindnet
and the 3 GB pull are the job's to show.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
The job run-pods-e2e of operator-image.yml: a kind cluster of its own (the
node image of the other jobs, v1.35.8 by digest, kindnet enforces
NetworkPolicy), the operator image that passed its smoke test loaded into it,
and run-pods-e2e.sh. The coder, the run image and Postgres are pulled by the
kubelet by digest. The scripts job also checks and runs the scripted model's
tests with node.

The job does not gate push, as chain-e2e does not: it pulls a 3 GB image of
another repository pinned in this one, and run pods are off by default, so a red
run is a reason to look and does not put a broken default into the release. To
make it gate, add it to the needs of push (the header and the comment at push
say so). An owner decision, recorded in section 60a.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
Nine checks, each broken on purpose once in a copy of the tree and each failing
as written:
- its values render (an unknown registry setting makes them refuse);
- the PriorityClass, the policy and its binding for the namespace
  aap-e2e-runpods, the Role of run pods and the settings that name them (run
  pods turned off in the values);
- the quota is two pods, the class standard, the run image pinned by digest
  (a quota of three);
- no registry, no Platform API, no Ingress, no ExternalSecret (a registry
  added);
- the operator watches the release's namespace, runs the image of the job and
  never pulls it (a watched namespace set);
- one flag apart: the same values render no trace of run pods with
  runPods.enabled=false and all of them without it (a template that renders
  without the flag);
- the coder's pin is on two lines of run-pods.yaml that are one pin, and its
  digest is in no other file of deploy/ or .github/ (a tag changed; the digest
  copied into another file);
- the support manifest runs the coder's pin and the pin of postgres.yaml by
  digest, every object carries the script's label, none names a namespace (a
  label removed);
- the script's constants are the ones the values and the fixtures name (a name
  changed), and the model agrees with the script on the first line of the
  answer, the probe file, the release file and the keys (the first line
  changed).

388 checks in all (379 before).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
The kind end-to-end of run pods is built: section 60a gains the "D15b built"
note (what the script shows step by step, how it was walked where it was written
and what that is not, what only the job can prove, seven decisions for the
owner to confirm), the D15 row and the D15a notes are amended, the revision
history and the status paragraphs of CLAUDE.md and the root README say it, the
operator chart's README and tests/e2e/README.md describe the script, its
fixtures and its limits. D15 stays open until the job has run green in CI.

Verified 2026-10-27 and recorded as such: the coder's pin by an anonymous
manifest request to ghcr.io (a Docker v2 image manifest; its digest is the one
in run-pods.yaml); the script walked under dash against a bare kube-apiserver
v1.35.8, the real operator running as its own service account (no Forbidden in
its log) and the real coder, with a stand-in for the kubelet. Unverified, and
said so: a real kubelet, kindnet enforcing the egress rule, the exec over a
WebSocket on 1.35, the 3 GB pull, the timings.

Assertions broken on purpose in fresh worlds, each failing in its step: a quota
of three (step 4; with that check neutralised, step 6), run pods off in the
second install (3), a run pod that mounts the whole workspaces directory (5), a
run pod that reaches the Service address (7; passes with a warning under
AAP_E2E_CNI_MAY_IGNORE_POLICIES=1), a quota named for the plain agent (2), a
model that drops the command's output (8), a model whose journal keeps its key
(11).

Closes #36

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
D15a's list of what stays unverified said a run pod runs with a read-only
root. The template sets RuntimeDefault seccomp, no privilege escalation and
drops every capability, but no readOnlyRootFilesystem, as the coder chart's
does not either; Pod Security restricted still admits it.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
In kind, the first run of run-pods-e2e refused both token cases, but by the
volume rule: the ServiceAccount admission plugin adds the token as a
projected volume before the policy runs, which a bare API server does not.
The two cases now accept the token rule's message or the volume rule's; any
other refusal still fails. §60a notes it.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
…ing it

run-pods-e2e's first full run (run 40432460348, step 9) gave e2e-plain
coder.runPods and took it away again. The operator deleted the account
before it applied the workloads, so the StatefulSet still named it: its
next pod was refused (serviceaccount "e2e-plain" not found) and the
service stayed Degraded at 0/1. The run pod template ConfigMap was
still there after 120 s: it was left to remove_stale_config_maps, which
runs only once the service is Ready, and it could not become Ready.

Applying the workload without the account was not enough either. A
real API server keeps the deprecated serviceAccount of a pod template
through a server-side apply that leaves serviceAccountName out (one
value on write; the apply releases the field it owned and conversion
fills serviceAccountName from the other). Verified 2026-10-27 on a bare
kube-apiserver v1.35.8, StatefulSet and Deployment: after an apply
without the field both names still read the account; a merge patch that
nulls only serviceAccountName changes nothing; one that nulls both
removes them, and a later apply keeps them removed.

Now ensure revokes the right and the run pods first, applies the
workloads without the account, frees any workload whose answer still
names an account its render does not with that merge patch (sent only
then: a render without run pods sends nothing, and a workload an earlier
operator left naming a deleted account is freed by the next pass), and
only then removes the quota, the network policy, the parameters, the
run pod template and, last, the account. Nothing that is not ours is
deleted. The delete path is unchanged.

The fake API server copies the two-name rule of the real one and
records an account deleted while a workload names it; the new tests fail
on the code before. tests/cluster.rs has one case against a real API
server (run on the bare kube-apiserver above: it passes with the fix,
fails without it, and fails on a patch of only serviceAccountName). Each
new guarantee was broken once on purpose and a named test failed.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
Lockfile only; no Cargo.toml is touched. Both moves are semver-compatible and
built with the CI toolchain, 1.94.1 (rust-version 1.94).

- RUSTSEC-2026-0386 (tokio, unsound: AsyncFd::new and related constructors
  violate I/O safety): 1.53.2 -> 1.53.3
- RUSTSEC-2026-0382 (rustls-webpki, certificate verification may accept
  distrusted intermediates): 0.103.15 -> 0.103.16

Left, none has a fix we can take:
- RUSTSEC-2023-0071 rsa 0.9.10 (Marvin timing attack, no patched release) and
  RUSTSEC-2026-0387 crypto-bigint 0.5.5 (fixed only in 0.7.6, a different
  major than the elliptic-curve 0.13 and p256 0.13 stack that needs ^0.5): both
  come from jsonwebtoken 11.1.0 with its rust_crypto backend, which only the
  dev-dependencies of aap-auth-oidc switch on (tests/provider.rs links it
  beside aws-lc-rs). They are not in the normal or build graph of any target,
  so neither the operator nor the API binary links them, and the
  authenticator installs its own aws-lc-rs provider.
- RUSTSEC-2024-0449 unsafe-libyaml 0.2.11 (unmaintained, no patched release):
  serde_yaml_ng 0.10.0, the newest release, still depends on it; it is used by
  aap-operator and aap-api-server to read YAML. Changing the YAML library is
  not a lockfile change.

cargo-deny 0.18.9 `check advisories`: 5 findings -> 3 (the ones above).

Verified with 1.94.1: `cargo fmt --all --check`, `cargo clippy --workspace
--all-targets --locked -- -D warnings` and `cargo test --workspace --locked`
pass (1266 passed, 0 failed, 1 ignored). The cluster suites (AAP_TEST_KUBECONFIG)
skip without a cluster. The reqwest 0.12 pin of aap-auth-oidc is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
…by default (D15c)

adam-rs closed the network of a run pod (its ADR 0042, ff10395 and 7598255): the run
pods' NetworkPolicy allows DNS, the pods of an HTTPS-only egress proxy on 3128 and
extraEgress, and nothing else; the proxy (adam-egress, from the coder's own image) is a
Deployment, Service, ServiceAccount, allow-list ConfigMap, PodDisruptionBudget and
NetworkPolicy; the run-pod template carries HTTPS_PROXY, HTTP_PROXY and NO_PROXY. The
operator now makes the same, per agent, so a run pod made by the platform is no more open
than one made by the chart.

* aap-ports: EgressProxy; RunPods::network is required (a spec with run pods and no policy
  for them is not one); except_cidrs moved into the proxy. Breaks every RunPods literal.
* aap-domain: runPods.egressProxy in the settings (allow, extraAllow, extraNoProxy,
  replicas, ports, resources) with the coder chart's checks; networkPolicy.enabled is gone;
  the template's six proxy variables; the proxy described with adam's names kept here.
* aap-runtime-kubernetes: the six objects, applied before the policy that names it and the
  binding, never taken for stale, cleared before the run pods' policy, which is the last
  policy to go; the proxy is no workload of the service and its pods carry no managed-by.
* operator chart: runPods.egressProxy.* (the default list is the coder chart's), the
  render-time refusals, enabled: false refused; the operator's Role does not change.
* parity: goldens regenerated at adam-rs 7598255 (extract.py projects the proxy's objects;
  two cases move the proxy off its defaults); the env contract of a coder with run pods off
  did not move; the proxy is compared object by object in four cases.
* run-pods-e2e.sh: the proxy's objects, and a rewritten step 7 (controls, refusals of a
  direct connection, of the API server and of another pod, curl and npm through the proxy,
  example.com 403, the proxy's log, IPv6); the coder pin moves to sha-7598255 (verified
  2026-10-27 at ghcr.io).

Run where written: fmt, clippy -D warnings, cargo test --workspace (1280), render-check.sh
(411 checks; the new ones broken on purpose in a copy of the tree, 24 mutations, each
failing the check made for it), regen.sh --check, run-pods-admission.sh against a bare
kube-apiserver v1.35.8 (every case as expected, with the new template), the run-pods case of
the cluster tests against it (real server-side apply of the proxy's objects), shellcheck and
dash -n on the script, and its jq programs against the operator's objects. Unverified until
CI has run the job: that a kubelet starts the proxy, that kindnet drops the direct
connections, and curl and npm through the proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
§60a gains the D15c built note: what is built and how it is proven, what only the job can
prove, and ten decisions for the owner (the allow-list is the deployment's; an image
without adam-egress, which defaultAgentImage is, is documented and not refused; the proxy
is no workload of the service; networkPolicy.enabled is gone; the proxy's pods carry no
managed-by; the order; pods made before the upgrade lose their way out; no scheduling for
the proxy; the checksum's value; the Role does not change). §59a's field row, the D15 rows
and amendments, the Risks, P-021 and §93's egress question follow, and the revision history
gets its row. D15b's job has run green (run 40435825771, verified 2026-10-27 with gh run
view); CLAUDE.md and the root README say so and say what D15c proves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
@github-actions

Copy link
Copy Markdown
Contributor

✅ AI Governance check passed

This PR declares AI usage, references a source of truth, and provides verification evidence. Thank you.

@changeset-bot

changeset-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5c94029

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

The coder pinned since D15c (adam-rs 7598255) carries adam-rs #133: a run pod mounts every slot of its run (a project, a
worktree) as a mount of its own on top of the run's directory, so the pod cannot rename the slot's directory. The first
run of run-pods-e2e on that pin (run 40482371992) failed on the assertion that A's pod mounts only its run's directory:
the pod mounted that and A's project, iso-a. A and B now expect the run's directory and their project's slot (and
A's annotation the two lines, the slot's with its generation), C its run's directory and its worktree's slot r1, and
still nothing else of the volume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZQyuMx8GXkV9VrUGeHMno
@stephane-segning
stephane-segning merged commit 9a71f59 into main Oct 27, 2026
25 checks passed
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.

2 participants