|
| 1 | +# ateapi resource API (issue #368) |
| 2 | + |
| 3 | +Proto: [`pkg/proto/ateapipb/ateapi.proto`](../../pkg/proto/ateapipb/ateapi.proto) |
| 4 | + |
| 5 | +This is the proposed API surface once ActorTemplate, WorkerPool, |
| 6 | +SandboxConfig and WorkerPoolGrant move out of Kubernetes CRDs into the |
| 7 | +substrate API ([#368](https://github.com/agent-substrate/substrate/issues/368)). |
| 8 | +It builds on [decoupling-planes.md](https://github.com/agent-substrate/substrate/blob/poc-decouple-upstream/docs/design/decoupling-planes.md) |
| 9 | +(on the POC branch), which settled the |
| 10 | +resource model (global pools + per-atespace grants); this doc covers the API |
| 11 | +shape. The transition from the CRDs/POC is covered separately in |
| 12 | +[ateapi-crd-migration.md](ateapi-crd-migration.md). |
| 13 | + |
| 14 | +The API follows the draft |
| 15 | +[API style guide](https://github.com/agent-substrate/substrate/pull/351); |
| 16 | +deliberate divergences are listed at the [end](#style-guide-conformance). |
| 17 | + |
| 18 | +## Resource model |
| 19 | + |
| 20 | +| Resource | Scope | Service | Mutable? | Notes | |
| 21 | +|---|---|---|---|---| |
| 22 | +| Atespace | global | Control | no | isolation boundary | |
| 23 | +| ActorTemplate | atespace | Control | no (immutable spec) | + Watch | |
| 24 | +| Actor | atespace | Control | `worker_selector` only | + Suspend/Pause/Resume | |
| 25 | +| WorkerPool | global | Admin | yes | + Watch; labels are the selector match target | |
| 26 | +| SandboxConfig | global | Admin | yes | de-facto registry of sandbox classes | |
| 27 | +| WorkerPoolGrant | atespace | Admin | no | at most one grant per (atespace, pool) | |
| 28 | +| Worker | — | Admin | — | debug projection, ListWorkers only | |
| 29 | + |
| 30 | +All managed resources carry `ResourceMetadata meta = 1` (atespace, name, |
| 31 | +uid, version, create_time, update_time, labels) and use the standard method |
| 32 | +shapes from the style guide: Get/Create/Update return the bare resource, |
| 33 | +Delete returns the deleted resource, List paginates with |
| 34 | +`page_size`/`page_token`, identity travels as `ObjectRef{atespace, name}`, |
| 35 | +and optimistic concurrency uses `meta.version` (int64, `ABORTED` on |
| 36 | +mismatch, 0 skips). |
| 37 | + |
| 38 | +## Design decisions |
| 39 | + |
| 40 | +**D1. Versioned package `ateapi.v1alpha1`.** gRPC method paths embed the |
| 41 | +package, so the package name is the API-versioning mechanism. |
| 42 | + |
| 43 | +**D2. Two services with distinct audiences.** `Control` is the tenant |
| 44 | +surface (atespaces, templates, actors); `Admin` is the platform surface |
| 45 | +(pools, sandbox configs, grants, debug). Watch RPCs live next to their |
| 46 | +resource. The split gives a future authz story a natural boundary. |
| 47 | + |
| 48 | +**D3. ActorTemplate spec is immutable — no UpdateActorTemplate.** Golden |
| 49 | +snapshots are only valid for the exact spec they were taken from, so |
| 50 | +in-place mutation is meaningless. Template changes = create a new template |
| 51 | +and migrate actors. Delete returns `FAILED_PRECONDITION` while any actor |
| 52 | +references the template. |
| 53 | + |
| 54 | +**D4. At most one WorkerPoolGrant per (atespace, worker pool).** Grants are |
| 55 | +fully conventional resources (caller-named, standard methods), but Create |
| 56 | +enforces uniqueness on the pair — a duplicate grant for the same pool |
| 57 | +returns `ALREADY_EXISTS` regardless of its name (same pattern as the |
| 58 | +single-default SandboxConfig rule). This keeps revocation unambiguous and |
| 59 | +lets the scheduler's `(atespace, pool)` check stay a point lookup via a |
| 60 | +server-side index. Grants are immutable; future policy fields (quotas) |
| 61 | +would add Update. |
| 62 | + |
| 63 | +**D5. One Kubernetes pocket: `WorkerPoolSpec.kubernetes`.** Everything |
| 64 | +k8s-specific about materializing a pool — namespace, node_selector, |
| 65 | +tolerations, node_affinity, resources, priority_class_name — lives in a |
| 66 | +single `KubernetesPlacement` message. Everything outside it is |
| 67 | +plane-neutral; a future non-k8s substrate adds a sibling placement message. |
| 68 | + |
| 69 | +**D6. Referential integrity on delete.** Deletes that would orphan |
| 70 | +dependents return `FAILED_PRECONDITION`: atespace↔actors/templates, |
| 71 | +template↔actors, pool↔grants and pool↔assigned actors, config↔pools. |
| 72 | + |
| 73 | +**D7. Status is a closed enum, not phase strings or conditions.** |
| 74 | +`ActorTemplateStatus.state` ∈ {PENDING, SNAPSHOTTING, READY, FAILED} plus |
| 75 | +`golden_actor_id`, `golden_snapshot`, `error_message`. Likewise |
| 76 | +`SnapshotsConfig` scopes are a `Scope` enum (FULL/DATA). |
| 77 | + |
| 78 | +**D8. Equality-only `Selector{match_labels}`** for template and per-actor |
| 79 | +worker selectors, matched against `WorkerPool.meta.labels`. The scheduler |
| 80 | +implements equality matching only; set-based expressions can be added as a |
| 81 | +new field if a consumer appears. |
| 82 | + |
| 83 | +**D9. Env vars are a `oneof {value | secret}`** with a plane-neutral |
| 84 | +`SecretRef{name, key, optional}` — value-vs-secret is exclusive by |
| 85 | +construction, and no k8s types leak into the container spec. |
| 86 | + |
| 87 | +**D10. `sandbox_class` is an open string** on templates and pools. |
| 88 | +SandboxConfigs are the class registry; discoverability comes from |
| 89 | +ListSandboxConfigs, not a proto enum. |
| 90 | + |
| 91 | +**D11. Update masks accept top-level paths only.** `update_mask` is |
| 92 | +required (per the style guide); paths one level below the resource root |
| 93 | +(`spec.replicas`, `spec.kubernetes`, `meta.labels`) are accepted, and |
| 94 | +naming a message or map field replaces it wholesale. Unknown, immutable, or |
| 95 | +deeper paths → `INVALID_ARGUMENT`. Servers may widen to deeper paths later |
| 96 | +without breaking clients. |
| 97 | + |
| 98 | +**D12. Watch = snapshot, SYNCED, then incremental events.** Streams |
| 99 | +(templates, pools) deliver an initial snapshot, a `SYNCED` marker, then |
| 100 | +at-least-once CREATED/UPDATED/DELETED events carrying full resource state. |
| 101 | +No resume tokens: consumers are in-cluster caches and re-syncing is cheap; |
| 102 | +`meta.version` lets them discard stale events. |
| 103 | + |
| 104 | +**D13. Worker stays a debug projection.** No `ResourceMetadata`, no CRUD. |
| 105 | +Making Worker a managed resource would promise lifecycle semantics the |
| 106 | +system doesn't offer — workers are pool-managed. |
| 107 | + |
| 108 | +**D14. Actor is a managed resource like the rest.** `meta` carries its |
| 109 | +identity; `actor_template` and `worker_pool` are `ObjectRef`s; lifecycle |
| 110 | +verbs (Suspend/Pause/Resume) are custom methods returning the Actor. The |
| 111 | +`ateom_pod_*` fields remain, marked output-only infrastructure debugging. |
| 112 | + |
| 113 | +**D15. Atespace and SessionIdentity are unchanged** beyond conforming to |
| 114 | +`meta` and standard method shapes. Redesigning them is a non-goal of #368. |
| 115 | + |
| 116 | +## Style-guide conformance |
| 117 | + |
| 118 | +The surface follows PR #351 (identity model, `ResourceMetadata`, |
| 119 | +`ObjectRef`, standard method shapes, required `update_mask`, `version` |
| 120 | +concurrency). Four deliberate divergences, feedback filed on PR #351: |
| 121 | + |
| 122 | +1. **`labels` in `ResourceMetadata`** — the guide's meta has no labels |
| 123 | + field, but scheduling requires pool labels as a selector match target. |
| 124 | +2. **Immutable resources may omit Update** (D3, D4) — the guide implies all |
| 125 | + five standard methods everywhere. |
| 126 | +3. **Top-level-only mask paths** (D11) — the guide requires the mask but |
| 127 | + leaves path granularity undefined. |
| 128 | +4. **Empty `atespace` = list across all atespaces** — the guide reads as if |
| 129 | + `atespace` were required on List; admin/debug flows need cross-atespace |
| 130 | + listing. |
| 131 | + |
| 132 | +## Open questions |
| 133 | + |
| 134 | +- **Secrets plane**: `SecretRef` resolves against the worker's environment |
| 135 | + (a k8s Secret today). Freeze that rule, or design secret distribution |
| 136 | + before v1beta1? |
| 137 | +- **Pagination defaults**: pick a server default page size (proposal: 500, |
| 138 | + max 1000) and document it once, centrally. |
| 139 | +- **Watch scope**: watches are global today (atelet caches everything). |
| 140 | + Add an optional `atespace` filter now, or when a consumer needs it? |
0 commit comments