Skip to content

feat(cli): alien operations publish/list custom plugin bundles (ALIEN-428) - #281

Open
ab-alien-dev wants to merge 21 commits into
mainfrom
alan/alien-428-implement-alien-operations
Open

feat(cli): alien operations publish/list custom plugin bundles (ALIEN-428)#281
ab-alien-dev wants to merge 21 commits into
mainfrom
alan/alien-428-implement-alien-operations

Conversation

@ab-alien-dev

@ab-alien-dev ab-alien-dev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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's metadata.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

  • Platform-gated (#[cfg(feature = "platform")]): talks to the platform API via the authenticated http client, exactly like alien managers.
  • Reads the ZIP with the zip crate and forwards the full metadata.json verbatim — the platform re-validates it authoritatively (the CLI doesn't depend on the private plugin crate, keeping the OSS boundary clean).
  • base64-inlines the bundle so there's no multipart handling.

Test plan

  • cargo test -p alien-cli --features platform operations::3 tests pass (reads metadata, rejects missing metadata.json, rejects metadata missing required fields)
  • cargo build -p alien-cli --features platform
  • alien operations --help renders the command group

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds customer-facing operations-plugin management and the operator loops required to execute approved plugin operations.

  • Adds platform-gated alien operations publish and alien operations list commands.
  • Adds access-request synchronization and operations-execution loop interfaces.
  • Threads injected loop hooks through normal and Windows-service startup.
  • Extends operator RBAC, command routing, generated manifests, API specifications, and dependencies for the new operations workflow.

Confidence Score: 4/5

The 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

Important Files Changed

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
Loading

Reviews (20): Last reviewed commit: "fix(operator): classify loop exit atomic..." | Re-trigger Greptile

Comment thread crates/alien-operator/src/lib.rs Outdated
Comment thread crates/alien-operator/src/cli.rs Outdated
@ab-alien-dev
ab-alien-dev force-pushed the alan/alien-428-implement-alien-operations branch from 2ec1add to 828ff66 Compare August 2, 2026 10:07
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).
@ab-alien-dev ab-alien-dev reopened this Aug 2, 2026
@ab-alien-dev ab-alien-dev changed the title feat(operator): add pluggable OperationsLoop seam (ALIEN-428) feat(cli): alien operations publish/list custom plugin bundles (ALIEN-428) Aug 2, 2026
…-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.
Comment thread crates/alien-operator/src/lib.rs Outdated
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.
Comment thread crates/alien-operator/src/lib.rs
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant