Commit Graph
143 Commits
Author SHA1 Message Date
Zoe Zhao 4753ef37d3 Convert ActorTemplate CRDs to the substrate proto during actor workflows (#1232)
Adds a temporary converter that allows each Actor workflow to use the
substrate resource rather than CRD, to prevent regression while we are
in the parallel state.
* Allows each workflow to use the ActorTemplate substrate proto under
the hood, in preparation for the full cut over, e2e tests start testing
the new code path.
* Changes any functions that consumes the ActorTemplate CRD to use the
substrate proto instead.
* The converter will be deleted once we fully cutover to the substrate
resource.
2026-08-27 15:19:45 -04:00
Da Huang e2092d0e68 ateapi: close atelet conns evicted from the dialer cache (#1259)
AteletDialer caches one grpc.ClientConn per atelet pod UID in a
1024-entry LRU with no eviction function, so a conn pushed out to make
room was forgotten, never closed. grpc does not reclaim an un-Closed
conn: its goroutines and buffers stay alive for the life of the process.
Close evicted conns via the cache's eviction hook, the same fix the
atelet ateom dialer received.

Fixes #470

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

- [ ] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-27 11:40:02 -04:00
Haven Xia 7e339137cf ateapi: reject unknown fields on the request, not just the resource (#1216)
Fix #1213, we should also reject unknown fields at request level.

Refactor all functionality back into a gRPC interceptor to check every
incoming request, no need to handle every new request method.

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

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-26 16:56:05 -07:00
Zoe Zhao d8c4f8a4b9 Add controller in ate apiserver to reconcile ActorTemplate state (#1096)
Part of #477.

The controller has 2 components
1. A "producer" that periodically lists ActorTemplates and add work
items to a client-go workqueue.
2. A "consumer" that gets work items from the queue, and reconcile it.

AT will have the FAILED condition if we encounter non-retriable errors
during the transition
2026-08-26 16:42:05 -07:00
Max Thompson 843ca2070e Mirror systemInfo volumes into the ActorTemplate substrate resource (#1134)
Part of #1131.

The substrate ActorTemplate's Volume union (#824, #1026) predates the
systemInfo volume source (#803, trustBundle in #941): it only had
durableDir and externalVolumeTemplate, so a template created through the
substrate API could not carry SystemInfo projections. This PR adds:

- `SystemInfoVolumeSource` with the `actorMetadata` and `trustBundle`
data sources, mirrored from the CRD types in the plain-field union shape
(#962) that `Volume` already uses ("SystemInfo" joins the `type`
discriminator).
- Declarative validation tags (#1215) mirroring the CRD's kubebuilder
rules: the SystemInfoDataSource one-of union, list bounds, string
lengths, and unique projected fields, with generated validators and
tests in controlapi.
- Store contract round-trip coverage: the shared fixture now carries a
systemInfo volume through both the redis and postgres backends.

Not in this PR (tracked in #1131): the CRD's cross-item CEL rules
(duplicate paths across data sources, at most one actorMetadata entry)
and the clean-relative-path checks, which have no declarative tag
equivalent and belong in the controlapi create/update handlers when
ActorTemplate serving lands (#477), and conversion of the substrate
resource in the actor-start path.
2026-08-26 14:52:51 -07:00
Aditya ShantanuandAditya Shantanu 66300d5c47 workersync: gate worker registration on ateom readiness (#1243)
Fixes #1106. Takes over #1107 (adopting @Stevenjin8's approach and
commits from that PR — credit to him for the design; trailer omitted
only for CLA) and incorporates the review feedback it accumulated.

Workers were registered as schedulable the moment their pod had an IP —
long before ateom's gRPC server was listening, which is the scheduling
race in #1106 (actors dispatched to a cold ateom fail at dial time).
Now:

- ateom (gvisor + microvm) serves HTTP `/readyz` on a dedicated port
(8080) once its gRPC listener is up, flipping to 503 on SIGTERM so
draining workers stop receiving work.
- The worker Deployment probes that endpoint (replaces the earlier TCP
probe against the TLS port, which spammed handshake errors).
- `isWorkerEligible` requires `PodReady=True` in addition to an IP.
Eligibility gates **registration only** — a readiness flap never
deregisters a worker with a bound actor (pinned by
`TestSyncer_ReadinessFlapDoesNotDeregister`).
- A readiness server that cannot come up exits the process rather than
leaving a pod that looks alive but can never receive work.

Beyond #1107's head: dropped the pre-migration
`controlapi/syncer_test.go` copy (didn't compile after #1203 moved the
syncer), fixed the `workersync` fixtures for the new gate, and added
`TestSyncer_PodWithIPButNotReadyIsNotRegistered` (+ the flap test
above).

## Testing

- `go test ./...`: green, zero failures.
- `-race -count=10` on `workersync` + `serverboot`, `-race -count=5` on
`controllers`, `-race` on all of `controlapi` incl. functional tests:
green.
- Green under a hostile ambient env
(`KUBECTL_CONTEXT`/`KO_DOCKER_REPO`/`PROJECT_ID` set to garbage).

## Scope notes

- The one post-#1160 failure with a *post-restore* signature (run
32891995583: restored actor's own container `/healthz` timing out, actor
stuck RESUMING) is a different layer — ateom was demonstrably serving —
and is **not** claimed as fixed here. If it recurs, it deserves its own
issue. (The other suspected post-merge failure, run 32911674788, turned
out to be that PR's own x509 regression, not this flake.)
- Mixed-version caveat: this controller pointed at pods running a
pre-readyz ateom image leaves those workers unready/unregistered. The
supported install flows build both from the same source, so this only
matters for hand-rolled skew.

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

Co-authored-by: Aditya Shantanu <aditya-shantanu@users.noreply.github.com>
2026-08-26 17:32:03 -04:00
sfunkenhauser 37735a698d Move WorkerPoolSyncer to ate-controller (#1203)
Fixes #729 

- [ x  ] Tests pass
- [ x ] Appropriate changes to documentation are included in the PR
2026-08-26 14:18:59 -04:00
Tim Hockin a8d83ee1b5 Use Kubernetes declarative validation (#1215)
This mostly eliminates the need to hand-write validation code.

This PR is a long series of commits which add DV for most of Actor and
all of Atespace.

Here is a map to the commits:
* The first few take a dep on a new Kubernetes tag, import the code into
third_party, and apply a single patch. Because we have different Go
modules for tools, I had to do it twice. When that patch lands, we can
revert these commits, but that won't be until the 1.38 cycle in a few
months.
* The next commits slowly add DV support, so a human can review each of
them in a reasonable amount of time. The emphasis is on great test
cases.
* I made a bad choice early on as to where to generate the code into, so
I moved it. Rebasing on that was exceedingly hard, so I left it as a
move.
* This required changing update/go-generate -> update/codegen -- we need
to get the ordering of tools right, which `go generate` does not
guarantee.
* Then I added a "middle" layer called "ServiceImpl" between the RPC and
storage layers. This allows things like workflow to call the same
business logic as the RPC layer, including validation. Lots of test
fixes.
* Then I finished the Create() and Update() paths for Actor. Those
represent the "right" (or closest to) way to implement resources, and
tests for validation.

I strongly encourage reviewers to read it commit-by-commit. Rebasing
this is VERY tedious, so the sooner it lands or dies completely, the
better. Then we can start converting the rest.

`./hack/run-tool.sh validation-gen --docs` will produce some docs on the
tool and the available tags.

@laoj2 @juli4n @EItanya @HavenXia
@lalitc375 @yongruilin @jpbetz  FYI
2026-08-25 20:24:59 -07:00
Julian Gutierrez Oschmann cc70a216b2 Rename Lock to Lease. (#1217)
Naming it a `Lock` is really misleading as it actually implements a
distributed lease.
2026-08-25 16:59:46 -07:00
eliranw afcd5cd718 Per-container CPU and memory limits for micro-VM actors (#859)
Per-container CPU and memory limits for micro-VM actors. First slice of
#752.

An `ActorTemplate` container can cap its own cpu and memory so it cannot
starve or kill its siblings in the same actor. A container that exceeds
its memory limit is OOM-killed on its own; the rest of the actor is
unaffected.

```yaml
sandboxClass: microvm
containers:
  - name: trainer
    resources:
      limits: {memory: 1500Mi}
  - name: sidecar
    resources:
      limits: {memory: 256Mi, cpu: "0.2"}
```

#752 asks for per-container device selection as well. This PR does the
cpu and memory half; devices are the remaining half, and when they land,
GPU assignment should follow the same field rather than staying
actor-wide as it is today.

## Why micro-VM only

The two sandbox classes support opposite halves of #752. Micro-VM actors
have a real guest kernel, so each container gets its own cgroup and the
limits bind. gVisor applies cgroup limits at the sandbox level: one
sentry backs every container in the actor, so a per-container cgroup is
created and then stays empty (google/gvisor#190). Measured on a running
actor, the workload container's cgroup reported `memory.current=0` while
all 20 sandbox processes sat in the pause leaf. A template that sets
`resources` with `sandboxClass: gvisor` is rejected at admission.

## How a limit travels

`ActorTemplate` → CEL validation → `ate-api-server` resolves each
`resource.Quantity` once → `ateletpb` → atelet writes OCI
`linux.resources` → `ateom-microvm` merges kata's defaults and checks
the guest envelope → `SpecToAgentPB` → kata agent → guest cgroup.

The limit is carried as a standard OCI field rather than a
substrate-private concept, so a runtime that gains per-container
enforcement picks it up without new plumbing.

## Composition with #679

#679 sizes the sandbox itself from `ActorTemplate.spec.resources`: guest
RAM and vCPUs for a micro-VM, the sentry for gVisor. This PR subdivides
that sandbox. The two are different fields and complementary layers.

They collided in one place. #679 applied the actor-level size to
**every** container's OCI spec, overwriting the per-container limits
atelet writes into the same field, so a container asking for 64Mi
silently received the whole guest. This PR removes that call on the
micro-VM path. A container now gets a cgroup limit only when it declares
one; an undeclared container is bounded by guest RAM, which is the real
ceiling. On micro-VM the stamp never reached the guest at all:
`SpecToAgentPB` carried neither `Memory` nor `CPU.Quota`, so it stopped
at the bundle's `config.json`. Running the pre-PR pipeline shows
`Quota=50000 Memory=2Gi` on disk arriving as `Quota=0 Memory=<nil>` on
the wire.

gVisor is untouched. There the actor-level size is applied to the
`pause` container, and since one sentry backs every container, that is
the only cgroup that binds.

Two consequences worth calling out for reviewers of #679:

- The `sizing.SandboxSize` threaded into `buildActorContainers` and
`ensureKataCompatibleSpec` had no remaining reader, so it and the
`guestSize` helper are removed. `resolveGuestMemMiB` still sizes the VM
and still rejects a declared limit too small to boot.
- `internal/sizing`'s package doc claimed both runtimes shared
`ApplyToOCISpec` for container cgroups. That is now gVisor-only, and the
comments say so. No code in that file changed.

`checkResourceEnvelope` runs after `resolveGuestMemMiB`, so it validates
the per-container sum against the post-reserve guest rather than the
SandboxConfig default. Its error now names
`spec.resources.limits.memory` (or `.cpu`) when the actor declared a
size, and `SandboxConfig` when it did not.

`internal/e2e/suites/sizing` is unaffected: it asserts on `num_cpu` and
`mem_total_bytes`, both of which come from VM sizing, and only logs the
cgroup files.

## Verified on hardware

Same template, run twice, one commit apart on a micro-VM actor:

| | before | after |
| :--- | :--- | :--- |
| `hog_ovl/memory.max` | `max` | `67108864` (the declared 64Mi) |
| `bystander_ovl/memory.max` | `max` | `max` |
| hog allocates 128MB | survived | OOM-killed |
| bystander | alive | alive |

The bug in between: `SpecToAgentPB` converted only `Devices` and
`CPU.Shares` out of `Linux.Resources`, so a memory limit reached the
bundle's `config.json` and was dropped on the way to the agent. Every
unit test passed and the on-disk spec was correct while the feature did
nothing.

## Notes for review

- `ContainerResources` deliberately does not reuse
`corev1.ResourceRequirements`, which also carries `requests` and
`claims`. There is no scheduler inside an actor to hint at, and for
memory a soft request cannot express "must have this much to come back
at all". #679 reached the same conclusion from the other direction and
now rejects `spec.resources.requests` and `.claims` at admission.
- `Limits` is a bounded named type. A `MaxProperties` marker on the
field lands at the wrong schema level for a named map, and without a
bound the CEL cost estimator rejects the whole schema. This CRD is close
to its schema-wide CEL cost ceiling; a rule that iterates containers and
parses quantities exceeds it by more than 100x and takes the existing
rules down with it.
- A cpu limit below `10m` is raised to `10m`: the kernel rejects a CFS
quota under 1ms.
- `SpecToAgentPB` drops a non-positive quota and a zero period rather
than forwarding them. A non-positive quota means unlimited in OCI but
was sent as a literal zero, which the guest applies as no CPU at all; a
non-nil zero period overwrote the CFS default set for a live quota. Both
now read a spec the way `cpuLimitMillis` does, so the envelope check and
the conversion agree on the same input.
- `mergeKataResources` fills the gaps kata's defaults cover rather than
allowlisting known fields, so a field it does not recognise survives the
merge. That holds only as far as the merge: `SpecToAgentPB` converts
`Devices`, `Memory` and `CPU` only, so `Pids`, `BlockIO`,
`HugepageLimits` and `Network` stop at the ttrpc boundary. Nothing sets
them today.

## Known gaps

- An OOM-killed container is not reported above the guest. The actor
stays `STATUS_RUNNING` and nothing records which container died. Raised
on #550. Note that #961 settled workload stats at the sandbox level and
deliberately does not attribute per container, so surfacing which
container died needs a separate signal: the guest cgroup's
`memory.events`, not the stats path.
- A container's writable rootfs is a guest tmpfs, so filesystem writes
are charged to its memory limit and a container writing more than its
limit to `/tmp` is OOM-killed. Documented in `api-guide.md`; separating
the two budgets needs an `ephemeral-storage` field.
- Over-subscription across containers is caught when the actor starts,
not at apply time. An admission-time sum check is not implementable
within the CEL cost budget.
- The atelet hop that attaches resources to the bundle spec has no test:
replacing `ctr.GetResources()` with `nil` leaves the whole suite green.
Closing it needs an imagecache fixture for `prepareOCIDirectory`.
- Restored actors inherit the golden's cgroups rather than applying
their own spec. Correct today only because `ActorTemplateSpec` is
immutable.
- The GPU section of `api-guide.md` still says `ActorTemplate` has no
per-container resource fields, which this PR makes false. Correcting it
is left to the devices half of #752, which is what will change the GPU
behaviour that paragraph describes.

---

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

---------

Signed-off-by: Eliran Wolff <eliranw@nvidia.com>
2026-08-25 16:43:46 -07:00
Julian Gutierrez Oschmann c597b48879 Define ActorSnapshotRef and ActorSnapshotTagRef types. (#1211)
This aligns the codebase with other resources which have corresponding
`<Resource>Ref` Go types.
2026-08-25 18:49:07 -04:00
Haven Xia 9a1c3430e9 Create reject unknown fields (#1186)
Part of #1011 , stack on #1174 

As I was writing [ate upgrade
proposal](https://docs.google.com/document/d/1JduAyGZFyqdNp4UhKiv0EN-is3iW5tf5BwGTWbt2ouI/edit?usp=sharing&resourcekey=0-MXG12QCkleIxhY7cOB7g6A),
we should have both rejection for unknown fields for creating and
updating. Add the creating part here.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-25 13:01:14 -04:00
sfunkenhauser 9b08310890 Implementation for the worker API (#1121)
Fixes #730 

- [ x ] Tests pass
- [ x ] Appropriate changes to documentation are included in the PR
2026-08-25 12:13:13 -04:00
Luiz Oliveira 790ee35229 Reject object updates containing unknown fields (#1174)
https://github.com/agent-substrate/substrate/issues/1011

An update containing unknown fields during a RMW indicates there's a
version skew between client and server.

So, the server enforces a policy to fail explicitly when it receives an
unknown proto field: so that it does not silently drop fields that the
user intended to set but the current server version cannot process

This also helps to avoid behaviour drift during rolling upgrades: During
rolling upgrades, different components (like the data plane and control
plane) can be on different versions. If an older server replica . If an
older server replica processes a request coming from a newer client, it
will reject it (it doesn't know how to validate/handle it) and it's the
client's responsibility to retry.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-25 08:21:25 -07:00
Julian Gutierrez Oschmann fbe580e4f6 Fix resource names for ActorSnapshotTag methods. (#1167)
Also clarified the style-guide about the naming convention.

Fixes #1163
2026-08-25 09:48:12 -04:00
Jet Chiang 60073ecd57 Replace ateredis with atepg (#940)
## Summary

A follow up to #640 where we introduced PostgreSQL as an alternative
storage backend, selected conditionally in ateapi.

- Deleted ateredis, its tests, and its dependencies
- Removed Redis backend selection and configuration so ateapi always
connects to Postgres
- Replaced Valkey resources with Postgres in the standard and Kind
deployment paths and simplified install script
- Replaced miniredis fixtures with isolated Postgres testcontainers and
added centralized helpers for seeding resources
- Renamed Redis-specific debug flush command to backend-neutral
`debug-clear-store` in CLI
- Updated comments and docs where applicable

## Benchmarking

Extensive benchmarking have been performed to evaluate Redis vs
Postgres, and results can be found in these two documents:

-
https://docs.google.com/document/d/10K0wB6aTeFkJCL4HN3NbLJCdFGoLYdhIcqkFnqFHkKc/edit?usp=sharing
-
https://docs.google.com/document/d/12-ko_BFHcBo_nJkx9f4B7zMbiiWKC2saGhMhZG3aQ-s/edit?usp=sharing

---------

Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
2026-08-25 07:23:40 -04:00
Tim Hockin 87361ea519 Rename controlapi.Service -> RpcService (#1148)
Having been reading this code for a while, and in light of changes we
want to make, I think something like this will help clarify.

I am not particularly attached to the name or style `RpcService` - it's
not usual Go convention but it is protobuf style. If anyone feels
strongly it could be `RPCService` or `ControlService` or something else.



@juli4n @laoj2
2026-08-24 17:54:51 -04:00
Tim Hockin 5ee2d05e4b Compartmentalize AtespaceExists() (#1149)
This needs to be a property of the storage layer. It is in PG but not in
redis. This commit moves it out of the store interface and down into
redis. It's still not CORRECT but it constrains it.

This requires a lot of tests to create the atespace before creating
other resources. These tests are wrong in the face of a correct storage
layer, anyway.

xref #989
2026-08-24 15:20:33 -04:00
Luiz Oliveira 31a2e3850a Simplify Update methods to do a whole object replace (#1108)
* Removed field_mask from the API
* Added a new protoupdate package to handle replacing mutable fields.
This makes sure that unknown fields in the server are not dropped by an
update from a stale/old client.

#1011 

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-24 15:18:55 -04:00
Jet Chiang f1e5d8e4e3 Harden PostgreSQL concurrency, integrity, and error handling (#1023)
Part of #731 and follow up to #640. This PR makes improvements to the
Postgres store by addressing review comments on #940 and optimizing
Postgres updates as suggested in #988.

- Replaces Postgres row-locking updates for actors, templates, and
snapshot tags with bounded optimistic concurrency control using
UID/version CAS checks.
- Strengthens relational integrity with missing FKs and indexes, and
replace snapshot-tag creation pre-check with FK
- Removes unnecessary UID uniqueness constraints
- Standardize pagination and validation across stores, map malformed
tokens and invalid sizes to appropriate gRPC errors
- Improves actor resume race handling when workers disappear or
concurrent assignments exhaust their retry budget
- Improves reliability with startup retries, expired-lease cleanup,
worker-watch reconnection after malformed msg
- Expand Postgres, Redis, Contract, and Control API tests to cover new
constraints, error semantics, concurrency

---------

Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
2026-08-24 12:56:01 -04:00
Benjamin Elder ecca2cac00 ateapi: move the ActorTemplate functional test into functionaltest (fix test build) (#1141)
The test calls `setupTest`, `namespaceForTest` and `assertGrpcError`,
which are defined in the `functionaltest` package, but the file landed
in `controlapi` declaring package `controlapi`, so the `controlapi` test
binary did not build.

This code passed on the PR branch, but it logically conflicted with a
previous refactor without merge conflict.


> 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
2026-08-21 20:38:36 -07:00
Zoe Zhao f7370c3726 Implement ActorTemplate CRUD in the control API (#895)
Part of #477.

Changes
* Implement ActorTemplate CRUD in the control API
* Added a shared generic ResourceRef behind the template and actor ref
types

Relevant:
* Following style guidances in
https://github.com/agent-substrate/substrate/issues/891
2026-08-21 17:23:39 -07:00
Max Thompson 074fd1c12b Add trustBundle as a SystemInfo volume data source (#941)
Part of #932 (PR 1 of 3). Adds the user-declarable trustBundle data
source for SystemInfo volumes (#802) and the end-to-end proof that the
projected anchors work against the MITM egress gateway. Live refresh for
running actors (PR 2) and auto-injection (PR 3) come separately.

What this adds

A SystemInfo volume data source that projects the trust anchors of a
named trust bundle to a PEM file:

volumes:
- name: trust
  systemInfo:
    dataSources:
    - trustBundle:
        name: egress-mitm.ate.dev
        path: egress-ca.pem

Inspired by the Kubernetes clusterTrustBundle projected volume source,
but source-neutral: the template names a bundle; where it's fetched from
is a deployment concern, not part of the API.

Design points

- Resolution lives on the node. The wire carries only {name, path};
atelet resolves the name at write time through an informer-backed lister
on ClusterTrustBundles and writes the sanitized PEM with the temp+rename
discipline from #803 (find-paths safe). Contents refresh on every
Run/Restore. ateapi is not involved, per review discussion — the same
informer is what live refresh (PR 2) will hang off.
- Allowlist in atelet, not the CRD schema. Today only
egress-mitm.ate.dev (the egress gateway CA bundle, #823), mapped to the
ClusterTrustBundle that atecontroller's EgressMITMTrustReconciler (#946)
derives from the egress-mitm-ca-pool Secret. The signer-linked object
name stays a backend detail; the future backend registry (#932) widens
the allowlist without an API change.
- The watch is scoped to the one backing object via a metadata.name
field selector — this informer runs on every node, so an unfiltered
watch would fan every ClusterTrustBundle in the cluster out to every
atelet. RBAC can't express this (resourceNames doesn't apply to
list/watch), so the field selector is the enforcement point.
get/list/watch on clustertrustbundles moves to the atelet ClusterRole.
- No availability probe. The informer registers unconditionally; a
cluster that doesn't serve the feature-gated certificates.k8s.io/v1beta1
blocks atelet startup at cache sync, with the reflector errors naming
the missing API (hack/create-kind-cluster.sh enables the gate).
- Fail-closed. Unknown names, missing bundles, and unusable bundles fail
actor start naming the bundle — an actor that declared a trust bundle
must not start without one.
- Kubelet-parity sanitization (internal/pemutil): CERTIFICATE blocks
only, deduplicated, headers stripped, and anchors deliberately shuffled
so consumers can't grow a dependence on order.
- Schema note: dataSources MaxItems tightened 32→8 while adding the
trustBundle member. Vacuous in practice (the old schema couldn't admit
more than one entry), but flagged since it's ratchet-shaped.

E2E — delivery and consumption

Delivery (identity suite, both sandbox classes): provisions the
egress-mitm-ca-pool Secret and drives the real #946 reconciler (writing
the bundle directly isn't possible — the reconciler reverts hand-edits),
asserts the projected file byte-exact, then rotates the pool across a
suspend/resume to prove refresh-on-restore. Since the probe fixture is
shared and fail-closed, e2e.DeployProbe itself ensures the bundle exists
for whatever suite deploys it.

Consumption (new egressmitm suite, both sandbox classes): deploys the
sdsmint (MITM) egress gateway and proves an actor completes a TLS
handshake with the gateway's per-SNI minted leaf using ONLY the
projected anchors — plus a system-roots negative control that must fail.
The pair is unambiguous in both directions: the positive can't pass
under passthrough (the bundle holds no public CAs), and the negative
can't fail under passthrough.

CI: two steps appended to the existing e2e job after the standard lanes
(the gateway swap is cluster-wide and breaks passthrough assumptions):
--deploy-atenet --experimental-use-sdsmint redeploys only the atenet
components, then the egressmitm suite runs once per sandbox class.

Flake mitigation: the probe fixture pool drops from 3 workers to 2. Each
suite deploys its own copy and drives one actor at a time, so the third
worker per copy was idle memory multiplied across suites on the one-node
CI cluster — pressure that has been killing sandboxes mid-test (runsc:
signal: killed, a vanished ateom socket) on this PR and on main's
identity suite. This reduces the pressure; right-sizing e2e concurrency
or worker-pod QoS cluster-wide is follow-up material.

Not in this PR

- Live refresh for running actors (#932 PR 2) — until then, a running
actor's file is the bundle as of its last Run/Restore, and correctness
rests on overlap rotation by the bundle publisher.
- Auto-injection of the egress trust volume (#932 PR 3).
- Configurable backend registry (#932) — the allowlist is the seam it
will replace.
2026-08-21 19:53:31 -04:00
Sneha-at b7080602c6 Allow deleting an actor from any state. (#788)
Fixes #643 
This feature introduces the ability to delete Substrate actors from any
lifecycle state (e.g., RUNNING, PAUSED, PENDING), rather than requiring
them to be hibernated into SUSPENDED (or CRASHED) beforehand. This is
crucial for cleaning up stuck or failed actors.
#### 1. Control Plane & gRPC API (pkg/proto/ateapipb, ateapi)
• DeleteActorRequest.any_state: Added a new boolean field any_state to
DeleteActorRequest.
• When false (default): enforces the existing behavior where only actors
in ACTOR_STATE_SUSPENDED or ACTOR_STATE_CRASHED (or already
ACTOR_STATE_DELETING) can be deleted.
• When true: allows deleting an actor in any state (e.g., RUNNING,
PAUSED).
• Orchestrated Cleanup Workflow (workflow_delete.go):
1. Transitions actor state to ACTOR_STATE_DELETING.
2. Calls atelet.Terminate to stop live workloads on the node.
3. Detaches external volumes from the worker node (with fallback logic
using actor status if the ActorTemplate was deleted).
4. Releases the assigned physical worker Pod back to the WorkerPool.
5. Deletes external storage volumes through the storage plugins.
6. Finalizes removal of the actor from the persistence store.
#### 2. Node Herder Agent (atelet)
• Terminate RPC (main.go): Added a new Terminate method to AteomHerder:
• Calls ateom.TerminateWorkload on the target worker pod.
• Unmounts external volumes mounted for the actor on the node.
• Cleans up and resets actor runtime host directories (OCI bundles,
checkpoints, pid files).
#### 3. In-Pod Sandboxes (ateom-gvisor & ateom-microvm)
• TerminateWorkload RPC:
• gVisor (main.go): Stops and deletes active runsc containers, unmounts
bundle rootfs overlays, and tears down the actor's interior network
namespace.
• Micro-VM (checkpoint.go): Shuts down the Cloud Hypervisor VMM,
unmounts bundle rootfs overlays, and cleans up the network namespace.
• Emits an "Actor terminated" lifecycle log and clears the active actor
attribution so the worker can host new workloads.
#### 4. CLI (kubectl-ate)
• --any-state Flag (delete_actor.go):
kubectl-ate delete actor <actor-name> -a <atespace> --any-state

──────
  ## Verification 
  [x] Changes tested in local with 
 ```
• go build ./...: Passed
go test ./...: Passed (all unit and functional tests pass)
    make lint: Passed (no lint errors) 
```
Detailed manual tests                                                                                                                                                                                                                                                                                                                                           
 ```                                                                                                                                                                                                                                                                                                                                                
  ### Scenario 1: Force Delete a Running Actor with --any-state                                                                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                                                                                                                                                     
  1. Create the actor:                                                                                                                                                                                                                                                                                                                                               
    kubectl ate create actor manual-test-1 --template=ate-demo-counter-microvm/counter-microvm -a demo                                                                                                                                                                                                                                                               
                                                                                                                                                                                                                                                                                                                                                                     
  2. Resume the actor:                                                                                                                                                                                                                                                                                                                                               
    kubectl ate resume actor manual-test-1 -a demo                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                                                                     
  3. Force delete the running actor:                                                                                                                                                                                                                                                                                                                                 
    kubectl ate delete actor manual-test-1 -a demo --any-state                                                                                                                                                                                                                                                                                                       
    actor "manual-test-1" deleted                                                                                                                                                                                                                                                                                                                                    
```
──────
### Scenario 2: Standard Delete a Suspended Actor
```                                                                                                                                                                                                                                                                                                                                  
  1. Create the actor:                                                                                                                                                                                                                                                                                                                                               
    kubectl ate create actor manual-test-2 --template=ate-demo-counter-microvm/counter-microvm -a demo                                                                                                                                                                                                                                                               
                                                                                                                                                                                                                                                                                                                                                                     
  2. Resume the actor:                                                                                                                                                                                                                                                                                                                                               
    kubectl ate resume actor manual-test-2 -a demo                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                                                                     
  3. Suspend the actor:                                                                                                                                                                                                                                                                                                                                              
    kubectl ate suspend actor manual-test-2 -a demo                                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                                                                     
  4. Standard delete the suspended actor:                                                                                                                                                                                                                                                                                                                            
    kubectl ate delete actor manual-test-2 -a demo                                                                                                                                                                                                                                                                                                                   
    actor "manual-test-2" deleted                                                                                                                                                                                                                                                                                                                                    
```
──────
### Unit & E2E Tests
# Unit & Functional Tests
go test ./cmd/ateapi/internal/controlapi -run
"TestDeleteActorWorkflow|TestEnsureMarkedDeleting"
# E2E Tests
E2E_TEMPLATE_NAMESPACE=ate-demo-counter-microvm
E2E_TEMPLATE_NAME=counter-microvm ./hack/run-e2e-kind.sh
./internal/e2e/suites/demo -run TestActorLifecycle
./hack/run-e2e-kind.sh ./internal/e2e/suites/demo -run
TestForceDeleteActorWithExternalVolume
```
- [ x] Appropriate changes to documentation are included in the PR
2026-08-20 13:17:59 -07:00
Ron Lev 0741459b15 Mount an OCI image read-only into an actor (#923)
Fixes #783
Adds an `image` source to `ActorTemplate`'s `VolumeSource`: a container
can mount the contents of an OCI image it does not run. This is how
tooling gets into images built by third parties without rebuilding them.

```yaml
spec:
  containers:
  - name: sandbox
    image: docker.io/example/benchmark@sha256:...
    command: ["/ate/agent"]
    volumeMounts:
    - name: agent
      mountPath: /ate
  volumes:
  - name: agent
    image:
      reference: registry.example.com/agent@sha256:...
```
## How it works

atelet pulls the image through the existing layer cache and records the
volume's layers in the bundle's overlay spec, next to the rootfs layers.
ateom composes the volume inside the bundle — the cached layers with no
writable layer on top, so the mount is read-only — and the container
binds it at the declared path. The volume is composed per container:
containers of one actor may mount the same volume, and each gets its own
mount point inside its own bundle, all backed by the same shared layers.
On resume the volume is re-composed the same way.

References must be digest-pinned, the same rule as container images: a
snapshot is only valid against the exact bytes it was taken with.

On micro-VMs the volume rides the same read-only virtio-fs share as the
container rootfs: ateom stages each composed volume beside the rootfs on
the host, and the guest binds it into the container at the declared
path.
2026-08-20 10:22:56 -07:00
Jeff Luo bc59db783a ateapi: stamp the worker pool on successful suspend and pause (#1075)
Fixes #957

`ate.actor.lifecycle.operation.duration` carried the pool pair on
suspend and pause only when they failed, so per-pool dashboards saw
those two operations exclusively as failures.

Both workflows record the histogram from a defer that reads the `actor`
variable, and the happy path reassigns it to the finalized record. The
finalize step commits the new state and the cleared `WorkerAssignment`
in one update, so the defer found no assignment and dropped both keys. A
failure returns the pre-finalize record, which still names the worker.

Both now snapshot `lifecycleOpAttrs(...)` just before the finalize step
— the same snapshot-before-clear crash.go does for the crash counter.
Paths that end earlier keep the current computation.

`delete` stays without a pool: it only runs from SUSPENDED or CRASHED,
which already released the worker, so there is none to name.

TestLifecycleOpPoolAttributesOnSuccess drives a real suspend and pause
through the gRPC service; both subtests fail without the fix.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-20 11:55:18 -04:00
Luiz Oliveira fc02127bfe ateapi - Require preconditions in Update methods (#1014)
https://github.com/agent-substrate/substrate/issues/954

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-19 21:01:29 -04:00
Max ThompsonandTaahir Ahmed 5edb1af1fe feat: add systemInfo volume source with actorIdentity data source (#803)
Part of #802 (first PR: the actorIdentity data source; does not close
the issue).

## What changed and why

Adds a systemInfo volume source to ActorTemplate — a read-only volume
whose files are generated by atelet on every Run/Restore, analogous to
Kubernetes projected volumes. The initial data source, actorIdentity,
writes the actor's own name to a configurable relative path:

```
spec:
  volumes:
  - name: system-info
    systemInfo:
      dataSources:
      # Part 1 (this PR): own-metadata projection, downwardAPI-style
      - actorMetadata:
          items:
          - field: name              # enum: name | atespace | uid
            path: actor-name
          - field: atespace
            path: atespace
          - field: uid
            path: actor-uid
  
  containers:
  - name: main
    image: app@sha256:...
    volumeMounts:
    - name: system-info
      mountPath: /run/ate  
```

Because the files are regenerated before the sandbox starts, they carry
the resumed actor's own values regardless of what checkpointed state it
boots from — the property the old hardcoded /run/ate identity mount
provided, now as an explicit, extensible API that future data sources
(identity JWTs, certificates — see #802) can slot into.

**Behavior change:** the automatic /run/ate/actor-id mount is removed;
actors must opt in by declaring the volume (the e2e identity probe in
this PR is the reference example).

### Reviewer notes:

- Over half the diff is vendored + generated code
(cmd/atelet/internal/third_party/atomicwriter/, atelet.pb.go,
zz_generated.deepcopy.go, the CRD manifest). The hand-written surface is
~700 lines.
- System-info volume roots live under a new ActorPath/system-info/ host
dir, deliberately separate from durable-dir/: the micro-VM durable
machinery snapshots everything under the durable-dir root, and generated
identity files must never be captured into snapshots.
- Supports microVM as well as gVisor.
- The e2e identity suite exercises the new API end-to-end with unchanged
probe binary and assertions. It runs in the kind-cluster CI job (not run
locally).

## Checklist

- [x] Issue is linked above
- [x] Tests pass locally (go test ./...)
- [x] Root-gated tests pass if applicable (N/A — no root-gated packages
touched)
- [x] Documentation updated if behavior changed (docs/api-guide.md:
SystemInfo Volumes section with example)

---------

Co-authored-by: Taahir Ahmed <taahm@google.com>
2026-08-19 15:09:33 -07:00
Noureldin 01c92e9da3 Allow actors to opt in/out of Linux capabilities (#795)
Fixes #744
- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-19 13:47:54 -07:00
sfunkenhauser 43fa6ccbc7 Introduce the worker API (#1000)
Make workers a gobal resource, identified by a unique name. Worker APIs
will be implemented in a follow up change.

Part of #730 

- [ x ] Tests pass
- [ x ] Appropriate changes to documentation are included in the PR
2026-08-19 12:06:51 -04:00
Keith Mattix II b9e42db348 atunnel: ingress CONNECT support (#993)
Finishes up the vision from #715 to have atunnel serve CONNECT on the
ingress path. This will give us the option to hit actors on other ports
besides 80. I haven't wired up atenet router yet because it's
nontrivial; we should do that in a second step so we can have a baseline
for performance

---------

Signed-off-by: Keith Mattix II <keithmattix2@gmail.com>
2026-08-19 11:16:58 -04:00
Luiz Oliveira 71a58cbf37 split controlapi functional tests into their own package (#918)
functional_test.go was also split into one file per resource, to match
what we're doing for the RPC handlers too. See #891

I also needed to make some changes to move the tests to a separate
package:

* `NewAteletDialer` now takes `options`, and `WithDialCredentials` lets
a test build its own transport credentials. The fake atelet is reached
over insecure transport. Didn't change existing callers. This is the
only non-test change.
* Moved a few unit tests that are actually functional tests to the
respective file under functionaltest/

Fixes #891


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-18 19:06:07 -04:00
Julian Gutierrez Oschmann 4c1bd9d36b Add status fields to Substrate resources (#1025)
Add `status` fields to all Substrate resources that need it. Move
server-owned fields under it.

Fixes #1006 .
2026-08-18 11:58:31 -07:00
ILLUM1N0X 2af5308b4d ateapi: return ResourceExhausted when no worker is free (#908)
ResumeActor returned `FailedPrecondition` both when the fleet had no
free worker and when the actor was not in a resumable state. Only the
former is fixable by suspending the actor to drop its node pinning, and
a client driving that recovery could not tell the two apart.

`assignWorkerAttempt()` now returns `ResourceExhausted` for
`scheduling.ErrNoCapacity`. `FailedPrecondition` was the de-facto
capacity signal, so three router consumers move with it:

- `retryable()`: `ResourceExhausted` is parked, so saturation behaves as
before. `FailedPrecondition` stays retryable on purpose. It no longer
carries saturation, but it does cover states a concurrent operation can
move the actor out of.
- `mapResumeError()`: `ResourceExhausted` now preserves the gRPC
description, keeping saturation at 503 as today. Its former 429 "rate
limited" branch was written speculatively as nothing in substrate
produced the code until now, and the fleet being full is not the caller
sending too many requests.
- `classifyOutcome()`: `no_capacity` moves to `ResourceExhausted`, and
`FailedPrecondition` gets its own label so the capacity bucket stops
collecting non-capacity errors.

`TestResumeActor_RelocatesAfterSuspendFromPaused` covers the recovery
flow this signal exists for: a PAUSED actor pinned to a full node cannot
resume, and after a suspend clears the pinning it schedules onto a
worker on another node.

Fixes #660

- `make test` clean (except the pre-existing macOS-only
`internal/atunnel` failures, untouched by this PR)
- `make fmt` clean
- `go vet ./cmd/...` clean
2026-08-17 15:00:15 -07:00
Anish Ramasekar 517e48eef4 fix(ateapi): retry stored-worker scan in syncer startup (#772)
Fixes #609

The startup stored-worker scan (`enqueueStoredWorkers`) returned on the
first `ListWorkers` error, skipping orphan cleanup until the next
restart. The `#674` workqueue only retries per-key reconciles, not this
initial scan, so retry the scan with capped `wait.Backoff` until it
succeeds or the context is cancelled.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-17 13:20:36 -04:00
Anish Ramasekar 775a5dfcae fix(ateapi): keep worker reclaimable when crashActor's release fails (#771)
Fixes #608

`crashActor` collected the `releaseWorker` error but still marked the
actor `CRASHED` and cleared its `WorkerAssignment`, stranding the
still-assigned worker with no actor left to reclaim it. On a release
failure, return instead so the actor stays intact and the workflow's
retry reclaims the worker (`releaseWorker` is idempotent).

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-08-17 12:31:21 -04:00
Jeff Luo 6dd24af84a ateapi: pair ate.workerpool.namespace with ate.workerpool.name (#953)
ate.actor.crashes, ate.actor.lifecycle.operation.duration and
ate.scheduler.assignment.duration named a WorkerPool by name alone. A
WorkerPool is namespaced, so same-named pools in different namespaces
merged into one series, and the three could not join the instruments
that already carry both keys.

ateattr.WorkerPoolAttributes now builds the pair, and omits both keys
when no pool is assigned, so a crash before the actor reached a worker
no longer reports an empty-string pool. This changes the series identity
of the three instruments.

Fixes #951

> 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
2026-08-17 09:18:53 -04:00
Tim Hockin e2f2b1ebe9 Use more specific interfaces than store.Interface (#986)
This makes it clear(er) which methods of store.Interface are used in
which paths.

No functional changes

@juli4n @laoj2 as a first step towards splitting the validation logic
out of the RPC layer and out of the storage layer. This at least gives
us targets for what to pay attention to first.
2026-08-16 19:03:03 -04:00
Keith Mattix II 89b83a5c89 Support CONNECT in atenet router (#715)
added CONNECT support for atenet ingress to support arbitrary actor ports
2026-08-14 16:12:27 -07:00
Chenyi Wang 71b006309f Right-size actor sandboxes to declared ActorTemplate resource limits (#679)
Actors previously ran in sandboxes sized to the whole node; there was no
way to declare how much CPU/memory a given actor should get. This adds
an explicit, immutable sizing knob on the ActorTemplate and plumbs it
into the sandbox's OCI spec for both the gVisor and micro-VM runtimes.

**API** — `ActorTemplate.spec.resources`
(`*corev1.ResourceRequirements`). The `limits` size the sandbox and are
baked into the immutable spec; the CRD and generated code are
regenerated accordingly.

**internal/sizing** (shared by both runtimes) — new `SandboxSize` value
(`FromLimits` / `VCPUs` / `ApplyToOCISpec`). `ApplyToOCISpec` writes CPU
quota+period and the memory limit onto the OCI spec, and is a no-op when
neither dimension is set, so 0 means "unconstrained". `VCPUs` rounds
milliCPU up to whole
vCPUs.

**Plumbing** — ateapi reads the template limits (`actorResourceLimits` →
`tmpl.Spec.Resources`) and supplies `CpuMilli`/`MemoryBytes` over the
actor RPCs (ateapi → atelet → ateom); ateom applies them — gVisor via
the cgroup leaf (`runsc --cpu-num-from-quota` provisions the sentry vCPU
count), micro-VM via the guest VmConfig. The two fields are carried on
the proto messages.

**Scheduling** — worker capacity is taken from the WorkerPool's
per-worker limits and advertised on the Worker; the scheduler only
places an actor on a worker whose capacity >= the actor's declared
limits. A missing worker or actor
dimension is treated as unconstrained, so placement is never blocked by
absent data.

**Docs & demos** — document the model in `api-guide.md`; the counter,
sandbox, and micro-VM demos declare actor limits, and their WorkerPool
comments now describe the real model (worker limits size the worker pod
+ advertise scheduling capacity; the sandbox itself is sized by
`ActorTemplate.spec.resources`).

- [x] Tests pass (unit tests for sizing + scheduling; e2e suite in
`internal/e2e/suites/sizing` resumes an actor and asserts, via the probe
fixture's `/resources` endpoint, that the running sandbox observes the
declared CPU/memory from the inside)
- [x] Appropriate changes to documentation are included in the PR
2026-08-14 14:32:26 -07:00
Julian Gutierrez Oschmann cf75cf31e8 Align ActorSnapshot/ActorSnapshotTag store methods to other CRUD methods. (#947)
* Align ActorSnapshot/ActorSnapshotTag store methods to other CRUD
methods.
* Add common struct types for List<Type> methods to carry parameters and
returned pages.
* Sort methods in store interface to group methods for each resource.
* Remove pessimistic locking from ActorSnapshot/ActorSnapshotTag RPC
handlers.
* Align CreateActorSnapshotTag method to API guideline. It should take
just the resource, no extra top level field.
2026-08-14 11:25:12 -07:00
Luiz Oliveira 9eeb1c2482 Fix actor snapshot test to use the new CreateActorSnapshotTag (#937)
https://github.com/agent-substrate/substrate/pull/914 got merged in
between the time I sent
https://github.com/agent-substrate/substrate/pull/898 for review and its
submission.

#898 wasn't rebased, so tests broke.
2026-08-13 17:36:26 -04:00
Luiz Oliveira 5fbfd774e3 Add UNSPECIFIED zero value for ActorSnapshotTagScope (#898)
Fixes #897


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-13 16:50:21 -04:00
Julian Gutierrez Oschmann a4121a4ca4 A few changes to the RPC methods for ActorSnapshot/ActorSnapshotTag. (#914)
* Remove the ActorSnapshotRef message.
* Add a `GetActorSnapshotTag` method for the `ActorSnapshotTag`
resource. The request should (as all Get requests) have a single
`ObjectRef` tag field.
* Change `GetActorSnapshotRequest` to have a single `ObjectRef` snapshot
field.
* Add a new field to `Actor,` called `source_snapshot`, which the
following schema:

```
message ActorSnapshotSource {
  // If specified, the actor will fork from this snapshot on the first resume.
  // Immutable.
  ObjectRef tag = 1;

  // A reference to the snapshot the tag resolved to at creation time.
  // Output only.
  ObjectRef snapshot = 2;

  // UID of the snapshot the tag resolved to at creation time.
  // Output only.
  string snapshot_uid = 3;
}
```

See #912 for a more detailed description of the rationale for this
change. Fixes #912
2026-08-13 11:30:58 -07:00
Luiz Oliveira a174f2880e Consolidate the actor controlapi handlers into one file (#913)
#891

No declaration added, removed, or renamed, import sets
unchanged, and every non-header line is identical to what it replaced.



- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-13 13:53:07 -04:00
Luiz Oliveira 953e38e03e Consolidate the atespace controlapi handlers into one file (#915)
No declaration added, removed, or renamed, import sets unchanged, and
every non-header line identical to what it replaced.

https://github.com/agent-substrate/substrate/issues/891

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-13 13:03:26 -04:00
Luiz Oliveira eda290e788 Consolidate the worker controlapi handlers into one file (#916)
Since we only have ListWorkers so far, this is just a file rename. The
rest of the handlers should be added to this file, instead of new
handler-specific files, as per #891

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-13 12:52:37 -04:00
Yuan Gao 45b7db7502 ateapi: bracket IPv6 atelet addresses when dialing
The atelet dial target concatenated the pod IP with ":8085", which
net.Dial rejects for IPv6; grpc.NewClient dials lazily, so it surfaced
as Unavailable on every snapshot and restore. Use net.JoinHostPort.

Also lifts the 8085 literal into internal/atelet.DefaultPort, shared by
atelet's --port flag and the dial target.
2026-08-12 16:02:51 -07:00
Luiz Oliveira c538b68ba6 Fix TOCTOU race in UpdateActorSnapshotTag (#854)
This is a very similar fix to #829

We're also removing the precondition as the storage layer function
arguments and passing it as a wrapper around the closure functions.

https://github.com/agent-substrate/substrate/issues/763

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-08-12 10:00:22 -04:00
Zoe Zhao 4b3423c01a Remove the secretKeyRef env sources feature (#835)
This reverts the ActorTemplate valueFrom.secretKeyRef support added in
#20 (issue #15).

We don't want actors to have any access to secrets, they will be
injected on the egress route instead. Removing this now so we don't need
to copy secrets into substrate resources.
2026-08-11 16:13:30 -07:00