Skip to content

Sync upstream API authorization and worker runtime updates - #85

Merged
EItanya merged 32 commits into
mainfrom
sync/upstream-main-20261016
Oct 16, 2026
Merged

EItanya merged 32 commits into
mainfrom
sync/upstream-main-20261016

Conversation

@EItanya

@EItanya EItanya commented Oct 16, 2026 •

Copy link
Copy Markdown

Merge 31 upstream commits through agent-substrate/substrate@c4518a1 into the fork. This brings in unified pause/suspend handling, dynamic worker runtime registration, bounded snapshot uploads, API authorization, and the go.ate.dev/substrate module path.

Preserve the fork's agentgateway dataplane, optional atelet TLS, workflow deadlines, and immediate worker-cache updates after suspend/pause. Update Helm for the required API config, authorization defaults, worker ServiceAccount permissions, and CRDs, and migrate the fork's imports and release build paths.

Breaking change

  • Upgrade clients and components together, and move Go imports to go.ate.dev/substrate for the new worker and snapshot APIs.
  • The API server now requires --config-file. Helm manages it; replace --log-level in ateApi.extraArgs with ateApi.logLevel.
  • API authorization is enabled by default. Helm bootstraps the release's shared ate-client identity for development. Shared installs should set ateApi.authorization.bootstrapOwners to operator principals and grant tenant access explicitly. See the chart upgrade notes and docs/authorization.md.
  • Kagent's mTLS controller identity is separate from ate-client and needs an explicit grant. Its current reconciliation calls CreateAtespace when creating actor templates, which requires global owner access even if the atespace already exists. Restricting kagent to atespace editor requires pre-provisioning atespaces and adapting that creation path; global worker inventory also needs separate operator access.

Please apply the breaking-change label.

Validation passed: env -u NO_COLOR go test -race -p 1 ./..., hack/verify-all.sh, Helm lint, all 18 chart tests, manifest rendering, and CRD parity. The race suite passed on retry with serial package compilation after the first run exhausted the shared 20 GiB temporary filesystem during concurrent compilation. Local runtime E2E has not been run; the PR workflows cover agentgateway with gVisor and microVM.

  • Tests pass
  • Upgrade documentation is included

Merge this PR with a merge commit to preserve upstream ancestry for the next sync.

