sylphx-cli 0.29.19

The Sylphx CLI: every Sylphx API method as a command, generated from the Sylphx schema registry.
sylphx-cli-0.29.19 is not a library.

sylphx CLI

Every Sylphx API method as a command, generated from the one schema, plus a small hand-written porcelain on the generated Rust SDK.

curl -fsSL https://github.com/SylphxAI/sylphx-clients/releases/latest/download/install.sh | sh   # or: npm i -g @sylphx/cli · cargo install sylphx-cli
sylphx login                          # approve selected orgs at sylphx.com/device; one role-scoped key per org
                                      #   keys go to the OS keychain (or an explicitly chosen 0600 file), never stdout
sylphx login --api-key -              # agents and CI: an Access key on stdin (or SYLPHX_API_KEY)
sylphx token --scope hosting:deploy   # one short-lived token (15 min) for one scope, on stdout only
sylphx org use acme                   # active org when no --org, resource name, or linked context selects another
sylphx whoami --org beta              # use beta's stored credential for this command only
sylphx logout                         # revoke and forget the selected org; keep other org logins
sylphx logout --all                   # revoke and forget every stored credential
sylphx auth status                   # first use discovers/provisions the default project and production environment
sylphx link --env orgs/…/envs/…       # optional advanced target override for this directory
sylphx data databases create main --spec.compute-units 2
sylphx data databases list -o json
sylphx devices run --app app.apk       # a smoke test on a fresh Android device: device-results/
sylphx deploy                         # a Release of this checkout's HEAD, followed until live (exit 0) or failed (exit 1, reason)
sylphx status                         # the linked environment's live Release: per-service artifact digest and source SHA
sylphx rollback                       # the previous served artifacts, without a build (docs/services/hosting/releases.md)
sylphx devices run --app app.apk --scenario capture.json --size tablet  # native-size scenario capture
sylphx build run -- cargo test        # this work tree's command on a remote build machine
sylphx build image                     # build and publish to the linked project; no registry setup
eval "$(sylphx build cache env)"      # point local sccache and Turbo at the project's shared build cache
sylphx build run --size standard -- cargo check   # home region; keep the default 30m cold-fallback wait
sylphx events listen --forward localhost:3000/hook   # a topic's events, signed like a webhook, to a local URL
sylphx data docs search --index docs rate limits    # Markdown docs in a search index: path:line hits
sylphx mcp                            # the MCP server over stdio
sylphx mcp setup                      # register it with Claude Code, Codex, Cursor, VS Code, …
  • Login needs no organization up front: select one, several, or all memberships in the browser. sylphx login --org acme locks approval to that organization. The CLI stores every issued key separately; there is no cross-tenant key. Commands choose credentials by --org (slug, id, or orgs/{id}), a full resource name, SYLPHX_ENVIRONMENT / the nearest project link, then the active org set by sylphx org use <slug>. Switching active org does not rewrite a checkout's link; specify the project/environment when working in another org. --key / --api-key and SYLPHX_API_KEY still override stored credentials. Existing single-key credentials files and keychain entries remain readable. Re-login merges approved org credentials without deleting other orgs; logout --org <slug> removes just that org, while logout --all removes all.

  • sylphx <service> <collection> <verb> [NAME|PARENT|ID] [--flags]: flags are the spec fields in dotted kebab-case (--spec.compute-units); --from-file takes a whole request; --output table|json|yaml|name.

  • First use follows the Vercel/Railway project-discovery pattern. After sign-in, an environment-scoped command (including auth status) discovers the selected org's default project and its production environment. When there is no standard target, a single existing target is reused; an empty collection is provisioned through public Access. Creation uses stable idempotency keys, and a concurrent slug conflict is rediscovered. The resolved full names are kept in .sylphx/project.json; reruns reuse them. Automatic persistence is best-effort: if the destination cannot be written, the command warns and uses the resolved context in memory; subsequent runs rediscover it. An explicit sylphx link still fails if it cannot write the link. Build's project-scoped commands bootstrap the project the same way. Sylphx's own projects use this same public path, with no tenant special case.

  • Multiple existing projects/environments without a standard default require a choice: the error lists their full names. Select one with sylphx link --project web --env production, or pass an explicit full name. Discovery never crosses the selected org/project, and --org does not overwrite a checkout's link to another organization.

  • Linking follows Vercel project linking: an explicit target is authoritative, not a hint. link --env accepts a full environment name without --project; a slug or environment id uses the selected project. --project accepts a slug, id, or exact project name. Conflicting parents and malformed names fail before the link is written. Without target flags, linking reuses SYLPHX_ENVIRONMENT, then the nearest link in the selected organization, then the credential scope and public discovery above. Changing project clears the old environment; changing only environment retains the selected project. No list-order choice is made. A successful link writes all three full names (org, project, env) to .sylphx/project.json; an incomplete selection is never reported as success. Explicit linking intentionally persists the selected process context; other commands never persist a process-only override. Build reads this same context and leaves machine shape, region, grants, and admission to the public platform.

  • A bare id (main) expands below the resolved environment. Explicit resource names and --env remain advanced overrides; SYLPHX_ENVIRONMENT=orgs/…/projects/…/envs/… overrides the nearest link for that process; only an explicit link persists it. A link in another shape (such as {orgId, projectId}) is skipped with a warning.

  • Mutations wait for their Operation (--no-wait to skip); --dry-run sets validate_only; updates send the etag they read and a mask of the flags given; destructive calls ask first (--yes to skip).

  • sylphx apply reads sylphx.toml (or --manifest PATH, including a Release's declarations.json) and uses the shared Resource planner and writer. --offline checks without a key or network; --dry-run plans without writing; -o json prints the Resource actions and unsettled names. --detailed-exitcode returns 0 for no changes, 2 for changes, and 1 for errors. --timeout SECONDS defaults to 600 per Resource. --release ID and --source REPO@SHA:PATH record provenance; a Release run also writes its compact JSON result to /dev/termination-log. --force allows a standalone run to write a Resource managed by a Release. Eligible kinds are listed in clients/apply/declarable.toml; this list remains empty until each kind's writer cutover is complete.

  • sylphx build image [PATH] publishes to a managed project repository by default and returns an immutable Artifact name and a digest-pinned source_ref for Hosting. Its registry credentials are minted on the public customer API, kept only in the lease's private directory, renewed during long builds, and scrubbed from output. --push REF --registry-auth FILE overrides that default for an external registry; --oci-out FILE copies the OCI archive back. Neither override is a prerequisite.

  • sylphx api GET /v1/whoami is the raw escape hatch.

  • sylphx data docs sync|search|read keeps a Markdown tree in one of the environment's search indexes. sync [GLOB…] --index ID --revision REV (run it on each merge) writes one document per # to ### section with its path, heading and line numbers, then deletes the documents it wrote for that source at any other revision (nothing else in the index); a run matching no file is refused. search QUERY prints the best sections as path:line with the matching line; read PATH[:LINE] [--lines A-B] prints a file or a range of it. --index and --source default to SYLPHX_DOCS_INDEX and SYLPHX_DOCS_SOURCE, which also give sylphx mcp its docs_search and docs_read tools.

  • sylphx events listen [TOPIC] --forward URL relays a topic (default: the environment's whole bus) to a local URL while you develop. It creates a temporary Queue and a Subscription of the topic into it, long-polls the Queue, and POSTs each event as the CloudEvent JSON body a webhook endpoint receives, with webhook-id, webhook-timestamp and webhook-signature (Standard Webhooks) under the whsec_… secret printed at start: --secret, else SYLPHX_LISTEN_SECRET, else one generated once and kept in the config directory (listen-secret, 0600). An event the receiver answers is acked and printed with its status; one it cannot reach is tried again after 3 s. --types and --sources filter; --count N exits after N events; -o json prints one JSON line per event. Ctrl-C deletes the Queue and the Subscription.

  • sylphx devices run installs an APK on a fresh Android device lease, launches it (or a --game-loop), watches it with screenshots, and writes result.json, junit.xml, logcat.txt, and crash.txt into --out (device-results/). Exit 0 passed, 1 failed, 2 infrastructure; the lease is released on the way out unless --keep. Add --scenario capture.json --size tablet for a named native screen sequence. Each invocation selects exactly one declared size on its own fresh lease; invoke again for another size rather than carrying app state across displays. The scenario uses the existing device-farm JSON vocabulary:

    {
      "package": "com.example.game",
      "sizes": {"phone": ["1080x2400", 420], "tablet": ["2560x1600", 240]},
      "steps": [{"launch": true}, {"wait": 30}, {"shot": "lobby"}]
    }
    

    Supported steps are launch, wait (seconds), shot, tap (two fractions), swipe (four fractions and optional milliseconds), key, text, and stop. Exactly one operation belongs in each step. Unsupported operations, malformed fractions and unsafe or duplicate screenshot filenames fail before leasing. The installed APK's application id must match package. Optional title and sourceCommit are caller-supplied provenance labels, not build attestations.

    Captures set and read back native wm size/density, then verify PNG dimensions in either orientation without rescaling. Tap/swipe pixels derive from that actual frame, not cached startup input bounds. Before any input or named shot, both resumed activity and focused-window ownership must match the package; unknown ownership fails closed. Input sequences also require a verified absent or disabled Play billing surface, with a positive Android package inventory control. This is not a substitute for reviewing the scenario: never include purchases or value-spending actions without authority. capture.json records APK/scenario/PNG hashes, native dimensions and step receipts, without entered text. Success means the sequence ran, not that a named image is visually the intended screen; inspect the PNGs. --keep leaves display and billing changes on the disposable lease for diagnosis.

  • Build workspaces are owned by the admitted lease, not a persistent client pool. A queued Build holds no workspace PVC reservation. After Kueue admits the suspended Job, Kubernetes creates the Pod's generic ephemeral raw-block workspace claim; lease end removes it through normal workload garbage collection after the guest unmounts. The catalog retains the current 64Gi workspace request. Explicit customer Sandboxes Volumes are unchanged.

  • sylphx build run [PATH] -- <command…> runs the command on a remote build machine against an ordinary source directory or its enclosing Git work tree when present. No Git initialization or manifest is required. It behaves like the local command: output streams back, the exit code is the command's own, and --artifact GLOB files are copied into --out (.sylphx/out/<run>/). Every lease gets a fresh /workspace, including with an explicit --region; the CLI never lists, creates, evicts or attaches a workspace Volume before admission. Cross-lease target/, local toolchains and BuildKit state are not retained. The existing scoped remote build cache and dependency mirrors remain; they do not replace all local BuildKit layers or mutable cache mounts. Fresh workspaces materialize source from content-addressed tree packs in the Build store: the CLI checks which packs are absent and sends only those. Versioned manifest keys reuse unchanged archives across clients; path-anchored boundaries keep edits local. No cache or infrastructure setup is required. Packs are verified before publication; missing/corrupt packs, links and unusual filenames use direct compressed transfer. --no-cache uses that direct path for the whole tree. This applies to both build run and build image. Exit codes: the command's own, 2 usage, 124 --timeout, 125 platform failure (retryable in the -o json result event; sync or known toolchain setup failures are 125). A lost machine or broken sync stream before the guest action launches is retried once after a short backoff. Once the action launches it is never replayed: a broken stream reattaches to that process, and losing it is 137 because the command may have run even if its start marker was lost. Reattachment recovers admission from a per-action guest monotonic timestamp; it restores running and the original command deadline without treating marker-free setup exits as user exits. Setup diagnostics precede running; the command's own status remains its exit code. Cargo uses CARGO_INCREMENTAL=0 with sccache when a remote cache is available, and CARGO_PROFILE_DEV_DEBUG=0; --env overrides these defaults and an explicit compiler wrapper is preserved. --region REGION pins placement, not a persistent workspace. Automatic placement remains the Sandboxes API's responsibility. --queue-timeout DURATION (default 30m) is the longest wait for a machine. Keep the default budget for admission and cold boot; override it only when the caller needs a different wait limit. The budget covers creating the lease (or, with --no-tree, the Build) and the SDK's retries of retryable API answers while doing so, not just the wait for READY. Past it the run releases its lease and exits 125, retryable, with the server's own message when the API answered, so a caller can try another region. -o json writes NDJSON events (queued, running, sync, stdout, stderr, artifact, result); --dry-run prints what a cold run would send. Git sources include git ls-files (tracked and untracked, not ignored); ordinary directories include files under PATH, including hidden files, honoring nested .gitignore files. Optional .sylphxignore (Gitignore syntax) subtracts files from either source type; negations do not reinclude Git-ignored files. Git metadata and local .sylphx output/state are not uploaded from ordinary directories. Both source types use the same content hashing, CAS transfer, and lease execution path. For ordinary directories, stat and pack-action indexes live outside the source under the user cache directory, keyed by canonical PATH; unchanged inputs reuse digests and archives, with CAS presence checked each run. --region, --size, --env, --no-cache, and --no-tree remain optional advanced overrides. --source-history still requires actual Git history.

  • Ungranted Cargo git dependencies come with the run: each locked commit is fetched here with your git credentials (depth 1, no tags). Like Cargo's locked source cache, immutable URL-and-commit archives live in the existing per-source user cache ($XDG_CACHE_HOME/sylphx/build/sources, otherwise ~/.cache/sylphx/build/sources), outside the source Git directory. Fetches run in a fresh owned bare repository, never the caller's repository or shared history. Only the pinned commit and its tree travel, not unrelated refs, ancestor history, or Git credentials. The archive is uploaded to the cold lease (git_deps event) and installed by the guest action. Cargo's fetch reads those mirrors, with no git-host egress or credential on the machine. A lock that names the ssh:// spelling of a GitHub repository is retried over https:// when the machine has no ssh key for it, since the two spellings name the same repository. One that cannot be fetched here stops the run with 125 before the command starts. Git prints warning: rejected refs/commit/… because shallow roots are not allowed to be updated once per new commit; the build is unaffected.

  • Public dependency routing is supplied by the managed guest before any command or toolchain provisioning, independently of the compiler-cache token. No registry environment is required from the caller. npm/Bun, pip/uv and Go managed hints become native config defaults rather than registry environment overrides, so project config and explicit environment/CLI choices keep normal precedence. Existing user registry/index choices are preserved. Cargo CARGO_HOME and CARGO_TARGET_DIR overrides are retained. --no-cache skips compiler/result caches, not the guest dependency mirrors.

  • GitHub source reads are derived from the synced Git origin and locked Cargo GitHub sources. The CLI mints repository-scoped sessions through the public gateway using your Access/Broker grants; the accepted lease selects the reachable public or cluster route. Indexed Git config composes with dependency mirrors, and GitHub remains the push and review origin. --no-cache and explicit --env GIT_CONFIG_… source overrides skip automatic source routing. Without a source grant, private Cargo dependencies retain the existing shallow mirrors fetched with your local Git credentials and synced to the lease. No other identity is tried. Ungranted dependencies that cannot be fetched fail before the command starts; disabled source routing prepares them before leasing.

  • The run also gets the project's shared build cache (sccache, Turbo): the CLI mints a cache token with your key concurrently with admission. Its lifetime includes the queue budget, setup and command; if the gateway's lifetime cap removes admission headroom, it refreshes once at READY. It merges the cache's environment into the command's (your --env wins). If the cache cannot be reached the run builds without it and prints one sylphx: warning: build cache unavailable (…) line; --no-cache skips it. Cache-server startup is lazy: echo OK does not start it. Cargo receives native source replacement without a network preflight, so aliases and global options cannot bypass routing and version/help queries never probe a mirror. The first compile initializes sccache once across concurrent compiler processes; on outage the wrapper executes the compiler directly and prints one warning on an actual compile, not a metadata probe Cargo suppresses. An explicit RUSTC_WRAPPER, including an empty value, wins. Provisioning, offline git inputs, source history and credential permissions are one guest start action after source sync, through public Sandboxes APIs. The token is never printed, logged or put in an event.

  • sylphx build cache env [--project ID] [-o json] mints a read-only token (12 h) and prints export NAME='value' lines for a POSIX shell (plus export RUSTC_WRAPPER=sccache when sccache is installed and the variable is unset); -o json prints the environment as an object. The output holds a credential. Exit 0; 2 usage; 125 platform failure. The gateway is https://build-cache.sylphx.net, or SYLPHX_BUILD_CACHE_URL.

  • sylphx build source env OWNER/REPO [-o json] works before cloning: evaluate its shell exports, then clone the ordinary GitHub URL. It derives the default public Broker policy and gateway from the existing cache locator, with no infrastructure setting prerequisite. --project selects a project and --trust-policy is an optional advanced override; the linked/key project is the default. JSON output is an environment object. The short-lived source token travels in a path-scoped HTTP header, never a URL. Keep output out of logs and the synced tree. Exit 125 on missing sign-in or denied source authority. Build runs keep source bearer headers in a private lease-owned Git include file and renew through the existing client keepalive before provider expiry. Each Git invocation rereads the file; no caller Access key goes to the lease. Source credentials are removed before releasing the lease, and output redacts tokens even when a process splits them across output events. In CI, scripts/ci/setup-source.py configures the same Git environment before checkout using the job’s own GITHUB_TOKEN and id-token: write OIDC identity (audience sylphx-build-source), with tokens masked and runner env files private. The default CI/Verify entrypoints bootstrap it at the workflow’s signed revision before checkout, rather than installing source settings after the source fetch. Source sessions are bounded by the signed OIDC expiry. A private Git wrapper renews on network Git commands only when the session nears expiry, using fresh job OIDC and the same workflow credential. A renewal denial fails the fetch; it never switches identities or bypasses source routing. The helper/state live in private RUNNER_TEMP files, cleared by the Actions runner at job end. The private Cargo action uses its own existing job-scoped App token to mint additional repository sessions through that same public endpoint, bound to locked revisions when present. Those scoped sessions join the workflow repository’s Git environment and renew on their own repository’s fetch demand. A pinned secondary checkout uses its existing workflow credential only after provider read access and the same Access organization are verified. If initial source setup finds an absent endpoint (404), transport failure or server outage (5xx), it warns once and keeps the normal checkout/private-helper path using the same job’s credentials. Authentication denials (401/403) fail; an unavailable cache is never reported as a cache hit. BUILD_CACHE=false or an existing indexed Git config preserves advanced CI source overrides.

sylphx work runner

A self-hosted runner for Sylphx Work: it takes ready steps from your workspace and runs your command for each one on this machine, as GitHub's self-hosted runners take jobs.

sylphx work runner --label kind:build,member:group:builder -- ./run-step.sh   # one step, then exit (default)
sylphx work runner --loop --label kind:build,member:group:builder,cap:repo-write -- ./run-step.sh
  • It long-polls Work (POST /v2/claim with n: 1, its labels, its runner name and wait) for a ready step whose required labels --label covers. The runner (--name, default the host name) is catalogued on that claim, so the workspace's coverage counts it as a live runner for each member:<group> label it advertises.
  • The command gets the claim (item, checkpoint, context, profile, workspaceProfile, token) as JSON on standard input and in $SYLPHX_WORK_CLAIM_FILE, and the claim token, scoped to that one item, in $SYLPHX_WORK_CLAIM_TOKEN; $SYLPHX_WORK_URL, $SYLPHX_WORK_ITEM, $SYLPHX_WORK_GENERATION and $SYLPHX_WORK_RUNNER name the rest. SYLPHX_API_KEY is removed from its environment: the step acts with the claim token, never the runner's key. SYLPHX_CONFIG_DIR points to an empty per-step directory rather than the runner's saved login; it is removed after the step. Each beat writes the renewed token to $SYLPHX_WORK_CLAIM_TOKEN_FILE. Work names no harness or model: the command is yours.
  • While the command runs the runner beats the claim at half its time-to-live (--ttl, default the step kind's lease). A beat that answers cancel (dropped, reassigned or revoked) stops the command's process group (SIGTERM, SIGKILL after 10 s).
  • When the command ends the runner releases the step with a checkpoint that names its exit status, unless the command already handed it over with the claim token. With --ephemeral (the default) it then exits 0, whatever the command's status; --loop takes the next step. Ctrl-C stops the command, hands the step over and exits 130. -o json prints one JSON line per step (item, generation, exitCode, cancelled, handedOverBy).
  • Work's address is --work-url, else SYLPHX_WORK_URL, else https://work.sylphx.com; the key is your login, SYLPHX_API_KEY or --api-key.

sylphx token --scope <scope>

Prints one token and a newline on stdout, for tools that need a credential for a single scope (cargo, deploy scripts). Everything else goes to stderr; the exit code is non-zero, with a one-line reason, when you are not signed in, the login may not grant the scope, or the scope is not registered.

  • The token is a child Access key in your login's own org/project/env, with only that scope and a 15 minute life (label token:<scope>). Your login key is never printed. Scopes are the registered ones (packages:read, packages:publish, hosting:deploy, ai:inference, ...); an unregistered scope is refused.
  • It is cached in <config dir>/token-cache/ (mode 0600) per login and scope, and reused until 2 minutes before it expires, so a cargo build mints one key, not one per crate. sylphx logout clears the cache.
  • With SYLPHX_API_KEY (CI) it mints from that key the same way. A key that may not mint keys is printed as-is only when it carries the scope and expires within 60 minutes (the key a GitHub Actions OIDC exchange issues); otherwise the command refuses.
  • A login that may not grant the scope is refused; a per-scope step-up device login needs server support that is not there yet.
# .cargo/config.toml
[registries.example]
credential-provider = ["cargo:token-from-stdout", "sylphx", "token", "--scope", "packages:read"]

Use --scope packages:publish for publishing. Neither is covered by the *:read / *:write wildcards, so a login must hold them by name.

generated/commands.json is sylphx-gen output; never edit it.

Container image

clients/cli/Dockerfile builds the CLI into a distroless image (no shell, CA certificates, uid 65532, writable /tmp); the Release v2 apply Job runs sylphx apply ... from it inside tenant environments. The build context is the repository root: docker build -f clients/cli/Dockerfile ..

.github/workflows/cli-image.yml publishes it on every push to main that changes anything under clients/, as registry.sylphx.com/library/sylphx-cli:<commit sha>, and prints the digest reference in the run summary. Tenant namespaces pull it with the platform registry-pull-secret.

The release controller pins the image by digest through one environment variable, set in SylphxAI/infra (infra/addons/release-controller, the controller Deployment's env):

- name: SYLPHX_APPLY_CLI_IMAGE
  value: registry.sylphx.com/library/sylphx-cli@sha256:<digest from the run summary>