# Cuitty — full doc corpus > Generated 2026-06-16T16:30:53.308Z from 82 doc pages. Each section below begins with a `` marker so agents can split this corpus into individual pages without re-fetching. --- # Quickstart This guide walks you from a clean machine to a running Cuitty portal in under five minutes. By the end you will have: - The Cuitty portal at `http://localhost:7700` - Postgres + SpiceDB + libSQL running in containers - A first project, an API key, and your first event in the audit module ## Prerequisites - Docker 24 or later - Docker Compose v2 - 2 GB of free RAM - Ports `7700`, `5432`, and `50051` available ## 1. Clone the repository ```bash git clone https://gitlab.com/cuitty/root cuitty cd cuitty ``` ## 2. Bring up the stack ```bash docker compose up -d ``` Compose starts four services: | Service | Purpose | | ----------- | -------------------------------------- | | `portal` | The Astro + Bun portal on `:7700` | | `postgres` | BetterAuth + project metadata | | `spicedb` | Fine-grained RBAC | | `libsql` | Per-module event storage | Wait roughly twenty seconds for the health checks to pass. ## 3. Open the portal Visit `http://localhost:7700`. The first request runs the bootstrap flow: you create the root admin account and pick an organization name. There are no seed users. ## 4. Create a project and an API key From the portal, navigate to **Projects → New Project**, then **Settings → API Keys → Create**. Copy the key — you will not see it again. ## 5. Send your first event ```bash curl -X POST http://localhost:7700/api/ingest \ -H "Authorization: Bearer $CUITTY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "audit", "ts": "2026-04-27T12:00:00Z", "data": { "actor": "you@example.com", "action": "quickstart.complete", "resource": "cuitty" } } ] }' ``` You should see `{"accepted":1,"rejected":[]}`. Open the **Audit** module and your event is there. ## Where to go next - [Wire protocol reference](/docs/reference/wire-protocol) — the canonical contract for `/api/ingest` - [Install on Kubernetes](/docs/install/kubernetes) — production deployment - [TypeScript SDK](/docs/sdk/typescript) — `@cuitty/sdk` for Node and Bun - [Modules overview](/docs/modules/audit) — what each module captures --- # Install with Docker Compose Docker Compose is the fastest way to run Cuitty in production-like conditions on a single VM. The reference compose file lives at the root of the `cuitty` repo. ## Reference compose file ```yaml version: "3.9" services: postgres: image: postgres:16 environment: POSTGRES_USER: cuitty POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: cuitty volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U cuitty"] interval: 5s timeout: 3s retries: 12 spicedb: image: authzed/spicedb:latest command: serve --grpc-preshared-key ${SPICEDB_PRESHARED_KEY} ports: - "50051:50051" portal: image: ghcr.io/cuitty/portal:latest depends_on: postgres: { condition: service_healthy } spicedb: { condition: service_started } ports: - "7700:7700" environment: DATABASE_URL: postgres://cuitty:${POSTGRES_PASSWORD}@postgres:5432/cuitty SPICEDB_URL: spicedb:50051 SPICEDB_TOKEN: ${SPICEDB_PRESHARED_KEY} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} volumes: pgdata: ``` Create a `.env` file with `POSTGRES_PASSWORD`, `SPICEDB_PRESHARED_KEY`, and a 32-byte random `BETTER_AUTH_SECRET`. Then: ```bash docker compose up -d ``` ## Backups The portal stores nothing important on its own filesystem — every byte that matters lives in the `postgres` and `libsql` volumes. Snapshot the named volumes nightly with `pg_dump` and a `tar` of the libSQL data directory. ## Upgrades ```bash docker compose pull portal docker compose up -d portal ``` The portal runs migrations on startup. Always snapshot first. ## See also - [Install on Kubernetes](/docs/install/kubernetes) - [Install on bare metal](/docs/install/bare-metal) - [Install on cloud providers](/docs/install/cloud) --- # Install on Kubernetes The official Helm chart deploys Cuitty as a stateful application backed by an external Postgres and an in-cluster SpiceDB. ## Prerequisites - Kubernetes 1.27 or newer - Helm 3.13 or newer - An external Postgres 15+ (RDS, Cloud SQL, or self-managed) - A persistent volume class for libSQL data ## Install the chart ```bash helm repo add cuitty https://charts.cuitty.com helm repo update helm install cuitty cuitty/cuitty \ --namespace cuitty --create-namespace \ --set postgres.url=postgres://cuitty@db.example.com/cuitty \ --set auth.secretRef=cuitty-secrets ``` The chart provisions: - `Deployment` for the portal (3 replicas by default) - `StatefulSet` for SpiceDB - `PersistentVolumeClaim` for libSQL data - `Service` and optional `Ingress` - `NetworkPolicy` restricting egress to allowlisted destinations ## Values | Key | Default | Notes | | --------------------------- | ------------- | ------------------------------------------------------------- | | `replicaCount` | `3` | Portal HTTP replicas. | | `postgres.url` | required | Postgres DSN. Use a `Secret` and `valueFrom` in production. | | `spicedb.replicaCount` | `2` | SpiceDB replicas. Increase for higher RBAC throughput. | | `libsql.storage.size` | `20Gi` | Per-module event storage. | | `ingress.enabled` | `false` | Enable to expose `/` via your ingress controller. | | `image.tag` | chart version | Pin to a specific portal tag in production. | ## Upgrades Helm runs database migrations as a `Job` before swapping the new pod template. A failed migration aborts the upgrade with the previous version still serving. ```bash helm upgrade cuitty cuitty/cuitty --version 0.4.0 ``` --- # Install on bare metal Cuitty ships a single Bun-bundled binary suitable for running directly on a Linux VM without containers. This is the lowest-overhead deployment option. ## Prerequisites - Linux x86_64 or arm64 - Postgres 15+ (local or remote) - A SpiceDB binary on `PATH` - Bun 1.3+ (only required to rebuild from source) ## Download ```bash curl -fsSL https://releases.cuitty.com/portal/latest/portal-linux-x86_64 \ -o /usr/local/bin/cuitty-portal chmod +x /usr/local/bin/cuitty-portal ``` ## Configure Create `/etc/cuitty/portal.env`: ```dotenv DATABASE_URL=postgres://cuitty@localhost/cuitty SPICEDB_URL=localhost:50051 SPICEDB_TOKEN=... BETTER_AUTH_SECRET=... PORT=7700 DATA_DIR=/var/lib/cuitty ``` ## systemd unit `/etc/systemd/system/cuitty-portal.service`: ```ini [Unit] Description=Cuitty Portal After=network.target postgresql.service [Service] Type=simple EnvironmentFile=/etc/cuitty/portal.env ExecStart=/usr/local/bin/cuitty-portal Restart=on-failure User=cuitty Group=cuitty WorkingDirectory=/var/lib/cuitty [Install] WantedBy=multi-user.target ``` ```bash systemctl daemon-reload systemctl enable --now cuitty-portal ``` ## Reverse proxy Run Caddy or nginx in front of the portal for TLS termination. The portal expects to be reached over HTTPS in production for cookie-based auth to work correctly. --- # Install on cloud providers If you would rather not manage Postgres or SpiceDB yourself, the cloud install templates wire Cuitty to managed equivalents on your provider of choice. ## AWS The AWS reference uses: - **App Runner** for the portal container - **RDS Postgres 16** for auth metadata - **EFS** for libSQL data - **VPC peering** to your existing subnets A Terraform module is published at `https://gitlab.com/cuitty/terraform-aws-cuitty`. ```hcl module "cuitty" { source = "gitlab.com/cuitty/terraform-aws-cuitty" vpc_id = aws_vpc.main.id private_subnet_ids = aws_subnet.private[*].id domain = "cuitty.example.com" } ``` ## GCP The GCP reference uses Cloud Run, Cloud SQL, and Filestore. The Terraform module is at `https://gitlab.com/cuitty/terraform-gcp-cuitty`. ## Azure Container Apps + Azure Database for PostgreSQL. Terraform module at `https://gitlab.com/cuitty/terraform-azure-cuitty`. ## Cuitty Cloud If you want zero-ops, [Cuitty Cloud](/cloud) is the official managed offering on top of the same OSS core. --- Cuitty Code Apps are installable automation units that run against Cuitty Code resources. Apps are owned by a user or organization, published with a versioned manifest, installed into a target owner, and granted only the permissions declared by the app. Apps can target a user, organization, repository, registry namespace, or package. The target decides which SpiceDB relationships are written for the install and which resources the app can read, write, publish to, or operate. ## Required services - Cuitty Git API for app, install, run, registry, and callback APIs. - Cuitty Git frontend for market, install, and settings flows. - Cuitty Auth for browser and API authentication. - Database for app metadata, installs, grants, and run state. - SpiceDB for app visibility, install, grant, owner, and run authorization checks. - Airflow for production app execution. - Registry artifact storage when apps publish package or image artifacts. - Optional object storage for larger bundles, logs, and execution artifacts. ## Environment variables ```bash PUBLIC_API_URL=http://localhost:4351 PUBLIC_CUITTY_AUTH_URL=http://localhost:7705 AUTH_ISSUER=http://localhost:7705 AUTH_CLIENT_ID=cuitty-git CUITTY_PUBLIC_URL=http://localhost:4350 CUITTY_GIT_SECRET_KEY=dev-secret-change-me SPICEDB_ENDPOINT=http://localhost:50051 SPICEDB_PRESHARED_KEY=dev-secret AIRFLOW_URL=http://localhost:8080 AIRFLOW_USERNAME=airflow AIRFLOW_PASSWORD=airflow CUITTY_APP_EXECUTION_MODE=airflow CUITTY_APP_RUN_CALLBACK_SECRET=dev-callback-secret ``` Use deployment-specific secret management for database URLs, SpiceDB keys, Airflow credentials, registry credentials, and callback HMAC secrets. ## Permissions model The app listing controls discovery and installation. The installation controls runtime access. Manifest-declared permissions are the upper bound: installers can approve fewer grants, but the API rejects grants that the manifest did not declare. Public apps can be discovered by everyone, private apps are visible only to explicit owners or admins, and organization-visible apps are visible to members of the shared organization. Unauthorized callers may receive `404` when the platform must hide private app existence. ## Common workflows 1. Publish an app release from an immutable Git tag. 2. Review the manifest, docs path, version, requested permissions, and runtime. 3. Install the app to a user, organization, repository, registry namespace, or package target. 4. Run app actions through Airflow with a scoped installation token. 5. Upgrade only after reviewing permission changes. 6. Uninstall by revoking grants and disabling app-owned schedules. ## Failure modes and recovery - If SpiceDB is unavailable, security-expanding writes such as app publish and install should fail instead of creating unprotected resources. - If Airflow submission fails, the app run should record the error and remain failed or retryable. - If Airflow callbacks are missed, reconciliation should poll Airflow and update terminal run state. - If an install requests undeclared permissions, reject the install and leave the previous installation unchanged. ## Related pages - [Code App Market](/docs/code/app-market) - [App permissions](/docs/code/app-permissions) - [App execution with Airflow](/docs/code/app-execution-airflow) - [Authorization with SpiceDB](/docs/code/authz-spicedb) - [Operator runbook](/docs/code/operator-runbook) --- Apps are described by `.cuitty/app.toml` at the root of an app repository. ```toml [app] name = "deploy-guard" display_name = "Deploy Guard" slug = "deploy-guard" summary = "Require deploy evidence before production merges." owner = "acme" license = "Apache-2.0" docs = "README.md" visibility = "private" [app.source] repo = "acme/deploy-guard" ref = "v1.0.0" [[app.permissions]] resource = "repository" actions = ["read", "create_pr", "manage_branches"] reason = "Reads deployment policy and opens remediation pull requests." [app.runtime] kind = "airflow" dag = "code_app_run" dagger_module = "./dagger" entrypoint = "run" [app.market] categories = ["security", "deployment", "policy"] support_url = "https://code.cuitty.dev/acme/deploy-guard/issues" ``` Release manifests are tied to immutable Git tags or commit SHAs. Branch refs are not valid for market releases. --- Cuitty Code Apps use least-privilege grants. The app manifest declares requested resources, actions, and reasons. The installer approves those permissions for a specific target. At runtime, the app uses an installation token. Cuitty maps that token to the installation and checks the requested operation against SpiceDB before doing any work. ## Upgrade behavior When a new app version asks for more access, Cuitty shows a permission diff before upgrade. Existing installs do not silently receive new privileges. ## Denied operations If an app installed to one repository tries to publish a package or change an organization registry policy, the request is denied unless that grant was explicitly approved for the install. --- The Code App Market is the browser surface for discovering, installing, and managing Cuitty Code Apps. It uses the Cuitty Git API and the same owners, organizations, slugs, visibility semantics, and SpiceDB relationships as repositories and registry packages. ## Required services - Cuitty Git API and frontend. - Cuitty Auth. - Database for app listings, versions, installs, and profile docs. - SpiceDB for listing visibility, install, manage, and publish checks. - Airflow for app publishing and runtime workflows. - Object or registry storage when listings include bundles, artifacts, or package output. ## Environment variables ```bash PUBLIC_API_URL=http://localhost:4351 PUBLIC_CUITTY_AUTH_URL=http://localhost:7705 AUTH_ISSUER=http://localhost:7705 AUTH_CLIENT_ID=cuitty-git SPICEDB_ENDPOINT=http://localhost:50051 SPICEDB_PRESHARED_KEY=dev-secret AIRFLOW_URL=http://localhost:8080 CUITTY_APP_EXECUTION_MODE=airflow ``` ## Routes - `/market/apps` lists discoverable apps. - `/market/apps/:owner/:app` shows README, permissions, versions, and metadata. - `/market/apps/:owner/:app/install` reviews requested permissions and installs the app. - `/market/apps/:owner/:app/versions` lists published versions. - `/market/apps/:owner/:app/settings` manages owner-controlled metadata and visibility. ## Permissions model Apps can be public, private, or organization-visible. Public discovery does not automatically mean public install; app owners can still restrict who may install an app. Private and organization-visible apps should return not found to unauthorized users when hiding existence is required. Operators should verify that market search, detail, install, and settings routes all use the same authorization policy. ## Common workflows 1. Publish a listing from a repository release and `.cuitty/app.toml` manifest. 2. Show README or manifest docs from the configured docs path. 3. Review requested permissions before installation. 4. Install to the selected user, organization, repository, registry namespace, or package. 5. Manage versions, metadata, and visibility from the listing settings page. ## Failure modes and recovery - If manifest validation fails, leave the existing listing and latest stable version untouched. - If requested grants are outside the manifest, reject the install and show the API error. - If SpiceDB relationship writes fail, pause security-expanding market changes and replay the authz outbox. - If the market UI cannot reach the API, confirm `PUBLIC_API_URL` and frontend proxy configuration. ## E2E checks From the `tests` directory in the Cuitty Git repository: ```bash bun run test:e2e:apps-registry -- --project=chromium bun run test:e2e:apps-registry:headed ``` The mocked specs set local browser auth state and intercept API responses by API path, so both direct API calls on `http://localhost:4351/api/v1/...` and proxied frontend calls on `http://localhost:4350/api/v1/...` are covered. ## Related pages - [Cuitty Code Apps](/docs/code/apps) - [App Manifest](/docs/code/app-manifest) - [App permissions](/docs/code/app-permissions) - [Authorization with SpiceDB](/docs/code/authz-spicedb) - [Operator runbook](/docs/code/operator-runbook) --- Cuitty Git uses SpiceDB for relationship-based authorization across repositories, organizations, apps, installs, packages, and registry resources. The database remains the source of metadata, while SpiceDB answers permission checks. ## Required services - Cuitty Git API. - SpiceDB reachable from the API. - Database with the `authz_outbox` migration applied. - A worker path that drains relationship writes from the outbox. ## Environment variables ```bash SPICEDB_ENDPOINT=http://localhost:50051 SPICEDB_PRESHARED_KEY=dev-secret AUTH_ISSUER=http://localhost:7705 AUTH_CLIENT_ID=cuitty-git ``` Use TLS and managed secrets for production SpiceDB deployments. ## Schema and object types Load the schema before API writes: ```bash zed schema write spicedb/schema.zed ``` Representative object types include: - `user` - `organization` - `org_invite` - `account_slug` - `repository` - `app_listing` - `app_installation` - `code_app` - `code_app_installation` - `code_app_grant` - `registry_namespace` - `package` - `code_registry_namespace` - `code_package` Verify representative permissions with known local data: ```bash zed permission check user:alice read repository:acme/demo zed permission check user:alice view package:acme/npm/acme-widget zed permission check user:alice install app_listing:acme/review-bot ``` ## Permissions model Security-expanding writes, such as creating public visibility, granting install permission, or publishing package access, should fail closed if SpiceDB writes cannot be committed. Security-reducing writes, such as revoking access or making a package private, should continue to be retried until the graph reflects the database state. Private apps and packages may return `404` to callers without view permission. Authenticated callers with partial permission may receive `403` for actions such as publish, install, or settings updates. ## Outbox statuses Authorization relationship writes are recorded in `authz_outbox` with these statuses: - `pending`: queued for dispatch. - `processing`: claimed by a dispatcher. - `succeeded`: written to SpiceDB. - `failed_retryable`: failed but should be retried. - `failed_terminal`: failed in a way that needs operator inspection. Inspect stuck rows directly when operational tooling is not available: ```sql SELECT id, operation_kind, resource_kind, resource_id, status, attempts, last_error FROM authz_outbox WHERE status IN ('pending', 'processing', 'failed_retryable', 'failed_terminal') ORDER BY created_at ASC LIMIT 50; ``` ## Replay and reconciliation Replay retryable relationship writes with the deployment's authz dispatcher or maintenance command. If the deployment exposes only SQL access, mark stale `processing` rows back to `pending` after confirming no dispatcher is running, then restart the API or worker that drains the outbox. ```sql UPDATE authz_outbox SET status = 'pending' WHERE status = 'processing' AND updated_at < datetime('now', '-15 minutes'); ``` After replay, spot-check resource permissions with `zed permission check` and compare database visibility against SpiceDB relationships. ## Failure modes and recovery - SpiceDB unavailable: pause security-expanding app, package, and transfer writes; restore SpiceDB; drain the outbox. - Schema not loaded: re-run `zed schema write spicedb/schema.zed` and retry failed rows. - Retryable backlog: inspect `last_error`, fix connectivity or schema drift, and replay. - Terminal failures: compare `payload_json` with the current schema and repair the row or resource manually. ## Related pages - [Cuitty Code Apps](/docs/code/apps) - [Code Registry](/docs/code/registry) - [App execution with Airflow](/docs/code/app-execution-airflow) - [Operator runbook](/docs/code/operator-runbook) --- Production Code App runs are submitted to Airflow. Cuitty Git owns app metadata, installs, grants, and run state; Airflow owns task orchestration and execution logs. ## Required services - Cuitty Git API and database. - Airflow webserver and scheduler. - Cuitty Auth for actor identity. - SpiceDB for app install, run, and log-view permissions. - Optional object storage for app logs and execution artifacts. ## Environment variables ```bash AIRFLOW_URL=http://localhost:8080 AIRFLOW_USERNAME=airflow AIRFLOW_PASSWORD=airflow CUITTY_PUBLIC_URL=http://localhost:4350 CUITTY_APP_EXECUTION_MODE=airflow CUITTY_APP_RUN_CALLBACK_SECRET=dev-callback-secret ``` For local development without a live Airflow deployment, use simulation mode: ```bash CUITTY_APP_EXECUTION_MODE=simulate ``` When the auth issuer or Airflow URL points at local test hosts, Cuitty Git may also simulate Airflow automatically. ## DAG run payload Airflow receives a DAG run `conf` payload shaped like this: ```json { "run_id": "app-run-uuid", "run_kind": "manual", "app_id": "app-uuid", "app_version_id": "app-version-uuid", "installation_id": "install-uuid", "actor_user_id": "user-uuid", "owner": { "kind": "org", "slug": "acme" }, "input": { "event_target": "airflow:deploy_guard.pr_opened" }, "callback": { "url": "http://localhost:4350/api/v1/apps/runs/app-run-uuid/callback", "hmac_header": "x-cuitty-signature", "timestamp_header": "x-cuitty-timestamp" } } ``` The DAG ID can be provided in run input as `airflow_dag_id`, derived from an `event_target` that starts with `airflow:`, or derived from the app and run kind. ## Callback signing rules Airflow should call the callback URL when a run changes state. The request must include: - `x-cuitty-timestamp` as a Unix timestamp. - `x-cuitty-signature` as an HMAC SHA-256 signature. - A body containing the Cuitty app run ID, Airflow DAG ID, Airflow run ID, state, optional log URL, and optional error message. The signature is computed over `timestamp.body` with `CUITTY_APP_RUN_CALLBACK_SECRET`. Timestamps outside the accepted skew window are rejected, and terminal callbacks should be idempotent. ## Reconciliation behavior Callbacks are not the only source of truth. Cuitty Git should reconcile submitted and running app runs by polling Airflow for DAG run state. Missed callbacks, transient API failures, and worker restarts should eventually converge to `succeeded`, `failed`, or `canceled`. Trigger reconciliation through the API when needed: ```bash curl -X POST http://localhost:4351/api/v1/apps/runs/reconcile -H "Authorization: Bearer $CUITTY_TOKEN" ``` ## Permissions model Users must be allowed to view the app installation to see run state. Managing, running, canceling, or configuring app runs requires installation-level permissions. Airflow log URLs should be displayed only when the current user can view the run and the Airflow deployment exposes the log securely. ## Failure modes and recovery - DAG submission fails: record the error and leave the app run failed or retryable. - API accepts a run but loses the Airflow response: reconciliation should find the deterministic DAG run or fail after timeout. - Callback signature fails: reject the callback, check the shared secret, and rely on reconciliation. - Run stuck in `submitted` or `running`: poll Airflow, inspect task logs, and force reconciliation. - Cancellation requested: map to Airflow cancellation when supported, otherwise keep local state cancel-requested until reconciliation completes. ## Related pages - [Cuitty Code Apps](/docs/code/apps) - [Code App Market](/docs/code/app-market) - [Authorization with SpiceDB](/docs/code/authz-spicedb) - [Operator runbook](/docs/code/operator-runbook) --- Cuitty Code Registry stores packages and artifacts owned by Cuitty users and organizations. It uses the same users, organizations, slugs, teams, apps, audit logs, and SpiceDB permissions as repositories. Packages default to private. Moving a package to organization or public visibility requires an explicit Cuitty visibility change and audit event. ## Required services - Cuitty Git API for package metadata and protocol endpoints. - Database for package records, versions, dist tags, visibility, ownership, and artifact references. - SpiceDB for namespace, package, publish, install, and view permissions. - Registry artifact storage for tarballs, crates, wheels, and image layers. - Airflow for publish validation, scans, provenance, index updates, and replication when enabled. - Optional object storage for larger deployments. ## Environment variables ```bash PUBLIC_API_URL=http://localhost:4351 AUTH_ISSUER=http://localhost:7705 AUTH_CLIENT_ID=cuitty-git SPICEDB_ENDPOINT=http://localhost:50051 SPICEDB_PRESHARED_KEY=dev-secret AIRFLOW_URL=http://localhost:8080 REGISTRY_STORAGE_URL=file:///var/lib/cuitty/registry ``` ## Permissions model Registry namespaces and packages have separate relationships. Namespace owners can administer the namespace and publish packages. Package owners and package maintainers can manage package metadata and versions. Public and organization visibility map to explicit SpiceDB relationships, and unauthorized callers may receive `404` for private packages. ## npm Use the npm endpoint for scoped and unscoped package metadata, publish, and tarball download: ```bash npm config set @acme:registry http://localhost:4351/api/v1/npm/ npm login --registry http://localhost:4351/api/v1/npm/ npm publish --registry http://localhost:4351/api/v1/npm/ npm install @acme/button --registry http://localhost:4351/api/v1/npm/ ``` For scoped packages, the npm scope must match the Cuitty owner namespace. For example, `@acme/button` must publish under the `acme` owner. ## Cargo Configure a sparse registry: ```toml [registries.cuitty] index = "sparse+http://localhost:4351/api/v1/cargo/index/" ``` Then publish or install with: ```bash cargo publish --registry cuitty cargo add package-name --registry cuitty ``` ## PyPI Upload with Twine and install from the simple index: ```bash twine upload --repository-url http://localhost:4351/api/v1/pypi/ dist/* pip install --index-url http://localhost:4351/api/v1/pypi/simple/ package-name ``` Package names should be normalized consistently between upload metadata, simple index pages, and install URLs. ## Docker and OCI Push images through the registry host: ```bash docker login localhost:4351 docker tag image localhost:4351/acme/image:tag docker push localhost:4351/acme/image:tag docker pull localhost:4351/acme/image:tag ``` Operators should decide whether tags are mutable, immutable, or protected by policy before opening production pushes. ## Common workflows 1. Create or claim a registry namespace for a user or organization. 2. Publish a package version through the native package manager. 3. Store package metadata, files, checksums, and visibility. 4. Update protocol indexes or tags. 5. Install through the native package manager using the same registry URL. ## Failure modes and recovery - `404` for a private package can be intentional when the caller lacks view permission. - `403` means the package exists but the caller is authenticated without the required permission. - Artifact storage failures can leave metadata and files out of sync; check API logs, package version records, and storage references before allowing new publishes. - Scope mismatch errors mean the package name does not match the target Cuitty owner namespace. ## Related pages - [npm Packages](/docs/code/npm) - [Cargo Crates](/docs/code/cargo) - [PyPI Packages](/docs/code/pypi) - [OCI and Docker Images](/docs/code/oci) - [Authorization with SpiceDB](/docs/code/authz-spicedb) - [Operator runbook](/docs/code/operator-runbook) --- Cuitty Code Registry supports npm-compatible publishing and install flows. Scoped package names must match the Cuitty owner slug or an approved alias. For example, `@acme/button` can only be published by the `acme` owner namespace or by an actor granted access to that namespace. `publishConfig.access = "public"` is not enough to make a package public. Public publishing requires a Cuitty permission and explicit visibility change. ```bash npm publish --registry https://registry.cuitty.dev/npm npm install @acme/button --registry https://registry.cuitty.dev/npm ``` Unscoped packages are allowed under a Cuitty namespace and default to private. --- Cuitty Code Registry supports Cargo-compatible crate publishing and installation. Cargo crate ownership is enforced through the Cuitty registry namespace and package owner records. Cargo indexes are Git-backed where compatibility or auditability requires it, with sparse index support for modern clients. ```bash cargo publish --registry cuitty cargo add my-crate --registry cuitty ``` Yank and unyank operations require package maintainer permissions and are recorded in the audit log. --- OCI coordinates map directly to Cuitty owner namespaces. ```text registry.cuitty.dev/acme/api:1.2.0 registry.cuitty.dev/acme/platform/worker:sha-abc123 ``` The first path segment after the host is the owner slug. Push requires publish permission on that owner namespace or package. Pull requires install/read permission. ```bash docker login registry.cuitty.dev docker push registry.cuitty.dev/acme/api:1.2.0 docker pull registry.cuitty.dev/acme/api:1.2.0 ``` Semver tags are immutable by default unless the owner policy allows mutation. --- Cuitty Code Registry supports PyPI-compatible upload and Simple API installation. Python package names are normalized before storage, and owner namespace is tracked separately from the package name. This prevents confusing duplicates while still allowing organization-scoped ownership. ```bash python -m twine upload --repository cuitty dist/* pip install --index-url https://registry.cuitty.dev/pypi/simple my-package ``` Yanking a version hides it from normal resolution without deleting the file by default. --- Organizations own repositories, apps, registry namespaces, and packages. Each organization has a globally unique slug. Org admins can invite users by username or email, assign teams, and grant initial repository, app, or registry roles. Invite acceptance checks that the inviter still has membership-management permission at the time of acceptance. Apps and packages can be shared with organizations through explicit grants or organization visibility. Cuitty writes SpiceDB relationships for those grants instead of inferring access from matching names. --- Users and organizations have globally unique slugs. Slugs identify owner namespaces for repositories, apps, registry namespaces, and packages. Profile pages can render custom markdown. When no explicit profile document is configured, Cuitty falls back to a generated profile view. Profiles may show pinned repositories, published apps, published packages, followers, public organization memberships, and activity the viewer is allowed to see. Slug history is retained for redirects and anti-squatting. --- Resource ownership can move from a user to an organization, from an organization to a user, or between organizations. Transfers check that the current owner can transfer and that the target can accept. Cross-account transfers require target acceptance unless an org admin is moving resources inside the same organization. Package ecosystems handle coordinates differently. npm scoped packages may need a new scope or alias. OCI paths can redirect if policy allows. Cargo and PyPI packages keep their package names while updating owner metadata. --- Self-hosted Cuitty Code Registry uses the same APIs and permission model as Cuitty Cloud. Required services: - Cuitty Code server for API, Git protocol, market, and registry metadata. - SpiceDB for authorization. - Airflow for app install/run, package publish, scans, reindexing, replication, and transfers. - Object storage for package blobs, OCI layers, wheels, tarballs, SBOMs, and attestations. - Git storage for repositories, app source, Cargo indexes, and docs. Air-gapped deployments can use manual sync bundles instead of public network replication. ## Operations Use the [Code Apps and Registry operator runbook](/docs/code/operator-runbook) for startup order, migrations, SpiceDB schema loading, Airflow reconciliation, registry smoke checks, and recovery steps. --- This runbook covers local and self-hosted operation for Code Apps, App Market, Code Registry, SpiceDB authorization, and Airflow execution. ## Required services | Service | Purpose | Local default | | --- | --- | --- | | Cuitty Git API | REST API, git HTTP, app, registry, and callback endpoints | `http://localhost:4351` | | Cuitty Git frontend | Browser UI | `http://localhost:4350` | | Cuitty Auth | OAuth/OIDC sign-in | `http://localhost:7705` | | Database | Persistent metadata and run state | Deployment-specific | | SpiceDB | Relationship authorization | `http://localhost:50051` | | Airflow | App run orchestration | `http://localhost:8080` | | Registry artifact storage | Package tarballs, crates, wheels, and image layers | Deployment-specific | | Object storage | Optional backing storage for large artifacts and logs | Deployment-specific | ## Startup order 1. Start the database. 2. Start SpiceDB. 3. Load the SpiceDB schema. 4. Start Cuitty Auth. 5. Start Airflow. 6. Run Cuitty Git API migrations, then start the API. 7. Start the Cuitty Git frontend. ## Environment variables ```bash PUBLIC_API_URL=http://localhost:4351 PUBLIC_CUITTY_AUTH_URL=http://localhost:7705 AUTH_ISSUER=http://localhost:7705 AUTH_CLIENT_ID=cuitty-git CUITTY_PUBLIC_URL=http://localhost:4350 CUITTY_GIT_SECRET_KEY=dev-secret-change-me SPICEDB_ENDPOINT=http://localhost:50051 SPICEDB_PRESHARED_KEY=dev-secret AIRFLOW_URL=http://localhost:8080 AIRFLOW_USERNAME=airflow AIRFLOW_PASSWORD=airflow CUITTY_APP_EXECUTION_MODE=airflow CUITTY_APP_RUN_CALLBACK_SECRET=dev-callback-secret REGISTRY_STORAGE_URL=file:///var/lib/cuitty/registry ``` ## Migrations Run migrations before traffic reaches a new API build: ```bash cargo run -p git-server -- migrate ``` Verify the API starts with the expected database URL and no pending migration errors. ## SpiceDB checks ```bash zed schema write spicedb/schema.zed zed permission check user:alice read repository:acme/demo zed permission check user:alice view package:acme/npm/acme-widget zed permission check user:alice install app_listing:acme/review-bot ``` Inspect failed authorization relationship writes: ```sql SELECT id, operation_kind, resource_kind, resource_id, status, attempts, last_error FROM authz_outbox WHERE status IN ('pending', 'processing', 'failed_retryable', 'failed_terminal') ORDER BY created_at ASC LIMIT 50; ``` ## Registry client smoke checks ```bash npm config set @acme:registry http://localhost:4351/api/v1/npm/ npm publish --registry http://localhost:4351/api/v1/npm/ npm install @acme/button --registry http://localhost:4351/api/v1/npm/ ``` ```toml [registries.cuitty] index = "sparse+http://localhost:4351/api/v1/cargo/index/" ``` ```bash cargo publish --registry cuitty twine upload --repository-url http://localhost:4351/api/v1/pypi/ dist/* docker push localhost:4351/acme/image:tag ``` ## Airflow checks Submit or trigger an app run, then confirm the run has an Airflow DAG ID and DAG run ID. Reconcile submitted runs when callbacks are delayed: ```bash curl -X POST http://localhost:4351/api/v1/apps/runs/reconcile -H "Authorization: Bearer $CUITTY_TOKEN" ``` Callback failures usually mean the timestamp is stale, the `x-cuitty-signature` HMAC is wrong, or Airflow is signing with a different secret than `CUITTY_APP_RUN_CALLBACK_SECRET`. ## Playwright E2E From the Cuitty Git `tests` directory, run the mocked market, registry, and social specs: ```bash bun run test:e2e:apps-registry -- --project=chromium bun run test:e2e:apps-registry:headed ``` The specs set browser auth state and mock API responses by path, so they cover both direct API calls on `http://localhost:4351/api/v1/...` and frontend-proxied calls on `http://localhost:4350/api/v1/...`. ## Failure modes and recovery - SpiceDB unavailable: pause security-expanding app, registry, and transfer writes; restore SpiceDB; drain the authz outbox. - Authz outbox backlog: inspect `last_error`, reset stale `processing` rows to `pending` only after confirming no dispatcher is active, and restart the dispatcher. - Airflow DAG submission failure: check `AIRFLOW_URL`, credentials, DAG ID derivation, API logs, and Airflow scheduler/webserver logs. - App run stuck in `submitted` or `running`: poll Airflow for the DAG run ID, check task logs, verify callback delivery, and run reconciliation. - npm publish succeeds but artifact is missing: check package version records, artifact storage, API logs, and upload temp directories. - Private package returns `404` versus `403`: `404` can be correct when the caller lacks view permission; `403` means the caller can see the resource but lacks the requested operation permission. ## Related pages - [Self-Hosted Registry](/docs/code/self-hosted-registry) - [Cuitty Code Apps](/docs/code/apps) - [Code App Market](/docs/code/app-market) - [Code Registry](/docs/code/registry) - [Authorization with SpiceDB](/docs/code/authz-spicedb) - [App execution with Airflow](/docs/code/app-execution-airflow) --- # Safe Quickstart Cuitty Safe lets you commit references such as `cuitty-safe:acme/dev/database-url` instead of raw secret values. The resolver injects the real value only at the runtime boundary. ## Install Repository contributors should route package changes through Socket Firewall and pin exact versions. ```bash sfw bun add @cuitty/safe@0.1.0 ``` The CLI binary target is `cuitty-safe`. Some Cuitty installs may also expose it as `cui safe`. ## Create a local Safe Local vault mode is the default first-run mode. ```bash cuitty-safe init cuitty-safe safe create acme/dev --provider local cuitty-safe put acme/dev/database-url --value "$DATABASE_URL" cuitty-safe put acme/ci/github-token --value "$GITHUB_TOKEN" ``` The values above come from your current shell. Do not paste real values into committed docs, scripts, or examples. ## Replace env values with references ```dotenv DATABASE_URL=cuitty-safe:acme/dev/database-url GITHUB_TOKEN=cuitty-safe:acme/ci/github-token ``` Bare paths are valid only when Cuitty Safe is the active resolver: ```dotenv DATABASE_URL=acme/dev/database-url ``` ## Run with resolved env ```bash cuitty-safe run --env-file .env -- bun run dev ``` `run` resolves references, injects values into the child process environment, and keeps terminal output masked by default. ## Use package scripts ```jsonc { "scripts": { "dev": "cuitty-safe run --env-file .env -- vite", "deploy": "cuitty-safe run --scope acme/prod -- bun run deploy:raw" }, "cuittySafe": { "env": { "DATABASE_URL": "acme/dev/database-url", "GITHUB_TOKEN": "acme/ci/github-token" } } } ``` Prefer `cuitty-safe run` over shell command substitution. Substitution can leak sensitive values into shell history, process listings, or logs. ## Next steps - [Reference syntax](/docs/safe/references) - [Storage modes](/docs/safe/storage-modes) - [CI/CD](/docs/safe/ci-cd) - [Security model](/docs/safe/security) --- # Safe References Every Cuitty Safe reference normalizes to: ```text account/safe/secret ``` `account` is the namespace, `safe` is the Safe name, and `secret` is the remaining logical key path. ## Accepted syntaxes ```dotenv # Bare path, valid when Cuitty Safe is the active resolver. DATABASE_URL=acme/dev/database-url # Explicit scheme, preferred for mixed env files. DATABASE_URL=cuitty-safe:acme/dev/database-url # URL-style scheme for tools that require URI-looking values. DATABASE_URL=cuitty-safe://acme/dev/database-url # Inline encrypted literal. This is ciphertext, not a pointer. DATABASE_URL=csafe:v1:aes-256-gcm:kid_localdev:base64url-nonce:base64url-ciphertext ``` ## Parsing rules - `account` and `safe` are required and cannot contain `/`. - `secret` is required and may contain additional `/` characters. - References are case-sensitive. - Tools should URL-encode reserved characters. - The resolver should warn when a configured backend is case-insensitive. - Decrypted values must not appear in URLs, logs, trace spans, crash reports, docs examples, or generated fixtures. ## Resolver strictness ```ts type SafeReferenceMode = "explicit-only" | "known-env-vars" | "all-values"; ``` Defaults: - `.env` and `.env.*` use `explicit-only`, plus `known-env-vars` when a `cuittySafe.env` map exists. - `package.json` resolves only `cuittySafe.env` and explicit `cuitty-safe:` values. - Shell scripts resolve only explicit CLI calls or explicit `cuitty-safe:` values. - CI/CD resolves explicit references only. ## Source-of-truth index Cuitty Safe tracks where each Safe lives in metadata-only indexes: ```text ~/.cuitty/safe/index.json .cuitty/safe/index.json ``` The index stores provider, status, key fingerprint, connector version, source pointer, and sharing stub metadata. It never stores secret values. --- # Safe Storage Modes Each Safe chooses one source of truth. The reference path stays stable even when the backing provider changes. | Mode | Source of truth | Best fit | | --- | --- | --- | | Local vault | Encrypted local Safe database | First-run development, locked workstations, local CI workers | | Inline encrypted text | Ciphertext in the referencing file | Portable fixtures and small local-only handoffs | | Persist adapter | Cuitty Persist `secret` storage class with E2EE | Local or remote sync experiments that accept alpha guardrails | | 1Password | 1Password vault/item/field | Team production credentials and headless CI service accounts | ## Local vault Local vaults are the default for first-run development. ```text ~/.cuitty/safe/vaults///safe.db ~/.cuitty/safe/vaults///safe.audit.jsonl ``` Key material should live in the OS credential store when available: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Headless passphrase-derived keys require explicit opt-in. ## Inline encrypted text Inline mode stores authenticated ciphertext next to the config that needs it: ```dotenv STRIPE_SECRET=csafe:v1:aes-256-gcm:kid_localdev:base64url-nonce:base64url-ciphertext ``` Inline values must use authenticated encryption, include versioned envelope metadata, and keep decryption keys outside the file. ## Persist adapter Persist mode stores Safe records as E2EE payloads in Cuitty Persist. ```ts import { createSafePersistAdapter } from "@cuitty/safe/server"; const adapter = createSafePersistAdapter({ profile: "local-first-laptop", account: "acme", safe: "dev", namespace: "safe/acme/dev", storageClass: "secret", syncMode: "manual", alpha: true, acknowledgeE2eeRisk: true, }); ``` The Persist adapter is alpha until Persist's E2EE profile runtime reaches GA security review, recovery testing, and multi-device revocation testing. For high-value production CI/CD credentials, prefer 1Password service accounts or a locked local vault until that review is complete. ## 1Password pass-through 1Password mode keeps 1Password as the source of truth. Cuitty Safe stores metadata that maps a Cuitty ref to a 1Password object. ```text acme/dev/database-url -> op://Development/Database/url ``` Users do not need to commit `op://...` references unless they choose the advanced escape hatch. --- # 1Password Connector The 1Password connector is a pass-through provider. Cuitty Safe stores the Safe index and mapping metadata; 1Password stores the secret value. ## Mapping model ```text acme/dev/database-url -> op://Development/Database/url ``` The Cuitty reference remains `account/safe/secret`. Vault names, item titles, field names, and stable 1Password object IDs belong in connector metadata. ## Auth modes | Mode | Use case | | --- | --- | | Desktop app | Local development and desktop UI flows with human approval | | Service account | CI/CD, headless workers, and automation with scoped vault access | | 1Password Connect | Self-hosted deployments that already run Connect and want cached private REST access | Service account auth should load from `OP_SERVICE_ACCOUNT_TOKEN` unless configured otherwise. Do not ask users to paste a 1Password account password into Cuitty Safe. ## Setup sketch ```bash cuitty-safe connector 1password login --account acme cuitty-safe connector 1password map acme/dev/database-url --op-ref op://Development/Database/url cuitty-safe connector 1password map acme/ci/github-token --op-ref op://CI/GitHub/token cuitty-safe connector 1password test --scope acme/dev ``` The `op://...` examples above are references, not values. ## Least privilege - Grant service accounts access only to required vaults and fields. - Prefer read-only permission for CI jobs that only resolve values. - Store stable object IDs when available and keep display names for UI fallback. - Cache metadata, not secret values. Secret value TTL defaults to `0`. - Emit metadata-only audit events for reads, writes, and auth-required states. ## Marketplace status The first milestone is a product connector. A 1Password Marketplace listing is a separate partner-readiness track with its own review, docs, support link, logo, and security checklist. --- # Persist Adapter The Persist adapter stores Safe records in Cuitty Persist's `secret` storage class with end-to-end encryption. > Alpha caveat: Cuitty Safe's Persist adapter is alpha until Persist's E2EE profile runtime reaches GA. Use it sparingly for high-value production credentials. Prefer 1Password service accounts or a locked local vault for production CI/CD until GA security review, recovery testing, and multi-device revocation testing are complete. ## Configuration ```ts import { createSafePersistAdapter } from "@cuitty/safe/server"; const adapter = createSafePersistAdapter({ profile: "local-first-laptop", account: "acme", safe: "dev", namespace: "safe/acme/dev", storageClass: "secret", syncMode: "manual", alpha: true, acknowledgeE2eeRisk: true, }); ``` ## Guardrails - `alpha: true` and `acknowledgeE2eeRisk: true` are required. - Manual sync is the default. - Remote writes are blocked unless encryption is `required`. - Remote Persist targets must show a visible warning in UI and CLI output. - Audit metadata includes device id, key fingerprint, profile id, provider, result, and timestamp. - Persist access controls are not the only boundary. Safe encryption remains mandatory. ## Record shape Persist Safe records contain encrypted envelopes and metadata: ```ts interface PersistSafeSecretRecord { id: string; ref: string; account: string; safe: string; secret: string; envelope: SafeEncryptedEnvelope; metadata: SafeSecretMetadata; createdAt: string; updatedAt: string; deletedAt?: string; } ``` List operations return metadata only. Read operations require explicit resolution and must redact values from errors and logs. --- # Safe CI/CD CI/CD should use explicit Safe references only. Resolve values at the job boundary, inject them into the child process, and install redaction rules before the process starts. ## Package script pattern ```jsonc { "scripts": { "deploy": "cuitty-safe run --scope acme/prod -- bun run deploy:raw", "registry:publish": "cuitty-safe run --scope acme/registry -- bun publish" }, "cuittySafe": { "env": { "DATABASE_URL": "acme/prod/database-url", "REGISTRY_TOKEN": "acme/registry/publish-token" } } } ``` ## Job flow 1. Load job configuration. 2. Build the Safe reference set. 3. Request a Safe run plan. 4. Resolve values from the configured provider. 5. Inject resolved env only into the child process. 6. Install redaction rules for logs. 7. Emit audit metadata without values. ## Service accounts For 1Password-backed production CI, use a least-privilege service account scoped to the required vault and fields. Cuitty Safe should read the token from the CI provider's secret store, commonly exposed as `OP_SERVICE_ACCOUNT_TOKEN`. ```bash cuitty-safe connector 1password test --scope acme/prod cuitty-safe run --scope acme/prod -- bun run deploy:raw ``` ## Cuitty Code and Registry Cuitty Code can resolve app manifest env, runner env, mirror credentials, webhook signing secrets, OAuth app credentials, deployment keys, Airflow tokens, and Dagger tokens. Cuitty Registry can resolve npm publish tokens, Cargo registry tokens, PyPI upload tokens, OCI credentials, artifact storage DSNs, signing keys, and provenance keys. Do not resolve Safe refs inside tight package download paths unless explicitly configured. --- # Safe SDK Safe follows the Cuitty SDK initiative: one package per product, with subpath exports for each runtime surface. ```text @cuitty/safe @cuitty/safe/client @cuitty/safe/server @cuitty/safe/types @cuitty/safe/react @cuitty/safe/solid @cuitty/safe/dsl @cuitty/safe/connectors @cuitty/safe/crypto @cuitty/safe/css ``` Do not create dash packages such as `@cuitty/safe-client`. ## Client half The client is a typed HTTP consumer generated from the root wire protocol. ```ts import { createSafeClient } from "@cuitty/safe/client"; const safe = createSafeClient({ baseUrl: "http://localhost:4361", auth, }); await safe.resolve("acme/dev/database-url"); await safe.secrets.put("acme/dev/github-token", { value: tokenFromShell }); await safe.safes.create({ account: "acme", safe: "dev", provider: "local" }); ``` Use `POST /api/safe/resolve` for resolution so references and values do not end up in URL paths or access logs. ## Server half The server half owns product logic and can be embedded instead of calling REST routes. ```ts import { createSafeServer } from "@cuitty/safe/server"; const safe = await createSafeServer({ indexPath: ".cuitty/safe/index.json", }); await safe.resolve("acme/dev/database-url"); ``` Provider implementations must redact values before throwing or logging. ## Core integration `@cuitty/core` remains a thin aggregator. It may lazy-load Safe and pass shared auth, transport, endpoint discovery, and mock mode into `createSafeClient`, but it must not implement resolving, crypto, provider mapping, 1Password auth, or Persist writes. ```ts const cuitty = createCuitty({ products: ["safe"], endpoints: { safe: "http://localhost:4361" }, auth, }); await cuitty.safe?.resolve("acme/dev/database-url"); ``` ## Framework subpaths React and Solid components should be functionally equivalent. Components may display refs, provider status, audit metadata, scan findings, and connector health. They must not receive decrypted values unless the user explicitly enters reveal mode. --- # Safe Desktop The Safe desktop app is the local control surface for vaults, connector setup, reveal workflows, and audit metadata. Suggested local ports: ```text Safe web app: http://localhost:4360 Safe API: http://localhost:4361 ``` ## Views - Safe index grouped by account. - Safe detail for `account/safe`. - 1Password connector setup. - Persist adapter setup with alpha acknowledgement. - Local vault lock and unlock. - Inline encrypted envelope builder. - Scan findings and migration suggestions. - Metadata-only audit timeline. - Sharing stub panel. ## Reveal behavior Secret values are hidden by default. Reveal mode must: - Require an explicit reveal action. - Show a countdown. - Record a metadata-only audit event. - Clear the value on timeout, blur, route change, or vault lock. - Keep copy reference separate from copy value. ## Native responsibilities The desktop backend may use a Rust crate for native keychain access, file locking, Tauri commands, local vault process isolation, and future secure enclave or keyring integrations. ## Sharing stub Sharing is reserved for a future Cuitty Auth grants integration. v0 can preview draft metadata, but it must not send invites, create cross-user decrypt grants, or expose invite links. --- # Safe Security Cuitty Safe treats references as safe to commit and values as sensitive at every boundary. ## Value handling - Never store decrypted values in Cuitty Code, Registry, Site, logs, wire-protocol fixtures, desktop snapshots, or docs examples. - Never place decrypted values in URLs, trace spans, audit payloads, crash reports, or generated config unless the user explicitly requested an output file. - `read` prints to stdout and must be treated as sensitive. - Terminal masking defaults to enabled. - Errors must redact values before leaving provider code. ## Encryption Inline encrypted values must use authenticated encryption, not reversible obfuscation. ```dotenv INLINE_ONLY=csafe:v1:aes-256-gcm:kid_localdev:base64url-nonce:base64url-ciphertext ``` Envelope metadata should include version, algorithm, key id, nonce, ciphertext, and optional authenticated data. Authenticated data should include the `account/safe/secret` ref, file path when available, and configured purpose. ## Local vaults Local vaults store encrypted records and keep key material in the OS credential store when available. Headless passphrase fallback requires explicit opt-in. Vaults should lock after inactivity. ## Connector safety 1Password connector metadata may include vault names, item titles, field names, and object IDs. It must not cache secret values by default. Persist-backed Safes require E2EE and alpha acknowledgement. Remote writes are blocked unless encryption is required. ## Audit Audit events are metadata-only: ```text reference, actor, process, provider, result, timestamp ``` Secret values are never audit fields. Reference hashing may be enabled where secret names are themselves sensitive. ## Sharing Sharing is a v0 stub. Future sharing is planned for Cuitty Auth grants, but v0 does not create invite links, cross-user decrypt grants, or direct provider token sharing. --- # Pilot Quickstart Cuitty Pilot records browser workflows against provider dashboards (AWS, Cloudflare, Vercel, etc.) and replays them on demand. If a provider changes their UI, Pilot's AI healing rewrites selectors so your automation keeps working. This guide walks through recording, replaying, and reviewing your first workflow. ## Install the CLI ```bash curl -fsSL https://cuitty.com/install.sh | sh cui --version ``` Or install with npm: ```bash npm install -g @cuitty/cli ``` Pilot uses Playwright under the hood. The first run will prompt you to install browser dependencies if they are missing. ## Record a workflow Start a recording session against the Cloudflare dashboard. Pilot opens a browser window and captures every click, navigation, and form input: ```bash cui pilot record \ --name "add-dns-record" \ --provider cloudflare \ --start-url "https://dash.cloudflare.com" ``` Walk through the steps manually: log in, navigate to DNS, add an A record, save. When you are done, press `Ctrl+C` in the terminal. Pilot writes a playbook file to `.cuitty/playbooks/add-dns-record.yaml`. ## Replay the workflow Run the recorded playbook headlessly: ```bash cui pilot replay add-dns-record --headless ``` Pilot replays each step, waits for network idle between navigations, and logs a pass/fail for every assertion checkpoint. Add `--headed` to watch the browser in real time. ## Review a failed run If a replay fails (e.g., Cloudflare moved a button), inspect the run: ```bash cui pilot review add-dns-record --run latest ``` The review command prints a step-by-step diff showing which selector broke, a screenshot of the page at the failure point, and the suggested AI-healed selector. Accept the fix with: ```bash cui pilot review add-dns-record --run latest --accept ``` This patches the playbook so future replays use the corrected selector. ## AI healing basics Pilot captures a DOM snapshot at every step during recording. When a selector breaks during replay, the AI healing engine compares the stored snapshot to the current page and proposes the best replacement selector. Healing runs locally by default; set `CUITTY_AI_KEY` to use the cloud model for higher accuracy on complex UI changes. ## What's next - [Playbook reference](/docs/pilot/playbook-reference) — full YAML schema for steps, inputs, and assertions - [Executors](/docs/pilot/executors) — Browser, Terraform, Git, Persist, Shell, HTTP, and Composite - [JavaScript SDK](/docs/pilot/sdk/javascript) — `@cuitty/pilot-sdk` for TypeScript and Node.js - [Recording tips](/product/pilot#recording) — best practices for stable, replayable workflows --- # Playbook Reference A playbook is a YAML document that describes an automated workflow. Pilot validates every playbook against a Zod schema before execution. ## Top-level structure ```yaml playbook: id: # e.g. cloudflare.dns.add-a-record version: 1 # integer, incremented on each edit target: # cloudflare, aws, vercel, etc. title: Human-readable title description: Optional longer description executor: browser # default executor for all steps (optional) executor_config: {} # executor-specific config map (optional) inputs: [] # input definitions outputs: [] # output definitions preconditions: [] # conditions checked before run starts steps: [] # ordered list of steps (min 1) ``` Required fields: `id` (dotted lowercase string), `version` (integer), `target`, `title`, `steps` (min 1). Optional: `description`, `executor` (default `browser`), `executor_config`, `inputs`, `outputs`, `preconditions`. ## Inputs Each input defines a variable the caller must supply. Referenced in steps as `{{name}}`. ```yaml inputs: - name: zone type: string required: true - name: proxied type: boolean default: true ``` Fields: `name` (pattern `^[a-z_][a-z0-9_]*$`), `type` (one of `string`, `number`, `boolean`, `ipv4`, `ipv6`, `url`, `email`, `secret`), `required` (default `true`), `default`, `description`. ## Outputs Outputs reference values captured during the run: `{ name, type, source }` where `source` is e.g. `capture.record_id`. ## Steps Each step is an atomic action. Steps run in order unless the composite executor orchestrates them differently. ```yaml steps: - id: goto-dns action: navigate executor: browser # override playbook-level executor url: "https://example.com" timeout_ms: 15000 ai_fallback: true destructive: false breakpoint: false intent: Human-readable goal for this step. observe: [text, a11y, screenshot] ``` Required: `id` (pattern `^[a-z][a-z0-9-]*$`, unique within playbook), `action`. Optional: `executor` (overrides playbook default), `selector`, `url`, `value`, `timeout_ms` (default 15000), `ai_fallback` (default false), `destructive` (default false), `breakpoint` (default false), `intent`, `observe` (`["text", "a11y", "screenshot"]`), `capture_as`, `assert`. ### Selectors A selector can be a plain string or an object with fallback chain: ```yaml selector: primary: "button:has-text('Save')" fallbacks: - "[data-testid='save-btn']" - "role=button[name=/save/i]" ``` ## Browser actions (14) These are the default executor's actions: | Action | Required fields | Description | |--------|----------------|-------------| | `navigate` | `url` | Navigate to a URL. Supports `{{input}}` interpolation. | | `click` | `selector` | Click an element. | | `fill` | `selector`, `value` | Type a value into an input field. | | `select` | `selector`, `value` | Choose a dropdown option. | | `set_toggle` | `selector`, `value` | Set a checkbox or toggle to true/false. | | `wait_for` | `selector` | Wait until an element is visible. | | `assert` | `assert` array | Verify page state without acting. | | `capture` | `capture_as`, `from` | Extract a value from the page or network. | | `capture_secret` | `capture_as`, `from` | Like `capture`, but the value is masked in logs. | | `ai_decide` | `allowed_outcomes` | Let the AI model choose from a list of outcomes. | | `scroll` | `selector` (optional) | Scroll the page or a specific element. | | `hover` | `selector` | Hover over an element. | | `press` | `value` | Press a keyboard key (e.g. `Enter`, `Tab`). | | `upload` | `selector`, `value` | Upload a file via a file input. | ## Assertions Post-step assertions verify expected page state: ```yaml assert: - visible: "[data-testid='dns-records-table']" - text_present: "Record added" - url_matches: "/dns" - network_complete: url_pattern: "/dns_records" method: POST status: 200 ``` | Assertion | Description | |-----------|-------------| | `visible: ` | Element is visible on page | | `hidden: ` | Element is not visible | | `text_present: ` | Text appears in page body | | `text_absent: ` | Text does not appear | | `url_matches: ` | Current URL matches pattern | | `network_complete` | A matching network request completed | ## Capture sources The `from` field on `capture` / `capture_secret` supports three source types: ```yaml # From a DOM element from: selector: ".result-id" attribute: textContent # optional, defaults to textContent # From a network response from: network_response: url_pattern: "/dns_records" method: POST json_path: "$.result.id" # From OCR on a screenshot region from: screenshot_ocr: region: "top-right" ``` ## Executor actions Actions prefixed with the executor name are routed automatically: ### Terraform (`tf_*`) `tf_init`, `tf_validate`, `tf_plan`, `tf_apply`, `tf_destroy`, `tf_output`, `tf_import`, `tf_state` ### Git (`git_*`) `git_clone`, `git_branch`, `git_commit`, `git_push`, `git_pr_create`, `git_pr_merge`, `git_tag`, `git_release` ### Persist (`persist_*`) `persist_provision`, `persist_migrate`, `persist_sync`, `persist_backup`, `persist_restore`, `persist_proxy_start`, `persist_proxy_stop` ### Shell (`shell_*`) `shell_exec`, `shell_script`, `shell_assert` ### HTTP (`http_*`) `http_request`, `http_assert`, `http_poll` ### Composite `run_playbook`, `parallel`, `conditional`, `loop` See [Executors](/docs/pilot/executors) for full field documentation on each action. ## Example A condensed playbook that adds a DNS A record on Cloudflare: ```yaml playbook: id: cloudflare.dns.add-a-record version: 1 target: cloudflare title: Add an A record to a DNS zone inputs: - { name: zone, type: string, required: true } - { name: record_name, type: string, required: true } - { name: ip_address, type: ipv4, required: true } outputs: - { name: record_id, type: string, source: capture.record_id } steps: - id: goto-dns action: navigate url: "https://dash.cloudflare.com/?to=/:account/{{zone}}/dns" ai_fallback: true - id: fill-name action: fill selector: "[name='dns-record-name']" value: "{{record_name}}" - id: save action: click selector: "button:has-text('Save')" assert: [{ text_present: "Record added" }] - id: capture-id action: capture capture_as: record_id from: network_response: url_pattern: "/dns_records" method: POST json_path: "$.result.id" ``` For a longer example using `ai_decide` with multiple outcomes and `breakpoint` on destructive steps, see the R2 playbook in `playbooks/cloudflare/r2.enable-and-create-buckets.yaml`. ## See also - [Executors](/docs/pilot/executors) — detailed executor reference - [JavaScript SDK](/docs/pilot/sdk/javascript) — run playbooks programmatically - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Executors Pilot routes each playbook step to an **executor** based on the step's `executor` field or its action prefix. The executor registry resolves the target executor in this order: 1. Explicit `executor` field on the step 2. Action prefix inference (`tf_` -> terraform, `git_` -> git, etc.) 3. Default: `browser` ## Browser (default) The browser executor drives a Playwright browser instance. It handles all 14 built-in actions: `navigate`, `click`, `fill`, `select`, `set_toggle`, `wait_for`, `assert`, `capture`, `capture_secret`, `ai_decide`, `scroll`, `hover`, `press`, `upload`. When `ai_fallback: true`, a failing selector triggers the AI healing engine, which compares the stored DOM snapshot to the current page and proposes a replacement selector. ### Configuration No explicit configuration required. The browser executor starts automatically. | Env var | Purpose | Default | |---------|---------|---------| | `CUITTY_AI_KEY` | AI model key for selector healing | (local model) | ## Terraform Wraps the Architect API for infrastructure provisioning. Steps are prefixed with `tf_`. | Action | Description | Key fields | |--------|-------------|------------| | `tf_init` | Create or ensure a target | `target_id`, `provider`, `kind` | | `tf_validate` | Validate target config | `target_id`, `resource_types` | | `tf_plan` | Generate a runtime plan | `target_id`, `resource_types` | | `tf_apply` | Apply runtime state | `target_id`, `resource_types` | | `tf_destroy` | Destroy runtime resources | `target_id`, `resource_types` | | `tf_output` | Read a specific output value | `target_id`, `output_name` | | `tf_import` | Import an existing resource | `target_id`, `resource_type`, `resource_id` | | `tf_state` | Read full runtime state | `target_id` | All Terraform steps accept an optional `capture_as` field and `allow_remote` boolean. ### Configuration | Env var | Purpose | Default | |---------|---------|---------| | `PILOT_ARCHITECT_URL` | Architect API base URL | `http://localhost:4470` | | `PILOT_EXECUTOR_TERRAFORM` | Enable/disable | `true` | ## Git Wraps the Code API for repository and pull request operations. Steps are prefixed with `git_`. | Action | Description | Key fields | |--------|-------------|------------| | `git_clone` | Create a repository | `name`, `visibility`, `auto_init` | | `git_branch` | Create a branch | `owner`, `repo`, `name`, `sha` | | `git_commit` | Create a commit | `owner`, `repo`, `message` | | `git_push` | Push a branch | `owner`, `repo`, `branch` | | `git_pr_create` | Open a pull request | `owner`, `repo`, `title`, `head`, `base` | | `git_pr_merge` | Merge a pull request | `owner`, `repo`, `number`, `strategy` | | `git_tag` | Create a tag | `owner`, `repo`, `tag_name`, `name` | | `git_release` | Create a release | `owner`, `repo`, `tag_name`, `name`, `body` | Merge strategies: `merge`, `squash`, `rebase`, `fast_forward`. ### Configuration | Env var | Purpose | Default | |---------|---------|---------| | `PILOT_CODE_URL` | Code API base URL | `http://localhost:4351` | | `PILOT_EXECUTOR_GIT` | Enable/disable | `true` | ## Persist Wraps the Persist API for storage provisioning and data management. Steps are prefixed with `persist_`. | Action | Description | Key fields | |--------|-------------|------------| | `persist_provision` | Provision storage stores | `profile`, `stores` | | `persist_migrate` | Run database migrations | `dry_run` | | `persist_sync` | Sync data between stores | | | `persist_backup` | Create a backup | | | `persist_restore` | Restore from backup | `archive_path` | | `persist_proxy_start` | Start the proxy | | | `persist_proxy_stop` | Stop the proxy | | ### Configuration | Env var | Purpose | Default | |---------|---------|---------| | `PILOT_PERSIST_URL` | Persist API base URL | `http://localhost:4290` | | `PILOT_EXECUTOR_PERSIST` | Enable/disable | `true` | ## Shell Executes shell commands with an allowlist for security. Steps are prefixed with `shell_`. | Action | Description | Key fields | |--------|-------------|------------| | `shell_exec` | Run a single command | `command`, `args`, `env` | | `shell_script` | Run a multi-line script | `command` | | `shell_assert` | Run a command and assert output | `command`, `assert` | Shell assertions support: `exit_code`, `stdout_contains`, `stderr_empty`. ### Configuration | Env var | Purpose | Default | |---------|---------|---------| | `PILOT_EXECUTOR_SHELL` | Enable/disable | `false` (disabled by default) | The shell executor uses an `allowedCommands` allowlist in `executor_config`: ```yaml executor_config: shell: allowedCommands: ["curl", "dig", "nslookup", "jq"] timeout: 30000 ``` ## HTTP Makes API requests, polls endpoints, and asserts response contents. Steps are prefixed with `http_`. | Action | Description | Key fields | |--------|-------------|------------| | `http_request` | Send an HTTP request | `url`, `method`, `headers`, `body` | | `http_assert` | Request + assert response | `url`, `assert` | | `http_poll` | Poll until condition met | `url`, `until`, `interval`, `max_attempts` | HTTP assertions support: `status`, `body_contains`, `header`. ### Configuration | Env var | Purpose | Default | |---------|---------|---------| | `PILOT_EXECUTOR_HTTP` | Enable/disable | `true` | The HTTP executor supports an `allowedHosts` restriction in `executor_config`: ```yaml executor_config: http: allowedHosts: ["api.cloudflare.com", "api.github.com"] timeout: 30000 ``` ## Composite The composite executor orchestrates other steps. It supports four meta-actions: | Action | Description | Key fields | |--------|-------------|------------| | `run_playbook` | Execute another playbook inline | `playbook`, `inputs` | | `parallel` | Run step branches concurrently | `branches` | | `conditional` | Execute steps if condition is true | `condition`, `then` | | `loop` | Repeat steps until condition or max iterations | `until`, `max_iterations`, `steps` | Conditions use the syntax `captures.key == value`, `captures.key != value`, or `captures.key` (truthy check). ### Example: Conditional + parallel ```yaml steps: - id: check-provider action: conditional executor: composite condition: "captures.provider == cloudflare" then: - id: setup-dns action: run_playbook playbook: cloudflare.dns.add-a-record inputs: zone: "{{zone}}" record_name: "{{subdomain}}" ip_address: "{{ip}}" - id: deploy-both action: parallel executor: composite branches: - steps: - id: provision-storage action: persist_provision profile: production - steps: - id: apply-infra action: tf_apply target_id: "{{target}}" resource_types: ["aws_s3_bucket"] ``` ## See also - [Playbook reference](/docs/pilot/playbook-reference) — full YAML schema - [JavaScript SDK](/docs/pilot/sdk/javascript) — run playbooks programmatically - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot JavaScript SDK `@cuitty/pilot-sdk` is the TypeScript/Node.js client for the Pilot API. It provides typed methods for managing playbooks, starting runs, streaming real-time events, and listing providers. ## Install ```bash sfw bun add @cuitty/pilot-sdk # or sfw npm install @cuitty/pilot-sdk ``` ## Quick start ```typescript import { PilotClient } from "@cuitty/pilot-sdk"; const pilot = new PilotClient({ baseUrl: "http://localhost:4320", auth: { apiKey: process.env.PILOT_API_KEY }, }); // List all playbooks const playbooks = await pilot.playbooks.list(); // Start a run const { id } = await pilot.runs.start({ playbookId: "cloudflare.dns.add-a-record", mode: "autonomous", inputs: { zone: "example.com", record_name: "app", ip_address: "1.2.3.4" }, }); // Stream events until completion for await (const event of pilot.runs.stream(id)) { console.log(event.type, event.stepId, event.status); } // Clean up pilot.close(); ``` ## PilotClient The main entry point. Creates service instances and manages shared configuration. ```typescript interface PilotClientConfig { baseUrl: string; // Pilot server URL auth?: { token?: string; // Bearer token cookie?: string; // Raw cookie string apiKey?: string; // X-Pilot-API-Key header }; timeout?: number; // Per-request timeout in ms (default 30000) } const pilot = new PilotClient(config); ``` | Property | Type | Description | |----------|------|-------------| | `pilot.playbooks` | `PlaybookService` | Playbook CRUD | | `pilot.runs` | `RunService` | Run lifecycle + streaming | | `pilot.providers` | `ProviderService` | Provider profile listing | | Method | Returns | Description | |--------|---------|-------------| | `pilot.health()` | `Promise` | Check server health | | `pilot.close()` | `void` | Close all active event subscriptions | ## PlaybookService ```typescript // List all playbooks, optionally filtered by target const all = await pilot.playbooks.list(); const cfOnly = await pilot.playbooks.list({ target: "cloudflare" }); // Get a single playbook (latest version) const pb = await pilot.playbooks.get("cloudflare.dns.add-a-record"); // Create a new playbook const { id, version } = await pilot.playbooks.create({ target: "cloudflare", title: "Add CNAME record", yaml: playbookYaml, authoredBy: "human", }); // Update (bumps version) await pilot.playbooks.update("cloudflare.dns.add-cname", { yaml: updatedYaml, }); // Delete all versions await pilot.playbooks.delete("cloudflare.dns.add-cname"); ``` | Method | Params | Returns | |--------|--------|---------| | `list(options?)` | `{ target?: string }` | `PlaybookSummary[]` | | `get(id)` | playbook id | `Playbook` | | `create(req)` | `CreatePlaybookRequest` | `{ id, version, created }` | | `update(id, req)` | id + `UpdatePlaybookRequest` | `{ id, version }` | | `delete(id)` | playbook id | `{ deleted }` | ## RunService ```typescript // Start a run const { id, status } = await pilot.runs.start({ playbookId: "cloudflare.dns.add-a-record", mode: "interactive", // "autonomous" | "interactive" | "review" inputs: { zone: "example.com", record_name: "api", ip_address: "1.2.3.4" }, providerProfileId: "cf-prod", }); // Get run details const run = await pilot.runs.get(id); // List runs with filters const recent = await pilot.runs.list({ status: "running", limit: 10 }); // Abort a running run await pilot.runs.abort(id); // Approve a step awaiting review await pilot.runs.approve(id, { stepId: "save", decision: "approve", // "approve" | "skip" | "abort" }); ``` | Method | Returns | |--------|---------| | `start(req)` | `{ id, status }` | | `get(id)` | `Run` (includes `steps` array) | | `list(options?)` | `Run[]` | | `abort(id)` | `{ aborted, runId }` | | `approve(id, req)` | `{ approved, runId, stepId, decision }` | ## Event streaming Two patterns for consuming real-time run events via SSE: ### Async iterator ```typescript for await (const event of pilot.runs.stream(runId)) { switch (event.type) { case "step.start": console.log(`Step ${event.stepId} started`); break; case "step.complete": console.log(`Step ${event.stepId} done in ${event.durationMs}ms`); break; case "step.failed": console.error(`Step ${event.stepId} failed: ${event.error}`); break; case "run.completed": console.log("Run finished", event.outputs); break; } } ``` The iterator yields events until the run reaches a terminal state (`run.completed`, `run.failed`, or `run.aborted`). ### Callback subscription ```typescript const unsubscribe = pilot.runs.subscribe(runId, { onStep: (event) => console.log("step", event.stepId, event.status), onComplete: (event) => console.log("done", event.outputs), onError: (event) => console.error("error", event.error), onEvent: (event) => {}, // every event }); // Later: close the SSE connection unsubscribe(); ``` ### Event types | Type | Fields | When | |------|--------|------| | `step.start` | `stepId`, `action` | Step begins executing | | `step.complete` | `stepId`, `action`, `durationMs`, `aiUsed` | Step succeeded | | `step.failed` | `stepId`, `action`, `error` | Step errored | | `step.awaiting_approval` | `stepId`, `action` | Step paused at breakpoint | | `run.completed` | `outputs` | All steps finished | | `run.failed` | `error` | Run failed | | `run.aborted` | | Run was aborted | ## ProviderService ```typescript const providers = await pilot.providers.list(); // Returns: Provider[] // { id, projectId, provider, label, baseUrl, lastAuthenticated, expiresAt } ``` ## Error handling The SDK throws typed errors: ```typescript import { PilotApiError, PilotConnectionError } from "@cuitty/pilot-sdk"; try { await pilot.runs.start({ playbookId: "nonexistent" }); } catch (err) { if (err instanceof PilotApiError) { console.error(`API ${err.status}: ${err.statusText}`, err.body); } else if (err instanceof PilotConnectionError) { console.error("Cannot reach Pilot:", err.message); } } ``` | Error class | Properties | When | |-------------|-----------|------| | `PilotApiError` | `status`, `statusText`, `body` | Non-2xx HTTP response | | `PilotConnectionError` | `message`, `cause` | Network / timeout failure | ## See also - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Executors](/docs/pilot/executors) — Browser, Terraform, Git, and other executor types - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot React SDK `@cuitty/pilot-react` provides React hooks and pre-built components for the Pilot API. It wraps `@cuitty/pilot-sdk` with idiomatic React patterns — context providers, data-fetching hooks, and SSE subscriptions that clean up automatically. ## Install ```bash sfw bun add @cuitty/pilot-react # or sfw npm install @cuitty/pilot-react ``` Peer dependencies: `react>=18`, `@cuitty/pilot-sdk`. ## Quick start ```tsx import { PilotProvider, usePlaybooks, useRun, useRunEvents } from "@cuitty/pilot-react"; function App() { return ( ); } function Dashboard() { const { data: playbooks, loading } = usePlaybooks(); const { run, start } = useRun(); const events = useRunEvents(run?.id); if (loading) return

