Commit Graph
251 Commits
Author SHA1 Message Date
Eric Curtin edaa0f7656 ateapi: reach an atelet again after its node restarts (#2118)
Fixes #2116

Redial when an atelet pod keeps its UID but gets a new IP.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-10-02 22:57:43 +00:00
Shruti Nair e9a31a9f51 ateapi: Implement Policy Governance CRUD Access Policy APIs (#1965)
Working on https://github.com/agent-substrate/substrate/issues/1563

Fixes https://github.com/agent-substrate/substrate/issues/1697,
https://github.com/agent-substrate/substrate/issues/1701

## Summary

Adds `AccessPolicy` singleton subresources for the Global
(`global:root`) and `Atespace` scopes, stored in PostgreSQL and enforced
through OpenFGA.

### Changes
- **API (`Control` service)**
- Global: `CreateGlobalAccessPolicy`, `GetGlobalAccessPolicy`,
`UpdateGlobalAccessPolicy` (no Delete; an update with empty bindings
clears all grants).
- Atespace: `CreateAtespaceAccessPolicy`, `GetAtespaceAccessPolicy`,
`UpdateAtespaceAccessPolicy`, `DeleteAtespaceAccessPolicy`.
  - `Binding.members` has set semantics.
- **Always-wired authz**: the OpenFGA server, `Authorizer` and
`PolicyManager` are always created. `--experimental-enable-authz` only
controls whether the interceptor enforces checks on regular RPCs. Policy
tuples therefore stay in sync with the store in every mode, and deleting
an atespace always removes its tuples.
- **Governance RPCs are always enforced**: AccessPolicy RPCs are marked
`alwaysEnforce` in the permission registry and are checked even when
enforcement is off, so nobody can grant themselves access before it is
turned on. A test fails if an AccessPolicy RPC is added without that
marker.
- **Bootstrap owners**: `--authz-bootstrap-owners` lists
server-configured owners (required when enforcing). They are never
written to the store: `Authorizer.Check` adds a contextual `owner
global:root` tuple for the calling user only, so access follows the
server config and is dropped on restart without them. A bootstrap owner
creates the global policy.
- **Storage**: migration `000003` adds `global_access_policy` and
`atespace_access_policies`. Deleting an atespace cascades to its policy
row and removes its OpenFGA tuples in the same transaction.
- **Consistency**: `PolicyManager` takes an explicit `pgx.Tx`, locks the
metadata row (`FOR UPDATE`) and reconciles OpenFGA tuples in the same
transaction. Optimistic concurrency via `uid` / `version`.
- **Hardening**: members with control characters are rejected, as are
blank, `*`, or control-character bootstrap owner entries; non-canonical
OpenFGA error codes are mapped to `Internal`.

### Testing
- Interceptor tests for both modes: with enforcement off, regular RPCs
skip the check while governance RPCs are still checked.
- Functional CRUD tests for both AccessPolicy scopes
(`functionaltest/access_policy_test.go`).
- PostgreSQL test that a policy update and its tuple writes commit or
roll back together, including under concurrent updates.
- Governance tests: creating the policy as a bootstrap owner, a
bootstrap owner also stored as owner, and a restart without a bootstrap
owner.

### Notes
- ateapi now needs OpenFGA available at startup even with enforcement
off, and atespace deletes always write OpenFGA tuples.
- TODO: move the authz settings into the hot-reloadable config proto
(#2021) once it lands.
2026-10-02 20:47:03 +00:00
Luiz Oliveira 31cc9ceb7f Don't crash actors on transport errors when calling atelet (#2060)
Today, we don't know if RPCs to atelet fail because of a transport error
or a real atelet error + we always crash the actor on atelet errors.

So, we end up crashing actors when atelet is upgrading or temporarily
unavailable.
 
This PR solve this by:
* Not crashing actors when the atelet RPCs return: `codes.Unavailable`,
`codes.Canceled` or `codes.DeadlineExceeded:`
* Never crashing actors after errors from atelet.Terminate. Terminate()
is called during DELETE/REVERT and we plan to also call it during CRASH
operations.
2026-10-02 16:28:35 +00:00
shrutiyam-glitch 6a35150e29 Remove onResume.fromData and the DATA_ON_GOLDEN restore scope (#2105)
Fixes #2027 

Actor lifecycle v2 will not restore an actor by combining the golden
snapshot's guest state with the actor's own data snapshot.
`onResume.fromData: GOLDEN` was the only way into that path, so this
removes the setting and everything behind it.

#### Behavior changes
- **API:** an ActorTemplate that sets `snapshotConfig.onResume` is
rejected (unknown field). Templates already stored lose the field
silently on read.
- **Resume of a DATA snapshot** always starts the containers fresh from
the image, with the durable dirs filled from the snapshot. Templates
that used `fromData: GOLDEN` now pay app startup on resume and lose any
in-memory state that came from the golden image.
- **Unchanged:** FULL snapshots, a just-created actor (borrows the
golden tag and restores it FULL, or cold-boots via `Run` if there is
none), committing the golden actor (always FULL), and creating actors
from a tag.
- **Restore path:** a DATA_ON_GOLDEN restore used to need two manifest
GETs and downloads from two snapshots; every restore now reads one
manifest and one snapshot.
- **Metrics:** `ate.snapshot.scope` takes only `full`, `data` or
`unknown`.
- **Rollout:** atelet rejects scope 3, so upgrade ateapi and atelet
together. Per the pre-v1.0 policy there is no compatibility shim.

**This is a Breaking change**




- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-10-02 05:08:42 +00:00
Taahir Ahmed 715f2daa63 SystemInfo: Allow multiple trustbundle names in one file (#2048)
Change the SystemInfo data source protobuf API so that multiple trust
bundle names can be specified in one data source. The intent is for the
contents of the trust bundles to be unified, deduped, and shuffled
together.

This will throw an error from atelet until a follow up commit implements
atelet-system-roots.ate.dev
2026-10-02 04:23:40 +00:00
sfunkenhauser 8a6b5e149a Support workers with multiple IPs (#2052)
Fixes #944

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-10-01 16:39:22 +00:00
Luiz Oliveira e8cad55675 Reject repointing an actor that owns a snapshot to a different storage location (#2014)
This is needed because DeleteActor cleans up all snapshots under the
actor's location.

If we were to allow repointing an actor's snapshot location, we risked
leaking historical snapshots upon actor deletion.
2026-10-01 03:53:35 +00:00
sfunkenhauser 8d5a5b70ba Clean up actors when ateom terminates (#1888)
Fixes #1882

When a worker pod's ateom container restarts, every sandbox it was
hosting is lost, but the control plane kept reporting those Actors as
running.

This change:

- **Tracks ateom restarts:** each Worker gets an `epoch`, set by the
syncer from the ateom container's restart count. It can only increase.
- **Stamps each binding:** every Actor assignment records the Worker's
epoch at bind time.
- **Crashes stale Actors:** a new reconciler in ateapi watches for
Workers whose epoch has risen past `status.observed_epoch`. It crashes
the Actors bound during earlier epochs, releases their assignments, then
advances `observed_epoch`.



[ x ] Tests pass
[ x ] Appropriate changes to documentation are included in the PR
2026-09-30 23:01:13 +00:00
Max Thompson ccecc788a3 ateapi: require an actor JWT lifetime and align the subject with the SPIFFE ID (#1902)
Part of #1756.

- Adds a required `MintActorJWTRequest.expiration_seconds`, validated to
[300, 3600]. Token exchange (#1661) wants longer-lived subject tokens.
- Adds `MintActorJWTResponse.expires_at`, equal to the `exp` claim, so
callers like the egress gateway's cache don't have to decode the token.
- Changes the subject to `actor/<atespace>/<name>`, matching the path of
the actor's SPIFFE ID. The token isn't a JWT-SVID.
- Rewrites the `MintActorJWTResponse` comment to match the settled
contract.

Field 8 skips 2, 3, 4, and 6, which the old ActorIdentity request used.

Testing: validation unit tests for the bounds. The functional test
checks the lifetime, `expires_at`, and `sub`; it needs Docker, so it
hasn't run locally.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR (docs
land later in the #1756 series)
2026-09-30 22:52:19 +00:00
Julian Gutierrez Oschmann 262af570fb Move DV validators under apivalidations. (#2007)
Both `Control` and `WorkerService` APIs need to access them so moving it
under a shared internal package.

This also aligns ate-apiserver with how atelet uses DV as serves as a
prefactor for upcoming changes to the `ServiceImpl` type.

Fixes #1823
2026-09-30 18:34:15 +00:00
haiyanmeng 12cf471c28 ateapi: release a suspend's replaced snapshot after committing SUSPENDED (#2006)
FinalizeSuspended deleted the external snapshot a suspend replaced
before committing SUSPENDED. By then the new checkpoint is uploaded and
the worker's assignment row is released, so a transient object-store
error (429/503) on that delete aborted the workflow with the actor stuck
in SUSPENDING: ResumeActor rejects that state with FailedPrecondition,
and a retried suspend re-sends Checkpoint to the already-released
worker, which most likely crashes the actor despite a good snapshot in
storage.

Commit SUSPENDED first, then release the replaced snapshot best-effort,
logging a failure instead of returning it. A failed release now leaves
the old snapshot's objects in storage until the actor is deleted, which
removes its whole prefix, rather than stranding the actor. A commit lost
to a concurrent update releases nothing, since the record still names
the old snapshot.

> 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-09-30 16:16:38 +00:00
Shruti Nair 6c60ec5d5a ateapi: [Part 1] Runtime Authorization check for Atespace CRUD (#1702)
Working on https://github.com/agent-substrate/substrate/issues/1563

Integrates runtime OpenFGA authorization checks for `Atespace` CRUD RPCs
(`CreateAtespace`, `ListAtespaces`, `GetAtespace`, `DeleteAtespace`) and
atomic OpenFGA tuple cleanup on atespace deletion, gated behind
`--enable-authz` (`ATE_API_ENABLE_AUTHZ`).

### What's Included
- Runtime Policy Decision Point
(`cmd/ateapi/internal/authz/authorizer.go`):
- gRPC Interceptor & Declarative Registry
(`cmd/ateapi/internal/authz/interceptor.go`, `registry.go`):
- Atomic Policy Cleanup on `DeleteAtespace` (`policy_manager.go`,
`atepg.go`):
- Server Wiring (`cmd/ateapi/main.go`):

### Next (PR 3)
- Policy management RPCs
2026-09-29 23:40:43 +00:00
shrutiyam-glitch 820591528e atelet: introduce declarative validation for the AteomSupport RPCs (#1903)
Fixes a part of #1709 

This adds declarative validation to atelet's `AteomSupport` RPCs.
Malformed requests are now rejected at atelet instead of being forwarded
to the control plane. Validation runs after authentication.
| RPC | Rules |
|---|---|
| `MintActorCertificate` | actor atespace/name are required short names;
actor UID is a required UUID; the CSR is required and at most 16 KiB |
| `RequestActorSuspend` | actor atespace/name are required short names;
actor UID is a required UUID |
| `SetWorkerCapacity` | `capacity` is required and must follow the
`WorkerResources` rules ateapi declares, shared through
`internal/resources` |


The `AteomHerder` RPCs and `WorkloadSpec` get their validation in a
follow-up PR.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-29 22:56:18 +00:00
Benjamin Elderanddependabot[bot] 51a71cb365 upgrade otel log to 0.21 (#1958)
fixes https://github.com/agent-substrate/substrate/pull/1952

security bump stuck by breaking changes.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 22:26:40 +00:00
Lior Lieberman 6621f2b4b4 egress policy enhancements for GA (#1751)
**BREAKING** API changes prior in preparation for GA. 

~This PR has proto-only change to the EgressPolicy API. No
implementation; the gateway,
store contract, and e2e helpers stop compiling until the follow-up
lands.~ Note: Impl is only to make ci green. Full implementation of the
api is a fast follow


- `EgressRule` is now a union of protocols: `http`, `https`,
  `tls_passthrough`. Allow-only, deny by default.
- The `hostnames`, `cidrs`, and `all` rule kinds are removed. **No field
numbers
or names are reserved: we are pre-GA and existing policies must be
recreated.**
- `rules` is an **unordered set**. When more than one rule matches,
precedence is
decided by two criteria, in order: a pattern without a wildcard wins
over one
with a wildcard, then a port other than `"*"` wins over `"*"`. This
applies
within a protocol and across `https` and `tls_passthrough` on the same
SNI.
Two rules with the same pattern on the same port are rejected on write.
  Only the winning rule's effects apply.
- `http`: cleartext HTTP, matched per request on the authority and port.
Carries `effects`.
- `https`: MITMed HTTPS that the gateway intercepts. Matched on SNI and
port at the
ClientHello, then per request on the authority. Carries `effects`. MITM
is off unless a name is
  listed here.
- `tls_passthrough`: TLS forwarded without decryption, matched once per
connection on SNI and port. No effects. Implicit TLS only; STARTTLS does
not match.
- Protocols are told apart by what the Actor sends first, a ClientHello
or an HTTP
  request. Anything else, or a server-first protocol, is closed.
- Name fields are `host_patterns` on `http` and `https`, `sni_patterns`
on
  `tls_passthrough`. Same wildcard grammar as before, documented once on
  `HTTPRule.host_patterns`.
- `ports` on all three protocols are strings: a port number, or `"*"`
alone for any
port. `http` defaults to `["80"]` and `https` to `["443"]` when empty;
`tls_passthrough`
requires at least one. The port is the destination the Actor connected
to.
A port in a request's authority is neither matched nor dialed. Format
documented
  once on `HTTPRule.ports`.

- `inject_static_headers` is renamed to `replace_headers`, since the
behavior is
replacement and the name leaves room for other header operations later.
The
`CredentialHeaderInjection` message keeps its name. Replacement is
conditional:
a header is replaced only when the Actor's request already carries it,
with any
  placeholder value. Requests without the header pass unchanged.

- Unsupported in v1 rules for arbitrary TCP, non-TCP protocols.

- `google/protobuf/empty.proto` import dropped; nothing uses it now.

Questions

- The handler is called `https`, not `mitmHttps`. Everything in an
`https`
  block is intercepted. is that clear enough?

follow-ups
- align egress implementation
- named CONNECT, or some way to preserve what the actor dialed.
- add tcp support
- hand-written validators for the new fields (`host_patterns`,
`sni_patterns`, `ports`,
  the rule conflict check) and regenerate `zz_generated.validation.go`



> 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

Fixes: #823 (there are probably more issues that i need to find)
2026-09-28 17:30:07 +00:00
Dmitry Berkovich 1d7ca8ced0 ateletpb: carry the Restore base snapshot as a typed source, dropping golden_snapshot_uri (#1602)
Refactors the atelet Restore API so the DATA_ON_GOLDEN base snapshot
travels as a typed restore source instead of a bare string, preparing
for the node-local snapshot cache (#690, #1551): `RestoreRequest` gets
its own external snapshot *source* message, and read-side attributes of
a restore source have a home — today the URI, next (in the M2 cache
work) a sharing property set by the control plane.

**This change is not backward compatible.** `golden_snapshot_uri` is
removed from `RestoreRequest` with no transition: an ateapi from before
this change sends a DATA_ON_GOLDEN restore an updated atelet rejects (no
`base_config`), and an updated ateapi sends one an old atelet rejects
(no `golden_snapshot_uri`). ateapi and atelet must be rolled together.
Fresh starts, FULL/DATA restores of an actor's own snapshot, and local
pause restores without a golden base are unaffected.

Three commits, each buildable and tested:

1. **`ateletpb: give Restore its own external snapshot source message`**
— `ExternalRestoreConfiguration` replaces
`ExternalCheckpointConfiguration` in `RestoreRequest`'s config oneof
(same wire shape; the checkpoint-side write-destination message is
untouched), and `base_config` is added for the DATA_ON_GOLDEN base. No
behavior change: nothing sets or reads the new field yet.
2. **`ateapi, atelet: carry the restore base snapshot in base_config`**
— the cutover: ateapi sets `base_config` on both golden-data resume
paths (external data snapshot, and local pause checkpoint combining with
the golden); atelet reads and validates only `base_config`. The
characteristic test of the ateapi→atelet request and the golden-data
functional test pin the new field.
3. **`ateletpb: remove golden_snapshot_uri from RestoreRequest`** — the
old field is deleted and `base_config` takes its field number, keeping
the numbering dense.

Tested: full ateapi + atelet suites including the controlapi functional
tests; gofmt, golangci-lint, boilerplate clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-26 14:35:20 +00:00
Taahir Ahmed 7f7a40c1b9 ateom/actor ID split: Clean up purpose from MintActorCertificate (#1810)
The purpose field in MintActorCertificate (and the certs it issues) is
no longer needed. The distinction between actors and ateoms-for-actors
is now built into the SPIFFE URI.

Stacked over #1809
2026-09-25 23:45:05 +00:00
Taahir Ahmed 8d6be5fb4e ateom/actor ID split: Ateom uses MintAteomActorCertificate (#1809)
Update atelet/ateom/atunnel to use the new MintAteomActorCertificate,
and correct anything that was coded to expect the old actor SPIFFE URI.
2026-09-25 21:08:23 +00:00
Max Thompson d3a556aae3 ateapi: make the actor JWT issuer configurable and add typ to the header (#1834)
Part of #1756.

- Adds `--actor-jwt-issuer`. Unset, it defaults to
`https://idp.<namespace>.svc`, the Service ate-idp-server will serve
discovery from. Before this, `iss` was hardcoded to
`https://api.ate-system.svc`, which points at ateapi's gRPC port.
- Adds `oidcdiscovery.ParseIssuer`, which requires a canonical https URL
with no query, fragment, or user info and strips trailing slashes.
ate-idp-server will use it too, so both emit the same bytes.
- Actor JWT headers now include `typ: JWT`.
- Drops the `actorIdentityJWTIssuer` plumbing into `RPCService`, unused
since #1315.

The default `iss` changes, but `MintActorJWT` has no production caller
yet.

Testing: new unit tests for the parser, the default, the header, and
flag resolution. `TestMintActorJWT_Success` now checks `typ`, `iss`, and
`sub`. It needs Docker, so it hasn't run locally.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR (docs
land later in the #1756 series)
2026-09-25 19:22:25 +00:00
shrutiyam-glitch 54cba01a8e feat(ateapi): send resolved sandbox assets during restore (#1866)
Fixes #1835 

`atelet` chose the sandbox binaries and pause image for a restore from
the snapshot's `manifest.json` rather than from the control plane. This
PR makes ateapi send them on every restore, the way it already does for
Run.

**Changes**
- **proto:** add `sandbox_assets` to `atelet.RestoreRequest`.
- **ateapi:** `ensureAteletRestored` resolves the ActorTemplate's
SandboxConfig once and sends it on the local restore, the external
restore and Run. A missing SandboxConfig now fails before any atelet
call.
- **atelet:** `sandbox_assets` is required on Restore. Restore selects
the binaries and pause image with `recordFromRequest`, the same path Run
uses. The manifest still supplies the snapshot files, sandbox class and
metrics labels.

Note - The field is required, so after atelet is upgraded, resumes on
that node fail until ate-api-server is upgraded too.

**Testing**
- ateapi: every Run and Restore sends the resolved assets; a missing
SandboxConfig fails on both restore paths.
- atelet: `TestRecordFromRequest`, a validation case for a missing
field, and `TestRestoreUsesRequestSandboxAssets` (checkpoint with pause
image v1, restore with v2, and the actor runs with v2).

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-25 16:06:02 +00:00
Luiz Oliveira e87c55fc2e Add the crash reason and timestamp to the ActorStatus (#1867)
Added a new field to actor status that has the crash reason (e.g., the
atelet response error) and the timestamp of the crash for better UX.
2026-09-25 15:33:08 +00:00
Luiz Oliveira d72edfbb3c Rename LocalSnapshotInfo to LocalSnapshot (#1856)
For consistency with ExternalSnapshot

#1378
2026-09-25 14:20:42 +00:00
sfunkenhauser fa24dcc514 Add plumbing to allow ateom to suspend actors (#1779)
Part of #483 

- [ x ] Tests pass
- [ x ] Appropriate changes to documentation are included in the PR
2026-09-24 20:59:46 +00:00
Krisztian F 945e44a5ec (ateapi): report worker occupancy by actor slots in ate.worker.state (#1854)
Part of #1664, implements #1799.

## Changes

- `ate.worker.state` now has `idle`, `partial`, `at_capacity` and
`unschedulable`. `assigned` is removed.
- ateapi picks the state by comparing allocated actor slots with
capacity, the same check the scheduler makes.
- Draining workers and workers with no reported capacity are
`unschedulable`. This wins over occupancy, so the sum over the states is
still the pool size.
- Every known pool reports all four states, set to 0 when empty.
- Updated the registry, `docs/observability.md` and the
autoscaled-workerpool demo (`assigned` -> `at_capacity`).

## Open questions

- Are we fine with `unschedulable` as a fourth state? cc. @JeffLuoo
Every new worker starts with capacity 0, so `at_capacity` would make an
HPA on it scale up on its own scale-up.
- This breaks queries on `ate_worker_state="assigned"`. Should be still
fine.
- The demo HPA is only right while each worker holds one actor. Pool
utilization needs slot counts, which needs a new instrument (#1664).

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-24 20:17:26 +00:00
Luiz Oliveira 34f3003326 Add DV for GoldenSnapshotStatus and unbounded strings (#1831)
Fixes  #1168

* Add DV for GoldenSnapshotStatus and truncate its error_message to fit
* Require ExternalSnapshot.actor_template_uid to be a UUID
* Bound ExternalVolumeTemplate.capacity to 32 characters
2026-09-24 16:59:12 +00:00
Sneha-at 444d7631d1 enhance tests to validate volume lifecycle for actor state changes (#1581)
Fixes #<issue_number_goes_here>
NA

This PR enhances the tests to validate volume lifecycle during state
transition failures

• **TestDeleteActor_VolumeDeletionFailure_RetrySuccess**- Verifies that
when volume deletion fails during DeleteActor, the actor transitions to
ACTOR_STATE_DELETING and its volumes to STATUS_DELETING. A
subsequent retry successfully finalizes volume deletion and deletes the
actor from the store.
• **TestResumeActor_VolumeAttachFailureAndRetry** - Verifies that when
volume attachment fails during ResumeActor, the actor remains in
ACTOR_STATE_RESUMING with worker assignment and provisioned volumes
intact. Retrying ResumeActor re-attempts volume attachment and
successfully transitions the actor to ACTOR_STATE_RUNNING.
• **TestResumeActor_VolumeAttachFailure_DeleteActor** - Verifies that an
actor stuck in ACTOR_STATE_RESUMING after an attach failure rejects
standard DeleteActor requests, but calling DeleteActor with
AnyState=true successfully cleans up worker assignments and provisioned
volumes.
• **TestResumeActor_MultiVolumePartialAttachFailure_Retry** - Verifies
that when attaching multiple volumes during ResumeActor and one volume
fails, previously attached volumes remain attached and the actor
stays in ACTOR_STATE_RESUMING. A subsequent retry attaches only the
remaining unattached volumes and transitions the actor to
ACTOR_STATE_RUNNING.
• **TestSuspendActor_VolumeDetachFailure_RetrySuccess** - Verifies that
volume detach failures during SuspendActor leave the actor in
ACTOR_STATE_SUSPENDING with its worker assignment held. A subsequent
retry
succeeds in detaching the volume, releasing the worker, and
transitioning the actor to ACTOR_STATE_SUSPENDED.
• **TestSuspendActor_VolumeDetachFailure_DeleteActorAnyState** -
Verifies that an actor stuck in ACTOR_STATE_SUSPENDING due to a volume
detach failure rejects standard deletion, but cleanly detaches volumes
and deletes the actor when called with AnyState=true.
• **TestPauseActor_VolumeLifecycle_DetachAndResumeAttach** - Validates
the volume lifecycle across pause and unpause, confirming external
volumes are detached when transitioning from RUNNING to PAUSED and re-
attached when resumed back to RUNNING.
• **TestPauseActor_VolumeDetachFailure_RetrySuccess** - Verifies that
volume detachment failure during PauseActor leaves the actor in
ACTOR_STATE_PAUSING. A retry of PauseActor re-attempts volume
detachment,
succeeds, and advances the actor state to ACTOR_STATE_PAUSED.
• **TestResumeActor_PausedLocalSnapshotMissing_Crashes** - Verifies that
if local snapshot files on the worker are missing when resuming a PAUSED
actor, the control plane marks the actor ACTOR_STATE_CRASHED
and frees the assigned worker pod.
• **TestDetachActorVolumes** - Tests control plane volume detachment
logic across multiple scenarios, including container mount filtering,
partial detach errors, unassigned workers, and idempotent not-found
responses from plugins.
• **TestCreateActorVolumes (Storage class parameters case)** - Verifies
that parameters configured on a StorageClass (such as disk and
filesystem types) are correctly propagated into the volume context of
newly created external volumes.
• **TestEnsureExternalSnapshotsReleased_DeletePrefixFailure** - Verifies
that transient object store errors during actor deletion preserve
un-deleted snapshot objects and return an error, which cleanly
completes deletion on a retry.
• **TestMountExternalVolumes** - Tests worker-side volume mounting,
verifying host mount directory creation, handling pre-existing paths,
plugin errors, and ensuring that partial failures during multi-volume
mounts do not unmount already mounted volumes.
• **TestVolumeHostDirectoryCleanup** - Verifies worker-side host
directory management, ensuring volume unmounting preserves mount
directories while directory reset removes empty directories and prevents
data
loss on non-empty ones.
• **TestExternalVolume_NodeMigration** - Verifies cross-node actor
migration with external volumes by suspending an actor, evicting its
worker pod, and resuming it on a new worker node while ensuring
  application state is preserved

- [ x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-24 16:25:33 +00:00
shrutiyam-glitch 5334f16476 feat(ateapi): track actor_template_uid per snapshot on ExternalSnapshot (#1713)
Follow up on the comment -
https://github.com/agent-substrate/substrate/pull/1675#discussion_r4031619230

Stamps `actor_template_uid` onto `ExternalSnapshot` so template
provenance travels with the snapshot itself, rather than being inferred
from the single `ActorStatus.current_actor_template_uid`.

#### Motivation

`current_actor_template_uid` records the template the **last sprint
booted with** (`finalizeRunning` stamps it on every resume). It does
*not* record the template the **snapshot** was captured under. Those two
diverge as soon as an actor runs a sprint that does not produce a new
durable snapshot, and `loadActorForResume` was using the former to
decide whether to force a `DATA`-only restore instead of `FULL`.

Walking an actor through a repoint and a crash:

| Step | Action | `current_…_uid` | `ExternalSnapshot` | 
|---|---|---|---|
| **a** | Suspend on template **A** | A | captured on **A** | 
| **b** | `UpdateActor` repoints the spec to **B** | A | captured on
**A** |
| **c** | Resume → `A != B`, restores `DATA`, sprint boots on **B** |
**B** | still on **A** |
| **d** | Actor crashes (or is reverted) — no new snapshot taken | B |
still on **A** |
| **e** | Resume → `current == target == B`, so **no repoint is
detected** | B | still on **A** |

At step **e** the old check reports "template not replaced" and restores
the external snapshot in `FULL` — replaying a memory image captured
under **A**'s sandbox on **B**. That is exactly the case the guard
exists to prevent; it was silently defeated by the sprint at step **c**
advancing `current_actor_template_uid` past the snapshot.

*(Note: Paused actors do not face this divergence. Because `UpdateActor`
is only allowed in the `SUSPENDED` state, a paused actor cannot have its
template updated. Therefore, local checkpoints do not need separate
template provenance tracking).*

#### Changes

- **Data Model Updates:** Added the `actor_template_uid` field to
`ExternalSnapshot`.
- **Snapshot Stamping:** Updated the capture logic to stamp external
snapshots with the active sandbox's `ActorTemplate` UID when suspending
an actor or creating an actor from a tag.
- **Resume Evaluation Logic:** Modified the external resume logic to
compare the target template UID against the specific snapshot's UID
(`ExternalSnapshot.actor_template_uid`) rather than the actor's overall
status. Local restores bypass this check entirely since templates cannot
change during a pause.

**Tests**
- `TestResumeActor_AteletWireRequest` (unit): snapshot fixtures now
carry `actor_template_uid`, since provenance is read from the snapshot
rather than from actor status.
- Functional expectations for create/update/resume/pause updated for the
new field on the external snapshot golden files.
- `TestUpdateTemplateLifecycle` (e2e): asserts
`external_snapshot.actor_template_uid` after suspend and re-suspend.
- **e2e (Update Template)**: Added coverage for repoint detection after
a revert (verifying the `resume` → `revert` → `resume` path described in
the table above).
- **e2e (Combined Volumes)**: Added coverage to explicitly verify which
volumes a revert rewinds.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-23 23:58:06 +00:00
Zoe Zhao 74bbfc529c Delete the ateerrors failure-reason taxonomy (#1817)
Stacked on #1220 — review only the last commit until that merges
2026-09-23 20:33:21 +00:00
Julian Gutierrez Oschmann 985c200247 Add DeletePreconditions to all Delete<Resource> methods (#1807)
Allow clients to do optimistic locking on deletes, as well as prevent
unintended deletes on name reuse.

#866
2026-09-23 18:55:59 +00:00
shrutiyam-glitch 47b67574ac docs: Document RevertActor and drop "terminal" from CRASHED (#1711)
`RevertActor` returns a RUNNING, PAUSED, or CRASHED actor to SUSPENDED
at its last external snapshot, so CRASHED is no longer a dead end that
only `DeleteActor` can clear. Docs and code comments still described it
as terminal and told operators to delete and recreate the actor, losing
its state.

Update the api-guide, architecture, upgrade guide, and kubectl-ate
README to cover the new verb, and correct the comments that justified
keeping a partial external snapshot by naming actor deletion as the only
remaining collector -- revert collects it too.

Follow up for the PR - #1675 
Issue - #1556 

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-23 17:51:56 +00:00
Taahir Ahmed 8ea4abe1a3 ateom/actor ID split: Define MintAteomActorCertificate (#1626)
We need to draw a strong distinction between credentials that will be
wielded directly by actor logic, and credentials used by system
components on behalf of an actor. This will help us avoid attacks where
an actor pretends to be a system component.
    
Ateom certificates are requested by ateom/atelet and used for the
atunnel connection to the egress gateway. They have a SPIFFE URI like
`spiffe://${trustdomain}/ateom-for-actor/${atespace}/${actor}`.
    
Actor certificates are requested by the egress gateway and used for
opening outbound requests on behalf of the actor. They have a SPIFFE URI
like `spiffe://${trustdomain}/actor/${atespace}/${actor}`. Note, this
use case is not actually implemented yet.
2026-09-23 17:17:35 +00:00
Zoe Zhao aae7df7810 Crash actor when Ateom Checkpoint or Restore fails; Also when the error is not marked as retriable (#1220)
Fixes https://github.com/agent-substrate/substrate/issues/292 

This flips the condition to crash actors: any errors returned by atelet
will cause actors to crash by default.
If we later discover specific error classes that are safe to retry, we
can explicitly mark them as retriable.
2026-09-23 16:40:27 +00:00
Luiz Oliveira fb4b31529a Rename SnapshotsConfig to SnapshotConfig everywhere in the codebase (#1791)
related to -> #1378

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-23 14:39:09 +00:00
Luiz Oliveira ee9aa9c247 Add DV for TagStatus + drop unused TagStatus.source_actor_uid field (#1772)
TagStatus.source_actor_uid is unused, we can add this back if we ever
need it.

#1168 
 
We were also missing some of the DV for TagStatus fields:

* Added maxLength=2048 requirement for ExternalSnapshot.snapshot_uri
(matching in_progress_snapshot_uri)
* Added validation for actor_template_uid
* Added validation for storage_location, matching what we have in
SnapshotsConfig.storage_location
2026-09-23 13:40:41 +00:00
Julian Gutierrez Oschmann d277088bc1 Rename ActorTemplate.containers.readyz to wakeupProbe. (#1794)
The name `readyz` is not very descriptive. We also want to avoid
`readinessProbe` to prevent confusion with the Kubernetes concept, which
represents continuous traffic gating. Renaming this field to
`wakeupProbe` clarifies its actual behavior, and the fact that it only
operates when the actor is woken up.

Part of #1378
2026-09-22 23:32:01 +00:00
Jonathan Jamroga 6f45aeb2dd Make substrate's namespace, Service names and ServiceAccount names configurable (#350)
# Description

**Substrate assumes the canonical install layout, and every deviation
fails closed.** The namespace `ate-system`, the Services `api` /
`atenet-router`, and the ServiceAccounts `atelet` / `atenet-router` are
compiled-in constants. An install in a per-developer namespace, or under
a deployment that prefixes resource names, breaks — and no failure
points at the naming.

This series makes the namespace, the Service names, and the
ServiceAccount names configurable. Every option defaults to the
canonical value. A canonical install is byte-for-byte unaffected.

| Hardcoded assumption | Failure | What it looks like instead |
|---|---|---|
| atelet's namespace in ateapi's SPIFFE check | ateapi rejects every
atelet | an mTLS handshake failure, not a naming error |
| atelet's namespace in the worker's broker check | no actor obtains a
certificate | `credential broker is not atelet` |
| NetworkPolicy's ingress namespace | the CNI drops every request to the
pool | a silent network fault |
| ateapi Service name and namespace in the client | the client cannot
authenticate | `services "api" not found`, then `invalid bearer token` |
| Resource names in `hack/install-ate.sh` and the e2e harness |
authorities land in the wrong namespace | suites die in preflight |

**A SPIFFE ID breaks on two axes.** It names a namespace and a
ServiceAccount. Both were constants, and the failure surfaces as a
rejected peer, not a missing object.

## Design notes

- **ate-controller passes the worker-side identities.** ateom already
took `--atunnel-client-identity` as a flag but relied on its default. A
new `--atunnel-broker-identity` flag alone would be inert: correct only
where the constant was already correct. The controller knows the control
plane's namespace, so it supplies both.
- **ateapi takes the whole expected identity, not a namespace.** The
dialer and `ateletauth` used the namespace only to build one string. One
place decides how atelet's identity is spelled.
- **`ateletauth` deduplicates without taking the dependency it was
avoiding.** Its constants are duplicated rather than imported so the
package does not depend on `controlapi` for three strings. `ateletdial`
would make a third copy, so the strings move to
`internal/installdefaults` instead — a leaf package of constants that
imports only the standard library, so consuming it does not reintroduce
the coupling the duplication was there to prevent.
- **The client reads environment variables, not flags.** It runs outside
the cluster. It has no downward API and nothing to discover from.
- **Namespaces come from the downward API, not flags.** Every supported
topology co-locates these components. Only Service and ServiceAccount
names, which a deployment may legitimately rename, get flags.
- **`hack/install-ate.sh` and the e2e harness are in scope.** A
relocated install cannot be bootstrapped or exercised without them. The
script refuses `ATE_NAMESPACE` or `ATE_API_SERVICE_NAME` overrides on
the manifest-applying subcommands, which would half-install:
`manifests/ate-install/` names `ate-system` and `api` literally.

## Why this is one PR

The series is stacked, not parallel. Commit 1 creates
`internal/installdefaults` and seeds seven imports; commit 2 adds
fourteen more; every later commit builds on those constants. Split into
separate PRs, each blocks on the previous merging and none reviews
independently.

The guard test cannot land first. Applied to `main` it fails on eleven
hardcoded literals across `ateletauth`, `controlapi/informer.go`,
`networkpolicy_controller.go`, both `ateom` mains, `ateclient`,
`ateletdial` and three e2e files. It is green only because the preceding
commits removed them.

A partial series still fails closed. Each commit's audit turned up more
places assuming `ate-system`, so landing commits 1-3 without 4-5 leaves
a relocated install broken later and less legibly than before.

`CONTRIBUTING.md` covers this case: when the intermediate steps are not
useful on their own, keep the change as one PR split into commits at
logical break points. Review commit by commit, and preserve the commits
on merge. If you would still rather split, the one defensible cut is by
axis — relocation (commits 1-3) then renaming (commits 4-5) with the
guard test rebased on top.

## Rebase notes (2026-09-15)

The series is rebased onto `main` at `d0d85c39`, ninety-two commits on
from the base the PR first carried:

- `main` moved actor JWT and certificate minting out of
`cmd/ateapi/internal/actoridentity` into `controlapi`, deleting the
package and dropping its inline atelet authorization check in favor of
the unified authorizer. This series previously threaded the expected
atelet identity into that check; the check no longer exists, so that
adaptation is gone. `ateletauth` survives with one caller,
`workerservice.SetWorkerCapacity`, which still takes the configured
identity.
- The same refactor replaced `BrokerConfig.ExpectedActorUID` with an
`ActorAtespace` / `ActorName` / `ActorUID` triple. `AteletSPIFFEID` is
additive and still required by `ateletdial.TLSConfig`.
- `ateletauth` (ateapi's side) and `ateletdial` (the worker's side) each
re-declared the hardcoded SPIFFE ID. Both take the expected identity as
a parameter.
- `main` deleted the atenet DNS subsystem. The series no longer touches
it, and `installdefaults` carries no DNS Service name.
- The controller passes `--atunnel-broker-identity` only when it differs
from the canonical default. An ateom old enough to predate the flag
exits on it, and `docs/upgrade.md` keeps such a pool serving during a
rolling upgrade. A relocated install gets the flag and necessarily runs
an image that accepts it.
- `main` added `internal/e2e/collector_metrics.go` (agentgateway CI
support), which addresses the router through the package-level
`routerNamespace` and `routerService` that this series replaces. Its two
call sites become `SystemNamespace()` and
`ResourceName("atenet-router")`, matching `router_client.go` and
`statusz.go`. A reviewer diffing against the older base sees those call
sites move; nothing else in that file changes.
- `main` added `cmd/credential-provider/kubernetes-secrets` (`#1335`),
whose `injectorSPIFFEID` constant is the mTLS peer check on the only
caller permitted to read Secrets. The comparison is an exact string
match, so a renamed install rejects every fetch at TLS. It becomes
`--injector-identity`, defaulting to
`installdefaults.EgressSPIFFEID(SystemNamespace)`, alongside a new
`EgressServiceAccount` constant and helper. The flag name follows
`--atunnel-client-identity`; the manifest beside it still names
`ate-system`, so a renaming deployment configures both.
- `main` replaced the dialer's worker indexer with
`DialForAteletOnNode`, dropped `WorkerPodInformer` from `controlapi`,
and removed `kataConfig` from ateom-microvm's `NewService`. The series
adapts to each narrower signature and keeps only its own added
parameter.
- The guard test's allowlist entry for the two inert `ate-system`
literals follows the code from `actoridentity.go` to
`controlapi/actor.go`. The refactor re-introduced exactly the class of
constant this series removes, so the guard earns its place.

# Testing

- `go test -race ./...` passes. `make verify` passes boilerplate,
codegen, go-modules, gofmt, golangci-lint, kube-api-linter, licenses,
metrics and postgresql-migrations. `proto-fmt` needs `clang-format`,
which is absent locally; this series touches no `.proto` files.
- Every commit builds and vets individually, via `git rebase -x 'go
build ./... && go vet ./...'`.
- On a fresh kind cluster, all twelve e2e suites pass: `capabilities`,
`combinedvolumes`, `demo`, `egressauthz`, `egressmitm`, `example`,
`identity`, `metrics`, `networking`, `networkpolicy`, `parking`,
`sizing`. The `demo`, `metrics`, `networkpolicy`, `parking` and
`networking` suites need the `--deploy-demo-counter` and
`--deploy-demo-egress` fixtures, and the MITM path needs
`--deploy-demo-egress-mitm`; without them they fail on `actor template
not found`, which reads as a control-plane fault rather than a missing
fixture.
- With the sdsmint egress gateway (`--deploy-atenet
--experimental-use-sdsmint`, `E2E_EGRESS_MITM=1`),
`TestActorEgressMITMTrust` and `TestActorEgressHTTPSByHostnameMITM` both
pass, and the `networking` suite is green at 25 passed. The two modes
are mutually exclusive by design:
`TestActorEgressHTTPSByHostnamePassthrough` covers the plain gateway and
skips under MITM, and the two MITM tests skip without it. Both modes
were run, so every case executed in one of them.
- The new unit tests use a relocated namespace and renamed
ServiceAccounts. Reintroducing each hardcoding makes them fail.

One e2e caveat that predates this series and misleads: the `identity`
suite calls `ReplaceEgressTrustPool`, which takes over the shared
`egress-mitm-ca-pool` Secret, overwrites it and registers no cleanup.
Any MITM test running afterwards fails with `certificate signed by
unknown authority`, which reads as a broken interception path rather
than a mutated fixture. Reinstall the gateway before running
`egressmitm`.

# Additional Notes

**Credential contents stay canonical.** `controlapi/actor.go` still
mints credentials naming `api.ate-system.svc`: the JWT issuer and the
certificate's Issuer CN. Relying parties validate these, so changing
them is a compatibility decision, not a lookup fix. The code carries a
TODO to make the issuer a globally unique, OIDC-compliant name.
2026-09-22 22:28:40 +00:00
Jeff Luo 9e503c42b8 Delete ate.scheduler.eligible_workers (#1804)
Closes #1802.

## What this does

Deletes the `ate.scheduler.eligible_workers` histogram. Nothing replaces
it in this PR.

## Why

**It is expensive on the resume path.** `Schedule` built a second slice
of the matching workers that scheduling never read, walked it again, and
repeated `HasRoom` for each entry. `HasRoom` parses resource quantity
strings up to three times for each worker. The metric roughly doubled
the per-worker arithmetic of a placement. The filter loop is now one
pass over the fleet and builds one slice.

**A metric cannot answer the question it was built for.** #564 wanted to
tell a full fleet apart from an empty intersection of the constraints,
such as a cordoned node behind a node requirement. That needs the
selectors, the node requirement and the actor.
`docs/metrics/substrate.yaml` bars all three from a metric label and
sends them to logs and spans, so no shape of this instrument reaches the
answer. A log record at the rejection is the replacement the issue
names, and it is not in this PR.

## Also removed

`ate.scheduling.constraint` goes with the instrument. No other signal
used the attribute.

## Scope of the change

- `cmd/ateapi/internal/scheduling/metrics.go` deleted, along with the
`WithMeter` option and the second filter pass in `Schedule`.
- The instrument and the attribute removed from
`docs/metrics/registry/metrics.yaml`. The `pool-keys-paired` exception
in `docs/metrics/substrate.yaml` dropped, because the empty pool pair
was this instrument's alone.
- `docs/observability.md`: the table row and the two label notes
removed.
- The e2e collector check and the label assertions for the instrument
removed.
- `TestSchedule_EligibleWorkersMetric` removed. The behaviors it covered
(draining workers, a sandbox class mismatch, busy workers) are already
in the `TestSchedule` table.

## Testing

`go test ./cmd/... ./internal/...` passes. `make verify` passes except
`hack/verify/metrics.sh`, which needs Weaver or Docker. Neither is
available on this machine, so CI is what checks the registry.
2026-09-22 21:53:54 +00:00
yufan-su 57234a866a egress: add an example credential provider which reads from k8s secret (#1335)
Add an example gRPC credential-provider plugin which reads from k8s
secrets for the egress
credential-injection path. 

 - It resolves `ate-secret://k8s.io/default/<namespace>/<secret>/<key>`
URIs to Kubernetes Secret values, so Substrate never stores secrets — it
only
brokers a read that the provider is authorized to perform. 
 - It is the only
component in the injection path with Kubernetes secerts access; the
egress gateway and
its injector never read Secrets directly.

### What's included

- **`cmd/credential-provider/kubernetes-secrets`** — the gRPC service:
- Parses and validates `ate-secret://` URIs, resolving the requested
Secret
    (with single-key fallback when the URI omits a key).
- Enforces an **atespace→namespace authorization policy**
(default-deny),
    derived from the caller's attested actor SPIFFE ID.
- Serves over **mutual TLS**, requiring the caller's client cert to
chain to
    the trust bundle *and* carry the egress injector's SPIFFE SAN.
- **Manifests** (`manifests/egress-credential-injection/`) — Deployment,
Service, ServiceAccount + RBAC, a sample namespace-policy ConfigMap, and
a
  sample Secret.
- Renames the default provider address/service from `credprovider` to
`k8s-credential-provider` across `ate-setup`, install scripts, and the
egress
  injection overlay.

### atespace → namespace authorization

Beyond the mTLS check that only the egress injector may call the
provider, each
request is authorized against a **default-deny atespace→namespace
policy**. The
provider derives the requesting actor's atespace from its attested
SPIFFE ID and
resolves a Secret only if that atespace is explicitly granted access to
the URI's
namespace.
2026-09-22 10:13:25 +00:00
Krisztian F f75e626485 (chore): Write both copies of an actor lifecycle event from one call (#1771)
An actor lifecycle record went to stdout and to OTLP in two separate
calls. The attributes were shared, but the severity was not, so the slog
level sat at the call site and the OTel severity sat on the `Event`.
Nothing made a caller write both copies either, so a new record could
reach stdout only and no test would notice.

Since this PR `actorevent.Log` now writes both copies. The level comes
from `Event.Severity`, so it is stored once. The OTLP only `Emit` and
the exported body constants are gone, so the dual write is the only way
out of the package.

Also normalizes `ate.actor.operation.name` on the crash event, which the
state change event already did.

Fixes #1744 

Testing

- Unit tests for the level mapping, the dual write, and the registry
check.
- End to end on a fresh kind cluster. Both event names arrive with the
right severity (9 and 17), the right attributes, and trace context on
the record fields. The stdout copies match record for record.

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-21 20:55:52 +00:00
Julian Gutierrez Oschmann bb0effed18 A few more changes to resource defaults (#1674)
* (Prefactor): Extract defaulting logic into
`cmd/ateapi/internal/defaults`.
* When reading resources from DB, perform a default backfill. 
* Change defaults for readyz probes.

Part of #1593
2026-09-20 23:40:46 +00:00
Luiz Oliveira 0ab30264bc Move DeleteTag orchestration to a workflow (#1720)
The tag deletion logic is getting too complex, so let's move it to a
workflow, following what we do for other resources.

#1510 #664

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-20 14:22:49 +00:00
shrutiyam-glitch 944abe3278 feat(api): introduce RevertActor lifecycle RPC (#1675)
#### Summary
This PR introduces the `RevertActor` RPC for actor lifecycle management.
It includes the API definition, the corresponding workflow execution
logic, observability metrics, and updates to the authorization model to
support reverting actors.

Fixes #1556
Docs updated in #1711 

#### Commit-wise Changes

**1. Add RevertActor RPC (`1145d99`)**
* Introduces the new `RevertActor` RPC to the API definitions.
* Updates the corresponding protobuf bindings (affecting files like
`ateapi.pb.go` and `ateapi_pb2.py`).

**2. Add the revertActor workflow and observability metrics
(`dcb1fae`)**
* Implements the core `revertActor` workflow logic, designed to be
idempotent and re-enterable. It progresses through the following steps:
* **Mark Reverting:** Validates that the actor is in a revertable state
(`RUNNING`, `PAUSED`, or `CRASHED`) and transitions its state to
`REVERTING`.
* **Discard Worker:** Safely tears down the execution environment by
terminating the workload, detaching volumes, and releasing the assigned
worker.
* **Collect In-Progress Snapshot:** Cleans up external object storage by
deleting any objects a previous suspend operation was partway through
writing.
* **Finalize:** Commits the actor to `SUSPENDED` and strips all
node-local and in-progress state pointers (clearing `WorkerAssignment`,
`LocalSnapshotInfo`, etc.), returning the actor to its untouched
external snapshot.
* Instruments the workflow with lifecycle operation metrics (e.g.,
updating `ate.actor.lifecycle.operation.duration` to track `revert`
operations).
* *Note/TODO:* Currently, when reverting a paused actor, the workflow
drops the pointer to the node-local state but does *not* actually prune
the local checkpoint bytes from the node (this is tracked in #641).

**3. Add `can_revert` to the authorization model (`2fc53ae`)**
* Adds the `can_revert` permission to the auth model, mirroring the
shape of `can_suspend` (editor tier of the parent atespace, plus a
direct grant so a machine identity can revert the actor it drives
without holding an atespace role).

**4. Serve RevertActor and add the CLI verb (`f04fab4`)**
* Wires the `Control.RevertActor` service method to the workflow
(replacing the generated stub that previously answered `Unimplemented`).
* Adds the `"ate revert actor"` CLI command, making the feature usable
end-to-end.
* Implements `Terminate` for the fake atelet. This was necessary because
reverting an actor from the `RUNNING` state is the first path to reach
this call in functional tests (previously, delete tests skipped this
step as they ran against actors with no worker assignment).

**5. Add a manual verify script for RevertActor (`8022db3`)**
* Adds a script to manually exercise `RevertActor` against a real
control plane, since unit and functional tests only run against a fake
atelet.
* Tests reverting from `CRASHED`, `RUNNING`, and `PAUSED` states, and
verifies that attempting to revert a `SUSPENDED` actor is properly
rejected.
* Simulates a crash by deleting the worker pod the actor runs on to
verify the workflow can handle the absence of a worker to terminate.


- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-18 20:18:17 +00:00
Krisztian F 5f7c690108 (feat): Actor lifecycle events over otlp (#1658)
## What this does

ateapi writes a record every time an actor changes state. Until now
those records only went to the pod's stdout, and nothing reads stdout.
This sends the same records to a collector as OTLP log events.

Actor name and uid cannot be metric labels (too many values), and traces
are sampled at 1%. So these records are the only way to answer "what
state is this actor in, and since when".

## Changes

- `serverboot.InitLogging` sets up a LoggerProvider, next to the
existing tracer and meter ones.
- New `internal/actorevent` package builds the log records.
- ateapi emits at the two places that already write the stdout records.
- Two event names: `ate.actor.state_changed` and `ate.actor.crashed`.
- Both names are registered in `docs/metrics/registry/events.yaml`, so
`make verify` checks them.
- kind gets a logs pipeline and a count connector. The e2e suite reads
the counts back.
- Docs updated. `otel-collector.md` said substrate has no
LoggerProvider, which is no longer true.

## Opt-in

`OTEL_LOGS_EXPORTER` defaults to `none`. Only the kind overlay sets it
to `otlp`. The base ConfigMap is untouched, so no deployed environment
changes when this merges.

## Notes on the design

- **No slog bridge.** Only two call sites emit these records, so
emitting twice costs two lines. A bridge would also send every ateapi
log over the wire, could not set the event name, and would loop, because
SDK export errors are logged through slog.
- **Batching processor, not the simple one.** These records sit on the
actor resume path. A processor that exports inside the emit call would
add a blocking gRPC call there, so a slow collector would become control
plane latency.
- **Two event names, not one per state.** `ate.actor.state` already says
which transition happened. A crash gets its own name because it carries
two extra attributes and a higher severity.
- **Both copies are kept on purpose.** No collector in this repo reads
pod stdout, so nothing is duplicated today. `kubectl logs` keeps
working. If a filelog agent is ever added, drop one of the two. The
escape hatch is written down in `docs/metrics/substrate.yaml`.

## Dependencies

Adds `otel/log`, `otel/sdk/log` and `otlploggrpc`, all pinned at
v0.20.0. That is the release that matches the pinned `otel v1.44.0`.
v0.21.0 would pull the core modules to v1.45.0, which this change does
not need. The logs API has a
v1.47.0 release candidate upstream, so it is on its way to stable.

## Testing

- Unit tests for the exporter resolver, the record builder, and both
ateapi emit sites.
- The record builder test checks the attribute set matches what the
event name declares, in both directions.
- The ateapi tests check the OTLP record carries the same attributes as
the stdout record.
- Ran end to end on kind. Records arrive with the right event name,
severity, attributes, and with trace context on the record's own fields
rather than as attributes.
- Checked the off state too. With `OTEL_LOGS_EXPORTER` removed, the
collector receives no log records and stdout is unchanged.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-18 18:11:12 +00:00
Julian Gutierrez Oschmann af2477e45d When connecting to Atelet, always resolve the atelet IP from the node name (#1742)
Dialing by resolving the worker pod is problematic, because in some
cases (e.g. during Worker Pod deletion), we need to dial to atelet (to
properly call `Terminate`), but the worker is gone.

A much simpler model is to always dial by resolving the IP of the atelet
on a given node.
2026-09-18 16:07:11 +00:00
yufan-su 85ce8ed531 egress: add credential injector (#1360)
Adds **egress credential injection**: on the sdsmint egress gateway's
decrypted MITM leg, when an actor's `EgressPolicy` rule matches and
carries an `inject_static_headers` effect, the gateway resolves the
referenced credential from a **credential provider** and sets it as a
request header (e.g. `Authorization: Bearer <token>`) before
re-originating upstream.

This implements the `applyEffects` in the egress handler. Injector runs
as part of the existing egress ext_proc handler that already
fetches/caches/evaluates each actor's `EgressPolicy`, so the MITM leg
gains injection without a second ext_proc hop.

### How it works

- New `CredentialProvider.FetchSecret` plugin API
(`pkg/proto/credproviderpb`): the gateway calls it with the credential
URI and the actor's attested SPIFFE identity; the provider authenticates
the gateway (mTLS) and resolves the secret.
- The egress handler dials the provider over mTLS
(`egress.DialProvider`) and injects on matched, allowed requests
(`egress.applyEffects`).
- Behavior:
- **TLS MITM leg + provider configured** → inject the credential
(overwriting any actor-set header).
- **Cleartext leg, or no provider configured** → skip injection and pass
the request through (never put a secret on a cleartext wire; don't block
allowed egress).
- **Attempted but failed** (unfetchable secret, empty/malformed secret,
credential URI of an unserved provider class) → fail closed.

## Installation

One flag stands up and wires the whole stack:
```
hack/install-ate.sh --deploy-ate-system --experimental-egress-credential-injection
```
`--experimental-egress-credential-injection` deploys credprovider and
the injector, then re-wires the sdsmint egress gateway to route through
the injector. It implies `--experimental-use-sdsmint`

### New install flags

Two flags configure which credential provider the injector targets:

| Flag | Purpose | Default |
|---|---|---|
| `--credential-provider-name` | Provider class, as a `ate-secret://`
prefix; a policy URI of any other class is refused |
`ate-secret://kubernetes.io` |
| `--credential-provider-address` | Where the injector dials the
provider | `credprovider.ate-system.svc:50051` |
2026-09-16 19:13:33 -04:00
Luiz Oliveira fd99858fb8 Store the in-progress snapshot as a URI instead of a name (#1609)
To build a snapshot URI, we need the external storage location (which is
stored in the actor template), actor atespace and UID (which are stored
in the actor resource). If an actorTemplate was deleted, we'd leak the
in-progress snapshot, because the storage information was gone.

Fixes #1608

- [x] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
2026-09-16 15:28:58 -04:00
Luiz Oliveira a0d306ee6c Pass the resync interval in the golden tag functional test (#1673)
#1522 added a resyncInterval parameter to NewActorTemplateReconciler and
#1523 added this call site. They landed without seeing each other, so
`main` doesn't compile.

SEt the resyncInterval to 7s, to match what we have in other tests

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
2026-09-16 10:36:35 -04:00
Eitan Yarmush a58481a18e Publish ActorTemplate golden snapshots as tags (#1523)
Fixes #1507

Golden snapshots currently remain owned by the temporary golden actor,
so another resume/suspend cycle or actor deletion can collect a snapshot
still referenced by its template. The controller now copies the warmed
snapshot into a published tag, deletes the golden actor, and records the
tag reference on the template. Interrupted tag creation and cleanup
remain retryable; template deletion cleans up both resources.

`CreateActor` resolves an explicit `sourceTag` or the template's golden
tag into the actor's initial snapshot. Actors created before the golden
tag is ready retain their cold-boot behavior. The golden tag uses the
template UID as its name in `ate-golden`. The proto replaces
`golden_snapshot` with `golden_tag` at field 1, without backward
compatibility.

This PR is based directly on `main` and does not depend on #1521.

Follow-up recommendation: move the create → resume → wait → suspend →
tag → delete sequence into a golden-template workflow using the existing
workflow conventions. The reconciler now coordinates multiple
recoverable steps; it could retain scheduling and retries while
delegating that sequence to the workflow. This refactor is outside this
PR.

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

Validation: full `env -u NO_COLOR make verify` passed after rebasing
onto `main`. After the final proto field-number change, bindings were
regenerated and the control API unit/functional tests plus proto-format
and Go-format checks passed.

---------

Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
2026-09-16 09:24:42 -04:00
Eitan Yarmush 02eef7e82b Make actor template resync interval configurable (#1522)
Actor template discovery currently uses a fixed 20-second resync
interval. This PR adds `--template-resync-interval` so deployments can
tune that delay, preserving the `20s` default and rejecting nonpositive
values. The setting also controls the reconciler's existing fallback
retry delay.

This is intentionally a small change to start a discussion about how
template builds should be dispatched as the template catalog grows.

The current resync fetches and decodes every template, including
completed ones. At 100,000 templates, a 20-second interval implies
roughly 5,000 template rows read per second per replica, assuming scans
finish quickly. This is an estimate from the code, not a benchmark.

Possible follow-ups:

- **Immediate enqueue:** start work after creation, retaining a slower
recovery scan for crashes between persistence and enqueue.
- **Outbox/watch:** consume changes instead of scanning the catalog. The
existing worker outbox still polls every 50 ms and broadcasts events to
each subscriber; write overhead and recovery scans need consideration.
- **Durable pending-build queue:** atomically record work, claim due
jobs using short `FOR UPDATE SKIP LOCKED` transactions, and recover
expired leases. This still polls, but queries pending work rather than
the full catalog. `LISTEN/NOTIFY` is not the proposed default because it
serializes notifying commits.
- **Imperative build / long-running operation:** give callers explicit
build control or a progress/completion handle. Either still needs
reliable execution underneath.

The main question is whether template builds need a broadcast change
feed or a queue where replicas claim different jobs. Neither alternative
is implemented here.

[Full research: database costs, execution options, recovery
requirements, and
sources](https://gist.github.com/EItanya/0a1d6893ede34f2f0e9d9d1929ecb823).

Validation: all Go race tests and repository verifiers passed using
module mode with `NO_COLOR` unset. CLI checks confirmed the default and
rejection of zero/negative intervals; the existing reconciliation test
checks a custom interval.


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: Eitan Yarmush <eitan.yarmush@solo.io>
2026-09-15 16:18:13 -07:00
Julian Gutierrez Oschmann 2b255001f1 Add support for field defaults.
Populate default fields during both Create and Update RPC calls so
that resources are stored with defaults instead of zero values.

This is a much cleaner approach as it means that a) consumers don't need to
be aware of the default value and b) default values can potentially change.

Only ActorTemplate had defaults, but added default functions for all
resources so that all RPC handlers are wired up.
2026-09-15 10:09:39 -07:00