Commit Graph
57 Commits
Author SHA1 Message Date
Julian Gutierrez Oschmann 9f8475493b Decouple ActorTemplates from WorkerPools. (#270)
This change removes the hard-link from templates to worker pools,
allowing pools to be selected using k8s labels.

Both actors and actor templates are augmented with a label selector
field.

When an actor is resumed, we resolve the set of *eligible* worker pools
by evaluating the intersection of both actor and template selectors.
Then we find a free worker from one of them. The overall design is
described in [1].

[1]:
https://github.com/agent-substrate/substrate/issues/47#issuecomment-4712334620
2026-06-18 10:58:34 -07:00
Benjamin Elder 00a90a8eda Decouple Sandbox Binaries / Config from ActorTemplate (#261)
Fixes https://github.com/agent-substrate/substrate/issues/253

> 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

Should make something like
https://github.com/agent-substrate/substrate/pull/239 less awkward, and
also https://github.com/agent-substrate/substrate/issues/255 (we don't
have to cram more fields into actortemplate for the shim binary and
update _all_ of the demo actortemplates to include them).
2026-06-17 09:46:46 -07:00
han2ni3bal-pixel 962ff6b1ae Add WorkerPool scheduling fields (#247)
**Updated Original PR Description**
## Summary

This PR adds a small, controlled set of scheduling fields to WorkerPool
so WorkerPool-managed Pods can be placed onto appropriate Kubernetes
nodes.

Added fields:

- `nodeSelector`
- `tolerations`
- `priorityClassName`
- `nodeAffinity`

## Scope

This intentionally does not expose a full `PodTemplateSpec`, and does
not add support for full `affinity`, `podAffinity`, or
`podAntiAffinity`.

Resource requests/limits are intentionally left out of scope for this CL
while the WorkerPool resource model is still being discussed separately.

The goal is to keep the first version small while addressing the
WorkerPool-to-Kubernetes-Node scheduling gap discussed in
[#212](https://github.com/agent-substrate/substrate/issues/212).

`nodeAffinity` is included to support heterogeneous node pools where
`nodeSelector` is too limited, such as requiring or preferring one of
several equivalent accelerator/local-SSD/cache node pools, or expressing
soft preferences during node pool migration.

`priorityClassName` is included to help preserve warm WorkerPool
capacity under cluster pressure. Since WorkerPool pods are the slots
available for actor resume, higher-priority pools can protect
latency-sensitive or interactive actor workloads from displacement by
lower-priority batch workloads, and allow different WorkerPools to
represent different service classes, e.g. interactive vs. batch.

## Related issue

Part of #212.
Related to #47.
2026-06-17 10:33:00 -04:00
Julian Gutierrez Oschmann a1ec7e3c5b Use k8s errors package to check AlreadyExists errors. (#260)
Don't rely on string matching.
2026-06-16 18:17:16 -07:00
Benjamin Elder 70beb3020f test: serialize envtest binary setup to fix concurrent-download race (#263)
## Problem

`go test -v ./...` (the `run-tests` job) intermittently fails during
envtest **setup** — not in any test logic:

- `fork/exec .../kubebuilder-envtest/k8s/1.36.0-linux-amd64/etcd: text
file busy`
- `setup-envtest: ... unable to create file kubectl from archive for
version-platform pair 1.36.0-linux-amd64`

Three packages use envtest (`cmd/ateapi/internal/controlapi`,
`cmd/atecontroller/internal/controllers`, `pkg/api/v1alpha1`). `go test
./...` runs them as separate test binaries **in parallel**, and each
`TestMain` shells out to `setup-envtest use`. On a cold cache they race
to download, extract, and exec the **shared** kubebuilder binaries —
hence `text file busy` (exec'ing etcd while it's still being written)
and the archive-extraction error.

Seen on `main` (e.g. the #259 push run) and on PRs. It only reproduces
on a **cold** cache, which is why it's intermittent and doesn't show up
locally once the cache is warm.

## Fix

Add `internal/envtestbins.BinaryAssetsDir()`, which runs `setup-envtest
use` under a **cross-process file lock** (`flock` on a fixed file under
`os.TempDir()`, shared by the sibling test binaries). The first caller
populates the shared cache while the others wait, then they hit a warm
no-op path lookup. The duplicated setup in all three `TestMain`s now
calls this helper.

The lock scope (user/machine-global) matches the resource it guards —
the kubebuilder binary cache is itself user-global, shared even across
repo checkouts.

## Testing

Reproduced locally by moving the warm cache aside and running the three
packages concurrently: it fails without the lock and passes with it.
Full `go test ./...` green.

Test-only change; no production code touched.
2026-06-16 18:11:11 -07:00
Dmitry Berkovich c1b51134e7 Actor Pause/Resume flow (#227)
Implements  `PAUSED` state for issue #119.

The snapshot files are kept locally on node VM in a separate folder. At
resume time, scheduler uses a node VM hint and picks up a worker from
the same node where files were stored at suspend time.

The local file management solution is temporary and will be replaced
once @msau42 introduces a new component that is supposed to manage files
on the node VM.

- [X] Tests pass
- [X] Manual tests with counter demo
```
>kubectl ate create actor my-counter-1 --template ate-demo-counter/counter
>curl -X POST -H "Host: my-counter-1.actors.resources.substrate.ate.dev" http://localhost:8000
hello from: 169.254.17.2 | preserved memory count: 1
>curl -X POST -H "Host: my-counter-1.actors.resources.substrate.ate.dev" http://localhost:8000
hello from: 169.254.17.2 | preserved memory count: 2
>kubectl ate pause actor my-counter-1 -o json
> curl -X POST -H "Host: my-counter-1.actors.resources.substrate.ate.dev" http://localhost:8000
hello from: 169.254.17.2 | preserved memory count: 3
```

### Breaking change
This PR introduces a breaking change in the Actor proto. All existing
actor needs to be recreated, prior testing PAUSE functionality.
2026-06-16 13:15:36 -07:00
Frederick F. Kautz IV 1f2c27b228 feat(atelet): expose actor identity via /run/ate/actor-id
Gives an actor a reliable way to learn its own ID without parsing the
Host header. atelet populates a per-actor identity directory (currently
the single file actor-id) and bind-mounts it read-only at /run/ate into
each app container; the pause container gets none.

- A directory rather than a single-file mount, per the discussion on
  #178, so future late-bound data can land as sibling files without
  changing the mount shape.
- The bundle is regenerated on every resume and external bind mounts are
  re-attached from the restore-time config.json, so the value stays
  correct per-actor through a golden-snapshot restore; an env var (or a
  file baked into the image) would be frozen at the golden actor's ID,
  since it lives in the checkpointed process memory.
- actor-id is written atomically (temp file, fsync, rename, parent
  fsync): the directory is visible to a possibly-running actor, so a
  reader must never observe a torn value.
- The identity dir is cleared and recreated with the other per-actor
  dirs in resetActorDirs.
- The in-rootfs mount point is created via os.Root, so a symlink planted
  in the image cannot redirect the write outside the extracted rootfs.

Documented for actor authors in docs/api-guide.md: read it fresh on use;
raw ID, no trailing newline.
2026-06-16 11:06:05 -07:00
Julian Gutierrez Oschmann 6803c664e5 Move internal component packages out of /internal
Move them under cmd/<component>/internal as dictated by substrate's code
layout guidelines.
2026-06-15 13:00:19 -07:00
Bowei Du 9503099f88 Move to the right place this time: cmd/atenet/internal (#241)
This follows the convention documented here:

AGENTS.md

```
**Where to put new Go code — quick rules:**

| Only used by one binary | `cmd/<binary>/internal/<pkg>` |
```

See the discussion here:
https://github.com/agent-substrate/substrate/issues/49
2026-06-12 17:45:45 -07:00
Bowei Du ea86f9a4dc Flatten the deep dir structure in atenet
I just noticed that the directory structure is pretty deep. I want to
make this less deep before we get too far into the project:

* cmd/atenet/internal/app << this directory level is not needed. get rid of it.
* cmd/atenet/internal/router/* move everything but the cobra flags to internal/router
* cmd/atenet/internal/* just has the cobra flags definitions

This is moving files -- no logic change.
2026-06-12 16:53:19 -07:00
Eitan Yarmush a3f44744d3 Use link-local actor veth addresses 2026-06-10 22:37:09 -07:00
Eitan Yarmush b923428f46 Fix ateom cleanup failure handling 2026-06-10 22:37:09 -07:00
Eitan Yarmush b9ab32a50d Address actor network cleanup review 2026-06-10 22:37:09 -07:00
Eitan Yarmush ef1c9edde8 Fix gvisor cleanup issue
What was happening:

  1. runsc checkpoint pause succeeded.
  2. After that, substrate tried to run runsc state and runsc delete for the app container.
  3. But after checkpointing the root sandbox container, the runsc control server was no longer usable,
     so those post-checkpoint commands failed with connection refused.

  4. That made CheckpointWorkload return an error even though the actual checkpoint had already
     succeeded.

  5. The ActorTemplate stayed stuck in WaitGoldenActor.

  The change removes the post-checkpoint state/delete calls from ateom-gvisor. After checkpoint
  succeeds, it only cleans up the actor network and returns success. This matches the existing contract
  in the code: atelet owns resetting the actor directories after checkpoint/upload, and it already
  calls resetActorDirs(...).

Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
2026-06-10 22:37:09 -07:00
Eitan Yarmush 62943d0dd2 Trim actor networking comment 2026-06-10 22:37:09 -07:00
Eitan Yarmush 9d9ad7369b Address actor inbound networking TODOs 2026-06-10 22:37:09 -07:00
Eitan Yarmush 709c346723 Group nftables rules with chain definitions 2026-06-10 22:37:09 -07:00
Eitan Yarmush 504cfd21ab Replace actor eth0 move with veth networking 2026-06-10 22:37:09 -07:00
Frederick F. Kautz IV 8c64c7bf3a fix(atelet): validate RPC inputs that become host filesystem paths (#206)
Fixes #202

atelet's gRPC server runs on an unauthenticated hostPort
(`manifests/ate-install/atelet.yaml`), so its
`Run`/`Restore`/`Checkpoint`
RPCs must treat every request field as untrusted. Several of those
fields
are turned into host filesystem paths and then deleted, created,
written,
or executed, none of them validated first. A caller that can reach the
port can smuggle a path separator or `..` through them and make atelet
operate outside the intended actor tree.

### Unvalidated inputs (each an independent escape route)
- **`actor_template_namespace` / `name` / `id`** are concatenated by
`ateompath.ActorPath` into the per-actor dir that `resetActorDirs` runs
  `os.RemoveAll` / `os.MkdirAll` on.
- **`target_ateom_uid`** builds the netns path
(`/run/netns/ateom:<uid>`)
  and the ateom control socket (`.../ateoms/<uid>/ateom.sock`).
- **`spec.containers[].name`** becomes an OCI bundle path component
  (`os.RemoveAll` / `MkdirAll` / `config.json` write).
- **`runsc.sha256_hash`** builds `static-files/runsc-<hash>`. The
cache-hit
`os.Stat` early return hands that path to ateom to **execute** before
the
  existing hex check runs, so an escaped path could execute an arbitrary
  on-disk binary as the sandbox runtime.

### Where the validators live
The field rules are exported from **`internal/resources`**
(`ValidateActorRef`, `ValidateAteomUID`, `ValidateContainerNames`,
`ValidateRunscHash`, `ValidateSnapshotURIPrefix`), alongside the
existing
`ValidateActorID`. Every one of these values originates upstream in
user-supplied API/CRD data (the runsc hash and snapshot location come
from
the `ActorTemplate` spec), so the API server and controller can apply
the
same rules at admission time instead of failing at actor-activation
time.
`ValidateContainerNames` takes names rather than the atelet proto so
`resources` stays proto-free. Unifying `ValidateActorID` with
`ValidateActorRef` is deferred: it changes ate-api-server's
client-facing
error responses.

The per-RPC glue stays in `cmd/atelet`: each of
`Run`/`Checkpoint`/`Restore`
calls its own typed validator (`validateRunRequest` etc.) as the first
statement at the boundary, before any path is constructed.

### What is validated
- namespace and actor ID as DNS-1123 labels, template as a DNS-1123
  subdomain, matching how these are named; additionally the combined
  `<ns>:<tmpl>:<id>` path component is capped at 255 bytes, since
  individually valid DNS names can join to 381 bytes, over common
  filesystems' filename limit;
- `target_ateom_uid` as a DNS-1123 label (pod UIDs are UUIDs, which
  qualify);
- each container name as a DNS-1123 label, additionally rejecting the
reserved name `pause` (collides with the sandbox-infra bundle and races
  its concurrent writer) and duplicate names (same bundle path, mutual
  corruption);
- `runsc.sha256_hash` as exactly 64 hex chars, checked in `fetchRunsc`
  before the path is built and before the cache-hit early return;
- `snapshot_uri_prefix` (Checkpoint/Restore) as a parseable URI naming a
bucket, with query/fragment/userinfo rejected: object names are appended
  by concatenation and the storage layer re-parses using only host and
path, so those components would silently redirect the upload/download to
  a different object. The scheme is deliberately not restricted; that is
  storage-backend policy.

Validation failures return `codes.InvalidArgument` with the reason.
Previously the interceptor surfaced them as `codes.Internal` with the
message masked as "internal server error".

DNS-1123 is a deliberate fit. These values already *are* Kubernetes
names,
so every legitimate value passes, while the charset forbids `/`, `.`
(for
labels), and `..`, which is exactly what enables traversal. For a
cooperating caller the change is behavior-neutral: inputs newly rejected
previously failed later or misbehaved silently.

### Testing
- Table tests for each validator in `internal/resources`, targeting the
exported functions (valid values, separators, `..`, empty, case, length,
  the label-vs-subdomain distinction, the combined-length cap, the
  `pause`/duplicate rules, and the URI component rejections).
- Per-request validator tests in `cmd/atelet` pin which rules each RPC
  enforces, one table per validator.
- `TestRPCBoundariesReject` exercises `Run`/`Checkpoint`/`Restore`
directly
and asserts a traversal value is rejected with `InvalidArgument`, which
  guards against future removal or reordering of the validation call.
- `TestFetchRunscRejectsBadHash` plants a real cache file at the path an
invalid hash resolves to and asserts `fetchRunsc` still errors. It
passes
  with the current code and fails if validation is moved after the
`os.Stat` early return, so it proves the ordering rather than just that
  validation runs.

- [x] Tests pass
- [x] No documentation changes needed
2026-06-10 16:38:05 -07:00
Davanum Srinivas a88ad188d6 docs: correct stale script and template references
Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-06-09 15:19:21 -07:00
Tim Hockin 2b7c2634b1 Use pflag instead of Go's flag lib (#152)
Better to get ahead of this early.
2026-06-09 11:03:03 -07:00
Tim Hockin 68bb5efc8f Add more CRD validation (#205)
It's not all done but the fields we are playing with now are mostly
done.
2026-06-09 13:19:25 -04:00
Eitan Yarmush 0076b20c1a Support actortemplate secret env (#20)
A couple of TODOs/Discussions which I think are important to have based
on these changes.
1. Do we need a caching client for this to avoid calling the apiserver
every time?
2. Where/how do we want to resolve secrets generally? I think there are
good arguments to be made for the control-plane OR the "node" component.
In my mind this also related to:
https://github.com/agent-substrate/substrate/issues/18

Fixes #15 

> 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-06-08 16:04:50 -07:00
Haven Xia 36b43f5b71 Update the variable name used in logging to be actual Actor Template Namespace (#191)
It was using a wrong name `Actor Namespace` before, which doesn't exist
yet.


- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-06-08 13:38:20 -04:00
Haven Xia 67bb44d090 Add Substrate Snapshot size metric and dashboard (#160)
This PR introduces a new OpenTelemetry histogram
`"atelet.snapshot.size"` from `atelet` over the existing
OTLP path. It records the uncompressed size in bytes of each gVisor
snapshot image written during checkpoint -- `checkpoint.img` ,
`pages.img` and `pages_meta.img` -- tagged by image `kind` and
`ActorTemplate`. This surfaces how large actor snapshots are and which
`ActorTemplate`s are expensive to checkpoint/restore.

Add a well-defined dashboard for snapshot metrics that contains 3
charts.
- snapshot image size p99 by `ActorTemplate`
- snapshot image size p50/p95/p99 (all kinds)
- snapshot QPS by `ActorTemplate`

<img width="996" height="717" alt="image"
src="https://github.com/user-attachments/assets/99bbab46-37dc-476e-bfd9-cdffeb174b4b"
/>


> 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-06-05 11:01:49 -07:00
Haven Xia a325d4118b Add Substrate routing-latency metric and Cloud Monitoring dashboard (#157)
This PR introduces a new OpenTelemetry histogram
`"atenet.router.route.duration"` from `atenet-router` over the existing
OTLP path. It measures substrate overhead -- from envoy receiving a
request to envoy sending the request to the the resolved worker
endpoint, excluding actor compute and the response. This is the
`Substrate E2E latency` that under our control.

Add a well-defined dashboard for E2E metrics that contains 6 charts.
- Substrate routing latency — P50/P95/P99
- Substrate routing latency P99 - by stages
- Substrate routing latency P99 — by ActorTemplate
- Substrate routing QPS - by Status
- Substrate E2E (full round-trip) Latency — P50/P95/P99
- Substrate E2E (full round-trip) QPS — by Response Code

<img width="1601" height="894" alt="image"
src="https://github.com/user-attachments/assets/be6fc572-dfd8-4ef0-a0a9-a77f85f2ecf8"
/>


Fixes b/508613998

> 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-06-05 10:42:06 -07:00
Grant McCloskey 06dbd13ace Feature/list actors scale w/ pagination (#156)
# Implement Pagination and Pipelining for ListActors

## Overview

**Fixes:** #153 

This PR introduces robust, memory-safe pagination to the `ListActors`
operation and optimizes the Valkey retrieval layer.

Previously, `ListActors` performed an unbounded parallel scatter-gather
across all Valkey cluster shards, pulling all actors into memory at
once. At scale (millions/billions of actors), this would cause OOM
errors and severe latency.

This update replaces the concurrent scatter-gather with a deterministic,
sequential shard scanning approach, adhering to
[AIP-158](https://google.aip.dev/158) pagination standards.

## Changes Included
- **gRPC API (`ateapi.proto`)**: Added `page_size`, `page_token`, and
`next_page_token` fields.
- **Client (`kubectl-ate`)**: Updated to seamlessly loop through
`next_page_token` values until all actors are fetched, presenting a
continuous stream to the user without blowing up memory on a single
request.
- **API Handler (`controlapi`)**: Bounded requests with a max
`page_size` limit of `1000`.
- **Persistence (`ateredis`)**: 
- Implemented `listActorsPageToken` to safely serialize and encode
cross-shard cursors.
- Replaced `ForEachMaster` concurrent execution with deterministic
sequential iteration.
- Implemented Valkey Pipeline batching (`Pipelined`) for `GET` requests
to eliminate N+1 network latency.
- **Testing**: Added `TestListActors_Pagination` at both the integration
level (`functional_test.go`) and data layer (`ateredis_test.go`).
Refactored sorting to use `cmpopts.SortSlices`.

## Design Decisions
During the design phase, several explicit trade-offs were made to
balance performance with API simplicity:
1. **Opaque Tokens (AIP-158):** The `page_token` is strictly an opaque
string at the `ateapi` layer. The client (`kubectl-ate`) is completely
unaware that it contains base64-encoded JSON mapping to `{"shard",
"cursor"}`. This keeps our API database-agnostic.
2. **Empty Pages & Client-Side Looping:** The persistence layer is kept
simple and "dumb". If a shard scan returns 0 keys but the cursor is not
`0`, we return an empty page. We rely on the client `kubectl-ate` to
`for` loop until `next_page_token` is completely exhausted. Update: I
have server side looping across shards until pagesize is met to limit
round trips due to high shard count or low actor count on distributed
shards.
3. **"Soft" Consistency (Missing/Duplicate Data):** `ListActors` is an
infrequent operations command, not on the critical path. Taking a global
cluster lock for iteration would severely degrade performance. We accept
eventual consistency here: if actors migrate shards during an active
scan, they might be duplicated or missed.
4. **Topology Flux Errors:** If a client passes a valid token pointing
to a shard address that no longer exists (due to a cluster resharding
mid-scan), the API simply returns an `Aborted` error. We do not attempt
complex recovery; the client must just restart the `ListActors` command.
5. **Bounded Page Size:** Requests are currently capped at a maximum of
`1000` to prevent single massive payloads. We can revisit/change the max
later.

NOTE: Actor filtering will be implemented later.

## Architecture Flow (Valkey)

Note: If there are future plugin-db models they will need to handle the
cusor/pages at their own implementation layer per the store.go
interface.

The system now seamlessly iterates across shards, handing off cursors
safely between nodes:

```mermaid
sequenceDiagram
    participant CLI as kubectl-ate
    participant API as ateapi
    participant Valkey as Valkey Cluster (Multiple Shards)
    
    CLI->>API: ListActors(page_size=1000, page_token="")
    
    Note over API: Request 1: Empty Token
    API->>API: Decode page_token (empty -> start fresh)
    API->>Valkey: Fetch all Master Nodes
    Valkey-->>API: [Master C, Master A, Master B]
    API->>API: Sort Masters alphabetically by IP/Port
    Note over API: Sorted: [Master A, Master B, Master C]
    
    API->>Valkey: SCAN (Master A, cursor=0, count=1000)
    Valkey-->>API: keys=[k1..k300], next_cursor=45
    
    Note over API: OPTIMIZATION: Pipelining avoids N+1 problem
    API->>Valkey: PIPELINE GET (k1..k300) directly on Master A
    Valkey-->>API: values=[v1..v300]
    
    API->>API: Decode Protobufs
    API->>API: Encode page_token={"shard": "Master A", "cursor": 45}
    API-->>CLI: 300 Actors, next_page_token="base64(eyJzaGFyZ..."
    
    %% SECOND REQUEST
    CLI->>API: ListActors(page_size=1000, page_token="base64(...)")
    
    Note over API: Request 2: Resuming Mid-Shard
    API->>API: Decode page_token -> Shard="Master A", Cursor=45
    API->>API: Fast-forward directly to Master A
    API->>Valkey: SCAN (Master A, cursor=45, count=1000)
    Valkey-->>API: keys=[k301..k400], next_cursor=0
    
    API->>Valkey: PIPELINE GET (k301..k400) directly on Master A
    Valkey-->>API: values=[v301..v400]
    
    Note over API: Shard A is empty. Token points to Shard B!
    API->>API: Encode page_token={"shard": "Master B", "cursor": 0}
    API-->>CLI: 100 Actors, next_page_token="base64(eyJzaGFyZ..."
    
    %% THIRD REQUEST
    CLI->>API: ListActors(page_size=1000, page_token="base64(...)")
    Note over API: Request 3: Starting New Shard
    API->>API: Decode page_token -> Shard="Master B", Cursor=0
    Note over API: Continues scanning Master B...
```


## Testing:

```
> ./bin/kubectl-ate get actors
NAMESPACE          TEMPLATE   ID                                     STATUS             ATEOM POD   ATEOM IP   VERSION
ate-demo-counter   counter    61b478cc-2e44-4ec7-ac6c-1af3f385de86   STATUS_SUSPENDED   <none>                 5
```

```
 ~/go/bin/grpcurl -insecure -d '{"page_size": 2}' localhost:9090 ateapi.Control/ListActors
{
  "actors": [
    {
      "actorId": "test-counter-4",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    }
  ],
  "nextPageToken": "eyJzaGFyZCI6IjEwLjI0NC4wLjE5OjYzNzkiLCJjdXJzb3IiOjI1MjU1fQ=="
}
```


<details>

```
bash -c '
export NEXT_TOKEN=""
while :; do
  echo "--- Fetching Page ---"
  
  # Fetch the data and store the response
  RESPONSE=$(~/go/bin/grpcurl -insecure -d "{\"page_size\": 2, \"page_token\": \"$NEXT_TOKEN\"}" localhost:9090 ateapi.Control/ListActors)
  
  # Print the response to the terminal
  echo "$RESPONSE"
  
  # Extract the token without needing jq
  NEXT_TOKEN=$(echo "$RESPONSE" | grep -o "\"nextPageToken\": \"[^\"]*\"" | cut -d"\"" -f4)
  
  # If no token is found, we reached the end
  if [ -z "$NEXT_TOKEN" ]; then
    echo "--- Reached the end! ---"
    break
  fi
  
  echo ""
  read -p "Press Enter to fetch the next page using the new token..."
done
'
```

--- Fetching Page ---
{
  "actors": [
    {
      "actorId": "test-counter-4",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    }
  ],
"nextPageToken":
"eyJzaGFyZCI6IjEwLjI0NC4wLjE5OjYzNzkiLCJjdXJzb3IiOjI1MjU1fQ=="
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
"nextPageToken": "eyJzaGFyZCI6IjEwLjI0NC4wLjIxOjYzNzkiLCJjdXJzb3IiOjB9"
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
  "actors": [
    {
      "actorId": "test-counter-1",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    }
  ],
"nextPageToken":
"eyJzaGFyZCI6IjEwLjI0NC4wLjIxOjYzNzkiLCJjdXJzb3IiOjQ1NjAwfQ=="
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
  "actors": [
    {
      "actorId": "test-counter-5",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    }
  ],
"nextPageToken":
"eyJzaGFyZCI6IjEwLjI0NC4wLjIxOjYzNzkiLCJjdXJzb3IiOjYyMDg2fQ=="
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
"nextPageToken": "eyJzaGFyZCI6IjEwLjI0NC4wLjI1OjYzNzkiLCJjdXJzb3IiOjB9"
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
  "actors": [
    {
      "actorId": "test-counter-2",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    }
  ],
"nextPageToken":
"eyJzaGFyZCI6IjEwLjI0NC4wLjI1OjYzNzkiLCJjdXJzb3IiOjE2OTkzfQ=="
}

Press Enter to fetch the next page using the new token...
--- Fetching Page ---
{
  "actors": [
    {
      "actorId": "test-counter-3",
      "version": "1",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED"
    },
    {
      "actorId": "61b478cc-2e44-4ec7-ac6c-1af3f385de86",
      "version": "5",
      "actorTemplateNamespace": "ate-demo-counter",
      "actorTemplateName": "counter",
      "status": "STATUS_SUSPENDED",
"lastSnapshot":
"gs://ate-snapshots/ate-demo-counter/61b478cc-2e44-4ec7-ac6c-1af3f385de86/2026-06-03T03:17:34Z-UMYVUZOWW6LDPS5X5FSG3KC4VV"
    }
  ]
}
--- Reached the end! ---

</details>
2026-06-03 15:06:28 -07:00
Benjamin Elder f46c67f0b7 ateom: avoid unix socket path length limits (#100)
Pod names alone can easily exceed the max length (253 > 107).

Add a symlink with the full pod name for debugging.

Follow-up #92 

> 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

---

TODO: I'm not sure about the symlink. It's annoying mapping pod =>
hash(pod) otherwise though. We could put a file inside the directory
with the pod name instead, but I think we should probably put the full
pod name _somewhere_ in the structure so it's easier to inspect these.
2026-06-03 11:37:52 -07:00
Julian Gutierrez Oschmann 7b3a41948f Move atelet code to work with object storage under atelet/internal.
Also add otel traces when uploading objects, not just when downloading. This is useful when looking at traces during suspend.
2026-06-02 17:09:36 -07:00
Taahir Ahmed 375801417e Reenable the "-direct" and "-background" runsc flags (#150)
These flags allow the guest workload to begin executing sooner during
restore, because gVisor loads only the necessary memory pages. Other
pages are lazily loaded in the background or as they are referenced.

This mode slightly complicates our state management, because it's more
difficult to tell when gVisor is done with the checkpoint files passed
to `runsc restore`.

We can work around this by ensuring that we always pass different
directories to `runsc checkpoint` and `runsc restore`.

`runsc checkpoint` properly handles the case where the gVisor sentry has
not finished background loading of pages from a previous `runsc
restore`.
2026-06-02 16:34:35 -07:00
Davanum Srinivas 94a5fff2c3 feat: stamp binaries with version/commit/build-date + --version flag
Adds internal/version with three vars (Version, Commit, BuildDate) set
via -ldflags -X at build time. When unset (e.g. plain `go build`
without LDFLAGS), runtime/debug.ReadBuildInfo() populates them from the
embedded vcs info so `go install`, `go run`, and ko-built images
(buildvcs is on by default) still produce something meaningful.

Wired through:
  - Makefile: VERSION / COMMIT / BUILD_DATE resolved from `git describe`
    / `git rev-parse` / `date`. LDFLAGS plumbed into every go-build and
    ko-build target (build-images, build-atectl, build-atenet,
    build-demos).
  - hack/install-ate.sh: untouched here — ko apply / resolve don't
    accept --ldflags, and ko's buildvcs embedding gives Commit +
    BuildDate automatically through runtime/debug.ReadBuildInfo() in
    the version package's init() fallback.

All six binaries respond to `--version`:
  - kubectl-ate and atenet (cobra): rootCmd.Version = version.String()
  - ateapi, atelet, ateom-gvisor, podcertcontroller (flag): explicit
    `-version` bool that prints and exits.

Sample output:
  kubectl-ate version 3b0a0d5 commit=3b0a0d5c2... \
      built=2026-05-26T14:16:53Z darwin/arm64

Closes the "kubectl-ate --version has nothing to print" gap; gives ops
a way to tell at a glance which commit a deployed substrate component
came from.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-06-02 16:34:18 -07:00
Benjamin Elder e4b86a4828 atelet: fix OCI untar last-layer wins (#142)
Follow-up to
https://github.com/agent-substrate/substrate/pull/101#issuecomment-4597137488

This isn't a regression from the previous PR, but I noticed this bug
while reviewing.

> 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-06-02 10:02:59 -07:00
Kyosuke Konishi 2821d364b7 fix(atelet): prevent path traversal in OCI tar extraction (#101)
Resolves the three TODO comments in `cmd/atelet/oci.go` that called for
a constrained filesystem to prevent path traversal, symlink escape, and
hardlink escape during OCI tar extraction.

Uses `os.Root` (Go 1.24+) to confine all file operations to the rootfs
directory. Adds `validateTarName()` as defence in depth.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-06-01 15:56:41 -07:00
Mark Chmarny a8c67399d6 fix(router): return error interface from reqError constructors
Address review feedback from @bowei: return error interface rather than
the concrete *reqError pointer so callers cannot accidentally fall into
the typed-nil pitfall. Tests use errors.As to access struct fields.
2026-06-01 09:27:39 -07:00
Mark Chmarny 19daf746f2 fix(router): map gRPC errors to proper HTTP codes (#31)
The Envoy ext_proc handler previously wrapped resume errors with %w,
leaking the gRPC encoding ("rpc error: code = ... desc = ...") into the
response body, and routed every non-NotFound failure through 500. It
also returned a generic "not found" body that did not identify which
resource was missing.

This change introduces a small mapping layer that translates gRPC status
codes into appropriate HTTP status codes with short, client-safe bodies:

  NotFound           -> 404
  FailedPrecondition -> 503 (desc preserved, e.g. "no free workers
                             available", which is actionable and not
                             security-sensitive)
  Unavailable        -> 503
  DeadlineExceeded   -> 504
  PermissionDenied   -> 403
  Unauthenticated    -> 401
  ResourceExhausted  -> 429
  other              -> 500 (no detail leak)

The original error is preserved via Unwrap so existing slog.ErrorContext
logging keeps full fidelity.

Layering: ActorResumer no longer translates codes.NotFound into a
router-layer sentinel; gRPC errors propagate untouched so the HTTP
boundary owns the mapping.
2026-06-01 09:27:39 -07:00
Tim Hockin 028ecaa3c0 Rename readme.md -> README.md 2026-05-31 19:45:36 -07:00
Tim Hockin 1b26d16aee Remove trailing space from other files 2026-05-31 19:45:36 -07:00
Tim Hockin d9773e7d03 Remove trailing space from md files 2026-05-31 19:45:36 -07:00
Tim Hockin 1ac5ba32a1 Fix boilerplate in go files 2026-05-31 19:45:36 -07:00
Taahir Ahmed 9291df0b6d ActorTemplate: Enforce that all images must be pinned 2026-05-28 13:25:10 -07:00
Adrien Delorme 27f239dcf5 fix(ateompath): validate ateom socket path length (#92)
## Symptom

When `<namespace>:<pod-name>` exceeds 107 bytes, `ateom-gvisor` exits
with `bind: invalid argument` and the controller surfaces a generic
`DeadlineExceeded`.

## Cause

Linux limits unix-socket paths to 107 bytes (`sockaddr_un.sun_path`). A
pod's ReplicaSet hash can jiggle between 8 and 10 chars across rollouts,
so a demo that fits at 8 silently breaks at 10 with no clear error.

## Fix

Add `ValidateAteomSocketPath` (with unit tests) and call it from
`ateom-gvisor` before the bind, so the operator sees the real cause in
the pod log instead of a kernel EINVAL.
2026-05-27 18:58:47 -07:00
Davanum SrinivasandJulian Gutierrez Oschmann 1a852c80e0 ateapi/syncer: release actor when host pod is deleted (#75)
Today, if a worker pod that hosts an active actor is forcibly destroyed,
substrate does not migrate the actor. The actor's AteomPodName still
points at the dead pod and the router times out forwarding to a dead IP.

WorkerPoolSyncer's pod-informer DeleteFunc and soft-delete branch now
check whether the worker being removed is bound to an actor, and if so,
reset that actor to `STATUS_SUSPENDED` before the worker row goes away.

The new helper releaseActorOnDeadWorker reads the actor, only acts if
the actor still claims this worker (a concurrent SuspendActor or
DeleteActor that already advanced state is respected), clears the
pod-binding fields and InProgressSnapshot, preserves LastSnapshot, and
writes via version-checked UpdateActor. On ErrPersistenceRetry we drop
the attempt and let the next informer event retry — no separate lock, no
in-handler retry budget.

`STATUS_SUSPENDED` is reused rather than introducing a new state. The
post-orphan invariant is identical and reusing keeps the proto, printer,
dialer, and router contracts unchanged. The next request through atenet
triggers an implicit resume; findFreeWorker picks any free worker in the
pool; LastSnapshot is restored.

Test plan: `TestSyncer_DeleteBoundWorker_ClearsActor` covers RUNNING
actor → pod delete → SUSPENDED with cleared bind fields,
InProgressSnapshot dropped, LastSnapshot preserved. All existing tests
in `cmd/ateapi/internal/controlapi/` still pass.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
Co-authored-by: Julian Gutierrez Oschmann <juliangut@google.com>
2026-05-27 16:17:33 -04:00
Benjamin Elder b42350f050 make sure building with ko still works by adding main_unsupported.go
ko resolves packages before resolving the build variants, so if you only have build tagged code it will error
2026-05-27 12:09:31 -07:00
Benjamin Elder 6fb16fc850 build: add linux build tags and relocate portable aetom-gvisor code to internal package 2026-05-27 12:09:31 -07:00
Haven Xia 3f4aa5d010 Refactor logging (#65)
This PR refactors `ateom-gvisor` and `kubectl-ate` to adopt a
flat-merged logging architecture, eliminating server-side and
client-side field guessing:
1. **Cloud Logging**: `ateom-gvisor` parses actor JSON, injects actor
labels/time, and outputs the flat map directly. GKE Cloud Logging
built-in rules parse native severity/message fields automatically.
2. **CLI Logging**: Removed the `--raw` flag. `kubectl ate logs` strips
Substrate metadata and outputs clean, consistent JSON lines with the
`time` field prepended first.
<img width="3235" height="306" alt="image"
src="https://github.com/user-attachments/assets/4a6ca291-db8d-45dc-b840-43efa74739f0"
/>

3. **Documentation**: Updated `docs/observability.md`

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-05-27 10:50:10 -07:00
Davanum Srinivas a436ea216a refactor: trim cmd/* boilerplate, extract internal/serverboot (#88)
Three related cleanups in cmd/{ateapi,atelet,ateom-gvisor}/main.go:

1. atelet Run/Checkpoint/Restore handlers shared three blocks of
cut-and-paste (fetchRunsc + MkdirAll, errgroup over pause + application
containers calling prepareOCIDirectory, dial-ateom + NewAteomClient, the
ateletpb -> ateompb WorkloadSpec projection). Factored into
fetchRunscAndPrep, prepareOCIBundles, dialAteom, buildAteomWorkloadSpec,
plus a uploadIfExists for the optional pages.img / pages_meta.img upload
in Checkpoint.

2. ateapi main() was a 242-line wall of inlined startup steps;
decomposed into named helpers (loadFlagsFromEnv, logFlagValues,
connectRedis with buildRedisTLSConfig + pingRedisWithRetries,
newKubeClients, buildServerCreds) so the top-level flow reads as
orchestration.

3. ateapi, atelet, and ateom-gvisor each carried their own ~30-line
initTracing + ~35-line initMetrics + slog/metrics-server boilerplate
that differed only in ServiceName, sampler choice, and whether the OTLP
exporter was wired. Lifted to a new internal/serverboot package exposing
InitLogger, InitTracing (with TracingOptions.NoExporter for ateom-gvisor
which loses egress once eth0 moves into the gvisor netns), InitMetrics,
Fatal, ShutdownProvider, and StartMetricsServer.

No behaviour change. Verified with go vet, GOOS=linux go build, and go
test ./cmd/ateapi/internal/...

Fixes #<issue_number_goes_here>

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

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

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-05-27 10:30:38 -04:00
Davanum Srinivas de2ad8add9 fix(ateom-gvisor): make eth0 move/restore idempotent + roll back on error
ateom-gvisor moves the pod's eth0 interface into the actor's interior
netns before invoking runsc run/restore. The current code assumes eth0
is in the pod netns at the start of every RunWorkload/RestoreWorkload.
That assumption breaks if a previous call failed mid-flight: eth0 is
left stranded in the interior netns, the next call errors with
"eth0: Link not found", and the worker pod is stuck until restart.

Add ensureEth0InPodNetns that moves eth0 back to the pod netns if it
is in the interior, and is a no-op otherwise. Call it at the top of
both entry points to recover from a previously-aborted call, and via
a deferred handler after the eth0-into-interior move to roll back on
errors in the current call. The success path is unchanged.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-05-26 15:59:04 -07:00
Davanum Srinivas 55923825c7 chore(hack): add golangci-lint
Adds golangci-lint v2.12.2 using the per-tool-module pattern under
hack/tools/golangci-lint/. The verifier at hack/verify/golangci-lint.sh
is auto-discovered by hack/verify-all.sh, so the existing CI verify
step picks it up with no workflow YAML change.

Ships .golangci.yaml with the standard linter set, a make lint
target, and a verify wrapper that pins GOOS=linux so ateom-gvisor's
netlink references typecheck on macOS the same way they do on CI.

Adjustments to keep the first run clean: two dead-code deletions
((*runsc).cmdList, MemoryPullCache.gcsClient), two SA9003 empty
branches replaced with slog.Error / t.Logf, one S1008 return-bool
simplification, and inline //nolint for one intentional best-effort
ReleaseLock plus four audit-tracked SA1019 deprecations.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-05-23 16:19:09 -07:00
Julian Gutierrez Oschmann b1fe163fcf Run tools/reorg tool.
This commit was automatically generated by the tools/reorg tool.

```
go run ./tools/reorg
go generate ./...
bash benchmarking/locust/generate_protos.sh
```
2026-05-22 14:43:26 -07:00
Haven Xia acee90f9e5 Support streaming actor logs with --follow/-f for kubectl ate logs (#28)
This PR adds live-streaming log support (`--follow/-f`) to `kubectl-ate
logs actors`. Unlike static container logs, this implementation is
actor-aware and resilient to virtual actor suspensions and migrations:

Suspend Lifecycle:
<img width="3700" height="1856" alt="image"
src="https://github.com/user-attachments/assets/38568108-8229-40b0-afc7-d1d5ddc817e1"
/>

Resume Lifecycle:
<img width="3696" height="1530" alt="image"
src="https://github.com/user-attachments/assets/cf934608-62fe-4fb8-bd2c-5e6e7a0af776"
/>



> 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-05-21 18:09:07 -07:00