This tutorial is for contributors who want to compile the gateway, exercise its attestation surface, and run the local integration suite. The production binary always uses dstack KMS keys and dstack TDX quotes, so even a local process needs a reachable dstack SDK endpoint.
Install:
- Rust stable with
rustfmtandclippy - Python 3.12
- uv
curl,jq, andopenssl- Node.js 24, npm, and Bun 1.4 when changing the TypeScript clients
- the Phala CLI and SSH access to a dev OS CVM when running outside dstack
You also need a dstack SDK endpoint. Outside a CVM, forward one from a dev CVM
with a single phala ssh command; see
Connect to the dstack SDK.
The default paths used by the test scripts are:
dstack endpoint: unix:/tmp/aci-dstack-sock-dev.dstack.sock
dstack verifier: http://localhost:18080
live artifacts: /tmp/private-ai-gateway-live-e2e
The external dstack verifier is needed only by provider adapters that declare it in their provider configuration, such as NEAR AI and PhalaDirect. The gateway's own identity path talks to the dstack SDK socket directly.
Inside a dstack CVM, the SDK socket is /var/run/dstack.sock. For local
development, forward it from a dev OS CVM you can access. Keep this command
running in its own terminal:
phala ssh <dev-cvm-name> -- -N -L /tmp/aci-dstack-sock-dev.dstack.sock:/var/run/dstack.sockThen set dstack_endpoint to unix:/tmp/aci-dstack-sock-dev.dstack.sock. If
the command fails, run phala status to check that you are logged in. If an
earlier tunnel left the socket file behind, delete it first.
In a second terminal, check the forwarded API before starting the gateway:
DSTACK_LOCAL_SOCKET=/tmp/aci-dstack-sock-dev.dstack.sock
test -S "$DSTACK_LOCAL_SOCKET"
curl --fail --silent --show-error \
--unix-socket "$DSTACK_LOCAL_SOCKET" \
http://dstack/Info \
| jqThe quote, key derivation, application identity, KMS behavior, and event log come from the remote CVM. A local process using this socket is suitable for API and binding tests, but its measurements are the remote dev CVM's measurements, not those of the intended production deployment.
From the repository root:
uv sync --locked
cargo build --all-targetsuv sync --locked installs the Python packages used by the provider-verifier
bridge. The bridge uses the vendored scripts/confidential_verifier package by
default. Set PRIVATE_AI_VERIFIER_DIR only when testing an intentional external
verifier checkout.
Run the same checks as the main CI workflow:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --all-targets
python3 -m compileall scriptsThe TypeScript workspace contains only @phala/aci-verifier. Use
pap for coding agents and provider access. Run
these commands from the repository root after changing anything under
clients/ or a shared ACI construction:
npm --prefix clients ci
npm --prefix clients run build
npm --prefix clients run check
npm --prefix clients test
npm --prefix clients run test:bun
npm --prefix clients run lint
npm --prefix clients run format:check
npm --prefix clients run lint:packages
bash clients/package-smoke.shCreate a writable state directory and a static config:
mkdir -p /tmp/private-ai-gateway-state
cat >/tmp/private-ai-gateway.config.json <<'JSON'
{
"bind": "127.0.0.1:8086",
"state_dir": "/tmp/private-ai-gateway-state",
"dstack_endpoint": "unix:/tmp/aci-dstack-sock-dev.dstack.sock"
}
JSONStart the binary:
PRIVATE_AI_GATEWAY_CONFIG_PATH=/tmp/private-ai-gateway.config.json \
RUST_LOG=info \
cargo run --bin private-ai-gatewayStartup performs these operations before binding the HTTP listener:
- Requests receipt-signing and E2EE keys from dstack KMS.
- Opens the gateway state files and takes the session-log writer lock.
- Loads
<state_dir>/upstreams.json. A missing or empty file means no routes. - Seals a workload keyset with the configured
not_afterlifetime. - Compacts the session log.
The process exits on a KMS, config, state, or session-log error.
In a second terminal:
curl --fail --silent --show-error http://127.0.0.1:8086/health
NONCE="$(openssl rand -hex 32)"
curl --fail --silent --show-error \
"http://127.0.0.1:8086/v1/aci/attestation?nonce=$NONCE" \
| jq '{api_version, workload_keyset_digest,
subject: .attestation.workload_keyset.subject}'Expected health response:
{"status":"ok"}The attestation response should report api_version as aci/1. This check
only confirms that the endpoint returned a report. A relying party still needs
to verify the report as described in
Verification and security model.
The active route file is <state_dir>/upstreams.json. For a first direct-mode
test, stop the gateway and create a route that matches an endpoint you control:
[
{
"name": "local-openai",
"provider": "openai-compatible",
"base_url": "http://127.0.0.1:9000",
"models": {
"local-model": "upstream-model"
}
}
]Restart the gateway, then inspect the public model catalog:
curl --fail --silent --show-error http://127.0.0.1:8086/v1/models | jqopenai-compatible routes have no attestation verifier and cannot satisfy
provider.aci_verified: true. Use them for protocol and transformation tests,
not confidential-inference claims.
For live providers, start from deploy/upstreams.example.json
and follow the relevant provider verification guide.
With the forwarded socket in place, run the credential-free local multi-upstream smoke test next, then the live end-to-end suite against real providers.
| Error | Cause | Fix |
|---|---|---|
PRIVATE_AI_GATEWAY_CONFIG_PATH must point to... |
The required config-path variable is missing or empty. | Set it to a readable JSON file. |
| dstack KMS or quote request failed | The dstack endpoint is missing, stale, or not reachable by the process. | Check the forwarded socket and the dstack_endpoint value. |
invalid upstream config |
upstreams.json or the configured seed violates the schema. |
Check the field and provider constraints in the configuration reference. |
| another gateway holds the session log lock | Two processes use the same state_dir. |
Give each local process a separate state directory or stop the other writer. |
address already in use |
Another process owns the configured listener. | Change bind or stop the existing process. |