Loading...

; return (
{playbooks?.map((pb) => ( ))} {events.map((e, i) => (
{e.type} {e.stepId}
))}
); } ``` ## PilotProvider Wraps your component tree with a `PilotClient` instance. Accepts all `PilotClientConfig` props plus `children`. The client is recreated when `baseUrl` or `auth.token` change, and closed on unmount. ```tsx import { PilotProvider } from "@cuitty/pilot-react"; {children} ``` | Prop | Type | Description | |------|------|-------------| | `baseUrl` | `string` | Pilot server URL | | `auth` | `{ token?: string; cookie?: string; apiKey?: string }` | Authentication credentials | | `timeout` | `number` | Per-request timeout in ms (default 30000) | | `children` | `React.ReactNode` | Child components | ### usePilot() Returns the `PilotClient` from context. Throws if used outside ``. ```tsx import { usePilot } from "@cuitty/pilot-react"; const pilot = usePilot(); const health = await pilot.health(); ``` ## Hooks ### usePlaybooks(options?) Fetches and caches the playbook list. Re-fetches when options change. ```tsx const { data, loading, error, refetch } = usePlaybooks(); const { data: cfOnly } = usePlaybooks({ target: "cloudflare" }); ``` | Return | Type | Description | |--------|------|-------------| | `data` | `PlaybookSummary[] \| null` | Fetched playbooks, `null` until loaded | | `loading` | `boolean` | `true` while fetching | | `error` | `Error \| null` | Fetch error, if any | | `refetch` | `() => Promise` | Manually re-fetch the list | ### useRun() Manages a single run lifecycle: start, abort, and refresh. ```tsx const { run, loading, error, start, abort, refresh } = useRun(); // Start a new run const result = await start({ playbookId: "cloudflare.dns.add-a-record", mode: "interactive", inputs: { zone: "example.com", record_name: "api", ip_address: "1.2.3.4" }, }); // Abort the active run await abort(); // Refresh run state from the server await refresh(); ``` | Return | Type | Description | |--------|------|-------------| | `run` | `Run \| null` | Current run object | | `loading` | `boolean` | `true` while starting a run | | `error` | `Error \| null` | Error from the last operation | | `start(req)` | `(StartRunRequest) => Promise` | Start a run and fetch its full details | | `abort()` | `() => Promise` | Abort the current run | | `refresh()` | `() => Promise` | Re-fetch current run state | ### useRunEvents(runId) Subscribes to real-time SSE events for a run. Automatically unsubscribes on unmount or when `runId` changes. Returns a growing array of events. ```tsx const events = useRunEvents(run?.id); // events: RunEvent[] events.forEach((event) => { console.log(event.type, event.stepId); }); ``` | Param | Type | Description | |-------|------|-------------| | `runId` | `string \| null \| undefined` | Run ID to subscribe to; `null`/`undefined` pauses subscription | Returns `RunEvent[]` — accumulates all events received since subscription started. ## Components ### RunStatusBadge Renders a colored pill badge for a run status. ```tsx import { RunStatusBadge } from "@cuitty/pilot-react"; ``` Supported statuses: `queued`, `running`, `awaiting_approval`, `completed`, `failed`, `aborted`. ### RunTimeline Renders a vertical timeline of run events with color-coded borders (blue for in-progress, green for complete, red for failures). ```tsx import { RunTimeline } from "@cuitty/pilot-react"; const events = useRunEvents(runId); ``` ### PlaybookCard Displays a playbook summary card with an optional "Run" button. ```tsx import { PlaybookCard } from "@cuitty/pilot-react"; start({ playbookId: pb.id, mode: "autonomous" })} /> ``` | Prop | Type | Description | |------|------|-------------| | `playbook` | `PlaybookSummary` | Playbook to display | | `onRun` | `(playbook: PlaybookSummary) => void` | Optional callback when "Run" is clicked | ## Error handling Errors from hooks are captured in the `error` field rather than thrown. For direct `PilotClient` usage via `usePilot()`, catch `PilotApiError` and `PilotConnectionError` from `@cuitty/pilot-sdk`. ```tsx import { PilotApiError, PilotConnectionError } from "@cuitty/pilot-sdk"; const pilot = usePilot(); try { await pilot.runs.start({ playbookId: "nonexistent" }); } catch (err) { if (err instanceof PilotApiError) { console.error(`API ${err.status}: ${err.statusText}`, err.body); } else if (err instanceof PilotConnectionError) { console.error("Cannot reach Pilot:", err.message); } } ``` ## See also - [JavaScript SDK](/docs/pilot/sdk/javascript) — underlying `PilotClient` API reference - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot SolidJS SDK `@cuitty/pilot-solid` provides SolidJS primitives and components for the Pilot API. It wraps `@cuitty/pilot-sdk` with idiomatic Solid patterns — context providers, `createResource` for data fetching, signals for run state, and automatic SSE cleanup. ## Install ```bash sfw bun add @cuitty/pilot-solid # or sfw npm install @cuitty/pilot-solid ``` Peer dependencies: `solid-js>=1.8`, `@cuitty/pilot-sdk`. ## Quick start ```tsx import { PilotProvider, createPlaybooks, createRun, createRunEvents } from "@cuitty/pilot-solid"; import { For, Show, createSignal } from "solid-js"; function App() { return ( ); } function Dashboard() { const [playbooks] = createPlaybooks(); const [run, { start }] = createRun(); const events = createRunEvents(() => run()?.id); return (
{(pb) => ( )} {(e) =>
{e.type} {e.stepId}
}
); } ``` ## PilotProvider Wraps your component tree with a `PilotClient` instance. Accepts all `PilotClientConfig` props plus `children`. The client is closed automatically via `onCleanup`. ```tsx import { PilotProvider } from "@cuitty/pilot-solid"; {props.children} ``` | Prop | Type | Description | |------|------|-------------| | `baseUrl` | `string` | Pilot server URL | | `auth` | `{ token?: string; cookie?: string; apiKey?: string }` | Authentication credentials | | `timeout` | `number` | Per-request timeout in ms (default 30000) | | `children` | `JSX.Element` | Child components | ### usePilot() Returns the `PilotClient` from context. Throws if used outside ``. ```tsx import { usePilot } from "@cuitty/pilot-solid"; const pilot = usePilot(); const health = await pilot.health(); ``` ## Primitives ### createPlaybooks(options?) Reactive resource for listing playbooks. Wraps Solid's `createResource` — re-fetches when the options accessor changes. ```tsx const [playbooks, { refetch }] = createPlaybooks(); const [cfOnly] = createPlaybooks(() => ({ target: "cloudflare" })); ``` | Param | Type | Description | |-------|------|-------------| | `opts` | `() => ListPlaybooksOptions \| undefined` | Optional accessor returning filter options | Returns a Solid `Resource` tuple: `[accessor, { refetch, mutate }]`. ### createRun() Manages a single run lifecycle with signals for run state and loading. ```tsx const [run, { start, abort, loading }] = createRun(); // Start a new run const result = await start({ playbookId: "cloudflare.dns.add-a-record", mode: "interactive", inputs: { zone: "example.com", record_name: "api", ip_address: "1.2.3.4" }, }); // Abort the active run await abort(); ``` | Return | Type | Description | |--------|------|-------------| | `run` | `Accessor` | Signal with the current run object | | `start(req)` | `(StartRunRequest) => Promise` | Start a run and fetch its full details | | `abort()` | `() => Promise` | Abort the current run | | `loading` | `Accessor` | Signal, `true` while starting | ### createRunEvents(runId) Subscribes to real-time SSE events for a run. Automatically unsubscribes via `onCleanup` and re-subscribes when `runId` changes. ```tsx const events = createRunEvents(() => run()?.id); // events is Accessor {(event) => {event.type}} ``` | Param | Type | Description | |-------|------|-------------| | `runId` | `() => string \| null \| undefined` | Accessor returning the run ID; `null`/`undefined` pauses subscription | Returns `Accessor` — accumulates all events since subscription started. ## Components ### RunStatusBadge Renders a colored pill badge for a run status. ```tsx import { RunStatusBadge } from "@cuitty/pilot-solid"; ``` Supported statuses: `queued`, `running`, `awaiting_approval`, `completed`, `failed`, `aborted`. ### RunTimeline Renders a vertical timeline of run events using ``, with color-coded borders (blue for in-progress, green for complete, red for failures). Uses `` for conditional step ID display. ```tsx import { RunTimeline } from "@cuitty/pilot-solid"; const events = createRunEvents(() => runId); ``` ### PlaybookCard Displays a playbook summary card with an optional "Run" button. Uses `` to conditionally render the button. ```tsx import { PlaybookCard } from "@cuitty/pilot-solid"; start({ playbookId: pb.id, mode: "autonomous" })} /> ``` | Prop | Type | Description | |------|------|-------------| | `playbook` | `PlaybookSummary` | Playbook to display | | `onRun` | `(playbook: PlaybookSummary) => void` | Optional callback when "Run" is clicked | ## Error handling Errors in `createPlaybooks` surface through the resource's error state. For `createRun`, errors thrown during `start()` propagate to the caller. For direct `PilotClient` usage via `usePilot()`, catch typed errors from `@cuitty/pilot-sdk`. ```tsx import { PilotApiError, PilotConnectionError } from "@cuitty/pilot-sdk"; const pilot = usePilot(); try { await pilot.runs.start({ playbookId: "nonexistent" }); } catch (err) { if (err instanceof PilotApiError) { console.error(`API ${err.status}: ${err.statusText}`, err.body); } else if (err instanceof PilotConnectionError) { console.error("Cannot reach Pilot:", err.message); } } ``` ## See also - [JavaScript SDK](/docs/pilot/sdk/javascript) — underlying `PilotClient` API reference - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot Rust SDK `cuitty-pilot-sdk` is an async Rust client for the Pilot API. It uses `reqwest` for HTTP, `serde` for JSON serialization, and `thiserror` for typed errors. ## Install Add to your `Cargo.toml`: ```toml [dependencies] cuitty-pilot-sdk = { path = "../packages/sdk-rust" } # or when published: # cuitty-pilot-sdk = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` ## Quick start ```rust use cuitty_pilot_sdk::{PilotClient, StartRunRequest, PilotError}; #[tokio::main] async fn main() -> Result<(), PilotError> { let client = PilotClient::new( "http://localhost:4320", Some("your-auth-token".into()), ); // List all playbooks let playbooks = client.list_playbooks(None).await?; // Start a run let resp = client.start_run(&StartRunRequest { playbook_id: "cloudflare.dns.add-a-record".into(), mode: Some("autonomous".into()), inputs: Some(serde_json::json!({ "zone": "example.com", "record_name": "app", "ip_address": "1.2.3.4" })), ..Default::default() }).await?; println!("Run started: {} ({})", resp.id, resp.status); // Get run details let run = client.get_run(&resp.id).await?; println!("Run status: {}", run.status); Ok(()) } ``` ## PilotClient The main entry point. All methods are `async` and return `Result`. ```rust // Without auth let client = PilotClient::new("http://localhost:4320", None); // With bearer token let client = PilotClient::new( "http://localhost:4320", Some("ey...".into()), ); ``` | Method | Returns | Description | |--------|---------|-------------| | `health()` | `HealthStatus` | Check server health | | `list_playbooks(target)` | `Vec` | List playbooks, optionally filtered | | `get_playbook(id)` | `Playbook` | Get playbook by ID (latest version) | | `create_playbook(req)` | `CreatePlaybookResponse` | Create a new playbook | | `update_playbook(id, req)` | `UpdatePlaybookResponse` | Update (version-bump) a playbook | | `delete_playbook(id)` | `DeletePlaybookResponse` | Delete all versions | | `start_run(req)` | `StartRunResponse` | Start a new run | | `list_runs(status, playbook_id, limit)` | `Vec` | List runs with optional filters | | `get_run(id)` | `Run` | Get run details including steps | | `abort_run(id)` | `AbortRunResponse` | Abort a running or queued run | | `approve_run(id, req)` | `ApproveResponse` | Submit approval decision for a step | | `list_providers()` | `Vec` | List all provider profiles | ## Playbooks ```rust // List all let playbooks = client.list_playbooks(None).await?; // Filter by target let cf = client.list_playbooks(Some("cloudflare")).await?; // Get one let pb = client.get_playbook("cloudflare.dns.add-a-record").await?; // Create let resp = client.create_playbook(&CreatePlaybookRequest { target: "cloudflare".into(), title: "Add CNAME record".into(), yaml: playbook_yaml, authored_by: Some("human".into()), }).await?; // Update client.update_playbook("cloudflare.dns.add-cname", &UpdatePlaybookRequest { yaml: updated_yaml, target: None, title: None, authored_by: None, }).await?; // Delete client.delete_playbook("cloudflare.dns.add-cname").await?; ``` ## Runs ```rust // Start a run let resp = client.start_run(&StartRunRequest { playbook_id: "cloudflare.dns.add-a-record".into(), mode: Some("interactive".into()), inputs: Some(serde_json::json!({ "zone": "example.com", "record_name": "api", "ip_address": "1.2.3.4" })), provider_profile_id: Some("cf-prod".into()), ..Default::default() }).await?; // Get run with steps let run = client.get_run(&resp.id).await?; if let Some(steps) = &run.steps { for step in steps { println!("{}: {} ({}ms)", step.step_id, step.status, step.duration_ms.unwrap_or(0)); } } // List recent running runs let running = client.list_runs(Some("running"), None, Some(10)).await?; // Abort client.abort_run(&resp.id).await?; // Approve a step client.approve_run(&resp.id, &ApproveRequest { step_id: "save".into(), decision: Some("approve".into()), }).await?; ``` ## Types ### Core structs | Struct | Key fields | |--------|-----------| | `Playbook` | `id`, `version`, `target`, `title`, `document_yaml`, `authored_by`, `created_at` | | `PlaybookSummary` | `id`, `version`, `target`, `title`, `authored_by` | | `Run` | `id`, `project_id`, `playbook_id`, `mode`, `status`, `inputs_json`, `outputs_json`, `steps: Option>` | | `RunStep` | `id`, `run_id`, `step_id`, `action`, `status`, `ai_used`, `duration_ms`, `error_message` | | `Provider` | `id`, `project_id`, `provider`, `label`, `base_url`, `last_authenticated`, `expires_at` | | `HealthStatus` | `status`, `database`, `driver_pool`, `provider_sessions`, `last_checked` | ### Request structs | Struct | Fields | |--------|--------| | `StartRunRequest` | `playbook_id`, `version?`, `mode?`, `inputs?` (`serde_json::Value`), `provider_profile_id?`, `project_id?` | | `CreatePlaybookRequest` | `target`, `title`, `yaml`, `authored_by?` | | `UpdatePlaybookRequest` | `yaml`, `target?`, `title?`, `authored_by?` | | `ApproveRequest` | `step_id`, `decision?` | All structs derive `Debug`, `Clone`, `Serialize`, and `Deserialize`. ## Error handling The SDK uses a single `PilotError` enum with three variants: ```rust use cuitty_pilot_sdk::PilotError; match client.start_run(&req).await { Ok(resp) => println!("Started: {}", resp.id), Err(PilotError::Api { status, message }) => { eprintln!("API error {}: {}", status, message); } Err(PilotError::Connection(err)) => { eprintln!("Network error: {}", err); } Err(PilotError::Other(msg)) => { eprintln!("Unexpected: {}", msg); } } ``` | Variant | Fields | When | |---------|--------|------| | `Api` | `status: u16`, `message: String` | Non-2xx HTTP response | | `Connection` | wraps `reqwest::Error` | Network or timeout failure | | `Other` | `String` | Any other error | ## See also - [JavaScript SDK](/docs/pilot/sdk/javascript) — TypeScript client with event streaming - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot Go SDK `github.com/cuitty/pilot-sdk-go` is a Go client for the Pilot API. It requires Go 1.22+ and has zero external dependencies beyond the standard library. ## Install ```bash go get github.com/cuitty/pilot-sdk-go ``` ## Quick start ```go package main import ( "encoding/json" "fmt" "log" pilot "github.com/cuitty/pilot-sdk-go" ) func main() { client := pilot.NewClient("http://localhost:4320", "your-auth-token") // List all playbooks playbooks, err := client.ListPlaybooks("") if err != nil { log.Fatal(err) } for _, pb := range playbooks { fmt.Printf("%s: %s\n", pb.ID, pb.Title) } // Start a run inputs, _ := json.Marshal(map[string]string{ "zone": "example.com", "record_name": "app", "ip_address": "1.2.3.4", }) resp, err := client.StartRun(&pilot.StartRunRequest{ PlaybookID: "cloudflare.dns.add-a-record", Mode: "autonomous", Inputs: inputs, }) if err != nil { log.Fatal(err) } fmt.Printf("Run started: %s (%s)\n", resp.ID, resp.Status) } ``` ## NewClient Creates a new Pilot API client. Pass an empty string for `authToken` to skip authentication. ```go // With auth client := pilot.NewClient("http://localhost:4320", "ey...") // Without auth client := pilot.NewClient("http://localhost:4320", "") ``` The `Client` struct exposes `BaseURL`, `AuthToken`, and `HTTP` (*http.Client) fields for customization. | Method | Returns | Description | |--------|---------|-------------| | `Health()` | `*HealthStatus, error` | Check server health | | `ListPlaybooks(target)` | `[]PlaybookSummary, error` | List playbooks; pass `""` for all | | `GetPlaybook(id)` | `*Playbook, error` | Get playbook by ID (latest version) | | `CreatePlaybook(req)` | `*CreatePlaybookResponse, error` | Create a new playbook | | `UpdatePlaybook(id, req)` | `*UpdatePlaybookResponse, error` | Update (version-bump) a playbook | | `DeletePlaybook(id)` | `*DeletePlaybookResponse, error` | Delete all versions | | `StartRun(req)` | `*StartRunResponse, error` | Start a new run | | `ListRuns(opts)` | `[]Run, error` | List runs with optional filters | | `GetRun(id)` | `*Run, error` | Get run details including steps | | `AbortRun(id)` | `*AbortRunResponse, error` | Abort a running or queued run | | `ApproveRun(id, req)` | `*ApproveResponse, error` | Submit approval decision for a step | | `ListProviders()` | `[]Provider, error` | List all provider profiles | ## Playbooks ```go // List all playbooks, err := client.ListPlaybooks("") // Filter by target cfPlaybooks, err := client.ListPlaybooks("cloudflare") // Get one pb, err := client.GetPlaybook("cloudflare.dns.add-a-record") // Create resp, err := client.CreatePlaybook(&pilot.CreatePlaybookRequest{ Target: "cloudflare", Title: "Add CNAME record", YAML: playbookYAML, AuthoredBy: "human", }) // Update resp, err := client.UpdatePlaybook("cloudflare.dns.add-cname", &pilot.UpdatePlaybookRequest{YAML: updatedYAML}) // Delete resp, err := client.DeletePlaybook("cloudflare.dns.add-cname") ``` ## Runs ```go // Start a run inputs, _ := json.Marshal(map[string]string{ "zone": "example.com", "record_name": "api", "ip_address": "1.2.3.4", }) resp, err := client.StartRun(&pilot.StartRunRequest{ PlaybookID: "cloudflare.dns.add-a-record", Mode: "interactive", Inputs: inputs, ProviderProfileID: "cf-prod", }) // Get run with steps run, err := client.GetRun(resp.ID) for _, step := range run.Steps { fmt.Printf("%s: %s\n", step.StepID, step.Status) } // List runs with filters runs, err := client.ListRuns(&pilot.ListRunsOptions{ Status: "running", Limit: 10, }) // Abort resp, err := client.AbortRun(runID) // Approve a step resp, err := client.ApproveRun(runID, &pilot.ApproveRequest{ StepID: "save", Decision: "approve", }) ``` ## Types ### Core types | Type | Key fields | |------|-----------| | `Playbook` | `ID`, `Version`, `Target`, `Title`, `DocumentYAML`, `AuthoredBy`, `CreatedAt` | | `PlaybookSummary` | `ID`, `Version`, `Target`, `Title`, `AuthoredBy` | | `Run` | `ID`, `ProjectID`, `PlaybookID`, `Mode`, `Status`, `InputsJSON`, `OutputsJSON`, `Steps []RunStep` | | `RunStep` | `ID`, `RunID`, `StepID`, `Action`, `Status`, `AIUsed`, `DurationMs *int`, `ErrorMessage *string` | | `Provider` | `ID`, `ProjectID`, `Provider`, `Label`, `BaseURL`, `LastAuthenticated`, `ExpiresAt` | | `HealthStatus` | `Status`, `Database`, `DriverPool`, `ProviderSessions`, `LastChecked` | ### Request types | Type | Fields | |------|--------| | `StartRunRequest` | `PlaybookID`, `Version *int`, `Mode`, `Inputs json.RawMessage`, `ProviderProfileID`, `ProjectID` | | `CreatePlaybookRequest` | `Target`, `Title`, `YAML`, `AuthoredBy` | | `UpdatePlaybookRequest` | `YAML`, `Target`, `Title`, `AuthoredBy` | | `ApproveRequest` | `StepID`, `Decision` | | `ListRunsOptions` | `Status`, `PlaybookID`, `Limit int` | ## Error handling The SDK returns `*APIError` for non-2xx responses. All other errors are standard Go errors from `net/http` or `encoding/json`. ```go import ( "errors" pilot "github.com/cuitty/pilot-sdk-go" ) resp, err := client.StartRun(&req) if err != nil { var apiErr *pilot.APIError if errors.As(err, &apiErr) { fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message) } else { fmt.Printf("Network error: %v\n", err) } } ``` | Type | Fields | When | |------|--------|------| | `*APIError` | `StatusCode int`, `Message string` | Non-2xx HTTP response | | `error` | standard | Network, JSON, or other failure | ## See also - [JavaScript SDK](/docs/pilot/sdk/javascript) — TypeScript client with event streaming - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # Pilot Python SDK `cuitty-pilot` provides both async (`PilotClient`) and synchronous (`PilotClientSync`) Python clients for the Pilot API. Built on `httpx` with dataclass types and context manager support. ## Install ```bash sfw pip install cuitty-pilot ``` Requires Python 3.10+ and `httpx`. ## Quick start ### Async ```python import asyncio from cuitty_pilot import PilotClient, StartRunRequest async def main(): async with PilotClient("http://localhost:4320", auth_token="pk_...") as client: # List all playbooks playbooks = await client.list_playbooks() # Start a run resp = await client.start_run(StartRunRequest( playbook_id="cloudflare.dns.add-a-record", mode="autonomous", inputs={"zone": "example.com", "record_name": "app", "ip_address": "1.2.3.4"}, )) print(f"Run started: {resp['id']} ({resp['status']})") # Get run details run = await client.get_run(resp["id"]) print(f"Steps: {len(run.steps)}") asyncio.run(main()) ``` ### Synchronous ```python from cuitty_pilot import PilotClientSync, StartRunRequest with PilotClientSync("http://localhost:4320", auth_token="pk_...") as client: playbooks = client.list_playbooks() resp = client.start_run(StartRunRequest( playbook_id="cloudflare.dns.add-a-record", inputs={"zone": "example.com", "record_name": "app", "ip_address": "1.2.3.4"}, )) run = client.get_run(resp["id"]) client.close() ``` ## PilotClient (async) The primary client. Supports `async with` as a context manager. ```python client = PilotClient( base_url="http://localhost:4320", auth_token="ey...", # Optional bearer token timeout=30.0, # Per-request timeout in seconds (default 30.0) ) ``` | Method | Returns | Description | |--------|---------|-------------| | `health()` | `HealthStatus` | Check server health | | `list_playbooks(target?)` | `list[PlaybookSummary]` | List playbooks, optionally filtered | | `get_playbook(id)` | `Playbook` | Get playbook by ID (latest version) | | `create_playbook(req)` | `dict` (`{id, version, created}`) | Create a new playbook | | `update_playbook(id, req)` | `dict` (`{id, version}`) | Update (version-bump) a playbook | | `delete_playbook(id)` | `dict` (`{deleted}`) | Delete all versions | | `start_run(req)` | `dict` (`{id, status}`) | Start a new run | | `list_runs(status?, playbook_id?, limit?)` | `list[Run]` | List runs with optional filters | | `get_run(id)` | `Run` | Get run details including steps | | `abort_run(id)` | `dict` (`{aborted, runId}`) | Abort a running or queued run | | `approve_run(id, req)` | `dict` (`{approved, runId, stepId, decision}`) | Submit approval decision | | `list_providers()` | `list[Provider]` | List all provider profiles | | `close()` | `None` | Close the underlying HTTP client | ## PilotClientSync Synchronous wrapper with an identical API. Supports `with` as a context manager. ```python client = PilotClientSync( base_url="http://localhost:4320", auth_token="ey...", timeout=30.0, ) ``` All methods are the same as `PilotClient` but without `await`. ## Playbooks ```python # List all playbooks = await client.list_playbooks() # Filter by target cf_playbooks = await client.list_playbooks(target="cloudflare") # Get one pb = await client.get_playbook("cloudflare.dns.add-a-record") print(pb.title, pb.document_yaml) # Create resp = await client.create_playbook(CreatePlaybookRequest( target="cloudflare", title="Add CNAME record", yaml=playbook_yaml, authored_by="human", )) # Update await client.update_playbook("cloudflare.dns.add-cname", UpdatePlaybookRequest(yaml=updated_yaml)) # Delete await client.delete_playbook("cloudflare.dns.add-cname") ``` ## Runs ```python # Start a run resp = await client.start_run(StartRunRequest( playbook_id="cloudflare.dns.add-a-record", mode="interactive", inputs={"zone": "example.com", "record_name": "api", "ip_address": "1.2.3.4"}, provider_profile_id="cf-prod", )) # Get run with steps run = await client.get_run(resp["id"]) for step in run.steps: print(f"{step.step_id}: {step.status} ({step.duration_ms}ms)") # List runs with filters running = await client.list_runs(status="running", limit=10) # Abort await client.abort_run(run_id) # Approve a step await client.approve_run(run_id, ApproveRequest( step_id="save", decision="approve", )) ``` ## Types All types are Python `dataclass` instances. ### Core types | Type | Fields | |------|--------| | `Playbook` | `id`, `version`, `target`, `title`, `document_yaml`, `authored_by`, `created_at` | | `PlaybookSummary` | `id`, `version`, `target`, `title`, `authored_by` | | `Run` | `id`, `project_id`, `playbook_id`, `playbook_version`, `mode`, `status`, `inputs_json`, `outputs_json`, `provider_profile_id?`, `started_at?`, `completed_at?`, `failed_step_id?`, `error_message?`, `steps: list[RunStep]` | | `RunStep` | `id`, `run_id`, `step_id`, `action`, `status`, `selector_tried_json`, `ai_used`, `started_at?`, `completed_at?`, `duration_ms?`, `error_message?` | | `Provider` | `id`, `project_id`, `provider`, `label`, `base_url`, `last_authenticated?`, `expires_at?` | | `HealthStatus` | `status`, `database`, `driver_pool: dict`, `provider_sessions: dict`, `last_checked` | ### Request types | Type | Fields | |------|--------| | `StartRunRequest` | `playbook_id`, `mode="autonomous"`, `version?`, `inputs?: dict`, `provider_profile_id?`, `project_id?` | | `CreatePlaybookRequest` | `target`, `title`, `yaml`, `authored_by?` | | `UpdatePlaybookRequest` | `yaml`, `target?`, `title?`, `authored_by?` | | `ApproveRequest` | `step_id`, `decision="approve"` | ## Error handling The SDK raises typed exceptions from a common `PilotError` base class: ```python from cuitty_pilot import PilotAPIError, PilotConnectionError try: await client.start_run(StartRunRequest(playbook_id="nonexistent")) except PilotAPIError as e: print(f"API error {e.status}: {e.message}") except PilotConnectionError as e: print(f"Cannot reach Pilot: {e}") if e.cause: print(f"Underlying: {e.cause}") ``` | Exception | Properties | When | |-----------|-----------|------| | `PilotError` | base class | All SDK errors | | `PilotAPIError` | `status: int`, `message: str` | Non-2xx HTTP response | | `PilotConnectionError` | `message: str`, `cause: Exception \| None` | Network or timeout failure | ## See also - [JavaScript SDK](/docs/pilot/sdk/javascript) — TypeScript client with event streaming - [Playbook reference](/docs/pilot/playbook-reference) — YAML schema for playbook documents - [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow --- # DSL quickstart Cuitty DSL is an HCL-style language for product workflows across Cuitty. A single `.cuitty` file can declare project metadata, test suites, Pilot playbooks, Video compositions, Code Apps, modules, and adapter targets. ```hcl cuitty { schema_version = 1 project = "demo" extensions = ["tests", "pilot", "video"] targets = ["ctest", "pilot-yaml", "video-yaml"] } test_suite "smoke" { test "health" { type = "http" request { method = "GET" url = "${env.BASE_URL}/api/health" } assert { status = 200 } } } ``` Open the hosted playground at [/playground/dsl](/playground/dsl), or run the local sandbox: ```bash cd ~/Code/cuitty/dsl ./run sandbox ``` The playground validates source, shows diagnostics, lists symbols, and exposes the normalized AST that adapters use. --- # DSL reference The canonical file extension is `.cuitty`. Bare `.cuitty` files, `.cuitty.hcl`, `.ctest`, and `.cuitty/**/*.hcl` are detected by the Cuitty DSL tooling and LSP. | Block | Product | Purpose | | --- | --- | --- | | `cuitty` | Core | Schema version, project metadata, enabled extensions, export targets. | | `test_suite` | Tests | HTTP, gRPC, Pilot, and unit test workflows. | | `playbook` | Pilot | Browser automation and provider operations. | | `video` | Video | Declarative composition timelines and output settings. | | `code_app` | Code | App manifests, permissions, ownership, and runtime. | | `module` | Observe | Module registry and portal metadata. | | `persist` | Persist | Storage profiles and adapter configuration. | Adapter targets keep product runtimes stable while authoring converges: ```bash cuitty-dsl export --target ctest examples/tests/auth-flow.cuitty cuitty-dsl export --target pilot-yaml examples/pilot/cloudflare-dns.cuitty cuitty-dsl export --target video-yaml examples/video/code-demo.cuitty ``` --- # DSL playground The hosted playground at [/playground/dsl](/playground/dsl) runs the same parser and validator core used by local DSL tooling. It is intended for authoring, debugging, documentation examples, and migration review. The local sandbox adds filesystem examples and live reload: ```bash cd ~/Code/cuitty/dsl ./run sandbox ``` Use `./run dev` when you want the initial parser/LSP checks before launching the sandbox. The playground is built from shareable components: - `@cuitty/dsl-playground-core` owns parsing, validation, summary metrics, AST, diagnostics, and symbols. - `@cuitty/brand/sdk/dsl/solid` owns the branded UI shell. - `@cuitty/brand/sdk/dsl/react` provides parity for React consumers. Validation runs directly in the browser. Import, export, and roundtrip modes are shown with copyable CLI commands when the browser cannot safely load Node-only YAML adapters. --- # DSL migration The unified DSL is additive. Existing product formats stay supported while adapters import and export equivalent `.cuitty` blocks. ## Tests `.ctest` maps to `test_suite` blocks. ```bash cuitty-dsl import --from ctest tests/auth-flow.ctest cuitty-dsl export --target ctest examples/tests/auth-flow.cuitty ``` ## Video Video YAML maps to `video` blocks and still exports to renderer-compatible YAML. ```bash cuitty-dsl import --from video-yaml video/examples/02-code-demo.yaml cuitty-dsl export --target video-yaml examples/video/code-demo.cuitty ``` ## Pilot Pilot YAML playbooks map to `playbook` blocks and still export to existing Pilot engine YAML. ```bash cuitty-dsl import --from pilot-yaml pilot/playbooks/cloudflare/dns.add-a-record.yaml cuitty-dsl export --target pilot-yaml examples/pilot/cloudflare-dns.cuitty ``` --- # Video quickstart Cuitty Video turns a TypeScript composition or YAML document into a rendered video. The local pipeline compiles the composition into IR, renders frames with Playwright, and encodes the final artifact with FFmpeg. ## Install the workspace From the video repository: ```bash cd ~/Code/cuitty/video bun install bun run typecheck ``` Rendering requires FFmpeg and a Playwright-compatible browser on the machine that runs the renderer. ## Create a composition Create `examples/hello.yaml`: ```yaml composition: id: hello-video width: 1280 height: 720 fps: 30 duration: 5s layers: - id: background render: type: gradient colors: ["#0f172a", "#312e81"] direction: "135deg" - id: title clips: - id: intro-title start: 0s end: 5s render: type: text text: "Hello from Cuitty Video" fontSize: 64 color: "#ffffff" animate: - property: opacity from: 0 to: 1 start: 0s duration: 1s easing: cubic-out output: codec: h264 crf: 18 ``` ## Preview the composition Use the CLI to validate the file and print its timeline summary: ```bash bun --cwd packages/cli src/index.ts preview ../../examples/hello.yaml ``` Inspect a single frame: ```bash bun --cwd packages/cli src/index.ts preview ../../examples/hello.yaml --frame 60 ``` ## Render the video Render to MP4: ```bash mkdir -p output bun --cwd packages/cli src/index.ts render ../../examples/hello.yaml -o ../../output/hello.mp4 --codec h264 --crf 18 ``` Use `--dry-run` when you want to parse and validate without invoking Playwright or FFmpeg: ```bash bun --cwd packages/cli src/index.ts render ../../examples/hello.yaml --dry-run ``` ## Next steps - Use the TypeScript SDK when the timeline is generated from code or application data. - Use YAML when compositions should live in config, tests, or content workflows. - Use [CLI reference](/docs/video/cli) for automation commands. - Use [E2E visibility](/docs/video/e2e) to leave inspectable render artifacts. --- # Video reference Cuitty Video has four core surfaces: | Surface | Package | Purpose | | --- | --- | --- | | SDK | `@cuitty/video` | Build a composition in TypeScript and export Composition IR. | | DSL | `@cuitty/video-dsl` | Parse YAML or JSON into the same Composition IR. | | Renderer | `@cuitty/video-render` | Render frames with Playwright and encode with FFmpeg. | | Player | `@cuitty/video-player` | Preview Composition IR in the browser. | The CLI and server are thin workflow layers over those packages. ## Composition IR Every composition resolves to the same shape: | Field | Type | Notes | | --- | --- | --- | | `id` | `string` | Stable composition identifier. | | `width` | `number` | Output width in pixels. | | `height` | `number` | Output height in pixels. | | `fps` | `number` | Frames per second. | | `durationInFrames` | `number` | Timeline length after duration parsing. | | `layers` | `LayerIR[]` | Ordered visual layers. | | `audioTracks` | `AudioIR[]` | Optional audio tracks. | | `transitions` | `TransitionIR[]` | Optional clip transitions. | Layers contain clips. Clips define `startFrame`, `endFrame`, `renderType`, `renderProps`, and optional animation metadata. ## TypeScript SDK Use the SDK when composition data comes from code: ```ts import { composition, tween, Easing } from "@cuitty/video"; import { render } from "@cuitty/video-render"; const video = composition({ id: "status-report", width: 1920, height: 1080, fps: 30, durationInFrames: 180, }); video.layer("title").clip({ startFrame: 0, endFrame: 120, render: (ctx) => { const opacity = tween(ctx.frame, [0, 30], [0, 1], { clamp: true, easing: Easing.out(Easing.cubic), }); return `

