Skip to main content

Tutorial 16: Keep — open workstation for an untrusted agent

Run a Keep: a personal Linux cell for an agent you do not have to trust, with a Sentinel policy you can read, approvals out of band, and an honest measured security profile.

Level: Intermediate
Time: 45 minutes
Repos: Fabric agent-runtime, FluxVM reachable, curl, jq, Node 20+ optional for bundles.

Honesty. Until Keep 0.2 on real SNP/TDX with a user-held key, the host can still see a measured VM. Evidence class software-test must never be marketed as “the operator cannot read this.” See docs/keep/KEEP.md.

GitHub Actions runs the unit/policy suite + stub e2e without KVM: .github/workflows/keep.yml.
Lab live FluxVM gate: ./scripts/keep-live-lab.sh · PRODUCTION.md.


What you will learn​

  1. How Keep maps onto Fabric agent-runtime + FluxVM (no third repo).
  2. How to run the CI suite and the Keep 0.1 live lab gate.
  3. How to run Keep mode (fail-closed signed policy).
  4. How to export/import keep.policy.yaml with keepctl.
  5. How to open cockpit + browser live view.
  6. How training stays off unless you mint an export token.

Step 0 — Run the CI suite locally​

cd fabric
cargo test --manifest-path agent-runtime/Cargo.toml --lib
cargo test --manifest-path agent-runtime/Cargo.toml policy -- --nocapture
./scripts/keep-e2e.sh
./scripts/keepctl --help

Expect ~143 lib tests green and stub e2e PASS. That is the same gate as the Keep workflow.

On a host with FluxVM up:

./scripts/keep-live-lab.sh
# Guest boot needs a registered template (Tutorial 11 node22-agent) or:
# KEEP_E2E_TEMPLATE=node22-agent ./scripts/keep-live-lab.sh

Step 1 — Start the agent runtime (Keep mode)​

# Generate a one-off Ed25519 seed for this lab (32-byte hex)
SEED=$(python3 -c 'import os; print(os.urandom(32).hex())')
PUB=$(./agent-runtime/target/release/examples/keep_sign_policy pubkey "$SEED" \
2>/dev/null || cargo run --manifest-path agent-runtime/Cargo.toml --example keep_sign_policy -- pubkey "$SEED")

export ZYVOR_AGENT_LISTEN=127.0.0.1:9096
export ZYVOR_AGENT_API_TOKEN=dev-token
export ZYVOR_AGENT_FLUXVM_URL=http://127.0.0.1:7788
export ZYVOR_AGENT_KEEP_MODE=1
export ZYVOR_AGENT_POLICY_TRUSTED_SIGNERS="$PUB"
export ZYVOR_AGENT_CONFINE=1 # optional but recommended
export ZYVOR_AGENT_SECURITY_PROFILE=measured # default under Keep mode
# optional: approval webhook your phone/CLI can reach
# export ZYVOR_AGENT_APPROVAL_WEBHOOK=https://…

cargo run --manifest-path agent-runtime/Cargo.toml --release

Keep mode refuses to start without trusted signers, and refuses unsigned PUT /v1/agents/{name}/policy.

export KEEP_API=http://127.0.0.1:9096
export KEEP_TOKEN=dev-token
curl -sS -H "Authorization: Bearer $KEEP_TOKEN" "$KEEP_API/healthz"

Step 2 — Deploy an agent (Keep cell)​

Use Tutorial 11’s deploy JSON shape, and add Keep fields:

{
"name": "keep-demo",
"bundle_base64": "<base64 of bundle.mjs>",
"manifest": {
"template": "node22-agent",
"egress_mode": "ask",
"egress_allow_hosts": ["api.github.com"],
"confinement": "strict",
"taint": { "trusted_hosts": ["api.github.com"] },
"model_socket": {
"base_url": "https://api.x.ai/v1",
"model": "grok-4",
"credential": "xai"
},
"cell_backend": "firecracker",
"browser_port": 9222
}
}
./scripts/keepctl create -f /tmp/keep-demo-deploy.json

model_socket is the BYO brain — swap Grok / local GGUF / vLLM without rebuilding the cell.
cell_backend: firecracker records the Keep 0.1 intent (agent kernel ≠ host kernel); the live sandbox still follows FluxVM’s sandbox API today.
Console UX: marketing /keep → Open Agents when signed in.


Step 3 — Readable Sentinel policy (must be signed)​

./scripts/keepctl policy show keep-demo > /tmp/keep.policy.yaml
# edit allow/deny/ask/taint, then sign and set:
./agent-runtime/target/release/examples/keep_sign_policy sign "$SEED" /tmp/keep.policy.yaml \
> /tmp/keep.policy.yaml.sig
./scripts/keepctl policy set keep-demo /tmp/keep.policy.yaml /tmp/keep.policy.yaml.sig

Unsigned PUT returns 403 in Keep mode. Example schema: docs/keep/sentinel/keep.policy.yaml.

