feat(cli): alien operations publish/list custom plugin bundles (ALIEN-428) - #281
feat(cli): alien operations publish/list custom plugin bundles (ALIEN-428)#281ab-alien-dev wants to merge 21 commits into
Conversation
Greptile SummaryThis PR adds customer-facing operations-plugin management and the operator loops required to execute approved plugin operations.
Confidence Score: 4/5The PR does not yet appear safe to merge because a concurrent shutdown can still hide a genuine supervised-loop failure and yield a successful operator exit. Completed loop tasks discard their join result and are classified by a later cancellation-token read, so the concurrently running CLI signal task or Windows stop thread can cancel between loop completion and classification; the operator then returns success despite the loop failure. Files Needing Attention: crates/alien-operator/src/lib.rs, crates/alien-operator/src/cli.rs
|
| Filename | Overview |
|---|---|
| crates/alien-cli/src/commands/operations.rs | Adds publishing and listing commands, including ZIP metadata extraction, validation, encoding, and platform API calls. |
| crates/alien-operator/src/cli.rs | Adds access-request and operations-execution hook injection and preserves all hooks across Windows service startup. |
| crates/alien-operator/src/lib.rs | Supervises the new operator loops and reports unexpected loop termination, while the previously reported concurrent cancellation classification defect remains. |
| crates/alien-operator/src/loops/access_requests.rs | Defines the injectable access-request synchronization loop and its cancellation-aware OSS fallback. |
| crates/alien-operator/src/loops/operations_exec.rs | Defines the injectable operations execution loop and its cancellation-aware OSS fallback. |
| crates/alien-helm/src/generator.rs | Extends generated operator manifests and RBAC resources for access-request and operations support. |
| crates/alien-commands/src/server/command_registry.rs | Extends command-name validation and target selection for plugin operation commands. |
Sequence Diagram
sequenceDiagram
participant User
participant CLI as alien CLI
participant API as Platform API
participant Operator
participant Plugin
User->>CLI: operations publish bundle.zip
CLI->>CLI: Read and validate metadata.json
CLI->>API: POST /v1/operations/plugins
User->>CLI: operations list
CLI->>API: Fetch plugin catalog
API-->>CLI: Builtin and custom plugins
API->>Operator: Queue approved plugin/operation command
Operator->>Plugin: Execute operation
Plugin-->>Operator: Operation result
Operator-->>API: Report result
Reviews (20): Last reviewed commit: "fix(operator): classify loop exit atomic..." | Re-trigger Greptile
2ec1add to
828ff66
Compare
Operations plugins run inside a deployment via the commands interface (`plugin/operation`). This adds the customer-facing CLI to manage them: - `alien operations publish <bundle.zip>` — reads the bundle's metadata.json, validates it, and uploads the bundle to the platform (POST /v1/operations/plugins). - `alien operations list` — shows the catalog (builtin + custom). Platform-gated (talks to the platform API via the authenticated http client, like `alien managers`). Reads the ZIP with the `zip` crate; forwards the full metadata.json verbatim for the platform to re-validate. base64-inlines the bundle so no multipart handling is needed. Tests: 3 (reads metadata, rejects missing metadata.json, rejects bad metadata).
…-428)
Phase A of customer-approved operations (turbopuffer model). Each proposed
remediation operation becomes an AlienOperation custom resource the customer
reviews and approves in their own cluster; the operator executes approved ones.
- Add the AlienOperation CRD (group operations.alien.dev, v1alpha1, Namespaced,
status subresource). Printer columns Approved/State/Age/Details so
`kubectl get alienoperations` reads like the turbopuffer example. spec:
{plugin, operation, params, approved, commandId}; status: {state, details,
message, approvedAt}.
- generate_operator_manifest prepends the (cluster-scoped) CRD doc.
- Grant the operator get/list/watch/create/update/patch on alienoperations (+
/status) — the one resource it writes; everything else stays read-only, and
no delete. Update the read-only-RBAC test to allow this intentional exception,
and the doc-list/namespace tests for the cluster-scoped CRD.
13 generator tests pass (2 pre-existing helm-lint failures are environmental).
Re-add a minimal injected-loop seam to alien-operator for the operations approval controller (the customer-approved AlienOperation CRD flow), mirroring DebugSessionLoop: - loops::operations_crd::OperationsCrdLoop trait + UnimplementedOperationsCrdLoop no-op stub. OSS builds keep the stub. - run_operator_with_cancel_and_loops(...) takes both the debug and operations-crd loops; run_operator_with_cancel_and_debug_loop kept as a back-compat wrapper. - OperationsCrdLoopHook + cli_main_with_all_loops(...). The controller spawns on Kubernetes (not airgapped) and joins the graceful-shutdown select. The real controller (materialize CRs, run approved) lives outside this OSS crate and is injected via the hook, so alien-operator keeps building standalone.
The operator chart is fully white-labeled (helm install mystartup-operator), so the operations CRD shouldn't be AlienOperation either. - alien-core::operations_crd: single shared derivation of the CRD's group/kind/plural/short from the deployment's brand domain (operations_crd_names(label_domain)). acme.dev → operations.acme.dev / AcmeOperation / acmeoperations. No domain → the Alien defaults. 4 tests. - alien-helm: generate_operator_manifest gains a label_domain option; the CRD doc + the operator RBAC (apiGroups/resources) are generated from the derived names instead of hardcoded operations.alien.dev/alienoperations. Test proves a branded build contains no "alien"-named operations resource. The operator runtime uses the SAME helper (next commit), so the CRD the manifest registers is exactly the one the operator creates/watches.
… sync loop Replace the inline operations-CRD execution model with an access-request authorization model. The operator no longer executes commands from a CR; it materializes a control-plane access request as a time-boxed grant the customer approves in-cluster, then reports the approval window back. The control plane dispatches the commands through the normal commands queue. - operator: rename loops::operations_crd -> loops::access_requests; the seam now exposes AccessRequestSyncLoop + AccessRequestBackend traits and the AccessRequest/AccessApproval/AccessRequestCommand types (with a reason field), plus an Unimplemented no-op stub. cli/lib wire the renamed hook. - alien-helm: the CRD becomes a time-boxed grant — spec.commands + spec.approvedForMinutes, status.state (PENDING_APPROVAL|APPROVED|EXPIRED) + approvedUntil; printer columns State/Commands/Approved-Until/Age. RBAC and white-labeling unchanged (materialize + status, no execution, no delete). - regenerate the copied platform OpenAPI spec.
The operator can now run authorized `<plugin>/<operation>` commands itself (e.g. `kubernetes/restart-pod`) in pull mode — an operator-level action with no workload receiver to run it. Delivery uses the NORMAL commands queue: the operator is a reserved command target with an embedded receiver. - alien-commands: `select_command_target` resolves the reserved `operator` id (`OPERATOR_COMMAND_TARGET_ID`) to a pull-mode daemon target without consulting the stack, so operations commands can be addressed to the operator (which is not a stack resource). The command flows through the same pending-index + lease path as any Container/Daemon command. - alien-operator: add the `operations_exec` seam — `OperationsExecutor` (the handler contract) + `OperationsExecLoop` (receives OperatorState) + a no-op stub, threaded through a 4th `cli_main_with_all_loops` hook. OSS builds inject none and run nothing; a downstream binary injects a real executor and embeds an `alien_commands::Receiver` bound to the operator target.
…space_with_bootstrap `alien deployments get/ls` (and other workspace-resolving commands) errored with "API keys are already scoped to a workspace" when the ai-agent ran them, because resolve_workspace_with_bootstrap rejected any api_key outright — even though the ai-agent hands the CLI a USER bearer token via ALIEN_API_KEY plus an explicit ALIEN_WORKSPACE. Honor an explicitly-set workspace even with an api_key present, matching the already-established behavior of resolve_workspace_query_with_bootstrap and resolve_platform_workspace_context. Harmless for real API keys (the API ignores the workspace for them); required for the ai-agent's user token.
The grant custom resource the operator materializes is an access request, not a generic "operation". Rename the white-labeled CRD noun accordingly so the customer-facing resource reads naturally: group: operations.<domain> -> accessrequests.<domain> kind: <Brand>Operation -> <Brand>AccessRequest plural: <brand>operations -> <brand>accessrequests short: <brand>op -> <brand>ar alien-core: operations_crd -> access_request_crd (module, OperationsCrdNames -> AccessRequestCrdNames, operations_crd_names -> access_request_crd_names, OPERATIONS_CRD_VERSION -> ACCESS_REQUEST_CRD_VERSION). alien-helm: the operator manifest's CRD, RBAC, and printer columns all derive from the renamed names, so the resource the chart registers stays in lockstep with what the operator watches at runtime.
…ed flow Regenerated from the platform API: the queue response now carries the customer's kubectl approve command, plus the operator-facing materialized report endpoint. Generated via pnpm generate:api-copy-openapi.
The operator ships a kubernetes operations plugin with mutating operations (restart-pod, scale) and logs, but the operator Role was read-only, so a customer-approved remediation reached the operator only to be 403'd by the apiserver. Grant exactly what the shipped operations require and nothing more: pods:delete (restart-pod), pods/log:get (logs), and patch on the workload scale subresource (scale). Base access stays read-only; Secrets remain inaccessible.
Dev helper that renders a full operator manifest (CRD + RBAC + Deployment) to stdout for local redeploys and inspecting the generated RBAC. Uses only generic example values.
…ent-alien-operations # Conflicts: # Cargo.toml # client-sdks/platform/openapi.json # client-sdks/platform/rust/openapi-3.0.json # client-sdks/platform/rust/openapi.json
Regenerated from the merged platform API (main's routes + operations/ access-requests) and copied into the platform client SDK. Cargo.lock picks up two transitive patch bumps (syn, thiserror) from re-resolution.
The assertion checked for a standalone `resources: ["pods/log"]` rule, but the helm chart's operator Role (`role_tpl`) folds `pods/log` into the core `""`-group rule alongside the other pod-level resources. Assert against the bundled rule the chart actually renders. The operator still grants `pods/log` for the on-demand `logs` operation — only the test's expected string was wrong. Verified with a real `helm template` + kubeconform render.
`alien operations list` / `publish` hit the platform's `/v1/operations/plugins` endpoint, which requires both `workspace` and `project` query params. Only `workspace` was sent, so every call 422'd with `project: expected string, received undefined`. Resolve the linked project (or a new `--project` override) the same way the other project-scoped commands do, and append it to the query.
Two review findings on the operations PR: - A background loop exiting on its own (return or panic) was swallowed by `h.await.ok()` and the operator returned `Ok(())`, so CLI and Windows-service callers saw a clean exit despite the failure. Track which loop fell out and return a new `OPERATOR_LOOP_EXITED` error for a non-zero exit. - The Windows service entry point passed `NOOP_*` for the debug-session, access-request, and operations-exec loop hooks, discarding whatever a downstream binary registered via `cli_main_with_all_loops` — pull-mode operations never dispatched. Stash all four hooks in per-process statics (like the init hook) and thread them through `service_main`.
Regenerated from the platform API after the schema pipeline picked up the merged resource-type / sync-reconcile changes. No hand edits.
Follow-up on the loop-exit propagation fix: on shutdown the token is cancelled and the supervised loops observe it and return, so a loop-handle branch can become ready in the same tick as the cancellation branch. The unbiased select could then pick the loop branch and misreport a clean shutdown as `LoopExited`. Add `biased` so the select prefers the cancellation branch, and — the authoritative guard — sample `is_cancelled()` before the idempotent `cancel.cancel()` and suppress the failure when shutdown was already requested. Extract the decision into `loop_exit_failure` with a unit test covering all four (exited_loop × shutdown_requested) cases.
`alien operations publish` no longer base64-inlines the ZIP into the API request. It now asks the platform for a presigned S3 URL, PUTs the ZIP straight to S3, then registers the plugin with metadata only — the bytes never flow through the API. Regenerated the platform SDK openapi specs for the new upload-url + enable/disable routes.
The previous fix sampled `is_cancelled()` AFTER the select resolved, leaving a window where an independent shutdown could flip the token between a loop branch winning and the sample — wrongly suppressing a genuine LoopExited as a clean exit. Move the clean-vs-failure decision INTO each loop branch (`loop_exit`), which reads the token in the same synchronous step the branch wins in (no `.await` between), so the verdict reflects the token state at that instant and can't be changed by a later cancel. `biased` still resolves the same-tick case.
What
Adds the customer-facing CLI to manage operations plugins. Operations plugins run inside a deployment via the commands interface (
plugin/operation); this lets a customer publish a custom plugin bundle so its operations become available in their workspace.Commands
alien operations publish <bundle.zip>— reads the bundle'smetadata.json, validates it (name/version present, valid ZIP), and uploads the bundle to the platform (POST /v1/operations/plugins).alien operations list— shows the catalog (builtin + custom).Notes
#[cfg(feature = "platform")]): talks to the platform API via the authenticated http client, exactly likealien managers.zipcrate and forwards the fullmetadata.jsonverbatim — the platform re-validates it authoritatively (the CLI doesn't depend on the private plugin crate, keeping the OSS boundary clean).Test plan
cargo test -p alien-cli --features platform operations::— 3 tests pass (reads metadata, rejects missingmetadata.json, rejects metadata missing required fields)cargo build -p alien-cli --features platformalien operations --helprenders the command group