Weekly status

`; }, }); await render({ composition: video, outputPath: "./output/status-report.mp4", codec: "h264", crf: 18, }); ``` ## YAML DSL Use the DSL when a composition should be editable as data: ```yaml composition: id: status-report width: 1920 height: 1080 fps: 30 duration: 6s layers: - id: title clips: - id: headline start: 0s end: 4s render: type: text text: "Weekly status" fontSize: 72 color: "#ffffff" animate: - property: opacity from: 0 to: 1 start: 0s duration: 1s easing: cubic-out ``` The DSL accepts YAML or JSON. `duration`, `start`, and `end` can be frame numbers or time strings such as `500ms`, `2s`, or `00:00:02`. ## Render options | Option | Default | Notes | | --- | --- | --- | | `outputPath` | Required | Final output file path. | | `codec` | `h264` | Supports `h264`, `h265`, `vp9`, `prores`, and `gif`. | | `crf` | `18` | Lower is higher quality for CRF-based codecs. | | `concurrency` | CPU-dependent | Parallel frame rendering. | | `onProgress` | None | Receives frame count, total frames, and render FPS. | ## Server workflow The render server accepts composition payloads, creates jobs, reports status, and runs the same render pipeline behind an HTTP boundary. Use it when renders should be queued or called by another service instead of run directly in the CLI. ## Observe integration Video render jobs can emit progress, failure, and encoding metadata into Observe. The standalone Video product owns authoring and rendering; the existing [Video module](/docs/modules/video) remains the Observe module for browser-session recording job metadata. --- # Video CLI The `cuitty-video` CLI validates, previews, renders, and backs up Video compositions. In a published install, run the binary directly: ```bash cuitty-video --help ``` Inside the local workspace, run the CLI source with Bun: ```bash cd ~/Code/cuitty/video bun --cwd packages/cli src/index.ts --help ``` ## Commands | Command | Purpose | | --- | --- | | `init [directory]` | Create a starter project with a sample composition. | | `preview ` | Parse a YAML or JSON composition and print timeline details. | | `validate ` | Validate schema and lint checks without rendering. | | `render ` | Render a composition to a video file. | | `backup ` | Back up a rendered video through the Persist integration. | | `login` | Authenticate with Cuitty. | | `logout` | Remove the local Cuitty auth session. | | `whoami` | Print the current auth identity. | ## Render ```bash cuitty-video render composition.yaml -o output.mp4 --codec h264 --crf 18 ``` Options: | Option | Description | | --- | --- | | `-o, --output ` | Output path. Defaults to `.mp4`. | | `--codec ` | `h264`, `h265`, `vp9`, `prores`, or `gif`. | | `--crf ` | Constant Rate Factor. Lower is higher quality. | | `--concurrency ` | Parallel frame renders. | | `--preset-dir ` | Directory containing reusable preset YAML files. | | `--dry-run` | Parse and validate only. Do not render. | Use `--dry-run` in CI checks that should validate authoring changes without requiring FFmpeg. ## Preview ```bash cuitty-video preview composition.yaml cuitty-video preview composition.yaml --frame 60 cuitty-video preview composition.yaml --open ``` `--frame` prints the clips visible at one frame. `--open` starts a local browser preview. ## Validate ```bash cuitty-video validate composition.yaml --strict cuitty-video validate composition.yaml --preset-dir ./presets --json ``` Validation checks the DSL shape, timeline bounds, empty layers, overlapping clips, and unknown render types. ## Init ```bash cuitty-video init my-video cd my-video cuitty-video preview composition.yaml cuitty-video render composition.yaml ``` Use `init` for a local starting point, then move reusable presets and data-driven scenes into version control. ## Backup ```bash cuitty-video backup output.mp4 -e http://localhost:3456 ``` `backup` is the bridge from rendered artifacts into Cuitty storage workflows. --- # Video E2E visibility The Video E2E path should make the whole generation pipeline inspectable: 1. YAML input 2. Parsed Composition IR 3. Rendered frame images 4. FFmpeg command and logs 5. Final encoded video 6. Summary metadata ## Run the deterministic E2E test From the video repository: ```bash cd ~/Code/cuitty/video node --import tsx --test packages/render/src/e2e.test.ts ``` The test parses a YAML composition, checks the generated IR, renders frames when Playwright and FFmpeg are available, and verifies the output artifact. ## Run the visible artifact workflow Use the visible workflow when you want to keep artifacts after the run: ```bash ./run e2e:visible ``` The visible workflow should write a timestamped directory under the E2E artifacts root. Keep that directory when debugging local failures or uploading CI artifacts. Expected files: | Artifact | Purpose | | --- | --- | | `input.yaml` | Exact DSL input used by the test. | | `composition.ir.json` | Parsed and normalized Composition IR. | | `render-options.json` | Render options passed into the renderer. | | `frames/` | Sampled frame HTML and PNG files. | | `ffmpeg/args.json` | Encoder command arguments. | | `ffmpeg/stderr.log` | Captured encoder stderr. | | `trace-events.json` | Renderer trace events from composition through encode. | | `output/e2e-smoke.mp4` | Final rendered video. | | `output/ffprobe.json` | ffprobe metadata when ffprobe is available. | | `manifest.json` | Artifact index with dimensions, frame count, codec, size, and timings. | | `summary.md` | Human-readable CI summary. | ## CI expectations CI should upload the visible artifact directory for every E2E run, including failures. That makes a failed render debuggable without reproducing the exact runner state locally. ## Troubleshooting If rendering is skipped, check for missing local prerequisites: ```bash ffmpeg -version node --import tsx --test packages/render/src/e2e.test.ts ``` Use CLI `--dry-run` to separate DSL or IR failures from browser and encoder failures: ```bash cuitty-video render composition.yaml --dry-run ``` --- # TypeScript SDK `@cuitty/sdk` is the canonical client for sending events from a TypeScript or Bun application. It is single-package, tree-shakeable, fail-silent, and batched. ## Install ```bash bun add @cuitty/sdk # or npm install @cuitty/sdk ``` ## Quickstart ```typescript import { createCuittyClient } from "@cuitty/sdk"; import { auditPlugin } from "@cuitty/sdk/plugins/audit"; const cuitty = createCuittyClient({ portalUrl: "https://app.cuitty.com", projectId: process.env.CUITTY_PROJECT_ID!, apiKey: process.env.CUITTY_API_KEY!, }); cuitty.use(auditPlugin()); cuitty.start(); await cuitty.emit({ type: "audit", timestamp: new Date().toISOString(), data: { actor: "alice@example.com", action: "secret.rotate", resource: "stripe.live_key", }, }); ``` ## Configuration ```typescript interface CuittyConfig { portalUrl: string; // Cuitty portal base URL projectId: string; // Project UUID apiKey: string; // cuitty_sk_... flushInterval?: number; // ms between flushes (default 5000) maxBufferSize?: number; // events before forced flush (default 500) timeout?: number; // per-request HTTP timeout (default 10000) debug?: boolean; // log to stderr (default false) enabled?: boolean; // kill switch (default true) } ``` ## Plugins | Plugin | Import | Captures | | ------------ | --------------------------------------- | ------------------------------------- | | `auditPlugin` | `@cuitty/sdk/plugins/audit` | HTTP request/response audit events | | `logsPlugin` | `@cuitty/sdk/plugins/logs` | Pino log records | | `deploysPlugin` | `@cuitty/sdk/plugins/deploys` | CI/CD deploy events | | `repositoryPlugin` | `@cuitty/sdk/plugins/repository` | Git metadata | | `configsPlugin` | `@cuitty/sdk/plugins/configs` | Config file change watcher | | `costsPlugin` | `@cuitty/sdk/plugins/costs` | Cloud cost metrics | ## Hono adapter ```typescript import { Hono } from "hono"; const app = new Hono(); app.use("*", cuitty.honoMiddleware()); ``` ## Lifecycle ```typescript cuitty.start(); // Begin buffering and flushing await cuitty.flush(); // Force flush (call before process exit) await cuitty.shutdown(); // Flush + stop timers ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/typescript](https://benchmarks.cuitty.com/sdks/typescript). The benchmark harness re-runs on every release tag and posts results back to the marketing site. ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) - [Python SDK](/docs/sdk/python) - [Curl examples](/docs/sdk/curl) --- # Python SDK > **Status: Preview.** The Python SDK is on the Phase 3 roadmap. Until it ships you can use [the wire protocol directly](/docs/reference/wire-protocol) — every Python HTTP client supports the small surface area required. ## Planned API ```python from cuitty import Client c = Client( portal_url="https://app.cuitty.com", project_id=PROJECT_ID, api_key=API_KEY, ) c.audit( actor="alice@example.com", action="secret.rotate", resource="stripe.live_key", ) c.flush() ``` ## Wire-protocol equivalent today ```python import hmac, hashlib, json, os, time import urllib.request body = json.dumps({ "events": [{ "type": "audit", "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "data": {"actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key"}, }], }).encode() sig = hmac.new(os.environ["CUITTY_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest() req = urllib.request.Request( "https://app.cuitty.com/api/ingest", data=body, headers={ "Authorization": f"Bearer {os.environ['CUITTY_API_KEY']}", "Content-Type": "application/json", "X-Cuitty-Signature": sig, }, ) print(urllib.request.urlopen(req).read()) ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/python](https://benchmarks.cuitty.com/sdks/python). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # Go SDK > **Status: Preview.** The Go SDK is on the Phase 3 roadmap. Use the [wire protocol](/docs/reference/wire-protocol) until then. ## Planned API ```go c := cuitty.New(cuitty.Config{ PortalURL: "https://app.cuitty.com", ProjectID: id, APIKey: key, }) c.Audit(ctx, cuitty.AuditEvent{ Actor: "alice@example.com", Action: "secret.rotate", Resource: "stripe.live_key", }) ``` ## Wire-protocol equivalent today ```go package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "os" ) func main() { body := []byte(`{"events":[{"type":"audit","ts":"2026-04-27T12:00:00Z","data":{"actor":"alice@example.com","action":"secret.rotate","resource":"stripe.live_key"}}]}`) mac := hmac.New(sha256.New, []byte(os.Getenv("CUITTY_WEBHOOK_SECRET"))) mac.Write(body) sig := hex.EncodeToString(mac.Sum(nil)) req, _ := http.NewRequest("POST", "https://app.cuitty.com/api/ingest", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("CUITTY_API_KEY")) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Cuitty-Signature", sig) http.DefaultClient.Do(req) } ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/go](https://benchmarks.cuitty.com/sdks/go). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # Rust SDK > **Status: Preview.** The Rust SDK is on the Phase 3 roadmap. ## Planned API ```rust let c = cuitty::Client::new(cuitty::Config { portal_url, project_id, api_key, }); c.audit(cuitty::AuditEvent { actor: "alice@example.com".into(), action: "secret.rotate".into(), resource: "stripe.live_key".into(), }).await?; ``` Use the [wire protocol](/docs/reference/wire-protocol) and `reqwest` + `hmac` until the typed client lands. ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/rust](https://benchmarks.cuitty.com/sdks/rust). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # SDK Parity & Divergence The Cuitty SDKs share a wire protocol but each takes the idiomatic shape of its language. This page documents the intentional differences. ## Capability matrix | Feature | TypeScript | Python | Go | Rust | | --- | --- | --- | --- | --- | | Async-first | Implicit (Promise) | Sync (async on roadmap) | Sync, ctx-aware | Async (Tokio) | | Plugin model | `client.use(plugin)` | `client.audit`, `client.logs` | `client.Audit`, `client.Logs` | `client.audit()`, `client.logs()` | | Path B retry | Yes (5xx + 429) | Yes (5xx + 429) | Yes (5xx + 429) | Yes (5xx + 429) | | Idempotency keys | Yes (UUID v4) | Yes | Yes | Yes | | Hono adapter | Yes | n/a | n/a | n/a | | Pino bridge | Yes | n/a (stdlib logging adapter on roadmap) | n/a (slog) | n/a (tracing) | | Concurrency-safe | Yes | Yes | Yes | Yes (Arc-cloned client) | ## Things that ARE the same across all four SDKs - Wire format: identical JSON bodies for the same logical event - Idempotency: UUID v4 in `X-Idempotency-Key` header on every Path B batch - Retry: 3 attempts, exponential backoff (250 ms → 1 s → 4 s) - 4xx behavior: drop with warn log, no retry - 429 behavior: honor `Retry-After` - Auth: `Authorization: Bearer ` plus `X-Project-Id` - Batch triggers: ≥1000 events or ≥1 MiB or ≥250 ms idle - Close semantics: `close()` flushes pending events synchronously If your code depends on something that's the same across SDKs but isn't on this list, it might still be safe — but it's not promised. Open an issue and we'll add it (or document why it's not promised). ## Where to find authoritative behavior The wire protocol is documented in [`packages/wire-protocol/openapi.yaml`](https://gitlab.com/cuitty/wire-protocol/openapi.yaml). The SDK-specific READMEs document divergences from that protocol in their "Spec Drift" sections. Live SDK-by-SDK benchmarks: [https://benchmarks.cuitty.com/sdks](https://benchmarks.cuitty.com/sdks). --- # cURL examples For one-off scripts, CI hooks, and shell-only environments, the wire protocol is reachable from a plain `curl`. ## Send an audit event ```bash BODY='{ "events": [{ "type": "audit", "ts": "2026-04-27T12:34:56Z", "data": { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key" } }] }' SIG=$(printf '%s' "$BODY" \ | openssl dgst -sha256 -hmac "$CUITTY_WEBHOOK_SECRET" -hex \ | awk '{print $2}') curl -X POST https://app.cuitty.com/api/ingest \ -H "Authorization: Bearer $CUITTY_API_KEY" \ -H "X-Cuitty-Signature: $SIG" \ -H "Content-Type: application/json" \ -d "$BODY" ``` ## Health check ```bash curl https://app.cuitty.com/api/health ``` ## Idempotent retries Add an `X-Cuitty-Idempotency-Key` header (any UUID). The portal deduplicates retries with the same key for 24 hours. ```bash curl -X POST https://app.cuitty.com/api/ingest \ -H "X-Cuitty-Idempotency-Key: $(uuidgen)" \ ... ``` --- # Protocol overview The SDK is a convenience layer. Everything underneath is a single HTTP endpoint accepting a batch of typed events. ``` ┌───────────────────┐ Application ───► │ @cuitty/sdk │ ─── HMAC-signed POST ───► Portal │ (or curl, etc.) │ └───────────────────┘ ``` ## Three things the SDK gives you 1. **Batching.** Events accumulate in memory and flush every 5 seconds, or on buffer fill, or on `shutdown()`. 2. **Signing.** HMAC-SHA256 over the body using your API key — no need to hand-roll OpenSSL. 3. **Plugins.** Tap into framework middleware, Pino, or your CI pipeline without rewriting your app. ## Three things the SDK does *not* do - Poll Cuitty for data — the SDK is push-only. - Retry indefinitely — failed batches drop after `maxRetries` and are logged locally. - Throw exceptions on transport errors — fail-silent is a deliberate design choice; SDK errors must never crash the host process. ## The contract The contract between SDK and portal is documented in full at [Wire protocol](/docs/reference/wire-protocol). Anything that conforms to the contract works — implement your own client in any language. --- # Wire protocol The wire protocol is the **single source of truth** for how data enters Cuitty. Every SDK, plugin, and integration is just a typed wrapper over this one endpoint. If you can sign a JSON blob and POST it over HTTPS, you can integrate Cuitty. ## Endpoint ``` POST {portalUrl}/api/ingest ``` `portalUrl` is the base URL of your portal — `http://localhost:7700` for a local install, `https://app.cuitty.com` for Cuitty Cloud, or whatever you configured for self-hosted production. ## Headers | Header | Required | Notes | | ---------------------------- | -------- | ------------------------------------------------------------------ | | `Authorization` | yes | `Bearer `. The key identifies the project. | | `Content-Type` | yes | `application/json`. | | `X-Cuitty-Signature` | yes | `hex(hmac_sha256(webhook_secret, body))` — verifies authenticity. | | `X-Cuitty-Idempotency-Key` | no | UUID. Repeated POSTs with the same key are deduplicated for 24 h. | ## Body ```json { "events": [ { "type": "audit", "ts": "2026-04-27T12:34:56Z", "data": { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key" } } ] } ``` `events` is a JSON array. Each entry has: - `type` — one of `audit`, `config`, `cost`, `deploy`, `error`, `feature_flag`, `log`, `performance`, `repository`, `tenant`, `trace`, `video`, or `webhook`. Determines which module receives the event. - `ts` — ISO 8601 timestamp in UTC. - `data` — module-specific payload. See the [event reference](#event-types) below. ## Response ```json { "accepted": 1, "rejected": [] } ``` If any event in the batch is malformed, the portal accepts the valid ones and reports the rest: ```json { "accepted": 2, "rejected": [ { "index": 1, "reason": "data.actor: required" } ] } ``` ## Status codes | Code | Meaning | | ---- | --------------------------------------------------------------------- | | 200 | Batch processed — see body for per-event accept/reject breakdown. | | 400 | Body is not valid JSON, or `events` is missing. | | 401 | Missing or invalid `Authorization` bearer. | | 403 | Signature does not match. | | 413 | Batch exceeds the 5 MB size limit. Split into smaller batches. | | 429 | Rate limit hit. Retry with exponential backoff. | | 503 | The portal is shedding load. The SDK retries; raw clients should too. | ## Event types ### audit ```json { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key", "method": "POST", "path": "/api/secrets/rotate", "statusCode": 200, "duration": 32, "ip": "203.0.113.5", "userAgent": "Mozilla/5.0 ...", "metadata": {} } ``` ### log ```json { "level": "info", "message": "request processed", "module": "api", "fields": { "requestId": "abc" } } ``` ### deploy ```json { "service": "api", "version": "v1.4.2", "environment": "production", "status": "succeeded", "commit": "ab12cd34" } ``` ### repository ```json { "provider": "github", "repo": "acme/api", "branch": "main", "commit": "ab12cd34", "author": "alice" } ``` ### config ```json { "path": "config/production.toml", "diff": "--- old\n+++ new\n@@ -1 +1 @@\n-foo=1\n+foo=2", "actor": "alice" } ``` ### cost ```json { "provider": "gcp", "service": "compute", "region": "us-central1", "amount": 1234.56, "currency": "USD", "period": "2026-04" } ``` ### performance ```json { "service": "api", "metric": "p99_latency", "value": 142.3, "unit": "ms" } ``` ### trace ```json { "traceId": "trace_01", "spanId": "span_01", "serviceName": "portal", "name": "GET /api/projects", "durationMs": 42 } ``` ### error ```json { "name": "TypeError", "message": "Cannot read properties of undefined", "release": "portal@0.3.0", "environment": "production" } ``` ### feature_flag ```json { "key": "checkout_redesign", "enabled": true, "rollout": 25, "actor": "alice@example.com" } ``` ### webhook ```json { "destination": "deploy-alerts", "url": "https://example.com/hooks/cuitty", "statusCode": 200, "attempt": 1 } ``` ### tenant ```json { "projectId": "proj_01", "plan": "team", "ingestLimitPerSecond": 1000 } ``` ### video ```json { "jobId": "video_01", "targetUrl": "https://app.cuitty.com", "status": "completed", "artifactUrl": "https://cdn.cuitty.com/videos/video_01.mp4" } ``` ## Signing ``` signature = hex(hmac_sha256(webhook_secret, body)) ``` The `webhook_secret` is generated alongside the API key in the portal. Send the hex digest with no `sha256=` prefix — the portal expects the bare hex string. A reference implementation in TypeScript: ```typescript import { createHmac } from "node:crypto"; function sign(body: string, secret: string): string { return createHmac("sha256", secret).update(body).digest("hex"); } ``` ## Idempotency Pass a UUID in `X-Cuitty-Idempotency-Key` to make retries safe. The portal stores the key for 24 hours and short-circuits duplicate POSTs to a 200 with the original response body. ## Rate limits The default per-project rate limit is **1,000 events per second**, **100 batches per second**, **5 MB per batch**. All three are configurable in self-hosted installs via `INGEST_RATE_LIMIT_*` environment variables. --- # API reference The Cuitty portal exposes a small REST surface beyond `/api/ingest` for project metadata, health, and module-specific reads. Every endpoint requires a bearer token unless explicitly noted. ## Health ``` GET /api/health ``` Returns `200 { "status": "ok" }` when the portal can reach Postgres and SpiceDB. Used by load balancers and the SDK during connection probes. Public — no auth required. ## Projects ``` GET /api/projects List projects in the org POST /api/projects Create a project GET /api/projects/{id} Read a project PATCH /api/projects/{id} Update a project DELETE /api/projects/{id} Soft-delete a project ``` ## API keys ``` GET /api/projects/{id}/keys List API keys POST /api/projects/{id}/keys Create an API key (returns key once) DELETE /api/projects/{id}/keys/{kid} Revoke a key ``` ## Module-specific reads Each module exposes its own read endpoints under `/api/modules/{module}/`. See the [module documentation](/docs/modules/audit) for the full surface. | Module | Base path | | ------------ | ------------------------------- | | Audit | `/api/modules/audit/` | | Costs | `/api/modules/costs/` | | Deploys | `/api/modules/deploys/` | | Logs | `/api/modules/logs/` | | Configs | `/api/modules/configs/` | | Repository | `/api/modules/repository/` | | Performance | `/api/modules/performance/` | | Traces | `/api/traces/` | | Errors | `/api/errors/` | | Feature Flags | `/api/flags/` | | Webhooks | `/api/webhooks/` | | Tenants | `/api/tenants/` | | Video | `/api/video/` | ## OpenAPI A live OpenAPI 3.1 document is published at `/api/openapi.json` on every running portal. --- # CLI reference `cui` is the official command-line companion. It wraps the REST API for everyday operator tasks. ## Install ```bash curl -fsSL https://releases.cuitty.com/cli/install.sh | sh ``` ## Auth ```bash cui login --portal https://app.cuitty.com cui logout cui whoami ``` ## Projects ```bash cui project list cui project create cui project use cui project keys create ``` ## Modules ```bash cui audit list --project --since 24h cui logs tail --project --service api cui deploys list --env production cui costs report --period 2026-04 ``` ## Send a one-off event ```bash cui send audit \ --actor alice@example.com \ --action secret.rotate \ --resource stripe.live_key ``` ## Self-hosted operator commands ```bash cui admin migrate # Run pending migrations cui admin backup # Snapshot Postgres + libSQL cui admin verify-chain # Verify the audit-chain integrity ``` --- # Audit module The audit module records every administrative action with a hash-chained log entry — a single mutation cannot be silently rewritten without breaking the chain. ## What it captures - Actor (user ID + email) - Action verb (e.g. `project.create`, `secret.rotate`) - Resource type and id - Before/after diff of the resource - HTTP method, path, status code, duration - IP and User-Agent - Optional metadata (auth method, scopes) ## How to send events The TypeScript audit plugin instruments your framework middleware automatically. From any other language, POST a `type: "audit"` event to `/api/ingest`. See [Wire protocol](/docs/reference/wire-protocol#audit) for the schema. ## Hash chain Every audit entry stores `prev_hash = hash(prev_entry || row_payload)`. The portal exposes `cui admin verify-chain` to re-walk and verify the chain. Tampering with a row breaks every subsequent hash. ## Reads | Endpoint | Returns | | ---------------------------------------- | -------------------------------------- | | `GET /api/modules/audit/events` | Paginated list of recent audit events | | `GET /api/modules/audit/events/{id}` | A single event with full payload | | `GET /api/modules/audit/verify` | Chain-integrity report | --- # Costs module The costs module aggregates daily spend from cloud billing exports and per-service tags. It is provider-agnostic — you push the rolled-up `cost` events to `/api/ingest` and Cuitty handles storage, attribution, and alerting. ## Supported providers - GCP (Cloud Billing export to BigQuery) - AWS (Cost and Usage Report) - Azure (Cost Management exports) - DigitalOcean (Billing API) ## Event shape ```json { "type": "cost", "ts": "2026-04-27T00:00:00Z", "data": { "provider": "gcp", "service": "compute", "region": "us-central1", "amount": 1234.56, "currency": "USD", "period": "2026-04" } } ``` ## Budgets Define budgets per project, service, or tag. The portal emits a webhook and an in-app alert when a budget crosses 50%, 80%, and 100%. --- # Deploys module The deploys module records every deployment event — when, where, what version, and whether it succeeded. It pairs naturally with CI pipelines and feature-flag systems. ## Event shape ```json { "type": "deploy", "ts": "2026-04-27T13:00:00Z", "data": { "service": "api", "version": "v1.4.2", "environment": "production", "status": "succeeded", "commit": "ab12cd34", "duration": 87 } } ``` ## Integrations Built-in plugins exist for GitHub Actions, GitLab CI, CircleCI, and Buildkite. Each emits the deploy event automatically when your pipeline reaches the `deploy` step. --- # Logs module The logs module is a low-noise structured log store. It indexes by `level`, `module`, and `service`, but does not attempt to compete with full-text logging stores. Cuitty assumes you keep heavy log volumes elsewhere — ship the high-signal subset here. ## Event shape ```json { "type": "log", "ts": "2026-04-27T13:00:00Z", "data": { "level": "info", "message": "request processed", "module": "api", "service": "api", "fields": { "requestId": "abc", "userId": "user_123" } } } ``` ## Pino bridge The TypeScript SDK ships a Pino multistream adapter so adding Cuitty as a destination is one line: ```typescript cuitty.use(logsPlugin({ logger: pino, minLevel: "info" })); ``` --- # Configs module The configs module records every configuration file change — who, what, when, and the unified diff. It is intentionally **not** a config server; it observes the changes and records them for audit and rollback. --- # Repository module The repository module ingests Git metadata events — commits, branches, tags — to give an at-a-glance view of activity across your repos. --- # Performance module The performance module captures lightweight process-level metrics: CPU, memory, event-loop lag, GC pauses. It is **not** an APM — it complements Datadog/Honeycomb rather than replacing them. --- # Traces module The traces module accepts OTLP-style JSON spans and stores them in PostgreSQL tables keyed by project, trace id, and timestamp for request waterfall and service-map views. ## What it captures - Trace id, span id, and parent span id - Service name, operation name, and span kind - Start and end timestamps - Status code and status message - Attributes, resource attributes, and events ## Ingest ``` POST /api/traces/ingest ``` The JSON path is the first supported transport. OTLP protobuf support is still planned. ## Storage Self-hosted installs use stock PostgreSQL. High-volume span tables are partitioned by timestamp, and the portal reads rollup tables for summary views so trace detail remains available without requiring a PostgreSQL extension. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/traces/status` | Module health | | `GET /api/traces?project_id=...` | Recent traces | | `GET /api/traces/{traceId}` | A trace waterfall | Product page: [Traces](/product/modules/traces). --- # Errors module The errors module captures exceptions, computes a stable fingerprint, groups related occurrences, and exposes grouped reads for triage. ## What it captures - Exception name, message, and stack trace - Release, environment, service, and request context - Fingerprint and group status - First seen, last seen, and occurrence counts ## Capture ``` POST /api/errors/capture ``` Unknown fields are kept in metadata so SDKs can ship richer context without a schema migration for every framework. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/errors/status` | Module health | | `GET /api/errors/groups?project_id=...` | Grouped exceptions | | `GET /api/errors/groups/{fingerprint}` | Group detail and recent occurrences | Product page: [Errors](/product/modules/errors). --- # Feature Flags module The Feature Flags module is a control-plane module for rollout decisions, not just an SDK utility. It manages flag definitions, group-level rollout state, cohort gates, kill switches, owner/developer self-overrides, and transactional history from the same permissioned surface as the rest of Cuitty. ## V2 model - Flags remain stable keys such as `new_checkout` or `admin_panel`. - Groups collect related flags, so an operator can disable or kill every flag in a rollout area without editing each flag. - Group kill and flag kill are emergency stops and always evaluate false. - Owner/developer self-overrides can opt one actor in or out without changing the default rollout. - Route-level checks use the Feature Flags permission vocabulary and are designed to sit behind SpiceDB-backed project permissions. - Local and staging environments can inspect and toggle flags from the Cuitty toolbar once the toolbar is installed. ## Evaluation SDK helpers evaluate locally using the same rule as the server and CUI preview: 1. Killed or disabled groups return false. 2. Killed flags return false. 3. Active self-overrides return their stored value. 4. Disabled flags return false. 5. Non-empty cohort dimensions must match the evaluation context. 6. 100% rollouts return true, 0% rollouts return false. 7. Percentage rollouts use SHA-256 of `flagKey:userId`, bucketed into 0-99. Self-overrides do not bypass a group kill switch. ## SDK examples TypeScript: ```ts const flags = createFlagsPlugin(); cuitty.use(flags); const enabled = flags.isEnabled("new_checkout", { userId: "user_123", env: "staging", role: "developer", }); ``` Python: ```python enabled = client.flags.is_enabled( "new_checkout", user_id="user_123", env="staging", role="developer", ) ``` Go: ```go enabled, err := client.Flags.IsEnabled(ctx, "new_checkout", cuitty.EvalContext{ "userId": "user_123", "env": "staging", "role": "developer", }) ``` Rust: ```rust let mut ctx = cuitty::plugins::flags::EvalContext::new(); ctx.insert("userId".into(), "user_123".into()); ctx.insert("env".into(), "staging".into()); ctx.insert("role".into(), "developer".into()); let enabled = client.flags().is_enabled("new_checkout", &ctx).await?; ``` ## API | Endpoint | Returns | | --- | --- | | `GET /api/flags/status` | Module health | | `GET /api/flags?project_id=...` | Flags for a project | | `GET /api/flags/{key}?project_id=...` | A single flag | | `GET /api/flags/groups?project_id=...` | Groups for a project | | `POST /api/flags/groups/{key}/kill` | Group emergency kill | | `PUT /api/flags/{key}/self-override` | Set the caller's self-override | | `GET /api/flags/stream?project_id=...` | Server-sent update stream | Product page: [Feature Flags](/product/modules/feature-flags). --- # Webhooks module The webhooks module gives Cuitty modules one shared delivery lane for customer-owned HTTP endpoints, Slack-style integrations, and internal notifications. ## Delivery model - Endpoints own their signing secret. - Deliveries record request and response metadata. - Retries are explicit attempts with their own status. - Secret rotation keeps old attempts inspectable. ## API | Endpoint | Returns | | --- | --- | | `GET /api/webhooks/status` | Module health | | `GET /api/webhooks/endpoints?project_id=...` | Endpoint list | | `POST /api/webhooks/deliver` | Internal delivery trigger | | `POST /api/webhooks/endpoints/{id}/rotate` | Rotate endpoint secret | Product page: [Webhooks](/product/modules/webhooks). --- # Tenants module The tenants module stores Cuitty-specific project configuration for multi-tenant installs, including ingest limits, retention defaults, and enabled module policy. ## What it manages - Project configuration - Ingest rate limits - Retention defaults - Enabled module lists - Admin-facing tenant metadata ## API | Endpoint | Returns | | --- | --- | | `GET /api/tenants/status` | Module health | | `GET /api/tenants/projects` | Project administration list | | `GET /api/tenants/projects/{projectId}/config` | Project config | | `PATCH /api/tenants/projects/{projectId}/config` | Update project config | Product page: [Tenants](/product/modules/tenants). --- # Plaintext Secrets module The plaintext secrets module scans source code across all projects in your workspace to detect hardcoded secrets — API keys, database credentials, tokens, and private keys that should never be committed. ## How it works Point the scanner at one or more root directories (e.g. `~/Code`). It walks every file that isn't generated (skipping `node_modules`, `target/`, `.venv`, `dist/`, etc.), matches against 53 secret detection patterns, and optionally runs Shannon entropy analysis to catch novel credential formats. Results are persisted in a dedicated SQLite database. Each finding tracks the file, line, column, pattern that matched, severity, and temporal metadata — when the secret was first detected, when it was last seen, and whether it's still present. ## Supported languages and bundlers | Ecosystem | Scanned files | | ---------- | ----------------------------------------------- | | JavaScript | `.ts`, `.js`, `.tsx`, `.jsx`, `.env*`, `.npmrc` | | Rust | `.rs`, `.toml`, `Cargo.toml` | | Python | `.py`, `.cfg`, `.ini`, `.env*` | | Go | `.go`, `.yaml`, `.yml`, `.env*` | | Config | `.json`, `.yaml`, `.yml`, `.toml`, `.properties` | Bundler-specific patterns detect tokens in `.npmrc`, `bunfig.toml`, `Cargo` registry config, PyPI tokens, and Docker registry credentials. ## Pattern categories - **Cloud** — AWS access/secret keys, GCP service accounts and API keys, Azure connection strings, Cloudflare, DigitalOcean - **SCM** — GitHub PATs (classic and fine-grained), GitLab PATs, Bitbucket app passwords - **Payments** — Stripe secret/publishable/webhook keys, PayPal client secrets - **Database** — Connection strings with credentials (Postgres, MongoDB, Valkey, MySQL), Turso, Supabase, Firebase - **Messaging** — Slack bot/user/app tokens, Discord tokens, Twilio, SendGrid - **Crypto** — PEM private keys (RSA, EC, PKCS8), JWTs, PGP/SSH private keys - **Generic** — Password assignments, API key assignments, bearer tokens, basic auth headers - **Bundler** — npm auth tokens, Cargo registry tokens, PyPI API tokens, Docker registry passwords ## Extraction modes The standalone JSON index can be configured with three extraction modes: | Mode | Stores | Use case | | ---------------- | ------------------------- | ---------------------------------- | | `key-only` | Key name only | Auditing without exposure risk | | `key-obfuscated` | Key name + masked value | Triage with partial visibility | | `key-raw` | Key name + full value | Remediation workflows | ## Cross-reference engine When the same secret value appears in multiple files or projects, the scanner groups them by SHA-256 hash. The dashboard highlights shared secrets so you can see which projects share credentials and prioritize rotation. ## Entropy detection Beyond pattern matching, the scanner uses Shannon entropy analysis to flag high-entropy strings that may be novel credential formats. Configurable threshold (default: 4.5 bits), minimum length (20 chars), and automatic exclusion of UUIDs, URLs, commit hashes, and long base64 payloads. ## API endpoints | Endpoint | Method | Returns | | ---------------------------------------------------------- | ------ | ------------------------------------ | | `/api/modules/plaintext-secrets/findings` | GET | Paginated findings with filters | | `/api/modules/plaintext-secrets/findings/{id}` | GET | Single finding with cross-references | | `/api/modules/plaintext-secrets/findings/{id}` | POST | Suppress a finding | | `/api/modules/plaintext-secrets/scans` | GET | Scan history | | `/api/modules/plaintext-secrets/scan` | POST | Trigger a new scan | ## Integration events The module emits events via the shared `ModuleEventBus`: - `secret:detected` — fired for each new finding during a scan - `cuitty.plaintext-secrets.scan-completed` — fired when a scan finishes, with summary stats --- # Video module The video module records browser workflows as reviewable artifacts. A job starts from a prompt, target URL, or explicit steps, then a browser runner captures screenshots and writes a video artifact when the run finishes. ## What it captures - Prompt and target URL - Resolved workflow steps - Run status and per-step progress - Duration, output format, and artifact URL - Error details when a run fails ## Submit a job ``` POST /api/video/jobs ``` Use the endpoint for long-running video generation requests. Ingest events with `type: "video"` are reserved for completed-run metadata that should appear in timelines and audit-style views. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/video/status` | Module health | | `GET /api/video/jobs?project_id=...` | Recent jobs | | `GET /api/video/jobs/{id}` | Job status, steps, and artifact metadata | Product page: [Video Generator](/product/modules/video). --- # Auth app The auth app owns sign-in, session cookies, API-key issuance, and role checks used by the portal and admin surfaces. It is infrastructure for the rest of Cuitty rather than a standalone observability module. ## Responsibilities - Bootstrap the first administrator for a new install - Issue and revoke project API keys - Maintain session state for browser users - Expose authorization context to app and module routes ## Operational notes Production installs should run the portal over HTTPS so browser cookies can be marked secure. API keys are shown once when created; store them in your secret manager and rotate them from the portal when needed. --- # Portal app The portal app is the main browser UI for Cuitty. It hosts project setup, API-key management, module views, and operational review workflows. ## Responsibilities - Project and tenant selection - Module read views and status pages - API-key creation and revocation - Links to audit, deploy, cost, log, trace, and error records ## Runtime notes Local installs serve the portal at `http://localhost:7700` by default. Self-hosted production installs should put the portal behind TLS and configure the public base URL used by SDKs and webhook destinations. --- # Admin app The admin app covers operator-only tasks that should not be mixed into day-to-day project views. It pairs with CLI commands for maintenance and verification. ## Responsibilities - Migration status and install health checks - Audit-chain verification summaries - Tenant and project administration - Backup and restore guidance for self-hosted installs ## Access model Admin actions require an authenticated operator with the required role. Changes that affect projects, keys, tenants, or module configuration should also produce audit records. --- # Forge app The forge app supports project-idea workflows: collect keywords, rank candidate names, and create a repository or workspace when the operator chooses one. ## Responsibilities - Store brainstorming sessions and selected candidates - Create or attach repositories for accepted ideas - Hand created workspaces to the repository module for indexing - Keep session state so interrupted work can resume ## Boundaries Forge records workflow metadata. Repository analysis, commit metadata, and workspace indexing belong to the repository module. --- # Site app The site app serves the public Cuitty website and documentation. It is the place for install guides, SDK notes, module docs, reference pages, changelog entries, and comparison pages. ## Responsibilities - Render docs from the content collection - Publish machine-readable docs as Markdown and JSON - Keep product copy aligned with implemented modules - Link readers to install, SDK, and API reference material ## Content rules Docs should describe implemented behavior plainly. When a feature is planned, mark it as planned or omit it until the implementation exists. --- # Store Quickstart ## Install ```bash # npm npm install @cuitty/store # bun bun add @cuitty/store # pnpm pnpm add @cuitty/store ``` ## Create a store ```ts import { createStore } from "@cuitty/store"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", }); ``` ## Basic CRUD ```ts // Put a record await store.records.put("users/alice", { name: "Alice", role: "admin", }); // Get a record const alice = await store.records.get("users/alice"); // List records by prefix const users = await store.records.list("users/"); // Delete a record await store.records.delete("users/alice"); ``` ## Enable sync Add a `sync` block to push local writes to a remote backend automatically. Writes never block -- sync runs in the background. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` See [Sync & Conflict Resolution](/docs/store/sync) for strategies and conflict handling. ## Enable encryption Set `encrypt: true` to encrypt all data before it leaves the device. The sync server never sees plaintext. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` See [End-to-End Encryption](/docs/store/encryption) for key management and zero-knowledge architecture. --- # Storage Adapters Store uses pluggable adapters for storage backends. Every adapter exposes the same API (`store.records`, `store.kv`, `store.blobs`, `store.events`), so you can swap backends without changing application code. ## SQLite The default adapter. Ideal for local-first apps, development, and mobile. ```ts import { createStore } from "@cuitty/store"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", }); ``` SQLite stores are single-file, need no external process, and support WAL mode for concurrent reads out of the box. ## Postgres Production-grade durability with full SQL support. Best for server-side workloads. ```ts const store = await createStore({ name: "my-app", adapter: "postgres", connection: { host: "localhost", port: 5432, database: "store", user: "store", password: process.env.PG_PASSWORD, }, }); ``` ### Connection pooling Pass `pool: { max: 20 }` to control the connection pool size. The default is 10. ## S3 Blob storage adapter for large files and binary data. ```ts const store = await createStore({ name: "my-app", adapter: "s3", bucket: "my-store-bucket", region: "us-east-1", credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }); ``` The S3 adapter maps `store.blobs` operations to S3 objects and `store.records` to JSON objects stored under a configurable prefix. ## P2P Peer-to-peer replication with no central server. Peers discover each other and sync directly. ```ts const store = await createStore({ name: "my-app", adapter: "p2p", discovery: { method: "mdns", // or "dht" for wide-area topic: "my-app-sync", }, }); ``` The P2P adapter uses CRDTs internally to merge writes from multiple peers without conflicts. ## Custom adapters Implement the `StoreAdapter` interface to build your own backend. ```ts import type { StoreAdapter } from "@cuitty/store"; const myAdapter: StoreAdapter = { async get(key: string) { /* ... */ }, async put(key: string, value: unknown) { /* ... */ }, async delete(key: string) { /* ... */ }, async list(prefix: string) { /* ... */ }, }; const store = await createStore({ name: "my-app", adapter: myAdapter, }); ``` All four methods must return promises. `list` returns an async iterable of `{ key, value }` pairs. --- # Sync & Conflict Resolution ## How sync works Store is offline-first. Writes go to the local store immediately and never block on network. A background sync queue pushes changes to the remote and pulls remote changes back. ``` local write --> local store --> sync queue --> remote ^ | pull remote changes ``` ## Configuration ```ts import { createStore } from "@cuitty/store"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", sync: { remote: "postgres", strategy: "last-write-wins", interval: 5000, // sync every 5 seconds (default: 10000) }, }); ``` ### Options | Option | Type | Default | Description | | ------------ | -------- | ------------------ | ----------------------------------- | | `remote` | `string` | -- | Remote adapter type or connection | | `strategy` | `string` | `"last-write-wins"`| Conflict resolution strategy | | `interval` | `number` | `10000` | Sync interval in milliseconds | ## Strategies ### Last-write-wins (LWW) The simplest strategy. Each record carries a timestamp; the most recent write wins on conflict. ```ts sync: { remote: "postgres", strategy: "last-write-wins", } ``` Good for settings, preferences, and data where overwrites are acceptable. ### CRDTs Conflict-free replicated data types that merge automatically without data loss. Best for collaborative data. ```ts sync: { remote: "postgres", strategy: "crdt", } ``` Store uses operation-based CRDTs under the hood. Counters, sets, and maps merge deterministically across peers. ### Custom merge Supply a function that receives both versions and returns the resolved value. ```ts sync: { remote: "postgres", strategy: "custom", merge(local, remote) { // Example: keep the record with more fields const localKeys = Object.keys(local.value); const remoteKeys = Object.keys(remote.value); return localKeys.length >= remoteKeys.length ? local : remote; }, } ``` ## Conflict resolution callbacks Register a callback to handle conflicts interactively or log them. ```ts store.events.on("conflict", (event) => { console.log(`Conflict on key: ${event.key}`); console.log("Local:", event.local); console.log("Remote:", event.remote); console.log("Resolved:", event.resolved); }); ``` ## Offline-first behavior When the device is offline, Store continues to accept reads and writes against the local store. Changes accumulate in the sync queue and flush automatically when connectivity returns. The sync queue is persisted to disk, so pending changes survive app restarts. ## Monitoring sync status ```ts const status = store.sync.status(); // { state: "synced" | "syncing" | "offline", pending: number, lastSync: Date } store.events.on("sync", (event) => { console.log(`Synced ${event.pushed} up, ${event.pulled} down`); }); ``` --- # End-to-End Encryption ## Enable encryption ```ts import { createStore } from "@cuitty/store"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, }); ``` When `encrypt: true` is set, all data is encrypted on the device before it is written to disk or sent over the network. The sync server, storage backend, and any intermediary never see plaintext. ## How it works Store generates a 256-bit device key on first run and stores it in the OS keychain (or a local keyfile as fallback). Every record is encrypted with AES-256-GCM using a per-record nonce derived from the key and the record's path. ``` plaintext --> AES-256-GCM encrypt --> ciphertext --> store / sync ``` Reads reverse the process transparently. Application code works with plain objects -- encryption and decryption are invisible to the caller. ## Key management ### Device keys Each device generates its own key on first use. The key never leaves the device unless explicitly exported. ```ts // Export the device key (for backup or migration) const exportedKey = await store.crypto.exportKey(); // Import a key on a new device const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, key: exportedKey, }); ``` ### Key rotation Rotate the encryption key without downtime. Store re-encrypts existing records in the background. ```ts await store.crypto.rotateKey(); ``` After rotation the old key is kept in a sealed keyring so previously-synced peers can still decrypt historical data until they receive the new key. ## Encrypted sync When encryption and sync are both enabled, peers exchange ciphertext. Decryption happens only on the receiving device using a shared key. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` ### Sharing keys between peers Peers use a Diffie-Hellman key exchange to establish a shared secret. No key material is transmitted in the clear. ```ts // On device A: generate an invite const invite = await store.crypto.createInvite(); // --> send `invite.code` to device B out-of-band // On device B: accept the invite await store.crypto.acceptInvite(invite.code); ``` Once paired, both devices derive the same data key and can decrypt each other's records. ## Zero-knowledge architecture The sync server stores only ciphertext and opaque metadata (record size, sync timestamps). It cannot: - Read record contents - Identify record types or schemas - Correlate records across stores (keys are encrypted too) Even if the server is compromised, an attacker gains no usable data without the device keys. --- # Tests Quickstart Cuitty Tests lets you write declarative HTTP smoke tests in `.ctest` files, run them from the CLI or CI, and review results in Test Studio. This guide takes you from install to a passing suite in under five minutes. ## Install the CLI ```bash curl -fsSL https://cuitty.com/install.sh | sh cui --version ``` Or install with npm: ```bash npm install -g @cuitty/cli ``` ## Write your first .ctest file Create `smoke.ctest` in your project root. Cuitty tests use an HCL-based DSL: ```hcl suite "api-smoke" { description = "Verify core API endpoints return 200" test "healthcheck" { request { method = "GET" url = "${env.BASE_URL}/health" } assert { status = 200 } } test "list-projects" { request { method = "GET" url = "${env.BASE_URL}/api/projects" headers = { Authorization = "Bearer ${env.CUITTY_API_KEY}" } } assert { status = 200 json = "$.data | length > 0" } } } ``` ## Run the suite Execute the test file against your running environment: ```bash cui tests run smoke.ctest --env BASE_URL=http://localhost:7700 ``` The CLI prints a pass/fail summary for each test. Use `--verbose` to see full request and response bodies. ## View results in Test Studio If you have the Cuitty portal running, results are automatically pushed to Test Studio. Open the portal and navigate to **Tests > Runs** to see the timeline, latency breakdown, and assertion details for every request. ## Add tests to CI Drop a step into your GitHub Actions workflow: ```yaml - name: Run Cuitty tests uses: cuitty/test-action@v1 with: files: "**/*.ctest" env: | BASE_URL=${{ vars.STAGING_URL }} CUITTY_API_KEY=${{ secrets.CUITTY_API_KEY }} fail-on: "any" ``` The action uploads results to Test Studio and fails the workflow if any assertion breaks. ## What's next - [Test DSL reference](/product/tests#dsl) — full syntax for suites, tests, variables, and assertions - [Test Studio](/product/tests#studio) — dashboard features, diffing, and trend analysis - [CI integration](/product/tests#ci) — GitHub Actions, GitLab CI, and generic webhook triggers - [Runner configuration](/product/tests#runner) — parallelism, retries, timeouts, and environment files --- # Toolbar Quickstart The Cuitty Toolbar is a floating dev-tools widget that embeds directly into your application. It gives your team instant access to logs, deploys, feature flags, secrets, and cost data without leaving the app. This guide covers three ways to add it. ## Script tag (fastest) Drop a single script tag into your HTML ``: ```html ``` Reload the page. The toolbar launcher appears in the bottom-right corner. No build step required. ## Astro integration Install the integration: ```bash npm install @cuitty/toolbar-astro ``` Add it to your Astro config: ```ts // astro.config.mjs import { defineConfig } from "astro/config"; import cuittyToolbar from "@cuitty/toolbar-astro"; export default defineConfig({ integrations: [ cuittyToolbar({ project: "your-project-id", env: "staging", }), ], }); ``` The integration injects the toolbar script automatically in dev and preview modes. Pass `environments: ["production"]` to include it in production builds. ## React component Install the React package: ```bash npm install @cuitty/toolbar-react ``` Render the component at the root of your app: ```tsx import { CuittyToolbar } from "@cuitty/toolbar-react"; export default function App() { return ( <> {/* rest of your app */} ); } ``` The component renders nothing visible in the DOM — it loads the toolbar script and initializes it with your configuration. ## What you'll see Once loaded, the toolbar shows a floating launcher with five icons: 1. **Logs** — live tail of application logs filtered to the current page 2. **Deploys** — recent deploy history with commit links and rollback buttons 3. **Flags** — feature flag overrides scoped to your session 4. **Secrets** — masked secret values with copy and rotation shortcuts 5. **Costs** — real-time cost breakdown for the current project Click any icon to open its panel. The toolbar communicates with the Cuitty portal API using your project ID, so the portal must be reachable from the browser. ## Configuration basics Pass configuration as `data-` attributes (script tag) or props (component). The most common options: | Option | Default | Description | | ----------- | --------------- | ---------------------------------------- | | `project` | — | Your Cuitty project ID (required) | | `env` | `"development"` | Environment label shown in the toolbar | | `position` | `"bottom-right"`| Launcher position on screen | | `modules` | all | Array of module names to enable | | `theme` | `"auto"` | `"light"`, `"dark"`, or `"auto"` | ## What's next - [Toolbar features](/product/toolbar#features) — full list of modules, keyboard shortcuts, and customization options - [Toolbar product page](/product/toolbar) — architecture overview, demo, and SDK reference ---