New
Apps 1,515Makers 1,513This week 1,515
Promote your app−30%
Tacy screenshot 1
0views in
the last 30 days

Tacy runs AI agents in confidential VMs. Your requests are encrypted to a key that exists only inside a VM whose code you can check, so nobody else can read them, us included.

What this is

The code inside Tacy's trust boundary: everything the agent VM runs, how its definition is built reproducibly, and the verifier that checks a running VM before anything is sent to it. Everything outside the boundary (the web app, the relay, the control plane) only sees ciphertext and metadata, and isn't here (What's not here).

What it proves

Attestation proves which code a VM runs. It proves nothing about what that code does. So the code has to be public, rebuildable, and every release signed in a public log. Together:

  • The key is the VM's. An Intel TDX quote binds the VM's per-boot encryption key to its measured boot chain and to the hash of its compose.yaml (How it works).
  • The VM runs a release from this repository. That compose hash must be signed by this repository's release workflow on a version tag, logged in Sigstore's public Rekor log (Releases and signing).
  • The release is this source. Anyone can rebuild a release and get the same compose, binary hashes and image digests (Reproduce).
  • Underneath is a known OS on a known VM shape. The firmware, kernel, initramfs and root filesystem are pinned measurements (Boot measurements).

What it doesn't prove, and what you still have to trust, is listed under What's not here.

How to verify a release

  1. The signature. Check that a release's compose.yaml was signed by this repository's release workflow on its tag (needs the GitHub CLI):

    gh release download v0.19.1 --repo tacyhq/tacy --pattern compose.yaml
    gh attestation verify compose.yaml --repo tacyhq/tacy \
      --signer-workflow tacyhq/tacy/.github/workflows/release.yml --source-ref refs/tags/v0.19.1
    
  2. The build. Rebuild the tag and compare it with its release.json and with the images ghcr.io serves (needs docker, git, curl and jq):

    scripts/reproduce.sh v0.19.1
    
  3. A running VM. The browser checks the VM's attestation before every request: 16 checks, in the same Go code as verifier/ and release/ (How the browser verifies a VM).

Release history is in CHANGELOG.md. To report a vulnerability, see SECURITY.md.

What's here

Path What
app/ agentd, the front door and runtime: attestation (GET /attest), end-to-end encrypted (EHBP) runs and tasks, schedules, connections and the hand-off of scheduled results, on the harness/v1 runtime with its queue and journal (docs/27). It holds the per-boot HPKE key but no API keys, and has no network: requests reach it through gatewayd.
brain/, cmd/brain/ Our agent loop as a harness/v1 brain: untrusted, no network, no keys, one connection to the runtime, run in a gVisor sandbox (below).
cmd/runnerd/, runner/, sandbox/ runnerd (docs/22): starts the brain in a gVisor sandbox for agentd (rootless runsc, no capabilities, no network), over the internal runner/v0 protocol; the brain's link is a socket passed in as fd 3. gVisor is Google's release, pinned by hash in sandbox/gvisor.go.
cmd/gatewayd/, gateway/ gatewayd, the only process with a network: the model gateway and the web and mail connectors (from guard/), leak scanning, one leased key per call; it also forwards the published port to agentd's socket without reading the encrypted bodies. gateway/ is the internal gateway/v0 protocol between agentd and gatewayd (docs/21).
cmd/vaultd/, vault/ vaultd, the vault: holds the API keys and the people's sealed email connections, no network; releases one key or password per call to gatewayd only, for a granted connector and exact host, and records every lease without the value.
imapro/ A read-only IMAP client for the mail connector: any command or FETCH item that could change a mailbox is refused in code before a byte is written; TLS only, certificates checked.
guard/ The runtime side the brain can't bypass: the model gateway (attested models only, with fallback), the web connector (search, page reading, images, site icons) and the mail connector, with PII masking and a public-only fetcher, leak scanning, the quarantined reader (untrusted text reaches the acting model only as checked fields and quotes by reference), permission prompts, built-in agents, file review, tasks and the events people see (docs/27).
harness/ The harness/v1 protocol between brain and runtime: messages, strict codec, runtime engine, fake runtime and the conformance suite every brain must pass (docs/20).
queue/ The runtime's task queue and journal in SQLite on the VM's encrypted disk, schema versioned and migrated forward (docs/27 section 2); a /run task's rows are deleted when it ends. A side database holds agent definitions, schedule state and scheduled results sealed to their space.
trigger/, identity/, approval/ Signed agent definitions (tacy.agent/v1), schedules, and the identity directory a definition's signature is checked against. approval/ (approve before send with a passkey) ships with no directory: nothing a personal space can install sends or changes anything.
safety/, cmd/safetysign/ Safety controls (docs/23): Ed25519-signed policies and stop lists that agentd, gatewayd and vaultd each verify against the firm key log, the kill switch, per-run and per-day budgets reserved before each call, the vault's lease rates. cmd/safetysign makes keys, signs documents and key log entries (not part of the VM).
keylog/, keysv2/ The firm key log: signed, hash-chained entries verified from a genesis pinned in the compose, with fork detection; keysv2/ replays the browser's test vectors so both verifiers agree.
runauth/ Every request to agentd names its space, proves it holds the space's seal key and is signed by an identity over the whole request, this boot's key, a fresh nonce and the time. Test vectors in runauth/testdata/, replayed by the browser too.
activity/ Activity records (docs/24): agentd writes a signed start and end record per agent run (calls, hosts, sizes, leases by name, codes, budget; never content or key values), chained per recipient and sealed to the person's key; anyone can check the chain, only the person can open it. The signing key comes from dstack's KMS.
storedfiles/ Reads the person's encrypted stored files inside the VM: each object fetched as ciphertext with its own key and checked chunk by chunk exactly as the browser checks them.
leakscan/ Credential scanner (patterns ported from IronClaw, MIT; decodes before scanning): blocks credentials in an agent's outbound arguments and redacts them from what comes back.
proto/ Wire format shared by the service and its verifiers.
internal/tinfoil/ Verifies the model provider's enclave (Tinfoil, AMD SEV-SNP, Sigstore-signed releases) before the VM sends it anything.
internal/tdx/ Intel TDX quote verification and dstack event log replay.
internal/procinfo/ Each process logs its uid, groups and network interfaces at start.
internal/testspend/ Test tooling only (agentclient/): adds up what test runs against a live VM cost. Not part of the VM.
verifier/ Checks a running agent VM's attestation (Go library), including the pinned boot measurements (measurements.go).
release/ Checks that a compose is a signed release of this repository (Sigstore).
wasm/ The verifier compiled to WebAssembly, for the browser.
agentclient/ Attest, verify, then send encrypted runs; used by the service's end-to-end tests.
cmd/compose/ Builds the binaries reproducibly, one OCI image each on a pinned distroless base, and generates compose.yaml (the VM's definition: every image by digest) and the release manifest.
cmd/setup/ setup, a one-shot step (root, no network) that gives the processes their directories on the shared volumes before they start.
compose.yaml, pins.go What the VM runs. Its sha256 is what the attestation measures and what a release signs. pins.go embeds it for the browser verifier.
scripts/ build-wasm.sh (browser verifier), reproduce.sh (rebuild a release and compare).
.github/workflows/ CI (tests, and the compose and image digests rebuilt on the runner and in the pinned Go container must match), the release workflow that pushes the images and signs, and reproduce.yml (rebuilds each release and checks what ghcr.io serves).
testdata/ A recorded attestation from a test VM on dstack 0.6.1 (public data: quote, event log, app compose), used by the verifier's tests.
docs/ Specs, listed in docs/README.md: the brain–runtime protocol, the gateway and vault, the sandbox runner, safety controls, activity records, and tasks with the quarantined reader and permission prompts.

How it works

  1. Boot. The VM (Intel TDX, dstack 0.6.1) runs compose.yaml: four processes, each in its own container with its own uid, linked only by unix sockets (docs/21, docs/22): agentd (front door and runtime), runnerd (service runner: runs the brain, the agent loop, untrusted, in a gVisor sandbox), gatewayd (service agentvm, the only one with a network) and vaultd (the keys). Each container runs one image from ghcr.io/tacyhq/tacy/<name>, named by digest in the compose (no tag, nothing built on the VM); a one-shot setup container (root, no network) first gives the processes their directories. agentd makes a fresh HPKE key in memory.
  2. Attest. GET /attest?nonce=<64 hex> returns a TDX quote whose report_data is sha512("sealed-agent-vm/v1" || 0x00 || nonce || hpke_public_key), plus the event log and the app compose.
  3. Verify. The client checks the quote (Intel signature chain, collateral, revocation, TCB, debug off), the key binding, the event log replay to RTMR3 and the compose hash, a production OS image, pinned boot measurements (MRTD, RTMR0-2: firmware, kernel, initramfs, root filesystem and VM size), the pre-launch script, no init script, that the compose is a signed release, and that it names every image by digest (16 checks).
  4. Run. Requests and responses are encrypted to the verified key. No request logs; a run's task and journal sit in the queue on the VM's encrypted disk while it runs and are deleted when it ends. What the run did (calls, hosts, sizes, leases, codes; no content) goes on a signed activity record sealed to the person's own key (docs/24).

How the browser verifies a VM

The web app loads the WebAssembly verifier (wasm/, built with scripts/build-wasm.sh; JavaScript entry point globalThis.sealedAgentVM). For each VM it:

  1. asks the VM for /attest with a fresh random nonce;
  2. fetches the Sigstore bundle for the attested compose's sha256 from GitHub's attestations API (through a relay, which can't forge one);
  3. runs verifyRelease: every check above, in the same Go code as verifier/ and release/;
  4. only if all checks pass, encrypts the request to the HPKE key the quote binds.

The release check accepts a compose only if this repository's release workflow signed it on a version tag at or above the pinned minimum (below).

Reproduce the compose, binary hashes and image digests

mise exec -- go run ./cmd/compose --check

It builds the six binaries (agentd, brain, gatewayd, runnerd, vaultd, setup) for linux/amd64 (CGO_ENABLED=0, -trimpath, no VCS stamp, Go 1.27.2) and makes one image per process: the pinned base gcr.io/distroless/static-debian13 (by its linux/amd64 manifest digest) plus one layer holding only /tacy/<name> (owner 0:0, mode 0555, time 0, gzip from Go's standard library), entrypoint the binary, every time in the config 0. The brain has no image of its own: it runs inside runnerd's sandbox, so runnerd's layer also holds /tacy/brain, and runnerd's image has one more layer below it with gVisor (/tacy/gvisor, from Google's release tarball, downloaded once into the user's cache and checked by SHA-256, then each binary by its pin, with gVisor's Apache-2.0 LICENSE next to them; docs/22 section 4). Same binaries and base, same image digests, on any machine. It checks that compose.yaml matches this tree byte for byte, then prints the compose sha256, each binary's sha256 and each image's digest (--oci DIR also writes the images as OCI layouts). CI runs the same check on the runner and in the pinned Go container and fails on any difference.

The rule the verifier adds (verifier.ComposeImages, the 16th check): the attested compose is one YAML document whose every service has image: <registry>/<path>@sha256:<64 hex> with no tag, no service has build: or extends:, and there's no top-level include:. So the compose hash covers exactly the images that run, and the images' digests cover the binaries.

The compose hash is sha256(compose.yaml). dstack copies the file unchanged into app-compose.json (docker_compose_file), whose own hash is the attested compose-hash. The verifier hashes that attested copy, so the signed value and the measured value are the same bytes.

To rebuild a published release and compare it with its manifest and with what the registry serves:

scripts/reproduce.sh v0.2.0            # in the pinned Go container (cmd/compose's image, by digest); needs docker
scripts/reproduce.sh --native v0.2.0   # with your own Go (must be 1.27.2, e.g. through mise)

It clones the tag, rebuilds, compares with the release's release.json (compose sha256, binary sha256s, image digests), then fetches each image's manifest from ghcr.io anonymously by digest and checks it hashes to that digest and is the rebuilt one. Needs git, curl and jq. .github/workflows/reproduce.yml runs both after every release. Releases before v0.2.0 (tacy-release/v1) built the binaries on the VM and have no images.

Releases and signing

A release is a tag vMAJOR.MINOR.PATCH (no suffixes). .github/workflows/release.yml builds it, pushes the images to ghcr.io/tacyhq/tacy/<name> (tagged with the version for people; the compose uses only the digests) and publishes a GitHub Release with three files:

File What
compose.yaml What the VM runs: five images by digest (setup, agentd, runnerd as service runner with the brain and gVisor, gatewayd as service agentvm, vaultd; before v0.4.0 brain had its own image and service).
release.json Manifest: schema (tacy-release/v2), repository, version, commit, compose_sha256, binaries (name → sha256), images (name → ghcr.io/tacyhq/tacy/<name>@sha256:…), base_image, go_version, build_image, and since v0.4.0 gvisor (release, URL, tarball and binary hashes). (tacy-release/v1, before v0.2.0, had no images: the VM built the binaries from source embedded in the compose.)
compose.sigstore.json The Sigstore bundle that signs the files and the images.

Signing uses GitHub artifact attestations (actions/attest-build-provenance) on Sigstore's public-good instance: the release job gets a short-lived Fulcio certificate for its GitHub Actions OIDC identity (no long-lived keys), signs one in-toto statement (SLSA provenance v1) whose subjects are compose.yaml, release.json and each image (named ghcr.io/tacyhq/tacy/<name>, with its manifest digest), and the signature is logged in Rekor. The workflow has two jobs: build (read-only token, tests and build, in the pinned Go container and again on the runner, which must agree) and release (checks out nothing, runs no repository code; only it can push packages or get the OIDC token), so test or build code can't push or sign anything. Before it signs, the release job pushes the images and pulls each one back without credentials (manifest, config and every layer, each checked against its digest), because a VM pulls anonymously; if one doesn't pull (a new ghcr package starts private), nothing is signed or published, an org owner sets the package public, and the job is re-run for the same tag.

The verifier (release/) accepts a bundle only if all of these hold:

Field Required value
Issuer https://token.actions.githubusercontent.com
Certificate SAN https://github.com/tacyhq/tacy/.github/workflows/release.yml@refs/tags/vX.Y.Z
Source repository https://github.com/tacyhq/tacy, ref refs/tags/vX.Y.Z (same tag)
Repository and owner ids 1423086047 and 233153967 (GitHub's numeric ids of tacyhq/tacy and tacyhq; certificate extensions 1.3.6.1.4.1.57264.1.15 and .17). Names can be taken again after a delete or rename, ids can't.
Build signer and build config the SAN above (release.yml is the top-level workflow)
Trigger, runner push (of the tag), github-hosted
Certificate chains to Sigstore's Fulcio, has an embedded SCT, valid when the signature was logged
Transparency log a Rekor entry with an inclusion proof
Statement SLSA provenance v1, with a subject named compose.yaml whose sha256 is the VM's compose hash
Version the tag, at least the pinned minimum

Sigstore's trusted root is embedded (release/trusted_root.json, from Sigstore's TUF repository); refresh it with RECORD=1 go test ./release -run TestRecordTrustedRoot and review the diff.

Getting the bundle: GET https://api.github.com/repos/tacyhq/tacy/attestations/sha256:<compose sha256> (no auth for a public repo). GitHub often answers with "bundle": null and a short-lived bundle_url to Azure blob storage serving the bundle Snappy-compressed; release.FetchBundles follows it (https to *.blob.core.windows.net only, no redirects, size and time limits). GitHub may also list its own attestation for immutable release assets ("initiator": "github", no Rekor entry), which the verifier refuses; try each bundle (release.VerifyAny). The release asset compose.sigstore.json is the same bundle. By hand:

gh attestation verify compose.yaml --repo tacyhq/tacy \
  --signer-workflow tacyhq/tacy/.github/workflows/release.yml --source-ref refs/tags/v0.2.0

Rollback protection: a relay could serve an older, validly signed release with a known bug, so the verifier refuses releases older than release.MinVersion (release/release.go, currently v0.2.0: the first release with images), raised when a release must stop being used. A caller can raise the minimum further but never lower it, and can pass the newest version it has accepted before (release.Policy.LastSeen; the optional last argument of the browser's verifyRelease and verifyReleaseBundle)

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#agents · #confidential-computing · #vm