# Continuity CLI reference Source: https://docs.omegas.dev/continuity/cli Every subcommand, flag and exit code of the @omegas/continuity command line. ```text theme={null} continuity [options] scan Read every declared configuration surface and print what was found report The same scan, rendered as a multi-section report compat The derived compatibility matrix, its losses, and per-item exceptions export Write a redacted, content-addressed bundle you can share diff Preview exactly what importing a bundle would write. Writes nothing import Walk the same plan and apply only what you consent to, item by item enable Turn on one quarantined item, after showing you what it is ``` ## Options | Option | Applies to | What it does | | --------------------------- | --------------------------------- | -------------------------------------------------------------------- | | `--home ` | all | Home directory to read (default: your home directory) | | `--root ` | all | Project root to scan for project-scope config; repeatable | | `--json` | all | Emit the machine-readable result envelope on stdout | | `--max-file-bytes ` | all | Per-file read cap; a breach is truncated and reported, never dropped | | `--out ` | `export` | Where to write the bundle | | `--payload-policy

` | `export` | `definition`, `definition+scripts`, or `full` | | `--bundle ` | `diff`, `import`, `report --html` | The bundle to read | | `--html ` | `report` | Write one self-contained HTML page rendered from `--bundle` | | `--from ` / `--to ` | `compat` | Restrict the matrix to one direction | | `--yes-inert` | `import` | Bulk-accept inert additions only | `--yes-inert` can never cover an authority change, an executable item, or a write that replaces content you already have. There is no blanket `--yes` for `import`. ## Exit codes Exit codes are part of the published contract; the hosted layer attaches to this CLI through exactly two things, the bundle format and these codes plus the `--json` envelope. | Code | Meaning | | ---- | ------------------------------------------------------------------------------------------------ | | `0` | Success | | `1` | Usage error | | `2` | No supported runtime found | | `3` | Completed with warnings | | `5` | The export gate refused: a high-confidence secret reached the serializer, so nothing was written | | `6` | Integrity failure | | `7` | Import blocked on containment | | `8` | Import aborted on drift | | `9` | Import rolled back | | `10` | A runtime older than this tool supports | ## The hosted flow's own flags Run with no subcommand, the CLI enters the optional hosted transfer flow. Its flags are separate on purpose: | Option | What it does | | ----------------- | ----------------------------------------------------------------------- | | `--root