High-risk actions (buy / send / delete) must go through /v1/approvals / your webhook — never confirm inside the agent chat.

Credentials use host-side authorize_resolve allowlists (vault/README.md); secrets remain host-readable until Keep 0.2.


Step 4 — Cockpit + browser live view​

After a session exists:

SID=<session-uuid>
./scripts/keepctl cockpit "$SID" | jq '{tainted_by, taint_visible, evidence_class, browser_view, honesty}'
# HTML (phone-friendly):
open "$KEEP_API/keep/cockpit?session=$SID"
# Tab listing (needs browser_port on the agent):
curl -sS -H "Authorization: Bearer $KEEP_TOKEN" \
"$KEEP_API/v1/sessions/$SID/browser/view" | jq .
open "$KEEP_API/keep/browser?session=$SID"

Untrusted reads paint tainted_by; egress flips to ask. Screencast and input takeover are not in Keep 0.1 — see browser/README.md.


Step 5 — Pack / unpack (two homes, one disk story)​

./scripts/keepctl pack /tmp/keep-pack keep-demo
./scripts/keepctl unpack /tmp/keep-pack keep-demo

This moves policy + manifest pin. Copy the FluxVM qcow2 / vault export beside the pack directory when you migrate hosts. Measured image pin travels with the FluxVM catalog alias you used for security_profile: measured VMs.


Step 6 — Training default off​

# Refused without an explicit scoped token:
./scripts/keepctl export-token 'trajectory:read:7d' 3600

Sanitization footnotes do not count. No token → no trajectory leaves the box.


Measured profile on FluxVM (optional on this host)​

If the node has OVMF + swtpm + a signed catalog:

# On the FluxVM host — see zyvorai/fluxvm docs/guides/security-profiles-howto.md
curl -sS -X POST http://127.0.0.1:7788/v1/vms \
-H 'Content-Type: application/json' \
--data @examples/qemu-measured.json

Under Keep mode the agent-runtime defaults ZYVOR_AGENT_SECURITY_PROFILE=measured on sandbox create. Cockpit still labels evidence software-test.

FluxVM CI for that path: ./scripts/test-security-profiles.sh in the fluxvm repo.


What not to do​

  • Approvals inside the agent chat
  • Calling software-test evidence “hardware attestation”
  • Treating QEMU extra_args as proof of confidential launch
  • Running production Keep without ZYVOR_AGENT_KEEP_MODE=1 and trusted signers
  • Opening a third Keep git repository — Keep lives in Fabric

Keep 0.2 soft scaffolding (receipt, screencast, user-held challenge, fabricd proxies) is already in tree — see KEEP-0.2.md. Hardware SNP/TDX + real user-held unwrap is still required before unread-by-operator claims.


Appendix — Brokered browser (browser-agent)​

Bake on a FluxVM host (Debian Chromium; not Ubuntu snap):

curl -fsSL -o /tmp/node20.tar.xz \
https://nodejs.org/dist/v20.18.1/node-v20.18.1-linux-x64.tar.xz
(cd /tmp && npm pack playwright-core@1.49.1 && mv playwright-core-*.tgz playwright-pack.tgz)
./scripts/keep-bake-browser-agent.sh

Deploy with template=browser-agent, browser_port=9222, confinement: strict. Driver listens on guest :9230 (a11y refs). Operator:

keepctl browser tabs "$SESSION"
keepctl browser shot "$SESSION"

MCP tools: browser_open / browser_snapshot / browser_act / browser_tabs / browser_close. Passwords: host POST /v1/sessions/{id}/browser/fill-secret. Details: docs/keep/browser/DRIVER.md.

CI syntax check: ./scripts/keep-bake-browser-smoke.sh.


Appendix — Packaged agents (infra, migration, deploy)​

Keep ships Fabric-facing packs under examples/keep-agents/:

PackReadsWrites (ask)Artifact
infra-opsalerts, VMs, lifecycle compliancerestart / remediationincident timeline
migration-op/api/migrations/*, GuestKit inspectcreate/cancel/rescuewave plan + checklist
deploy-op/readyz, /healthnone by defaultFABRIC_DOCTOR readiness
browser-researchallowlisted browse via a11y drivernone by defaultresearch markdown
pdf-brieflocal PDF onlynonebrief.md (0 CONNECT)

Shared _fabric connector + fabric-api credential recipe. Goals/artifacts API: docs/keep/goals/README.md.

# Runtime must be up; credentials file includes fabric-api; FABRIC_API_TOKEN set
./scripts/keep-pack-demo.sh infra-ops
# Stub bundle without fabric-agent:
KEEP_PACK_DRY=1 ./scripts/keep-pack-demo.sh deploy-op
# Stage demo (no browser):
./scripts/keep-demo-pdf.sh

Loop: create goal → session work → POST /v1/artifacts → advance requires_approval step → /v1/approvals → audit. Cockpit exposes active_goal / recent_artifacts.

PDF demo deep-dive: Tutorial 17 · demos/pdf-brief.md.