Use this source-checkout guide to evaluate Ardur's authenticated Docker demo: a SPIRE-backed local proxy that applies mission policy before tool execution and returns signed session evidence.
For the provider-free, no-bearer first-run path, use the No-Key MVP Demo instead. The relaxed auth mode in that guide is deliberately loopback-only and temporary.
From the repository root, configure a fresh local bearer token and start the stack in terminal 1:
export ARDUR_API_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
make demoWait until docker compose ps reports the SPIRE server, SPIRE agent, proxy, and
hub as healthy. Keep terminal 1 running. make demo-down stops the stack and
removes its named volumes after the walkthrough.
Paste this entire block into terminal 2 from the same repository root. If
ARDUR_API_TOKEN is not already exported there, the block reads the configured
token from the running proxy container. It never prints the bearer value or
places it directly in curl's argument list.
(
set -euo pipefail
PROXY_URL="${ARDUR_PROXY_URL:-https://localhost:${ARDUR_PROXY_PORT:-8443}}"
if [[ -z "${ARDUR_API_TOKEN:-}" ]]; then
ARDUR_API_TOKEN="$(docker compose exec -T proxy sh -c 'printf %s "$VIBAP_API_TOKEN"')"
fi
if [[ -z "$ARDUR_API_TOKEN" || "$ARDUR_API_TOKEN" == *$'\n'* || "$ARDUR_API_TOKEN" == *$'\r'* ]]; then
echo "No configured proxy token found. Start make demo with ARDUR_API_TOKEN set." >&2
exit 1
fi
umask 077
AUTH_HEADER_FILE="$(mktemp "${TMPDIR:-/tmp}/ardur-evaluator-auth.XXXXXX")"
REQUEST_BODY_FILE="$(mktemp "${TMPDIR:-/tmp}/ardur-evaluator-body.XXXXXX")"
cleanup() {
rm -f "$AUTH_HEADER_FILE" "$REQUEST_BODY_FILE"
}
trap cleanup EXIT
printf 'Authorization: Bearer %s\n' "$ARDUR_API_TOKEN" > "$AUTH_HEADER_FILE"
curl_public() {
curl --insecure --silent --show-error --fail "$@"
}
curl_auth() {
curl --insecure --silent --show-error --fail --header "@$AUTH_HEADER_FILE" "$@"
}
post_json() {
local path="$1"
local payload="$2"
printf '%s' "$payload" > "$REQUEST_BODY_FILE"
curl_auth \
--request POST \
--header 'Content-Type: application/json' \
--data-binary "@$REQUEST_BODY_FILE" \
"$PROXY_URL$path"
}
json_string() {
python3 -c '
import json
import sys
value = json.load(sys.stdin).get(sys.argv[1])
assert isinstance(value, str) and value, value
print(value, end="")
' "$1"
}
json_value() {
python3 -c '
import json
import sys
value = json.load(sys.stdin).get(sys.argv[1])
assert value is not None, value
print(value, end="")
' "$1"
}
json_object_with_stdin() {
python3 -c '
import json
import sys
print(json.dumps({sys.argv[1]: sys.stdin.read()}), end="")
' "$1"
}
json_evaluate() {
python3 -c '
import json
import sys
print(json.dumps({
"session_id": sys.stdin.read(),
"tool_name": sys.argv[1],
"arguments": {"path": "/tmp/ardur-evaluator.txt"},
}), end="")
' "$1"
}
HEALTH_RESPONSE="$(curl_public "$PROXY_URL/health")"
test "$(printf '%s' "$HEALTH_RESPONSE" | json_value status)" = "ok"
echo "health=ok"
MISSION_PAYLOAD='{"mission":{"agent_id":"evaluator-guide","mission":"evaluate the governance proxy","allowed_tools":["read_file","delete_file"],"forbidden_tools":["delete_file"],"resource_scope":["**"],"max_tool_calls":4}}'
ISSUE_RESPONSE="$(post_json /issue "$MISSION_PAYLOAD")"
PASSPORT="$(printf '%s' "$ISSUE_RESPONSE" | json_string token)"
echo "issue=passport-created"
START_PAYLOAD="$(printf '%s' "$PASSPORT" | json_object_with_stdin token)"
START_RESPONSE="$(post_json /session/start "$START_PAYLOAD")"
SESSION_ID="$(printf '%s' "$START_RESPONSE" | json_string session_id)"
echo "session=started"
PERMIT_PAYLOAD="$(printf '%s' "$SESSION_ID" | json_evaluate read_file)"
PERMIT_RESPONSE="$(post_json /evaluate "$PERMIT_PAYLOAD")"
PERMIT_DECISION="$(printf '%s' "$PERMIT_RESPONSE" | json_string decision)"
test "$PERMIT_DECISION" = "PERMIT"
echo "read_file=$PERMIT_DECISION"
DENY_PAYLOAD="$(printf '%s' "$SESSION_ID" | json_evaluate delete_file)"
DENY_RESPONSE="$(post_json /evaluate "$DENY_PAYLOAD")"
DENY_DECISION="$(printf '%s' "$DENY_RESPONSE" | json_string decision)"
test "$DENY_DECISION" = "DENY"
echo "delete_file=$DENY_DECISION"
SESSION_PAYLOAD="$(printf '%s' "$SESSION_ID" | json_object_with_stdin session_id)"
ATTEST_RESPONSE="$(post_json /attest "$SESSION_PAYLOAD")"
ATTESTATION_TOKEN="$(printf '%s' "$ATTEST_RESPONSE" | json_string token)"
test -n "$ATTESTATION_TOKEN"
echo "attest=signed-token-created"
END_RESPONSE="$(post_json /session/end "$SESSION_PAYLOAD")"
END_ATTESTATION="$(printf '%s' "$END_RESPONSE" | json_string attestation_token)"
test -n "$END_ATTESTATION"
echo "session=ended"
METRICS_RESPONSE="$(curl_auth "$PROXY_URL/metrics")"
[[ "$METRICS_RESPONSE" == *"ardur_"* ]]
echo "metrics=prometheus-ok"
)Expected output:
health=ok
issue=passport-created
session=started
read_file=PERMIT
delete_file=DENY
attest=signed-token-created
session=ended
metrics=prometheus-ok
Every curl call uses --fail, so an authentication or schema error makes the
block exit non-zero instead of turning an HTTP 4xx body into a misleading pass.
The same lifecycle and payload shapes are also exercised by
scripts/verify-mvp.sh.
/issuesigns a Mission Passport for a structured mission declaration./session/startbinds a governed session to that passport./evaluatereturnsPERMITfor an allowed tool andDENYwhen a forbidden rule overlaps the allowlist; deny wins./attestand/session/endreturn signed behavioral-attestation JWTs./metricsis authenticated and exposes Prometheus-formatted Ardur metrics.
The walkthrough verifies that non-empty signed tokens are returned. For a local
cryptographic signature-verification demonstration, run
scripts/run-no-key-mvp-demo.py, which
verifies the session-end token with its ephemeral public key before cleanup.
The local Compose stack contains a SPIRE server, a SPIRE agent, the governance proxy, and the Personal Hub. The proxy governs calls presented at its HTTP/tool boundary; it does not claim visibility into provider-hidden reasoning or every subprocess, filesystem, kernel, or network side effect caused below that boundary.
The emergency kill switch is an authenticated administrative control. It is not
part of the copy-paste lifecycle above because it changes shared proxy state and
would disrupt other evaluator sessions. The CLI uses ARDUR_PROXY_URL and
ARDUR_API_TOKEN when the explicit flags are omitted:
export ARDUR_PROXY_URL="https://localhost:${ARDUR_PROXY_PORT:-8443}"
export ARDUR_API_TOKEN="$(docker compose exec -T proxy sh -c 'printf %s "$VIBAP_API_TOKEN"')"
ardur kill-switch
ardur kill-switch --deactivateHealth remains public while the switch is active. Authenticated governance operations fail closed until it is deactivated.
In a third terminal, or after stopping the attached make demo process, run:
make demo-downThis removes the Compose containers, network, and named volumes for the project.
- Capture boundary: Ardur governs at the tool-call boundary. See
docs/coverage-map.mdfor current coverage and roadmap boundaries. - Development TLS: the local proxy uses a generated self-signed certificate,
so the walkthrough uses curl's loopback-only
--insecuremode. Do not carry that TLS policy to a remote deployment. - Single-user demo: the local stack is not a multi-tenant isolation model.
- Token Status List scope: Credential-level Token Status List revocation
checking lives in the Go credential verifier (
go/pkg/credential). The Python path checks mission-level status lists (vibap.mission.mission_is_revoked) but does not yet implement the credential-level check.