` | Root to scan; repeatable; defaults to the current directory | | `--output ` | Sensitive local preview path (default: `~/.omegas/state/previews`) | | `--dry-run` | Discover and write the preview without contacting Ωmegas | | `--no-open` | Print the browser link without opening it | | `--yes` | Open the browser automatically; secrets still require a separate answer | The development-only `--api` override requires `--unsafe-development-api` and accepts only HTTPS or loopback origins. # Continuity Source: https://docs.omegas.dev/continuity/overview Move a Claude Code or Codex setup without moving a secret. Open source, local-first, MIT. Continuity is a local CLI for understanding and moving the configuration that shapes an AI coding setup. It shows what is present, which value wins, what can move, and what should stay behind. ## The everyday loop ```sh theme={null} npx @omegas/continuity scan --root ~/Code npx @omegas/continuity report --root ~/Code npx @omegas/continuity export --out ./continuity.ocb.jsonl npx @omegas/continuity diff --bundle ./continuity.ocb.jsonl ``` 1. **Scan** the global and project surfaces you name. 2. **Report** the effective value and the contributors behind it. 3. **Export** a redacted bundle that can be inspected and shared. 4. **Diff** the proposed import. It writes nothing. 5. **Import** only after reviewing the recorded plan. The core is local-first: it does not need an account or a network connection for these commands. Requires Node.js 20 or newer. **Current package** — `@omegas/continuity` 0.2.0 is published on npm. The source is public at [github.com/OmegaAgent/continuity](https://github.com/OmegaAgent/continuity). ## What gets explained Continuity reads the setup surfaces that commonly drift between machines: instructions, skills, commands, subagents, hooks, MCP servers, memories, settings, permission rules, and sandbox profiles. `report` makes precedence visible instead of flattening it into one unexplained value. ## What moves safely `compat` classifies each item as `NATIVE`, `CONVERT`, `ADVISE`, `UNSUPPORTED`, or `UNKNOWN`. `export` redacts credential values, and `diff` shows the exact write plan before import. Executable items arrive inert; authority changes are shown as explicit review points. ## Know the boundary * Cross-runtime conversion is preview-only; same-runtime imports are the only writes available today. * Transcripts, chat history, and session state are intentionally out of scope. * Credential detection is defence in depth. Read a bundle before sharing it. For the detailed threat model, see [Security model](/continuity/security). For every flag, see the [CLI reference](/continuity/cli). # Continuity quickstart Source: https://docs.omegas.dev/continuity/quickstart Scan, explain, export and re-apply an agent setup in five commands. Everything on this page runs locally and contacts nothing. Everything on this page is local. The subcommands below cannot reach the network; that property is enforced by the package's own test suite, not by convention. You need Node.js 20 or newer. ## 1. See what is configured ```sh theme={null} npx @omegas/continuity scan --root ~/Code ``` `scan` reads every declared configuration surface for Claude Code and Codex, under your home directory and the project roots you name with `--root`, and prints one summary: instructions, skills, commands, subagents, hooks, MCP servers, memories, settings, permission rules. ## 2. See why each value is the one in effect ```sh theme={null} npx @omegas/continuity report --root ~/Code ``` The same scan, with the reasoning shown. Each contested setting is printed with its winner, every contributor that lost, and the reason it lost. Add `--json` for the machine-readable envelope. To read it as a page instead, render the report from a redacted bundle: ```sh theme={null} npx @omegas/continuity export --root ~/Code --out setup.ocb.jsonl npx @omegas/continuity report --html setup.html --bundle setup.ocb.jsonl ``` The HTML file is self-contained: no script, no stylesheet link, no font, no image, no JavaScript. Open it with the network unplugged and it looks identical. It is written `0600` on purpose. ## 3. Export a bundle you can share ```sh theme={null} npx @omegas/continuity export --root ~/Code ``` Credential values are replaced by `{{OMEGA_REDACTED::}}` placeholders before anything touches disk. Before the bundle is written, the tool re-scans its own serialized bytes with the full detector and aborts on any high-confidence hit, writing nothing and exiting `5`. ## 4. Land it on another machine On the target machine, preview first. `diff` writes nothing at all: ```sh theme={null} npx @omegas/continuity diff --bundle setup.ocb.jsonl npx @omegas/continuity import --bundle setup.ocb.jsonl ``` `import` walks the same plan item by item and applies only what you consent to. Anything executable (hook commands, stdio MCP servers, skill scripts) lands written but inert, in the runtime's own disabled idiom. Turn each one on as a separate act, after reading it in place: ```sh theme={null} npx @omegas/continuity enable claude:user:hook:Stop.0.0 ``` `enable` shows the current content, verifies it still matches the hash recorded at import, and refuses if it changed. ## The hosted flow is separate, and opt-in Run with no subcommand and you get the optional authenticated Ωmegas transfer flow, which is the only code path in the package that contacts a server. The two are deliberately hard to confuse: every read-only command requires an explicit subcommand, and none of them can reach the network. ```sh theme={null} npx @omegas/continuity --dry-run # scan and write a local preview; contacts nothing npx @omegas/continuity # the hosted review flow ``` `--dry-run` writes a local preview containing the full text of everything discovered, so the preview itself is sensitive. It lands in `~/.omegas/state/previews` at mode `0600`, and the run ends by offering to delete it. # Continuity security model Source: https://docs.omegas.dev/continuity/security How Continuity reads hostile filesystems, redacts credentials in five layers, and imports bundles as untrusted input. This page condenses the project's threat model. The full version, including the out-of-scope list and the private reporting channel, is [SECURITY.md](https://github.com/OmegaAgent/continuity/blob/main/SECURITY.md) in the repository. ## Reading is defensive * Source files are opened read-only, with `O_NOFOLLOW`, and re-checked for change while being read. * Symlinks get one of three outcomes: resolved inside a declared root, recorded as crossing between roots, or refused and listed. Never a silent skip. * Never-export rules keep whole classes of data out of any artifact: account and billing identity, machine fingerprints, trust decisions, OAuth locks, transcripts, history. ## Redaction runs as five layers The results are unioned, so a value caught by any layer is redacted: 1. Declared secret positions, per surface. 2. Key names. 3. Clean-room provider patterns. 4. Entropy at declared sinks. 5. A deep walk of every parsed leaf, with one bounded decode. A false positive costs a span, never a file. Recall is measured rather than claimed: the test suite seeds 212 fake credentials across every surface the tool reads and asserts the per-tier percentage on every build. Before any bundle reaches disk, the export gate re-scans the serialized bytes with the full detector. A high-confidence hit aborts the write with exit code `5` and nothing is written. There is no `--include-secrets` and no second "complete" file. **The honest limit** — shapeless high-entropy secrets with nothing naming them are caught only by position. Detection is defence in depth, not a guarantee; read a bundle before you share it. The redaction report tells you exactly what was found, the class assigned to each value, and the confidence. ## Imported bundles are hostile input Entry names are canonicalized and re-checked on read. Traversal, case-fold collisions, duplicate entries, count and size bombs and tampered digests are refused with distinct exit codes rather than repaired. The import path is built so that consent is specific and revocable: * Nothing is applied without a recorded consent against a specific rendered diff. * Permission rules are additions; importing rule 3 resolves to an append and can never overwrite yours. * Everything executable lands written but inert, in the runtime's own disabled idiom: `"disabled": true`, `enabled = false`, a `hooks_disabled` block, no execute bit. `enable` is a separate act that re-verifies the content hash recorded at import. * Writes are staged at `0700`, snapshotted, fingerprint-re-verified immediately before each write, made through `O_NOFOLLOW | O_EXCL` temp files plus `rename`, and rolled back in full on any failure. ## What the tests hold down The named release gates each pin one property, and a failure names the property that broke: `gate:purity` (no I/O modules in the core), `gate:secrets` (the seeded-credential recall floor), `gate:network` (the stubbed-network cycle), `gate:adversarial` (hostile bundles), `gate:noop` (`diff` leaves the target home byte-identical), `gate:compat`, and `gate:cutover`. Every test runs against a committed fake home; nothing in the suite reads a real `~/.claude`, `~/.codex` or `~/.claude.json`. # Handoff conformance Source: https://docs.omegas.dev/handoff/conformance How to run the Handoff compatibility suite against your own deployment. Conformance is the compatibility suite for Handoff. It checks the wire contract and the observable behavior of a deployment, so an implementation can prove what it supports without relying on a marketing claim. ## The current level Level 1 contains 26 cases: C-1 through C-16, plus C-6b and C-18 through C-26. C-17 is the only Level 2 case. It requires the optional `continuation` extension and is intentionally not implemented by the current reference server because it would require encrypted storage for `resume_payload`. The reference server passes all 26 Level 1 cases. A deployed service exposes its supported level from `GET /meta`; use the runner below to check another deployment. ## What the runner checks The suite includes negative profiles so a broken or incomplete deployment is visible. It also checks the case map before running, so a missing or extra case stops the run instead of silently changing the total. ## Run it against your own deployment ```sh theme={null} handoff-conformance --base-url https://your-deployment.example.com/v1 \ --profile your-profile.yaml ``` The runner takes a base URL and does not care what is listening there. The profile supplies credentials for the principal aliases the cases name and the small set of below-HTTP checks required by the suite. A case whose requirement is missing fails; it is never silently skipped. Against a local checkout, the whole loop is one script that creates a disposable database per run: ```sh theme={null} cd handoff/core dev/run-conformance.sh dev/run-conformance.sh --case C-8 ``` A clean store per run matters: fixture requests can remain pending, and a reused `dedupe_key` would make a required `201` look like a `200` from an earlier run. ## Independent implementations You can read the protocol and implement it yourself. The suite exists so you can check the observable contract without asking the reference server to vouch for you. # Handoff Source: https://docs.omegas.dev/handoff/overview Ask a person once, receive typed data, and redeem one effect with a durable receipt. Handoff is an open protocol for the moment a program needs a person. Your agent creates a request, a person answers the declared fields, and your code receives a durable record it can use exactly once. ## The contract in one picture ```text theme={null} agent creates request → person answers typed fields → receipt is redeemed once durable wait ← delivery record ← one effect key ``` The protocol keeps the waiting and the evidence. Your application decides what to do with the answer. ## A small request ```python theme={null} import handoff handoff.configure(base_url="https://your-handoffd.example/v1", api_key=...) outcome = handoff.approve("Refund $2,400 to Acme Corp?", mode="gated") if outcome and outcome.redeem("stripe:refund:ch_1B").first_redemption: stripe.refund("ch_1B") ``` The effect key makes a retry safe: the same answer cannot authorize the same effect twice. ## What Handoff does and does not do Handoff stores the request, routes a delivery, validates the answer, and records the receipt. It does not capture a stack, resume your process, or pretend that an email/SMS provider is included. Your code may wait with a blocking call, webhook, or poll, then apply the answer. The public capability check needs no account: ```sh theme={null} curl https://handoff.omegas.dev/v1/meta ``` Authenticated operations require a credential minted by the deployment or a self-hosted instance. ## Where to go next * [Quickstart](/handoff/quickstart) — run the first request. * [The protocol](/handoff/protocol) — read the wire model and signing rules. * [SDKs](/handoff/sdks) — install from the repository and use Python or TypeScript. * [Conformance](/handoff/conformance) — run the compatibility suite locally. # The Handoff protocol Source: https://docs.omegas.dev/handoff/protocol A tour of the normative v0.1 specification: the object model, the state machines, receipts, authorizations, and the signing scheme. The normative text lives in the repository at [`spec/handoff-protocol-v0.1.md`](https://github.com/OmegaAgent/handoff/blob/main/spec/handoff-protocol-v0.1.md), written with RFC 2119 keywords, alongside the OpenAPI 3.1 wire contract ([`spec/openapi.yaml`](https://github.com/OmegaAgent/handoff/blob/main/spec/openapi.yaml)), JSON Schemas, and the signing scheme with reproducible test vectors ([`spec/signing.md`](https://github.com/OmegaAgent/handoff/blob/main/spec/signing.md)). This page is an orientation, not a replacement; where they disagree, the spec wins. ## The object model Five objects carry the protocol. Each has its own state machine in the spec. | Object | What it is | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Request** | The question. Declares what it needs (fields, capabilities, authority), never a `kind`. | | **Delivery** | One attempt to put the request in front of a person, over a named channel, with a grade recording what that delivery actually proves. | | **Receipt** | The tamper-evident record that a specific actor answered a specific presentation of a specific request. | | **Waiter** | The durable server-side wait, keyed by your `waiter_ref`, so the process that asked can die and a successor can resume. | | **Authorization** | What an approving answer mints: a value that is redeemed against exactly one effect key, idempotently. | ## Declarations, not kinds There is no `kind` enum on the wire and no branch behind one. A request declares three things: the shape of the answer (`answer.fields`), what the person must be handed to decide (`capabilities`, as opaque handles their own client resolves), and the authority the answer requires (role, freshness, quorum). All eight interaction patterns the spec requires are eight declarations over that one shape. Two consequences worth knowing before you design against it: * A `secret` field never carries its value through the protocol. The answer says `{"provided": true}` and the value goes to a sink your runtime owns and can audit (§12, out-of-band secret custody). Declaring a secret field also raises the authority floor server-side, as a consequence of the request's shape. * Unknown field types are rejected at raise time rather than degraded. A field a renderer cannot draw is a field a human cannot answer. ## Identity, and who may answer Requests have three deliberately distinct keys: the server-minted id, your `dedupe_key` (what makes two raises "the same request"), and your `waiter_ref` (what a successor process presents to resume the wait). Identity is tenant-scoped throughout. The requester may not answer its own request (§4.2), an identifier is not an authorization (§4.6), and a delivery channel never confers authority (§4.7). Treat a request id as a capability: in this protocol it can be one, so it belongs in the same custody as a credential. ## Defaults are declared, not guessed A request that can proceed without an answer declares that at raise time, as a TTL policy (`on_expiry: "default"` with the default value). When it fires, the server mints a **policy receipt** with `actor.type = "policy"`, so no audit can mistake a timeout for consent. Guessing the same value client-side afterwards produces identical behaviour and no record at all; the difference between those two is most of the reason to use the protocol. ## Receipts are evidence, not logs A receipt binds the actor, the authority they held, and the exact presentation the decision was made against (§9.3), and it lands on a hash chain (`signing.md` §2.2) that any reader can walk with the standard library. The chain makes tampering evident; it does not make the operator honest, which is why the project says plainly that independent attestation requires a party who is not the operator. Corrections are new receipts that reference the old one. Nothing is edited in place. ## Callbacks Outbound callbacks are signed over the raw bytes on the wire. Verification rules that both SDKs enforce: * Verify the bytes as received; re-encoding a parsed body changes the hash. * A valid signature proves the sender, never the tenant. Resolve tenancy from your own stored state, keyed on the endpoint or the secret, and never from a field in the body. * `delivery_id` is your deduplication key. ## Errors Every error carries a stable machine-readable `code` (§13). Branch on the code, never the message. The SDKs raise typed classes that mirror the codes and pass unknown codes through intact rather than coercing them into the nearest familiar class. ## Level 2 exists and is deliberately not implemented Conformance Level 2 adds the `continuation` extension (§14): a server that stores an encrypted `resume_payload` alongside the wait. The reference implementation refuses such payloads, because it has no encryption at rest to protect one with, and it advertises Level 1, derived from that same capability check so the advertisement and the behaviour cannot drift. # Handoff quickstart Source: https://docs.omegas.dev/handoff/quickstart Probe the live service, run the reference server and its conformance suite locally, and make your first request from Python or TypeScript. ## 1. Prove something is there No account, no key: ```sh theme={null} curl https://handoff.omegas.dev/v1/meta ``` You get the deployment's capability document: protocol version, conformance level, channels, field types. Anything beyond `/meta` requires an authenticated caller and answers `401 authentication_required` without one. Credentials on the deployed service are minted by its operator; to hold your own keys, self-host. ## 2. Run the reference implementation locally `handoffd` is a Rust binary backed by Postgres. The fastest honest proof that your checkout works is the conformance run, which creates a disposable database, seeds it, starts `handoffd`, points the whole Level 1 suite at it, and tears everything down: ```sh theme={null} git clone https://github.com/OmegaAgent/handoff cd handoff/core dev/run-conformance.sh # every Level 1 case dev/run-conformance.sh --case C-8 # one case KEEP=1 dev/run-conformance.sh # leave the database up for inspection ``` You need a Rust toolchain and a local Postgres the script may create databases in. It refuses to touch a database named `omega` or `omega_e2e`. For a durable deployment rather than a test run, `core/` carries a `Dockerfile` and the Fly.io configuration the live service deploys from. Two operational rules from the project's own runbook are worth repeating here: * Run `handoffd` as a Postgres role that owns its own schema and has neither `SUPERUSER` nor `BYPASSRLS`. Twenty of the twenty-one tables enforce row-level security, and a role that can bypass it leaves that defence inert while every test still passes. * The store is append-heavy and the audit trail is exactly as durable as your backups. A promise needs a promisor; a self-hosted deployment's promisor is you. ## 3. Ask a person, from Python The SDK is standard library only. It is not on PyPI yet, so install it from the checkout (this exact sequence was run while writing this page): ```sh theme={null} pip install ./handoff/sdk/python ``` ```python theme={null} import handoff handoff.configure(base_url="https://your-handoffd.example/v1", api_key=...) # Ask a person. Blocks until they answer. address = handoff.ask("Which shipping address should I use?") # Ask for a decision, then spend it on exactly one effect. outcome = handoff.approve("Refund $2,400 to Acme Corp?", mode="gated") if outcome and outcome.redeem("stripe:refund:ch_1B").first_redemption: stripe.refund("ch_1B") ``` The redeem step is the point of the protocol: one answer mints one authorization, and redemption is idempotent per effect key, so a retried turn cannot refund the customer twice. ## 4. Survive your own crash The wait is a durable row on the server keyed by `waiter_ref`, so the client holding it is disposable: ```python theme={null} pending = handoff.raise_request( waiter_ref="run:0198f2a1", prompt=handoff.prompt("Refund $2,400 to Acme Corp?", "Invoice INV-8821 was double-charged."), requires=handoff.requires( [handoff.fields.choice("decision", "Decision", ["approve", "reject"])], authority=handoff.authority("editor", "session"), ), ) ``` Your process can die here. Later, from anywhere: ```python theme={null} waiter = handoff.resume("run:0198f2a1") # the only thing that had to survive with waiter.receive() as received: apply(received.values) # your work # the ack is sent here, after the block completed ``` The ordering is the point. The ack is what consumes a signal, not reading it and not returning 2xx to a callback. If the block raises, nothing is acked and the signal stays queued for the next process to find. ## TypeScript The TypeScript client mirrors the Python one and is consumed from source; see [SDKs](/handoff/sdks) for the installation state of both and the full client walkthrough. # Handoff SDKs Source: https://docs.omegas.dev/handoff/sdks The Python and TypeScript clients, their honest installation state, and the patterns they make easy. Both SDKs are MIT, deliberately thin, and dependency-free. Both assert byte-identical serialization against every fixture in `spec/fixtures/` and reproduce the signature vectors in `spec/signing.md`, including the negative cases. Both prove, by killing a real subprocess mid-poll, that a client can die at any point without losing an answer. **Installation state, checked while writing this page** — neither SDK is on a public registry yet. `pip install handoff-human` and `npm install @handoffproto/sdk` do not resolve today; both installs below work from a checkout of [github.com/OmegaAgent/handoff](https://github.com/OmegaAgent/handoff). When the registry releases land, the package names will be the ones shown here. ## Python: `handoff-human` Standard library only. Version 0.2.0 in the repository. ```sh theme={null} git clone https://github.com/OmegaAgent/handoff pip install ./handoff/sdk/python ``` ```python theme={null} import handoff handoff.configure(base_url="https://handoff.example.com/v1", api_key=...) address = handoff.ask("Which shipping address should I use?") outcome = handoff.approve("Refund $2,400 to Acme Corp?", mode="gated") if outcome and outcome.redeem("stripe:refund:ch_1B").first_redemption: stripe.refund("ch_1B") ``` The durable-wait pattern (`raise_request`, `resume`, `receive`) is covered in the [quickstart](/handoff/quickstart#4-survive-your-own-crash). Two details that matter in production: * `receive()` acks when its block completes. If the block raises, nothing is acked and the signal stays queued. Acking first and applying second would turn at-least-once delivery into at-most-once application, which is the exact bug the protocol exists to make impossible. * To record that a decision arrived and could not be acted on, call `received.unable("the refund API was down")` inside the block. That is not an error; it is a fact worth keeping. Callback verification is standard library too: ```python theme={null} from handoff import verify_callback result = verify_callback(request.headers, request.body_bytes, active_secrets=[...]) # result.delivery_id is your deduplication key ``` Pass the raw bytes as received; passing a `str` is refused rather than silently encoded. Receipt-chain verification (`verify_receipt_chain`, `verify_chain`) needs nothing outside the standard library; only the optional detached Ed25519 layer needs the `cryptography` package. If you are coming from the 0.1.x hackathon package: the module is now `handoff`, and `import human` still works in 0.2.x with a deprecation warning. ## TypeScript: `@handoffproto/sdk` Zero runtime dependencies, and no Node built-ins: hashing, HMAC and randomness go through WebCrypto, so the same source runs on Node, Deno, Bun, and Workers. The package ships TypeScript source, consumed directly by any runtime that strips types (Node 22.18 or newer, Deno, Bun) or by any bundler. ```ts theme={null} import * as handoff from "@handoffproto/sdk"; handoff.configure({ baseUrl: "https://handoff.example.com/v1", apiKey }); const address = await handoff.ask("Which shipping address should I use?"); const outcome = await handoff.approve("Refund $2,400 to Acme Corp?", { mode: "gated" }); if (outcome.approved && (await outcome.redeem("stripe:refund:ch_1B")).firstRedemption) { await stripe.refunds.create({ charge: "ch_1B" }); } ``` The durable wait mirrors Python: ```ts theme={null} const waiter = await handoff.resume("run:0198f2a1"); await waiter.receive(async (received) => { await apply(received.values); // the ack is sent after this resolves }); ``` Because hashing rides WebCrypto, `verifyCallback`, `verifyChain`, and `digest` all return promises. ## Shared error model Every error carries a stable `code` and raises or throws a class that mirrors it: `AlreadyAnswered` (carrying the receipt id), `RequesterMayNotAnswer`, `InsufficientAuthority`, `AuthorizationSpent`, `AnswerValidationFailed` (with per-field detail), and the rest of the spec's §13. A code the SDK version does not recognize surfaces as `HandoffProtocolError` with the code intact. Never branch on `.message`; it is written for people and may change at any time. # Overview Source: https://docs.omegas.dev/index Start with the Omega product that matches the work your agent needs to do. # Build the next step into the workflow These docs are organized around a job, not a tour of the repository. Pick the product that matches the moment in your workflow, then follow its quickstart. ## Which path is yours? ### I am moving an agent setup Start with [Continuity](/continuity/quickstart). It scans the configuration surfaces that shape Claude Code and Codex, explains which value wins, and lets you preview a redacted bundle before anything is imported. ```sh theme={null} npx @omegas/continuity report --root ~/Code ``` ### My workflow needs a human decision Start with [Handoff](/handoff/quickstart). Your program creates a durable request, a person answers the declared fields, and your code redeems the result for one effect. ```sh theme={null} curl https://handoff.omegas.dev/v1/meta ``` ### My agent needs a real browser Start with [Operator](/operator/overview). The API covers devices, tasks, live view, takeover, webhooks, usage, and audit events. Operator is currently in preview; access is granted per organization and every request is authenticated. ```sh theme={null} curl https://api.omegas.dev/v1/whoami \ -H "Authorization: Bearer $OMEGAS_OPERATOR_API_KEY" ``` ## Keep moving The pages state what is available today. When a page says a command or flow was verified, that is the boundary of the claim. # Cancel task Source: https://docs.omegas.dev/operator/api/endpoints/cancel-task /schemas/operator.json post /v1/tasks/{id}/cancel Request a task cancel (202): stamp the durable cancel flag; the worker that owns the sprite performs the sprite stop + terminal write + claim release + token revoke on its next pass. Loads the task org-scoped and checks its Space first. Idempotent — cancelling an already-terminal task replays its stored row. Needs no browser provider (the worker owns the sprite + the proxy token). # Create device Source: https://docs.omegas.dev/operator/api/endpoints/create-device /schemas/operator.json post /v1/devices Create + provision an Operator device. Runs the write gate (scope + entitlement), resolves the target Space (org-wide key: `space_id` required + Space check; Space-scoped key: its held Space), enforces the device-count and rolling-duration quotas, provisions the device sprite, and returns the active device (201). `Idempotency-Key` replays a prior success and 409s a reused key + a different body. # Create endpoint Source: https://docs.omegas.dev/operator/api/endpoints/create-endpoint /schemas/operator.json post /v1/webhook_endpoints Register a webhook endpoint (201). The response carries the signing secret ONCE. # Create session Source: https://docs.omegas.dev/operator/api/endpoints/create-session /schemas/operator.json post /v1/devices/{id}/sessions Mint a live-view session for a device (201). # Delete device Source: https://docs.omegas.dev/operator/api/endpoints/delete-device /schemas/operator.json delete /v1/devices/{id} Terminate a device (202): accrue final duration, destroy the sprite, mark `terminated`. A second delete of an already-terminated device is `410 device_terminated` (documented: delete reports the terminal state rather than silently no-oping). # Delete endpoint Source: https://docs.omegas.dev/operator/api/endpoints/delete-endpoint /schemas/operator.json delete /v1/webhook_endpoints/{id} Delete an endpoint (204). Idempotent for a caller: a second delete of an id that is gone is the same 404 a foreign-org id gets, because by then the two are indistinguishable. # Get device Source: https://docs.omegas.dev/operator/api/endpoints/get-device /schemas/operator.json get /v1/devices/{id} Fetch one device by `dev_`. A foreign-org (or malformed) id is a 404, never a 403. # Get endpoint Source: https://docs.omegas.dev/operator/api/endpoints/get-endpoint /schemas/operator.json get /v1/webhook_endpoints/{id} Fetch one endpoint. Carries no secret — a lost signing key cannot be recovered here. # Get escalation Source: https://docs.omegas.dev/operator/api/endpoints/get-escalation /schemas/operator.json get /v1/escalations/{id} One escalation attempt. # Get session Source: https://docs.omegas.dev/operator/api/endpoints/get-session /schemas/operator.json get /v1/sessions/{id} Read one session's state. Carries no token — a lost `session_token` cannot be recovered here. # Get task Source: https://docs.omegas.dev/operator/api/endpoints/get-task /schemas/operator.json get /v1/tasks/{id} Fetch one task by `task_`. A foreign-org (or malformed) id is a 404, never a 403. # List audit Source: https://docs.omegas.dev/operator/api/endpoints/list-audit /schemas/operator.json get /v1/audit The org's Operator audit trail, newest first. This is the pull half of the milestone: webhooks tell you when something happens, the audit log tells you what happened while you were not listening. Same records, same ids — a delivery's `id` is the `id` here. # List device tasks Source: https://docs.omegas.dev/operator/api/endpoints/list-device-tasks /schemas/operator.json get /v1/devices/{id}/tasks List one device's tasks, newest first, with keyset pagination. The device is resolved (and its Space checked) first, so a foreign-org device id is a 404 before any task is read. # List devices Source: https://docs.omegas.dev/operator/api/endpoints/list-devices /schemas/operator.json get /v1/devices List the caller org's devices, newest first, with keyset pagination and optional status/Space filters. An org-wide key may omit `space_id`; a Space-scoped key MUST name a Space it holds. # List endpoints Source: https://docs.omegas.dev/operator/api/endpoints/list-endpoints /schemas/operator.json get /v1/webhook_endpoints List the org's webhook endpoints, newest first. # List escalations Source: https://docs.omegas.dev/operator/api/endpoints/list-escalations /schemas/operator.json get /v1/escalations The org's escalation attempts, newest first. # Release Source: https://docs.omegas.dev/operator/api/endpoints/release /schemas/operator.json post /v1/sessions/{id}/release RELEASE the device back to the agent (202): resume the sprite and un-park the task, then drop the hold. This is the EXIT from the `needs_human` wall a takeover parks on. # Resolve escalation Source: https://docs.omegas.dev/operator/api/endpoints/resolve-escalation /schemas/operator.json post /v1/escalations/{id}/resolve The escalation destination's callback. Authenticated by the per-escalation bearer, not an API key. # Revoke session Source: https://docs.omegas.dev/operator/api/endpoints/revoke-session /schemas/operator.json delete /v1/sessions/{id} Revoke a session (204). The gateway re-checks liveness on every relayed message, so any socket the session holds dies at once rather than at its next reconnect. # Sleep device Source: https://docs.omegas.dev/operator/api/endpoints/sleep-device /schemas/operator.json post /v1/devices/{id}/sleep Sleep a device (202): accrue duration, suspend the sprite, mark `sleeping`. Idempotent. # Submit task Source: https://docs.omegas.dev/operator/api/endpoints/submit-task /schemas/operator.json post /v1/devices/{id}/tasks Submit a browser task to a device (202). Runs the write gate (scope + entitlement), loads the device org-scoped and checks its Space, requires the device be `active` (terminated → 410, otherwise → 409 `device_not_ready`), claims the device's single task slot (a held slot → 409 `device_busy`), and persists a `running` task the worker driver then drives to terminal. `Idempotency-Key` replays a prior success and 409s a reused key with a different body. # Takeover Source: https://docs.omegas.dev/operator/api/endpoints/takeover /schemas/operator.json post /v1/sessions/{id}/takeover TAKE the device: pause the agent so a human can drive (200). # Task events Source: https://docs.omegas.dev/operator/api/endpoints/task-events /schemas/operator.json get /v1/tasks/{id}/events Stream a task's steps as Server-Sent Events, then a terminal `done` frame. Loads the task org-scoped and checks its Space, then emits `event: step` (data = the step payload) for each step with `seq > after_seq` in order, and `event: done` (data = the terminal Task) once the task finishes. Poll-based (no long-held DB connection); a never-terminal task is closed after [`SSE_MAX_SECS`] and the client reconnects with `?after_seq=`. # Update endpoint Source: https://docs.omegas.dev/operator/api/endpoints/update-endpoint /schemas/operator.json patch /v1/webhook_endpoints/{id} Update an endpoint (200). Re-enabling clears the failure streak — the one path out of auto-disable. # Usage Source: https://docs.omegas.dev/operator/api/endpoints/usage /schemas/operator.json get /v1/usage The caller org's Operator usage rows over the rolling 24h window (metered dimensions, priced where the rate card prices them). Reads only — no browser provider required. # Wake device Source: https://docs.omegas.dev/operator/api/endpoints/wake-device /schemas/operator.json post /v1/devices/{id}/wake Wake a device (202): re-provision/attach the sprite and mark `active`. Terminated → 410; the rolling-duration quota is enforced here too. # Watch device Source: https://docs.omegas.dev/operator/api/endpoints/watch-device /schemas/operator.json get /v1/devices/{id}/watch `WS /v1/devices/{id}/watch` — upgrade, validate, then relay. The credential is offered as the `omega.token.` subprotocol, NEVER as a query parameter (see [`token_from_subprotocols`]). Validation happens BEFORE the upgrade so a rejected client gets a real HTTP status rather than a WebSocket that immediately closes. Note the deliberate asymmetry with the rest of `/v1`: this route authenticates with the SESSION token, not an API key. The session row is the tenancy claim — its org and device are read from the row, never from the request. # Whoami Source: https://docs.omegas.dev/operator/api/endpoints/whoami /schemas/operator.json get /v1/whoami Reflect the authenticated machine principal. Requires a valid `omg_…` API key; this is the end-to-end proof that [`ApiKeyPrincipal`] resolves, verifies, and scopes a key. # Operator: authentication and scopes Source: https://docs.omegas.dev/operator/authentication One credential, a scope table with no surprises left in it, and the credential-custody rules that keep a key out of your logs. One credential: an API key on the `Authorization` header. ```text theme={null} Authorization: Bearer omg_operator___ ``` Keys are minted by the Ωmegas team and shown exactly once. The TypeScript client reads `OMEGAS_OPERATOR_API_KEY` from the environment; over raw HTTP you send the header yourself. ```sh theme={null} curl https://api.omegas.dev/v1/whoami \ -H "Authorization: Bearer $OMEGAS_OPERATOR_API_KEY" ``` `GET /v1/whoami` answers with the key's identity and is the cheapest way to check a credential without touching anything. ## Scopes A key carries scopes and, optionally, a list of Spaces. Missing either is `403 insufficient_scope`. | Scope | What it unlocks | | --------------------------- | ------------------------------------------------------------------------------- | | `operator:devices:read` | list and get devices | | `operator:devices:write` | create, sleep, wake, delete | | `operator:tasks:read` | get and list tasks, stream events | | `operator:tasks:write` | submit, cancel | | `operator:sessions:write` | mint, read, and revoke live-view sessions | | `operator:sessions:control` | additionally required to mint a `control` session, to take over, and to release | | `operator:usage:read` | `GET /v1/usage` | | `operator:audit:read` | read the audit trail at `GET /v1/audit` | | `operator:webhooks:read` | list and get webhook endpoints | | `operator:webhooks:write` | create, update, delete endpoints | | `operator:escalations:read` | list and get human escalations | Two rows deserve emphasis: * `operator:sessions:control` is separate on purpose. A key issued for monitoring cannot be turned into a key that types into a logged-in browser. * `GET /v1/audit` reads with `operator:audit:read`. Keys minted before that scope existed carried `operator:webhooks:read` for the audit route, and the historical scope is still accepted there, so an existing key keeps working. A **Space-scoped** key must name a Space it holds on every list; an org-wide key may omit it and sees the whole organization. ## A credential must not reach a URL A URL is a publishing surface: access logs, span fields, error reporters, proxies, `Referer` headers. This product has already had one credential leak that way, which is why the client refuses to let it happen again rather than asking integrators to be careful: * The key is wrapped in a `Credential` whose `toString`, `toJSON` and inspection output all return `omg_operator___***`. The only function that reveals the secret returns an `Authorization` header. * Query strings are scanned for credential-shaped values and the request throws before it is sent. * Live-view session tokens ride the `Sec-WebSocket-Protocol` header (`omega.operator.v1, omega.token.`), never a query parameter, and the server-supplied `ws_url` must match the API origin and must be `wss:`. * Redirects are refused outright; following one would replay the `Authorization` header at a host nobody validated. If you integrate over raw HTTP, adopt the same rules. They are cheap before a leak and expensive after one. # Operator: errors and conventions Source: https://docs.omegas.dev/operator/conventions The error taxonomy and which codes are retryable, idempotency on the two routes that accept it, and cursor pagination. ## Errors Every failure carries a stable machine-readable `code`. Branch on the code, never on the message; messages are written for humans and will change. The TypeScript client raises one `OperatorError` subclass per class below. | Class | Status | Codes | | --------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `InvalidRequestError` | 400 | `invalid_cursor`, `invalid_status`, `invalid_space_id`, `space_id_required`, `invalid_product`, `invalid_prompt`, `invalid_mode`, `invalid_idempotency_key`, `invalid_url`, `unknown_event_type`, `duplicate_endpoint_url` | | `AuthenticationError` | 401 | `invalid_api_key` | | `PermissionError` | 403 | `insufficient_scope`, `product_not_entitled` | | `NotFoundError` | 404 | `device_not_found`, `task_not_found`, `session_not_found`, `webhook_endpoint_not_found`, `escalation_not_found` | | `ConflictError` | 409 | `device_busy`, `device_not_ready`, `takeover_conflict`, `idempotency_conflict` | | `GoneError` | 410 | `device_terminated`, `session_expired` | | `RateLimitError` | 429 | `rate_limited`, `quota_exceeded` | | `ServiceError` | 5xx | `provider_error`, `browser_unavailable`, `signing_unavailable` | Only six codes are retryable, in the sense that waiting and re-sending the same request can succeed: `rate_limited`, `quota_exceeded`, `device_busy`, `device_not_ready`, `provider_error`, `browser_unavailable`. Everything else is a verdict. A 404 means "no such row **in your organization**". A row in another organization answers identically, so a 404 is never evidence that an id does not exist. ## Idempotency Exactly two routes accept an `Idempotency-Key` header: `POST /v1/devices` and `POST /v1/devices/{id}/tasks`. A create that times out and is retried with the same key replays the first result instead of provisioning a second browser or submitting a second task. Derive your key from the work, not from a clock; a generated key dies with the process that made it. `provision:${orderId}` survives a crash, `${Date.now()}` does not. Reusing a key with a different body is `409 idempotency_conflict`. No other route accepts the header. ## Pagination Lists are keyset-paginated with an opaque cursor. * `next_cursor` is server state. Do not parse it, construct one, or persist it across deploys; a tampered cursor is `400 invalid_cursor`. * Page size defaults to 20 and is clamped to 100. * The TypeScript client exposes lists as async iterators (per item, per page, or `collect(n)`), so most code never touches a cursor. ```ts theme={null} for await (const device of omega.devices.list({ status: "active" })) { /* … */ } for await (const page of omega.audit.list({ eventType: "operator.task.failed.v1" }).pages()) { await sink.writeBatch(page.data); } ``` # Operator: limits, stated plainly Source: https://docs.omegas.dev/operator/limits Properties of what ships today, not a roadmap. Each limit is enforced or observable, and integrations should be designed against them. These are properties of what ships today. None of them is an apology, and none of them is a roadmap. * **Operator is a closed preview.** Keys are minted per organization and shown exactly once. There is no self-serve signup, and provisioning an organization is done by the Ωmegas team; see the [overview](/operator/overview) for exactly what was exercised end to end. * **One task per device.** A second submit while a task is live is `409 device_busy`. More concurrency means more devices, subject to your organization's device cap. * **A task-event stream is capped at 300 seconds.** The client resumes from the last `seq`; a hand-rolled consumer must too, or every long task appears to stall at five minutes. * **Watch cannot type.** Read-only is genuinely read-only. Input requires a `control` session, the `operator:sessions:control` scope, and a held takeover lease, and the gateway re-checks all three on every message. * **Webhook delivery is at-least-once and unordered.** Dedupe on the signed envelope's `id`, order on its `created_at`, and answer `200` to duplicates, because 20 consecutive non-2xx answers disable the endpoint. * **`operator.webhook.*` events are audit-only.** They appear in `GET /v1/audit` and are never delivered; delivering them would loop a failing endpoint against itself. * **No residential or geo-selected IPs.** Devices egress from the provider's datacenter addresses. `region` picks a datacenter, not a country's consumer IP space. * **No proxy, browser-fingerprint, or captcha-solving controls.** A wall parks the task as `needs_human` and waits for a person. * **Usage is a rolling 24-hour window** of metered rows. `cost_micro` is absent on an unpriced dimension. Unpriced means unpriced, not free. * **The escalation resolve callback is not for API-key clients.** `POST /v1/escalations/{id}/resolve` is called by the escalation destination with a per-escalation bearer handed to it; an API-key client has no business holding that credential. # Operator: live view and takeover Source: https://docs.omegas.dev/operator/live-view Watch sessions, control sessions, the takeover lease, and why read-only is genuinely read-only. A live-view session lets a person see a device's browser as it works, and, under a separately scoped permission, take over its controls: for a `needs_human` wall, or because a human wanted to check on the work. ## Minting a session `POST /v1/devices/{id}/sessions` mints a session and returns its connection details exactly once. There is no route that reads a session token back; a lost token means a new session. The request's `mode` accepts: | `mode` | What it grants | Scope required | | ----------------------- | --------------------------------------------- | ------------------------------------------------------------- | | `watch` (alias: `view`) | Frames only. No input, ever. | `operator:sessions:write` | | `control` | Frames, plus the right to request a takeover. | `operator:sessions:write` **and** `operator:sessions:control` | Both `watch` and `view` are accepted for the read-only mode; they name the same thing. An omitted `mode` defaults to `watch`, the less privileged of the two, so a forgotten field can never accidentally mint control. Minting a `control` session without the `control` scope is `403 insufficient_scope`. `ttl_secs` is a request, clamped to the server's allowed band; a client does not get to decide how long a live-browser capability lives. The session token rides the WebSocket handshake in `Sec-WebSocket-Protocol` (`omega.operator.v1, omega.token.`), never a query parameter, and the returned `ws_url` is only honoured if it matches the API origin over `wss:`. See [authentication](/operator/authentication#a-credential-must-not-reach-a-url) for why. ## Watching and driving are different privileges In the TypeScript client, the type system holds the line: a `WatchConnection` has no method that sends input, and the only route to a `ControlConnection` is a successful `takeover()`. ```ts theme={null} const connection = await omega.devices.watch(device.id, { mode: "control", ttl_secs: 900 }); for await (const frame of connection.frames()) render(frame.data); // base64 JPEG const control = await connection.takeover("clearing a captcha"); control.sendInput({ t: "mouse", type: "mousePressed", x: 412, y: 233 }); await control.release(); // back to the agent; the returned view cannot send input ``` The gateway is authoritative either way: it re-checks the mode and the takeover lease on every message and drops what it will not relay. It drops silently, so the client mirrors the check and throws on a send without a held lease, instead of producing a click the browser appears to ignore. If you build your own client, mirror it too. ## The takeover lease requires presence Taking over acquires a lease on the device; the socket heartbeats it. If the holder vanishes (a closed laptop, a dropped connection), the device is handed back to the agent about a minute later. Releasing explicitly with `POST /v1/sessions/{id}/release` is the polite version of the same thing. A takeover can carry a reason (`"clearing a captcha"` above, or a ticket id). It is recorded on the takeover's audit event, which is what makes a human intervention a recorded fact rather than an anecdote. # Operator Source: https://docs.omegas.dev/operator/overview Cloud browser devices driven by tasks, with live view, human takeover, webhooks, and an audit trail. In closed preview; access is granted per organization. Operator is the Ωmegas `/v1` machine surface: cloud browser **devices**, the **tasks** that drive them, **live view** with human takeover, **usage** metering, **webhooks**, and the **audit trail**. **Preview status, stated plainly** — Operator is in a closed preview and is not generally available. API keys are minted per organization by the Ωmegas team and shown exactly once. In the current preview build, device provisioning, sleep and wake, live-view session minting, task execution, and task cancellation were exercised end to end against live infrastructure: a real browser was provisioned through this API, driven by a submitted task, and returned its result. Webhook delivery to a receiving endpoint and usage attribution have not passed that bar yet, and these pages do not claim they have. The rest of the surface is documented from the same OpenAPI contract the server is checked against, as the contract an integration can be built against. ## The shape of the product A device is a cloud browser your organization holds: created, slept, woken, and deleted through the API, egressing from the provider's datacenter addresses. A task is a natural-language instruction submitted against one device. While a task runs, its steps stream as events; when it hits a login, a captcha, or a two-factor wall, it parks as `needs_human` and waits for a person, who can watch the browser live and take over the controls under a separately scoped permission. Operator events reach you two ways: signed webhook deliveries your receiver can verify offline, and the audit trail at `GET /v1/audit`, which holds the same records under the same ids for everything that happened while you were not listening. ## What is documented here * [Authentication and scopes](/operator/authentication): the key format, the scope table, and the credential-custody rules the client enforces. * [Tasks and event streams](/operator/tasks): submitting, reading, streaming, and cancelling, including the routing fact most integrations trip over. * [Live view and takeover](/operator/live-view): watch and control sessions, the takeover lease, and why watching genuinely cannot type. * [Webhooks](/operator/webhooks): signature verification, replay handling, and what your response codes do to your endpoint. * [Errors and conventions](/operator/conventions): the error taxonomy, idempotency, and pagination. * [Limits, stated plainly](/operator/limits): properties of what ships today, not a roadmap. * The **API reference** in the sidebar is generated from the OpenAPI document the Rust API is checked against, so it cannot drift from the deployed contract. A zero-dependency TypeScript client (`omegas-operator`) exists and informs much of these pages; it is not on a public registry yet. Examples are therefore shown against the raw HTTP surface, with client snippets where the client's behaviour is itself the point. ## Request API access Access is granted per organization, and keys are shown exactly once at mint. There is no self-serve signup. If your organization wants to build against the preview, reach the team through an existing Ωmegas channel or through [github.com/OmegaAgent](https://github.com/OmegaAgent). # Operator: tasks and event streams Source: https://docs.omegas.dev/operator/tasks Submitting a task, reading it back at the right route, streaming its events past the 300-second cap, and what needs_human means. Task execution was exercised end to end in the current preview build: a real browser was provisioned through this API, a task was submitted against it, and the task completed with its result on the wire. What follows is the contract the server enforces for submission, reads, streaming, and cancellation. ## Submit against a device, read back at the top level The routing fact most integrations trip over, stated first: * **Create** a task with `POST /v1/devices/{device_id}/tasks`. * **Read** it back with `GET /v1/tasks/{task_id}`. There is no `GET /v1/devices/{device_id}/tasks/{task_id}`. Guessing that URL returns `404`, and a 404 here never means the task is gone; it means the route does not exist. `GET /v1/devices/{device_id}/tasks` lists a device's tasks, and each task in that list is read individually at the top-level route. ```sh theme={null} curl -X POST https://api.omegas.dev/v1/devices/$DEVICE_ID/tasks \ -H "Authorization: Bearer $OMEGAS_OPERATOR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4711-status-check" \ -d '{"prompt": "Open the orders page and read the status of order 4711"}' ``` ```sh theme={null} curl https://api.omegas.dev/v1/tasks/$TASK_ID \ -H "Authorization: Bearer $OMEGAS_OPERATOR_API_KEY" ``` One task per device: a second submit while one is live answers `409 device_busy`. Submission accepts an `Idempotency-Key`, so a create that times out and is retried replays the first result instead of starting twice; see [conventions](/operator/conventions#idempotency). A task carries its outcome in `result`: the agent's answer on success, the failure payload on failure, and `null` while it runs. A caller is never told only that a task failed with nothing to act on. ## Streaming events `GET /v1/tasks/{id}/events` streams the task's steps as server-sent events, each carrying a monotonic `seq`. Resume from the last `seq` you stored. **The server closes a stream after 300 seconds by design.** It is poll-based and holds no database connection between polls. A consumer must reconnect from its last `seq`; one that treats the close as the end sees every long task stall at exactly five minutes. The TypeScript client does this for you, with reconnect bounds so a broken deployment cannot turn your consumer into a hot loop: ```ts theme={null} for await (const event of omega.tasks.events(task.id, { afterSeq: lastSeqYouStored })) { if (event.type === "step") save(event.seq, event.step); else return event.task; // terminal; the iterator ends here and only here } ``` Reconnects that fail to advance the cursor are capped (default 5), total reconnects are capped outright (default 500), every reconnect waits at least 100 ms with jitter, and a `429` on reconnect honours its `Retry-After`. If you hand-roll the SSE loop, you own all of that. ## `needs_human` is not terminal A task that hits a login, captcha, or two-factor wall parks as `needs_human` and waits for a takeover. It is not finished and not failed; treat it as a signal to put a person in front of the device via [live view](/operator/live-view). The task resumes when the human releases control. ## Cancel ```sh theme={null} curl -X POST https://api.omegas.dev/v1/tasks/$TASK_ID/cancel \ -H "Authorization: Bearer $OMEGAS_OPERATOR_API_KEY" ``` Cancellation is scoped `operator:tasks:write` and was exercised end to end in the current preview build. Cancel *requests* the stop; the terminal transition lands when the worker settles the task, so read the task back rather than assuming the 202 finished the job. # Operator: webhooks Source: https://docs.omegas.dev/operator/webhooks Verifying signed deliveries, answering duplicates correctly, and keeping your endpoint out of the auto-disable path. Webhook deliveries carry three headers: ```text theme={null} X-Omega-Signature: t=,v1=.")> X-Omega-Event: operator.task.succeeded.v1 X-Omega-Delivery: ``` Endpoint secrets are returned once, in the `POST /v1/webhook_endpoints` response. There is no reveal route; a lost secret is replaced by creating a new endpoint. Subscribe with `event_types` from the published catalogue; an unknown type is `400 unknown_event_type` rather than a subscription that silently receives nothing. ## Four verification rules The TypeScript client's `WebhookVerifier` enforces all four. If you verify by hand, so must you. 1. **Verify the raw bytes.** The signed string is `"."`. A framework that parsed the JSON and re-serialized it has changed the bytes and the MAC will not match. Use `express.raw`, `fastify` with `rawBody`, `await request.text()` in Workers, or the request stream in `node:http`. 2. **Compare in constant time.** A `===` on hex strings leaks the correct MAC one byte at a time. 3. **Bound the timestamp in both directions** (default plus or minus 300 seconds). `t` is inside the MAC so it cannot be re-stamped, but a captured delivery can be re-sent, and a one-sided check would let a single forged far-future stamp replay forever. 4. **Dedupe on the signed envelope's `id`, never on a header.** The signature covers the body and no header, so `X-Omega-Delivery` is attacker-controlled on a captured delivery. The verifier returns the signed `eventId` for exactly this; `deliveryId` is echoed for logging and is explicitly not authenticated. ```ts theme={null} import { WebhookVerificationError, WebhookVerifier } from "omegas-operator"; const verifier = new WebhookVerifier({ secret: process.env.OMEGAS_WEBHOOK_SECRET! }); app.post("/hooks", express.raw({ type: "application/json" }), async (req, res) => { try { const { event } = await verifier.verify({ body: req.body, headers: req.headers }); await handle(event); // 200 fast; do the slow part out of band res.sendStatus(200); } catch (error) { if (error instanceof WebhookVerificationError) { res.sendStatus(error.reason === "replayed_delivery" ? 200 : 400); } else { res.sendStatus(500); // our bug, not theirs } } }); ``` ## What you answer matters The sender treats every non-2xx as a delivery failure and **disables an endpoint after 20 consecutive failures**. Re-enabling is an explicit `PATCH /v1/webhook_endpoints/{id}` with `{"enabled": true}`; nothing turns an endpoint back on for you. | Situation | Answer | Why | | ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Verified and handled | `200` | | | A replayed delivery | `200` | A duplicate is expected traffic under at-least-once delivery, not an error. Answering `400` burns your endpoint's failure budget on normal traffic. | | Any other verification failure | `400` | It will never verify; a retry cannot help. | | Your handler threw | `500` | You want the retry. | ## Receipt is not processing By default the verifier records an event as seen the moment it verifies. If your handler then throws, the retry is refused as a replay and that event is gone from your system's point of view. That default makes the common case, an idempotent handler, effectively-once. For the other trade: ```ts theme={null} const verifier = new WebhookVerifier({ secret, manualCommit: true }); const delivery = await verifier.verify({ body, headers }); await handle(delivery.event); // throws: nothing recorded, the retry is accepted await delivery.commit(); // only now is a redelivery treated as a replay ``` The cost of `manualCommit` is that two deliveries of the same event arriving concurrently can both be processed. Neither default is universally right; pick the one your handler's failure mode deserves. The default replay store is in-memory and correct for one process; several instances need a shared store, such as Redis or a unique index on the event id. ## Delivery semantics Delivery is at-least-once and unordered. Order on the envelope's `created_at`, never on arrival. `operator.webhook.*` events are audit-only: they appear in `GET /v1/audit` and are never delivered, because delivering them would loop a failing endpoint against itself.