ILLUM1N0X and others added 30 commits October 15, 2026 16:31
…ims (agent-substrate#2671)

Part of agent-substrate#777

The objectstore control plugin will verify actor JWTs minted by
ate-api-server. No component serves the JWKS yet, ate-idp-server has not
landed, so the verifier needs keys from somewhere other than discovery,
and it must not be handed the signing keys to get them.

`oidcjwt.NewVerifier` now takes a `KeySource`. `HTTPKeySource` is the
existing discovery fetch. `FileKeySource` reads a JWK set from a file
with the same parser. The `actor-id-jwt-pool` Secret carries that set
under `jwks` next to `pool`: ate-setup, `make-jwt-pool`, and
`add-jwt-key` write both in one update, and ate-setup brings `jwks` in
step with the pool on every deploy, for a Secret from before the key
existed or one rewritten by an older tool. The sidecar will mount only
the `jwks` item, in the manifests PR.

`Claims` gains `SubstrateClaims`, decoded only after the signature
check: `Kind`, actor or worker-for-actor, and the claim it names, with a
token carrying both refused. Decoding them is opt-in through
`WithSubstrateClaims`, which only the actor-id verifier sets, so tokens
from other issuers verify as on main whatever they carry under those
names.

No behavior change for ate-api-server, its call site wraps the discovery
client in `HTTPKeySource`.
…odes (agent-substrate#2495)

Dense nodes starve the kubelet's heartbeat; after the 300 s toleration
the taint manager evicts the node's pods.

- `ATE_KIND_RESERVED_CPU=<cpus>` adds `systemReserved.cpu` and
`kubeReserved.cpu` to the kind `KubeletConfiguration` patch. Unset,
nothing changes.

Not verified at the density that triggered it. `shellcheck` clean.
…nt-substrate#2622)

Part of Actor Lifecycle v2 agent-substrate#2069. Second PR for agent-substrate#2570, after agent-substrate#2612 added
the API.

This serves `WorkerService.ReportActorSnapshotState`. With asynchronous
suspend, atelet uploads a suspended actor's snapshot and later detaches
the actor from its node, on its own timers. The report is how
ate-api-server learns about both. Nothing sends reports yet; that comes
with atelet's asynchronous upload work.

A report lists the snapshots atelet holds for an actor on its node.
ate-api-server acts on two things in it:

- **`latest_durable` means an upload finished.** If it names a recorded
snapshot with the same uuid and upload URI, that snapshot becomes
`COMPLETED`, with its manifest's fidelity.
- **A report with neither `latest_local` nor `uploading` means atelet
deleted its copies (a detach).** It covers this node's snapshots up to
the reported `latest_durable` generation. Those localities are cleared,
and a suspended actor with no snapshot left on the node is released from
it.

ate-api-server ignores the rest of the report.

<details open>
<summary>Flowchart: how a report is applied</summary>

```mermaid
flowchart TD
    A["WorkerService.ReportActorSnapshotState(ReportActorSnapshotStateRequest)<br/>TODO: atelet does not send it yet"] --> B{"Caller presents atelet's certificate?"}
    B -- no --> X1["Unauthenticated or PermissionDenied"]
    B -- yes --> C{"Request passes validation?"}
    C -- no --> X2["InvalidArgument, including a report with none of<br/>latest_local, uploading, and latest_durable"]
    C -- yes --> E{"Actor exists, actor_uid equals Actor.metadata.uid,<br/>and Actor is on the caller's node?"}
    E -- no --> X4["NotFound"]
    E -- yes --> D{"Actor's lease free?"}
    D -- no --> X3["Aborted, atelet retries"]
    D -- yes --> E2{"Same check again, under the lease"}
    E2 -- no --> X4
    E2 -- yes --> S{"status.state?"}
    S -- "CRASHED" --> X9["FailedPrecondition"]
    S -- "SUSPENDING, RESUMING,<br/>DELETING, REVERTING" --> X10["Aborted, atelet retries"]
    S -- "any other state" --> X12["Internal"]
    S -- "SUSPENDED or RUNNING" --> G{"latest_durable set?"}
    G -- no --> K{"latest_local and uploading both unset?"}
    G -- yes --> GF{"At or below the latest COMPLETED generation?"}
    GF -- yes --> K
    GF -- no --> GR{"Its generation is in status.snapshots?"}
    GR -- "no, and not newer than<br/>last_assigned_generation" --> K
    GR -- "no, and newer" --> X5["FailedPrecondition"]
    GR -- yes --> U{"Same uuid, and an upload URI is recorded?"}
    U -- no --> X6["FailedPrecondition"]
    U -- yes --> V{"Reported URI equals the recorded one?"}
    V -- no --> X7["InvalidArgument"]
    V -- yes --> H["Mark it COMPLETED"]
    H --> K
    K -- yes --> K2{"Latest snapshot on this node, up to<br/>latest_durable's generation, is durable?"}
    K2 -- no --> X8["FailedPrecondition, nothing recorded"]
    K2 -- yes --> J["Detach: clear this node's localities up to that generation.<br/>Clear assigned_node if status.state is SUSPENDED, it is this node,<br/>and no snapshot is left on this node"]
    K -- no --> P{"status changed?"}
    J --> P
    P -- no --> Q["Done, nothing written"]
    P -- yes --> R["Write the actor"]
```

</details>

## Outcomes

| Report | Result |
| --- | --- |
| Any report for a `CRASHED` actor | `FailedPrecondition` |
| Any report for a `SUSPENDING`, `RESUMING`, `DELETING`, or `REVERTING`
actor | `Aborted`, retried |
| `latest_durable` for a recorded snapshot with matching uuid and upload
URI | That snapshot becomes `COMPLETED`, with the fidelity its manifest
records |
| `latest_durable` at or below the latest `COMPLETED` generation | No
change |
| `latest_durable` for a generation `status.snapshots` no longer has,
such as one a revert discarded | No change |
| `latest_durable` for an unrecorded generation newer than any assigned
| `FailedPrecondition` |
| `latest_durable` with another uuid, or for a snapshot with no recorded
upload URI | `FailedPrecondition` |
| `latest_durable` uploaded to a URI other than the recorded one |
`InvalidArgument` |
| None of `latest_local`, `uploading`, and `latest_durable` set |
`InvalidArgument` |
| Neither `latest_local` nor `uploading` set (a detach) | Clears this
node's localities up to the `latest_durable` generation. Clears
`assigned_node` if the actor is `SUSPENDED` there and no snapshot is
left on this node. Refused with `FailedPrecondition` while the latest
covered snapshot isn't durable |
| A late detach, after a newer snapshot on this node | Leaves the newer
snapshot and `assigned_node` alone |
| A detach that's refused | Nothing is recorded, including an upload the
same report carries |
| A report that changes nothing | Nothing is written |

## Notes

- **Nothing is pruned or deleted.** The existing workflows prune old
snapshots and delete their objects at the next suspend or delete.
- **A detach covers only generations up to its `latest_durable`,** so a
late or repeated report can't clear a newer snapshot. Repeating any
report is safe.
- **A detach waits for a durable snapshot.** Clearing the locality of a
snapshot that isn't durable would let the actor resume from an older
one.
- **A report applies as a whole or not at all.** atelet's detach uploads
the latest snapshot first, so a refused detach is rare.
- **The actor is checked before the lease is taken,** and again under
it, so an atelet can't hold the lease of an actor on another node.
- **Reports apply only to a `SUSPENDED` or `RUNNING` actor.** A
transitioning actor's snapshots belong to its workflow, and a `CRASHED`
actor's wait for a revert.
- **Every UID or node refusal is `NotFound`,** so an atelet learns
nothing about actors on other nodes.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
Nothing in `.github/workflows` or `hack/verify` ran the benchmarking
Python tests. The four suites under `benchmarking/analysis`,
`automation`, `locust/unit_tests` and `nighthawk-ingress/tests` only ran
when someone remembered to, and the egress-tunnels benchmark that
follows adds a fifth. A test nothing runs is not a test.

`hack/verify/benchmarking-python.sh` gives each suite a venv from its
own `requirements.txt` plus pytest and runs it from the suite directory,
because the tests import their modules by bare name; pytest rather than
unittest because the nighthawk-ingress tests are plain functions. Before
running, it stubs `docker`, `gcloud` and `kubectl` on `PATH` to fail and
points `KUBECONFIG` at nothing, so a test that escapes its mocks cannot
reach a cluster or a registry with the developer's credentials.
`hack/verify-all.sh` picks the script up by glob.

Testing: the script passes 107 tests across the four suites locally;
shellcheck, boilerplate and the rest of `hack/verify/*.sh` are green.

Prepared with help from an AI coding agent; reviewed and verified by the
author.
Pause and suspend are being merged into one workflow, so PauseActor and
SuspendActor become one lifecycle operation. Label both as "suspend" in
metrics, state-change events and crash records, and drop the "pause"
value from the ate.actor.operation.name registry.
Match the actual behavior of handling unknown fields: reject if server
side doesn't know, preserve if client doesn't know.

Found this when iterating version skew doc.

Part of agent-substrate#1185

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
…trate#2655)

Replaces the stock `envoyproxy/envoy` image reference in
`manifests/ate-install/atenet-router.yaml` with the
`${ENVOY_DATAPLANE_IMAGE}` placeholder so both `atenet-egress` and
`atenet-router` deploy the same `envoy-dataplane` image built from
`cmd/dataplane/envoy/Dockerfile` (or pinned from a release via
`--image-repo`).
- Updates `renderAtenetRouterManifest` and `renderSystemManifests` in
`cmd/ate-setup` to resolve and substitute `${ENVOY_DATAPLANE_IMAGE}`.
- Caches resolved Dockerfile image references on `steps.Env` so
deployments rendering multiple manifests (`atenet-router` +
`atenet-egress` + system overlay) only build or pin `envoy-dataplane`
once.
- Extends `TestRenderAtenetEgressManifestPrebuilt` to verify
`${ENVOY_DATAPLANE_IMAGE}` substitution across router, egress, and
system manifests.

Part of agent-substrate#1330

AI was used in the development of this change.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
…tity signer (agent-substrate#2389)

We should rely on the checks enforced by kube-apiserver. We don't need
to double-check whether the pod exists.
    
Upcoming e2e test changes will rely on the fact that non-Kubelet clients
are allowed to create arbitary PodCertificateRequests, even if the pod
or service account they reference does not exist. This will allow the
test code running in Github Actions (or on the developer's machine) to
authenticate to the router running within the cluster.
…ogs stale snapshot reports (agent-substrate#2698)

Part of Actor Lifecycle v2 agent-substrate#2069, for agent-substrate#2570.

atelet's `Checkpoint` response carries the actor's snapshot state.
ate-api-server discards it today. This adds what's needed to apply it,
with the same rules `ReportActorSnapshotState` uses. It also logs
reports that change nothing.

- **Conversion.** atelet's `SnapshotReport` nests an atelet `Snapshot`.
The control plane's carries the same fields inline, with its own
fidelity enum and object type. `snapshotReportFromAtelet` converts one
to the other, and `fidelityFromAtelet` maps the fidelity back.
- **`applyCheckpointState`** records an upload the state says finished,
through `applyLatestDurable`. atelet echoes the `Snapshot` it was asked
for, so the fidelity recorded is the one requested. Unlike a report, a
state with no local snapshot is not a detach, since the workflow that
called `Checkpoint` records where its snapshot lives.
- **Stale reports are logged at Info.** A `latest_durable` older than
the latest `COMPLETED` generation, or at a generation the actor no
longer records, changes nothing. That's expected for a late report, so
each case is logged rather than refused. A repeat of the latest isn't
logged, since atelet retries reports freely.

Nothing calls `applyCheckpointState` yet. A follow-up applies it in the
suspend workflow.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
…apshot and frees the node (agent-substrate#2725)

Part of Actor Lifecycle v2 agent-substrate#2069.

**API change (atelet-internal):** adds
`Atelet.DetachActor(DetachActorRequest) returns (DetachActorResponse)`
to `internal/proto/ateletpb`. No public API changes.

The control plane needs to detach an actor from a node on demand. The
first caller is resuming an actor on another node when its latest
snapshot is only local: the detach uploads that snapshot first, so the
actor can restore from object storage anywhere.

`DetachActor` makes the actor's latest local snapshot durable, then
deletes the actor's local snapshots on the node. atelet also runs this
detach on its own once a suspended actor has stayed on the node for its
detach delay. The RPC:

- does nothing when atelet holds no local snapshot for the actor;
- waits for an operation already in progress on the actor, such as an
upload or a detach;
- returns `ABORTED` while a Run, Restore, or Checkpoint of the actor is
in progress or the actor runs on the node;
- returns the actor's `ActorSnapshotState`, with `latest_local` unset
and `latest_durable` set to its latest snapshot, if atelet knows one.
That's the shape of a detach report, so the control plane can apply it
with the `ReportActorSnapshotState` rules.

The request names the actor by atespace, name, and UID. Golden actors
aren't rejected, since an `IMMEDIATE` checkpoint keeps a local copy too.

This PR adds the API and its validation only. atelet embeds
`UnimplementedAteletServer`, so it returns `UNIMPLEMENTED` until the
snapshot tracker's detach lands. The handler then calls the tracker's
detach and returns its state.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
…CS tokens (agent-substrate#2672)

Part of agent-substrate#777

Two packages the control plugin's credential exchange will use. Nothing
calls them yet, the endpoint comes in a follow-up PR.

`actorscope.FromActor` reads `status.snapshots` and returns the prefixes
an actor may touch. Every durable snapshot URI names the storage
location it sits under, and for each location the actor gets read-write
on its own prefix there, built with the same helper ate-api-server
places snapshots with, so the grant exists from the actor's first
generation on. Each completed TAG-owned generation adds read-only on the
tag's prefix, taken as is. This parses URIs until `Actor.status` carries
a base snapshot URI, as agreed with Julian. An owner that is neither
ACTOR nor TAG is an error, so a new kind is never silently dropped. The
status is otherwise taken as ate-api-server wrote it.

`gcsboundary.FromGrants` turns grants into Credential Access Boundary
rules, one per prefix, `objectAdmin` for read-write and `objectViewer`
for read-only. `gcsissuer` exchanges the pod's credential at STS through
`golang.org/x/oauth2/google/downscope` and caches tokens by actor UID in
a bounded LRU. Each entry keeps the grants it was minted for and is
served only when they cover the request, so the grants narrowing after
the first suspend is a hit and a repoint to a new location mints. That
means the exchange derives the grants, `GetActor` included, before it
consults the cache. A cached token is served only while it has twenty
minutes left, a minted token shorter than that is returned but not
cached, and a forced request skips the cache for the node plugin's retry
after a 403. The downscoped token expires with the source credential. On
GKE the metadata server serves a cached source token until shortly
before it expires, so for a stretch at the end of each hour every
exchange reaches STS and returns a token shorter than the cache floor,
which is handed out but not cached. Minting the source directly is a
follow-up.

Verified on a GKE cluster as the ate-api-server service account: a token
with these rules reads and writes inside its prefixes and gets 403
outside them, and its expiry equals the source token's.

`golang.org/x/oauth2` moves from an indirect to a direct dependency and
`google/downscope` is vendored.
…e#2021)

Initially, it hosts nothing but logging config.

A follow-on PR will move the authentication config into the unified
config.
…agent-substrate#2678)

Part of agent-substrate#2078 (Envoy dataplane, @EItanya @howardjohn for agw)

The router accepted any podidentity cert in the trust domain as atunnel,
so any pod in the cluster could pose as a worker. It now requires the
exact SPIFFE ID of the worker each request is routed to.

- atecontroller's syncer records the worker pod's SPIFFE ID on the
Worker (new `worker_spiffe_id`, built from the pod's namespace and
ServiceAccount), and ateapi copies it into the actor's worker
assignment. ateapi rejects a value that isn't a valid SPIFFE ID.
- ext_proc passes that ID to Envoy as is and refuses to route without
it. Envoy requires that exact SAN, on both the :443 and CONNECT paths.
`--upstream-spiffe-prefix` is removed; the cluster still checks the
trust domain.
- The Worker carries the SPIFFE ID rather than the ServiceAccount, so
the router doesn't need to know how the ID is built. A different ID
scheme only changes atecontroller.
- WorkerPools that don't set serviceAccountName now run as an
`ateworker` ServiceAccount the controller creates, instead of `default`,
so the worker's ID isn't shared with every other pod in the namespace.

Remaining gap: another pod running as the same ServiceAccount in the
same namespace can still pose as the worker. Closing it needs a signer
that puts the pod in the worker's SPIFFE ID; atecontroller would then
record that ID, with no router change.

#### Breaking change

- The router rejects workers registered before this change until their
pods are replaced. Pools on the default ServiceAccount roll
automatically; pools that set `spec.template.serviceAccountName` need a
restart. Upgrade atecontroller and ate-api-server before atenet-router.
- Workers now run as an `ateworker` ServiceAccount by default instead of
`default`. RBAC, Workload Identity bindings and imagePullSecrets
attached to `default` no longer apply to them; set `serviceAccountName:
default` or move those to `ateworker`.
- atenet-router no longer has `--upstream-spiffe-prefix`.
- atecontroller now needs create and patch on serviceaccounts (included
in the generated role).

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
…t-substrate#2693)

Fixes agent-substrate#2685

The MITM and cleartext chains write one JSON access-log line per actor
request. At 2,500 req/s on one replica that is 1.2 MB/s, and with the
kubelet's default rotation only about 45 s of history survives, so a
failure from a minute ago is already gone.

Both chains now log a request only when its status is 400 or above, when
it carries a response flag, or as a 1% sample of the rest. At the same
rate the log drops to 25 lines/s and the rotation window is over an
hour, with every failure still recorded. The threshold and the sample
rate are runtime keys, `access_log.min_status` and `access_log.sample`,
so an operator can restore full logging at the admin port without a
rollout. The log no longer counts successful requests per host;
`docs/metrics/substrate.yaml` records that.

Testing: the manifest test compares each per-request log's filter
against the expected YAML and asserts no other access log carries one.
`go test -race ./cmd/atenet/...` and `hack/verify/*.sh` pass.

Prepared with help from an AI coding agent; reviewed and verified by the
author.
…gent-substrate#2652)

Expand the top-level README messaging to give security equal billing
with performance. Add a dedicated Security Model section highlighting
VM-grade sandboxing, per-agent SPIFFE identity, zero ambient
credentials, default-deny networking, and pluggable edge policy.

Also some word cleanup on the performance claims.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
… is assigned to (agent-substrate#2702)

MintWorkerActorCertificate authenticated the atelet but ignored its
node, and atelet did not say which worker was asking. Any node, or any
worker on the actor's node, could mint the actor's certificate from its
name and UID. This applies the same check MintWorkerActorJwt got in
agent-substrate#2668.

- ate-api-server mints only when the caller's node hosts the actor and
the requesting worker is the one assigned to it. Otherwise it returns
NotFound, before the stale-UID check.
- `MintWorkerActorCertificateRequest` gains a required `worker` field.
atelet fills it from the caller's certificate, as it does for
RequestActorSuspend. A paused actor has no worker, so it gets no
certificate.
- `worker` is required with no fallback; upgrading atelet before
ate-api-server, as docs/upgrade.md already orders it, covers the skew.

Tested with unit tests for the node, worker, paused, release/rebook and
stale-UID cases, plus controlapi tests against a real PostgreSQL store.
`go test -race ./cmd/ateapi/internal/... ./cmd/atelet/...` passes.
…hestrator (agent-substrate#2717)

The actor CPU limit was never piped through the benchmark deploy path.
Now that every ActorTemplate must declare one, the templates default to
1 vCPU, which caps how many actors a worker hosts; this makes it
configurable the same way actor memory already is.


Fixes #<issue_number_goes_here>

> It's a good idea to open an issue first for discussion.

- [ X ] Tests pass
- [ X ] Appropriate changes to documentation are included in the PR
…strate#2323)

Each time an actor finishes its trajectory, sweperf now deletes it and
starts
a new actor to run the trajectory again. Before, the same actor replayed
the
trajectory over and over.

The last cycle is not suspended: the actor is deleted straight from
running
with DeleteActor(AnyState). A failed delete is retried once with a fresh
timeout, then logged with the actor name. The atespace is created once
per
worker, and again only if CreateActor reports it missing
(FailedPrecondition).
Liveness on a new actor is polled at --sweperf-poll-interval-ms, and a
failed
start is logged with its actor and template.

Shutdown:
- It logs "sweperf shutdown started", blocks new iterations and cancels
the
  ones in flight, including slow liveness polls.
- It waits (bounded) for in-flight iterations, then deletes every
remaining
  actor directly within the pod's termination grace period.
- Calls cut short by this cancellation record no failure rows, and no
task
rows are recorded once shutdown has begun, so the end of a run will not
  show up as a burst of failures.

This also fixes a liveness failure decrementing locust_users twice.

Metric changes (README updated):
- Cycle 1 runs on a freshly started actor, so its no-op resume is not
  recorded: one fewer ResumeActor sample per task.
- A task of n cycles records n-1 SuspendActor samples.
- DeleteActor now times deleting a running actor, including sandbox
  teardown, so it is not comparable with earlier runs.

Follow-ups (since this PR is large):
- Bring back ActorStartup that liveness polls at the job interval.
- Bound resume retries: a resume that keeps failing retries the same
cycle
  forever. That was already true before this change.
- Workers tend to recreate actors around the same time, which can cause
  bursts of creates.
- Record actor startup failures. An actor whose sandbox never answers
  liveness is deleted and logged, but no failure row is recorded. That
was **already true**, but since every task now starts an actor, startup
  failures can happen per task and are invisible in failure ratios. The
  ActorStartup row will add both its latency and its failures.


Tested with go test -race and on a GKE cluster with the astropy
template.

- [X] Tests pass
- [X] Appropriate changes to documentation are included in the PR
When several agents idle-suspend at the same time, their snapshot
uploads allocate compression and storage buffers independently. Ten
simultaneous snapshots with eight files each can therefore produce a
large memory spike in the objectstore plugin.

This adds a shared upload memory budget across requests, with a 512 MiB
default configurable through `--upload-memory-budget`. Uploads reserve
capacity for compression and S3/GCS buffers before starting, reduce
compression concurrency when needed, and wait when the budget is
exhausted. Cancellation and failures release capacity after workers
finish. Manifests included with data files are uploaded only after those
files succeed.

The budget limits reserved upload buffers and compression state; it is
not a process RSS limit. Queuing trades burst throughput for lower peak
memory. The plugin README documents configuration, minimum budgets, and
accounting limits.

Part of agent-substrate#2553. This addresses simultaneous snapshot uploads and is
independent of the small-file allocation optimization in agent-substrate#2633.

Validation:

- `hack/verify-all.sh` passed.
- Object storage, plugin, and affected atelet snapshot/upload tests
passed twice with the race detector. Coverage includes concurrent
snapshots, cancellation, failures, manifest ordering, and S3/GCS
buffering through mocked endpoints.
- The full race suite (`go test -race -p 2 ./... -timeout=5m`) hit the
existing `TestServerTLSConfigReloadsCACertsWithoutRestart` flake in the
benchmarking fake-atelet, reproduced on unchanged upstream (3/5
attempts). The production atelet package passed.
- Color-formatting tests failed with this shell's `NO_COLOR=1` and
passed twice with that variable unset.
- No live cluster or cloud throughput tests were run.

One synthetic run with 80 small files and a simulated 20 ms destination
stall measured the following tradeoff:

| Admission | Sampled peak heap growth | Batch completion |
| --- | ---: | ---: |
| Shared 512 MiB budget | 279.7 MiB | 225.146 ms |
| All 80 uploads admitted | 1,305 MiB | 44.592 ms |

These are compression backpressure measurements, not cloud throughput or
process RSS measurements.

- [ ] Tests pass (affected tests pass; existing TLS flake noted above)
- [x] Appropriate changes to documentation are included in the PR

---------

Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
… actor cgroup OOM (agent-substrate#1917)

## Summary

When a gVisor (`runsc`) sandbox is paused or suspended under a tight
memory limit (`ActorMemory`), writing the snapshot (`pages.img`) inside
the actor's own cgroup doubles memory pressure (`guest RAM + snapshot
page cache`), causing severe swap thrashing or cgroup OOM-kills.

This PR decouples snapshot I/O from the actor's memory cgroup and
deterministically reclaims snapshot page cache after restore:

1. **Checkpoint Cgroup Isolation**: Run `runsc checkpoint` in a
dedicated sibling cgroup(-_ckpt) so snapshot page cache is not charged
against the actor's memory limit during pause or suspend.
2. **Post-Restore Cache Reclamation**: After `runsc wait -restore`
completes, evict snapshot page cache (guarded by `cachestat(2)` so
`FADV_DONTNEED` never forces writeback). Staged files in `restore_dir`
are removed only when `preserve_restore_dir` is unset (GCS
`restore-state/`); for local resume (agent-substrate#1876) the files are kept and only
their cache is evicted.

## Notes

Rebased on agent-substrate#1876: file cleanup now keys off `preserve_restore_dir`
instead of `Nlink`, since a local restore no longer has a hard-link.
`DATA_ON_GOLDEN` case dropped after agent-substrate#2105.

## Benchmark

### 1. Memory-Constrained Local Pause/Resume
(`glutton_1user_500mi_gvisor_pause`)
1 user, 500 MiB active guest RAM (64 MiB dirtied before each pause), 600
MiB actor memory cgroup limit (`ActorMemory`), 1m duration.

| Metric | Baseline (`main`) | With Fix (agent-substrate#1917) | Improvement |
| :--- | :---: | :---: | :---: |
| **Completed Cycles (in 60s)** | 6 (1 actor OOM-crashed) | **23** |
**3.8x throughput** |
| **`PauseActor` Failure Rate** | 25.0% (2/8 failed, `runsc checkpoint`
OOM) | **0.0% (0/23)** | **Eliminates checkpoint OOM** |
| **`PauseActor` P50 / Avg / P95** | 3,400 ms / 3,245 ms / 5,800 ms |
**620 ms / 980 ms / 2,900 ms** | **-82% P50 (-70% Avg, -50% P95)** |
| **`ResumeActor` P50 / Avg / P95** | 3,000 ms / 2,957 ms / 3,000 ms |
**370 ms / 404 ms / 660 ms** | **-88% P50 (8.1x faster)** |

### 2. Memory-Constrained GCS Suspend/Resume
(`glutton_1user_500mi_gvisor_suspend`)
1 user, 500 MiB active guest RAM (64 MiB dirtied before each suspend),
600 MiB actor memory cgroup limit (`ActorMemory`), 1m duration.

| Metric | Baseline (`main`) | With Fix (agent-substrate#1917) | Improvement |
| :--- | :---: | :---: | :---: |
| **Completed Cycles (in 60s)** | 2 (5 actors OOM-crashed) | **9 (10
suspends)** | **4.5x–5x throughput** |
| **`SuspendActor` Failure Rate** | 83.3% (10/12 failed, `runsc
checkpoint` OOM) | **0.0% (0/10)** | **Eliminates checkpoint OOM** |
| **`SuspendActor` Latency (successful)** | 7,700 ms – 8,844 ms |
**2,800 ms P50 / 3,105 ms Avg / 4,400 ms P95** | **-64% to -68%** |
| **`ResumeActor` P50 / Avg / P95** | 1,800 ms / 1,715 ms / 1,815 ms
($N=2$) | **1,500 ms / 1,605 ms / 2,300 ms ($N=9$)** | **-17% P50 (-6%
Avg)** |

#### 3. Multi-User Local Pause/Resume
(`glutton_mem_512mi_4_user_gvisor_pause`)
4 concurrent users, 512 MiB active guest RAM (64 MiB dirtied before each
pause), 768 MiB actor memory cgroup limit (`ActorMemory`), 1m duration.

| Metric | Baseline (`main`) | With Fix (`agent-substrate#1917`) | Improvement |
| :--- | :---: | :---: | :---: |
| **Completed Cycles (in 60s)** | `20` (2 actors OOM-crashed) | **`27`**
| **+35% throughput** |
| **`PauseActor` Failure Rate** | **`15.4%`** (4/26 failed, `runsc
checkpoint` OOM) | **`0.0%`** (0/30) | **Eliminates checkpoint OOM** |
| **`PauseActor` P50 / Avg / P95** | `3,100 ms / 3,272 ms / 8,600 ms` |
**`1,000 ms / 1,008 ms / 1,400 ms`** | **-68% P50 (-69% Avg, -84% P95)**
|
| **`ResumeActor` P50 / Avg / P95** | `3,800 ms / 3,698 ms / 6,000 ms`
($N=20$) | **`5,000 ms / 4,365 ms / 5,600 ms`** ($N=27$) | **-7% P95** |

> **Note:** `ResumeActor` P50/Avg is slightly higher because `main`
OOM-crashes 2 of the 4 actors during `PauseActor`, whereas with `agent-substrate#1917`
all 4 actors stay alive and share the worker pod's 2-vCPU limit
concurrently (+35% more completed cycles).

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
…gent-substrate#2379)

Each SWE-perf task is now one line in
`manifests/sweperf/sweperf-images.json`: the name, the image pinned by
digest, and the step count. `generate_sweperf_templates.py` renders
`sweperf-template.yaml.j2` into `manifests/sweperf/generated/`. Pass
`'*'` to generate every task, or give a task name to generate just that
one. It rejects catalog entries that are incomplete or not pinned by
digest. The generated templates are checked in, and `--check` fails if
any are missing, out of date, or no longer in the catalog. To add a
task, add it to the catalog, run the generator and commit the new file.

`deploy.sh` now reads `sweperf-*` templates from
`manifests/sweperf/generated/`. Before touching the cluster, it checks
that every requested template has a manifest, and if a sweperf one is
missing it points you to the generator. Because images are pinned in the
catalog, the `${SWEPERF_IMAGE}` substitution is gone.

The hand-written `swebench-astropy-7336` template is replaced by the
generated `sweperf-astropy-7336`, which is now the default
`--sweperf-template`. The client still reads the step count from
`--sweperf-total-steps`, so set it to match the catalog. The template
keeps the value only for reference. Reading it straight from the
template will be a follow-up.

Testing: `make verify` and `test_generate_sweperf_templates.py` pass,
and `--check`. On GKE I deployed `sweperf-astropy-7336` and
`sweperf-django-11099`, both got golden snapshots, and a Locust run
created actors from the astropy template.

- [X ] Tests pass
- [ X] Appropriate changes to documentation are included in the PR
agent-substrate#2707)

Fixes agent-substrate#2676.

If worker deletion or another crash writer marks an actor CRASHED after
checkpoint succeeds but before finalize re-reads it, SuspendActor
currently overwrites that crash with SUSPENDED, while PauseActor reports
success for the CRASHED actor.

Both finalize steps now require the re-read actor to be SUSPENDING
before validating snapshots, releasing the worker, or writing the final
state. Otherwise they return FailedPrecondition and preserve the
existing state, crash reason, and snapshot records. The existing version
precondition still rejects a concurrent crash after the read with
Aborted. Completed suspend and pause calls retain their existing success
paths at workflow entry.

The full-workflow regression now checks FailedPrecondition and
preservation of the original crash in both modes, updating the behavior
pinned by agent-substrate#2688. The already-crashed pause fixture also clears
WorkerAssignment, matching a real crash; it previously reached
FailedPrecondition only through the missing-node branch.

Validation of the production change on Linux with CI=true and real
PostgreSQL test containers:

- RED: before the production fix, the corrected already-crashed pause
test and both modes of the full-workflow regression fail because the
calls incorrectly return OK.
- GREEN: `go test -race -count=1 -json ./...` passes: 158 test-bearing
packages and 9,670 test entries, including all 763 controlapi entries
and 169 API functional entries.
- `go build ./cmd/ateapi ./cmd/atelet ./cmd/kubectl-ate`,
`hack/verify-all.sh`, and the tools/apitool race tests pass. The
verifier leaves the source tree unchanged.
- The full suite skips 41 tests requiring root privileges, IPv6
loopback, or configured external object storage. The affected PostgreSQL
regressions do not skip.

E2E follow-up:

The original CI passed both gVisor suites but failed both microVM suites
in `TestGracefulWorkerTerminationSuspend`. That test unconditionally
expected SuspendActor to succeed after worker deletion, even when
deletion had already crashed the actor.

The test now ties the RPC result to the stored state. OK requires
SUSPENDED with no crash record. FailedPrecondition or Aborted requires
CRASHED, a crash reason, and the previous completed durable snapshot.
Both outcomes require the worker and its assignment to be removed. All
other errors fail the test.

Follow-up validation on Linux with CI=true and real PostgreSQL and
envtest:

- The four affected controlapi regressions pass with `-race -count=3`.
- All 763 controlapi test entries and all 169 functionaltest entries
pass, with no skips. The functional package was rerun after initializing
the isolated source directory's Git metadata, which envtest setup
requires.
- `go test -race -c ./internal/e2e/suites/demo` and `go vet
./internal/e2e/suites/demo` pass. Native compilation, gofmt, and
whitespace checks also pass.
- Real runtime E2E has not run at this head. The new [pr-workflow
run](https://github.com/agent-substrate/substrate/actions/runs/38817919692)
is `action_required` and needs maintainer approval.

Draft agent-substrate#2694 refactors these same workflows and currently preserves an
out-of-band state while returning success. This focused fix also reports
FailedPrecondition, as required by agent-substrate#2676; that behavior should carry
forward into the refactor.

- [x] Local validation passes; runtime E2E awaits workflow approval.
- [x] Documentation reviewed; finalize and regression-test comments
describe the corrected behavior. No guide change is needed.
…KiB pages and root (agent-substrate#2624)

Both unit tests failed on an arm64 host with 64 KiB pages where the test
user is root, and neither failure pointed at the code under test.

- `TestSetupBundleRootfs_ManyLayers` built 64 production-length lowerdir
paths, about 8 KiB: more than a 4 KiB page, less than a 64 KiB one, so
its own guard failed it. Each path is now padded to the 255 bytes one
fsconfig(2) string value carries, and the layer count follows
`os.Getpagesize()`: 17 layers on 4 KiB pages, 257 on 64 KiB pages.
- A chain that long makes the overlay's `/proc/self/mountinfo` record
longer than `bufio.Scanner`'s 64 KiB default token, so `UnmountAllUnder`
failed with "token too long". `mountPointsIn` now reads lines with no
length cap, with a regression test.
- `TestGetFileToPublishedCopyIsWriteProtected` expects a write to a 0444
file to fail. Root bypasses file permissions, so the test skips as root,
like the other permission-based tests.

- [x] Tests pass: `go test -race ./internal/imagecache/...
./cmd/atelet/internal/filecache/...` and `golangci-lint` are clean; the
root-gated overlay test needs Linux root and runs under
`hack/run-root-tests.sh`, which CI does under sudo.
- [x] Appropriate changes to documentation are included in the PR: none
needed.
…nt-substrate#1906)

Durable-dir volumes were created `0700`, so a container running as a
non-root user could not write its own volume. Now `0777`, as Kubernetes
creates an emptyDir: one actor's containers can run as different users,
and the `0700` actor directory above keeps other actors out.

A restore keeps the mode the snapshot recorded, as emptyDir keeps a
pod's own chmod. Volumes created earlier stay `0700`, fine while every
container runs as root. No effect until an image's `USER` is honored
(agent-substrate#1918). Cleanup of files a non-root container leaves behind landed in
agent-substrate#1910.

Tests: `TestPrepareDurableDirVolume`. Exercised on kind with the
micro-VM class: uid 65532 writes a durable dir, then suspend and resume.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
…data in trial_summary (agent-substrate#2232)

Part of agent-substrate#1590

Adds facts to each run's `trial_summary` row so runs can be compared and
traced later, even after the cluster is deleted.

**New keys**
- `kubernetes_version`: the worker nodes' Kubernetes version, as the
kubelet reports it.
- `worker_actor_capacity`, `worker_memory_limit_gb`, `sandbox_class`:
what each worker pod declares (ateworker `--max-actors`, the container
memory limit, and gvisor or microvm from the worker image). Each value
is read on its own, so an unreadable one is skipped (and logged once)
without dropping the others.
- `multi_actor_worker`: `true` when `actors_per_pod_p50` > 1.
- `run_start`, `run_end`: Unix seconds bracketing the run, for finding
its server-side metrics afterwards.
- `metadata_<key>`: one per key of a new free-form `--metadata '<json>'`
runner flag, for facts only the caller knows.

**Orchestrator**
- Passes `--metadata` to locust Jobs with the target cluster's
`cluster_name`, `cluster_location` and `project_id`, merged with the
`BENCHMARK_METADATA` env var (a JSON object), if it's set.
- A test's own `--metadata` in `tests.yaml` is merged in too, its keys
winning on conflict, so the runner gets a single `--metadata`.
- Fails at startup if `BENCHMARK_METADATA` or a test's `--metadata` is
not a valid JSON object.
- Nighthawk Jobs are unchanged (that runner has no `--metadata`).

**Example** (from a test run):
```
"kubernetes_version": "v1.37.1-gke.1506000",
"worker_actor_capacity": "1000",
"worker_memory_limit_gb": null,
"sandbox_class": "gvisor",
"multi_actor_worker": "true",
"run_start": "1791245420",
"run_end": "1791246036",
"metadata_cluster_name": "<cluster>",
"metadata_cluster_location": "us-east1-b",
"metadata_project_id": "<project>",
"metadata_test": "manual",
```

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
)

This is the next step toward [async
suspend](https://github.com/zoez7/substrate/blob/async-suspend-design/docs/dev/async-suspend-en.md),
following agent-substrate#2688. `SuspendActor` and `PauseActor` now run one set of
shared steps behind a `suspendMode`:

- `suspendSync` (`SuspendActor`): the snapshot is durable before the
workflow returns.
- `suspendAsync` (`PauseActor`): the snapshot stays on the worker's
node.

Both APIs stay for now. `workflow_pause.go` is removed.

Behavior is unchanged. The tests agent-substrate#2688 moved testing of ensurePaused*
and ensureSuspended* functions onto `PauseActor` and `SuspendActor`. The
only test edit is in `workflow_test.go`, which called the removed
`ensurePausedFinalized`.


The fix for agent-substrate#2676 (finalize not checking that the actor is still
`SUSPENDING`) will come in a separate PR.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
…rate (agent-substrate#2181)

This commit changes everything except HTTP(s) links.

  github.com/agent-substrate/substrate -> go.ate.dev/substrate

I chose "go.ate.dev" for brevity, but "pkg.ate.dev" would be OK, too.

This is implemented as "CloudFlare Pages" on my personal account. If we
like it, we need to make it more "community operated".

This does not yet convert other repos.

This breaks a TON of open PRs, so we should consider if/when we want to
merge it.

/cc @BenTheElder @rakyll @EItanya
some base images include `/dev/*` device nodes in their layer tars.
atelet runs without `CAP_MKNOD` and the runtime mounts `/dev` anyway, so
skip char/block device entries when unpacking layers (like rootless
containerd does) instead of failing the pull.

while here:
- if an entry replaces a directory that had subdirectories in the same
layer, drop the removed paths from `dirModes` so the post-unpack chmod
pass doesn't fail on them
- include the tar entry path in `unhandled tar entry typeflag` errors
and the image ref in `EnsureImage` errors


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
… for dynamic worker registration (agent-substrate#2660)

Part of agent-substrate#587. Follows agent-substrate#2521.

agent-substrate#2521 added the initial `atelet` syncer that wrote
`sandbox_runtime_hash` per enabled `SandboxConfig` version to each local
worker pod's directory.

This PR completes the runtime and hardware compatibility pipeline from
`atelet` through the worker into `WorkerService.RegisterWorker`:

1. **Renames `sandbox-versions.json` to `sandbox-runtimes.json`
(`internal/sandboxruntimes`)**: `atelet` now writes the complete set of
`VersionedSandboxCompat` attributes for `defaultRuntime` and
`restorableRuntimes` (`architecture`, `sandbox_runtime_hash`,
`cpu_features` for `gvisor`, and `cpu_vendor` + `cpu_model` for
`microvm`).
2. **Probes CPU identity and gVisor `cpu_features` in `atelet`**:
`atelet` owns all hardware and runtime probing on the node — reading
`/proc/cpuinfo` for microVM CPU vendor/model and running `<runsc>
cpu-features` (memoized by asset SHA-256) for each enabled gVisor
version, encoded as a canonical lowercase hexadecimal bitmask whose bit
numbers are gVisor's own `Feature` constants (`internal/hardware`).
3. **Watches `sandbox-runtimes.json` in the worker**: `ateworker-gvisor`
and `ateworker-microvm` watch
`/var/lib/ate/workers/<pod-uid>/sandbox-runtimes.json` via
`register.WatchFile` and keep `default_runtime` and
`restorable_runtimes` registered through `WorkerSupport.RegisterWorker`
(`internal/ateworker/register`).

## End-to-End Flow

```mermaid
flowchart LR
    subgraph Node["Kubernetes Node"]
        direction TB
        SC["SandboxConfig CRD"] --> Atelet["atelet (workerSandboxSyncer)"]
        CPU["Host /proc/cpuinfo\n(microVM vendor & model)"] --> Atelet
        Runsc["Cached runsc binary\n(<runsc> cpu-features)"] --> Atelet
        Atelet -->|"Atomic rename"| File["/var/lib/ate/workers/<pod-uid>/\nsandbox-runtimes.json"]
        File -->|"register.WatchFile (fsnotify)"| Worker["ateworker-gvisor / ateworker-microvm"]
        Worker -->|"WorkerSupport.RegisterWorker"| Atelet
    end
    Atelet -->|"WorkerService.RegisterWorker"| AteAPI["ateapi (Control Plane)"]
```

## How it works

```mermaid
sequenceDiagram
    participant Atelet as atelet
    participant FS as /var/lib/ate/workers/<pod-uid>/
    participant Watcher as register.WatchFile (fsnotify / poll fallback)
    participant Reconciler as reconciler.run / sync
    participant Support as WorkerSupport.RegisterWorker

    Note over Watcher,FS: Watches parent dir for Create/Write on sandbox-runtimes.json
    Reconciler->>FS: Initial sync(): Read sandbox-runtimes.json
    alt File not written yet
        FS-->>Reconciler: ErrNotExist (wait for watch event or 5m resync)
    end

    Atelet->>FS: Probe hardware/runsc & atomic rename -> sandbox-runtimes.json
    FS-->>Watcher: fsnotify Create/Write event
    Watcher->>Reconciler: signal on changes channel

    Reconciler->>FS: Read sandbox-runtimes.json
    Reconciler->>Reconciler: proto.Equal(req, registered)?
    alt Changed (or first registration)
        Reconciler->>Support: RegisterWorker(capacity, default_runtime, restorable_runtimes)
        alt RPC succeeds
            Support-->>Reconciler: OK -> record registered = req
        else InvalidArgument (malformed runtime attributes)
            Support-->>Reconciler: Log Error, do not retry until file changes
        else Transient RPC error (e.g. Worker record not in ateapi yet)
            Support-->>Reconciler: Retry with exponential backoff (500ms..30s)
        end
    end
```

- **`atelet` owns all probing**:
- For **`gvisor`** workers, `atelet` fetches the version's `gvisor`
asset via `ensureSandboxAssets`, executes `<runsc> cpu-features`
(memoized in-memory by the asset's SHA-256 digest), and encodes the
reported names via `hardware.EncodeCPUFeatures` into a compact lowercase
hex bitmask alongside `architecture` and `sandbox_runtime_hash`.
- For **`microvm`** workers, `atelet` reads `/proc/cpuinfo` once
(memoized after the first successful read) to record `cpu_vendor` and
`cpu_model` alongside `architecture` and `sandbox_runtime_hash`.
- **`cpu_features` bit layout**:
- A feature's bit is the value of its gVisor `Feature` constant: on
amd64, `32*block + bit` in that block's CPUID register (`X86FeatureAVX2
= 2*32 + 5 = 69`); on arm64, the value gVisor declares. Intel/AMD never
reassign a CPUID bit, so a bit never moves when gVisor adds, reorders,
or renames a feature, and a bit with no entry is one the hardware
reserves.
- `internal/hardware/cpufeatures_table.go` is a committed table,
produced once from gVisor's `pkg/cpuid` sources at
`gvisor.dev/gvisor@v0.0.0-20261014212059-ddc37c698141`: for every
`Feature` constant with an entry in `allFeatures`, the constant's value
keyed to the display name `runsc cpu-features` prints. Each entry's
comment names the constant and, on amd64, its block and bit. The file
header records this and how to add features from a newer gVisor by hand;
because a bit is the hardware's, existing entries never change.
- **This mapping is temporary.** Once `runsc` can print its feature set
as this bitmask directly, `atelet` records that value as printed and the
table, `EncodeCPUFeatures`, and `DecodeCPUFeatures` go away (TODO in the
table header). The mask format and subset matching stay. No gVisor
sources or generator are vendored for it.
- **Subset matching**: `hardware.MatchesCompat` decodes `cpu_features`
into a stack-allocated `[6]uint64` mask (384 bits; amd64 bits reach 261
today, with room for gVisor blocks through 11) and checks bitwise subset
inclusion ($S_{\text{snapshot}} \subseteq S_{\text{worker}}$) while
requiring exact equality on all other attributes. `DecodeCPUFeatures`
rejects a mask that sets a bit with no name.
- **Resilient watching & registration in the worker**:
- `register.WatchFile` watches the parent directory for `Create`/`Write`
events on the target file (reused by both `register.Report` and
`ateworker-gvisor`'s `flagStore`), and falls back to periodic polling
(`5s`) with a warning if `fsnotify` initialization fails (e.g. when
`fs.inotify.max_user_instances` is exhausted on dense nodes).
- `reconciler` runs a `5m` periodic resync tick alongside watch events,
deduplicates consecutive registrations via `proto.Equal`, retries
transient read/RPC failures with capped exponential backoff
(`500ms..30s`), and drops permanent `InvalidArgument` rejections until
the file changes.

## Compatibility

`cpu_features` values recorded before this PR used a different bit
numbering and a narrower mask. They do not compare meaningfully against
workers built from this PR: any dev-cluster snapshot carrying one should
be retaken rather than restored. No compatibility is guaranteed before
v1.0.0, so there is no migration.

- [x] Tests pass (`go test -race ./internal/hardware/...
./internal/sandboxruntimes/... ./internal/ateworker/register/...
./cmd/atelet/... ./cmd/ateworker-gvisor/...
./cmd/ateworker-microvm/...`)
- [x] Appropriate changes to documentation are included in the PR
SHRUTI6991 and others added 2 commits October 16, 2026 05:36
…with kubectl ate (agent-substrate#2591)

Part of agent-substrate#1563.

Fixes agent-substrate#2103,
agent-substrate#2106,
agent-substrate#2186

## What

- **Authz on by default** in ate-setup, through two settings:
`ateapi.authz.enabled` and `ateapi.authz.bootstrapOwners`. The bootstrap
owner defaults to the shared `ate-client` ServiceAccount, so demos,
benchmarks and CI keep working. ate-setup and `kubectl ate` warn about
it, and the docs tell production installs to name their own owners.
- **`kubectl ate access-policy`** get/create/update/delete, for an
atespace (`-a`) or the global scope (`--global`).
- **Tests:** an authz e2e suite (basic flow plus security regressions),
`model_test.fga.yaml` run in CI, and Postgres tests that fail rather
than skip when CI requires Docker.
- **Benchmarks:** the nightly run adds `benchmark-runner` as a bootstrap
owner.
- **Docs:** new `docs/authorization.md`, covering roles, the checks
every RPC makes, managing access and limitations.

Follow-up: replace the `ate-client` default with per-caller identities
(token options, a `kubectl ate` credential plugin, issuer-qualified
principals), then require explicit owners.

## Testing

Unit tests and `hack/verify-all.sh` pass. The authz e2e suite runs in
the CI e2e lanes.
Integrate the unified suspend workflow and vanity Go module while preserving fork workflow deadlines and immediate worker cache updates. Align Helm API configuration, authorization, controller permissions, and CRDs with the current control plane.

Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
@EItanya
EItanya merged commit 4df1cd3 into main Oct 16, 2026
5 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.