Skip to content

feat(cli): teleport dira into cloud agent runtimes - #132

Merged
azlekov merged 5 commits into
developfrom
claude/dira-cli-cloud-runtime-3eb50e
Sep 2, 2026
Merged

feat(cli): teleport dira into cloud agent runtimes#132
azlekov merged 5 commits into
developfrom
claude/dira-cli-cloud-runtime-3eb50e

Conversation

@azlekov

@azlekov azlekov commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Agents increasingly run in cloud runtimes — Claude Code on the web and Cursor cloud agents execute sessions in ephemeral VMs — and none of that work is captured today: dira init's hook configs embed a machine-specific absolute path, linking is interactive-only, and the VM's TLS-intercepting egress proxy defeats the binary's bundled-roots-only trust store (D-0011). This PR adds the repo side of capturing cloud agent work.

Teleport

  • dira cloud init generates portable, repo-committed artifacts: .dira/hook.sh (run-time dira resolution, always exit 0) and .dira/bootstrap.sh (in a cloud VM: sha256-verified install of the pinned release from GitHub release assets, daemon start, runner-token device claim, then forwards its own SessionStart event; on a laptop: straight to the forward), plus portable hook commands merged into the project .claude/settings.json / .cursor/hooks.json — replacing machine-specific dira init entries rather than duplicating them. Idempotent/fixpoint, covered by a new e2e test.
  • dira device link --runner-token (env DIRA_RUNNER_TOKEN): the headless claim variant — same endpoint and invariants as the code claim (server-assigned id, persist nothing until authoritative, nonce idempotency), no TTY anywhere on the path. The endpoint contract for dirahq-cloud is specified in .zavet/specs/cloud-runtime.md.
  • Cursor cloud agents provision from .cursor/environment.json rather than a hook, via two non-hook bootstrap modes: --install-only (build phase, cached) and --provision-only (boot phase: install, start the daemon, claim a device, forward nothing — there is no hook payload on stdin and it must not fabricate one). Cursor exposes no session-start hook, and its hooks are skipped during the agent's early read-only turns, so provisioning cannot hang off one.
  • dira_core::runtime::detect(): conservative runtime detection (CLAUDE_CODE_REMOTE, explicit DIRA_RUNTIME) used for device labels, diagnostics, and the 1.4 rollup stamp below — capture and accounting are byte-identical everywhere.

Contract 1.4: the runtime rides the wire

  • SessionRollup.runtime + runtimeSessionRef (additive minor, 1.3.0 → 1.4.0): the cloud runtime the session ran in (claude-web, cursor-cloud, …) and the harness's own session reference, so the dashboard can badge cloud-agent work and deep-link transcripts. Both optional + omitted-when-absent; still metadata-only (a runtime name, never content).
  • The daemon stamps both at flush time from runtime::detect() — a property of the running environment, not of stored events, and batch ids never derive from session rollups, so the post-assembly stamp cannot perturb chunk identity or dedup. Outside a cloud runtime the keys stay off the wire, byte-identical to 1.3.
  • The signing vector now populates the new fields, so the cross-language fixture exercises them; schema + vector regenerated via just contract with the drift gates green. An older cloud strips unknown keys and gates on schema major only, so rollout order is free.

Transport, attribution, ephemerality

  • DIRA_EXTRA_CA_CERTS (new decision DIRASH-0033, amending D-0011): every device→cloud reqwest client builds through dira_core::httpclient::builder(), which additively appends PEM anchors — never a store swap, never SSL_CERT_FILE, and a bad bundle degrades to the default client with a warning. The bootstrap takes the CA the runtime itself declares ($SSL_CERT_FILE$NODE_EXTRA_CA_CERTS~/.ccr/ca-bundle.crt → system bundle).
  • Configurable sync cadence: the 3s debounce / 90s backstop become clamped config knobs; cloud bootstraps set DIRA_SYNC_BACKSTOP_SECS=15 so an abruptly reclaimed VM loses at most ~15s of un-synced tail (batches were already idempotent with per-chunk cursors).
  • DIRA_IDENTITY_EMAIL: attribution override for VMs whose git config user.email is the platform bot.

Observability

  • dira doctor gains cloud.runtime, cloud.reachability (TLS failures name the extra-CA opt-in; other transport failures name the egress allowlist), and cloud.bootstrap.
  • dira status --json / dira sessions --json: status --json carries today's rollup, so it still reports sessions that have already ended — the scriptable "what did dira capture" surface.

Measured, not assumed

Everything below was measured inside a live Claude Code cloud session:

  • The egress proxy re-signs all TLS: with bundled roots only, every dira HTTPS call fails invalid peer certificate: UnknownIssuer, and succeeds once the runtime's CA is added. Nothing to configure — the CA is environment-provided.
  • app.dirahq.sh is refused at the proxy with CONNECT tunnel failed, response 403, before TLS. That one does need the environment's Custom network access setting. The two failures are separate, and the docs now say which is which.
  • Hooks fire in claude -p headless mode (SessionStart, UserPromptSubmit, Stop, SessionEnd) with no trust dialog, and CLAUDE_PROJECT_DIR is set. The committed hook command is nonetheless spelled ${CLAUDE_PROJECT_DIR:-.} so an unset variable can't resolve hooks to /.dira/… and fail them.

CI: a real agent session is captured

New .github/scripts/cloud-capture-smoke.sh + cloud-capture.yml. Every other test covers half the story — the unit tests pin what cloud init writes, cloud_init_e2e pins the artifacts, doctor --probe drives dira's own synthetic hook. None prove the part the design rests on: that a harness we do not control, wired only through committed config, fires hooks that land as counted events.

The job wires a throwaway repo, sets CLAUDE_CODE_REMOTE=true so the bootstrap takes the real cloud branch, runs Claude Code headless, and asserts on dira status --json that a session was captured and attributed to the fixture's canonical repo ref — which proves the writer's git enrichment ran, not merely that an event arrived.

Needs a CLAUDE_CODE_OAUTH_TOKEN repo secret (claude setup-token); without it — fork PRs, where GitHub exposes no secrets — it exits 0 with a SKIP so contributors never see a red check they cannot fix. Bounded by --max-budget-usd, path-scoped, GitHub-hosted runner.

Test plan

  • just ci passes — run as its constituent commands (cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace — all 32 suites green)
  • sh -n clean on both generated scripts; --provision-only verified to start the daemon, pick the runtime-declared CA, and leave the store empty (no phantom event), and to be a clean no-op outside a cloud runtime
  • The cloud-capture smoke test was run for real inside a Claude Code cloud VM: PASS — sessions=1 agent_seconds=6 project=github.com/dira-smoke/cloud-capture
  • The full bootstrap → install → daemon → hook-capture loop exercised against the published v0.5.1 release assets (download, checksum verification, daemon up, events captured)
  • /contract changed (schema 1.4, additive): just contract regenerated attestation.schema.json + the signing vector with the drift gates green; new serde test pins camelCase round-trip and byte-identical omission; the daemon stamp is covered by a dirad test that pins both branches (stamped inside a runtime, absent outside)
  • The signed byte stream changed only via the regenerated fixture: the vector now populates runtime/runtimeSessionRef and was re-verified cross-language by the cloud's TS verifier (VALID — Rust signature verified by TypeScript over JCS(payload))
  • If this changed install.sh / install.ps1: not applicable (shellcheck unavailable in this environment; the new scripts are sh -n / bash --posix -n clean)

Follow-ups

  • The cloud counterpart (runner-token minting/claiming, fail-closed revocation, runtime badges, grouped runner devices) is implemented in dodi-smart/dirahq-cloud#150; end-to-end sync works once it deploys.
  • dira init cursor wires sessionStart/sessionEnd, which are absent from Cursor's currently documented hook list — pre-existing behaviour in cli/sources, worth verifying separately.

Checklist

  • Commits are signed off (git commit -s) per the DCO
  • Commit messages follow Conventional Commits with a mandatory scope from cli, daemon, contract, ci, release, repo, deps (see CLAUDE.md)
  • This PR targets develop (the integration trunk), not main
  • Capture stays metadata-only — no prompt text, file contents, or diffs added to the store, an event, or an attestation (the runner token's value never enters diagnostics or facts; only its presence — and the 1.4 fields carry a runtime name and a session id, never content)

🤖 Generated with Claude Code

https://claude.ai/code/session_01S6tcfjfNEYvR926UrwVvfn

@azlekov
azlekov marked this pull request as ready for review August 27, 2026 06:45
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Knowledge layer

Check Result
Decision checks passed
Guard trailers passed

Spec currency is reported in the job summary and never fails a build, the same severity the local pre-commit hook uses.

@azlekov
azlekov force-pushed the claude/dira-cli-cloud-runtime-3eb50e branch from 6684610 to 2f5f6af Compare August 28, 2026 20:46
azlekov added a commit that referenced this pull request Sep 2, 2026
Split `command_is_portable_wrapper` out of `command_invokes_hook` and make
`event_is_wired` crate-visible: two upcoming callers need the wrapper /
direct-form distinction rather than the union — `dira init` in merge mode
must treat a committed portable entry as already wired instead of adding an
absolute-path twin beside it, and the portable hook must yield to user-scope
wiring only when that wiring is not itself a wrapper.

Add `claude_user_settings_path()` honouring CLAUDE_CONFIG_DIR, used by both
`dira init --global` and the wiring reader, so writer and reader follow
Claude Code when its user config directory has been relocated instead of
reporting a stale `~/.claude/settings.json` as live wiring.

Why: prerequisite seam for the #132 review follow-ups (double hook delivery).
Refs: DIRASH-0022
Spec: doctor
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
@azlekov
azlekov force-pushed the claude/dira-cli-cloud-runtime-3eb50e branch from 2f5f6af to 36c299f Compare September 2, 2026 15:21
azlekov added a commit that referenced this pull request Sep 2, 2026
Split `command_is_portable_wrapper` out of `command_invokes_hook` and make
`event_is_wired` crate-visible: two upcoming callers need the wrapper /
direct-form distinction rather than the union — `dira init` in merge mode
must treat a committed portable entry as already wired instead of adding an
absolute-path twin beside it, and the portable hook must yield to user-scope
wiring only when that wiring is not itself a wrapper.

Add `claude_user_settings_path()` honouring CLAUDE_CONFIG_DIR, used by both
`dira init --global` and the wiring reader, so writer and reader follow
Claude Code when its user config directory has been relocated instead of
reporting a stale `~/.claude/settings.json` as live wiring.

Why: prerequisite seam for the #132 review follow-ups (double hook delivery).
Refs: DIRASH-0022
Spec: doctor
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
@azlekov
azlekov force-pushed the claude/dira-cli-cloud-runtime-3eb50e branch from 36c299f to 9eb6cdc Compare September 2, 2026 15:31
azlekov and others added 5 commits September 2, 2026 19:19
`SessionRollup` gains `runtime` (`claude-web`, `cursor-cloud`, …) and
`runtimeSessionRef` (the harness's own session id), both optional and
omitted when absent, so the dashboard can badge cloud-agent work and
deep-link transcripts without inferring anything from device labels.
Additive minor bump 1.3.0 → 1.4.0: an older cloud strips the unknown keys
and gates on the schema major only, so rollout order is free. Still
metadata-only — a runtime name and an opaque id, never content (D-0001).

The batch builder leaves both fields unset; the daemon stamps them at
flush time from the running environment. Batch ids never derive from
session rollups, so the post-assembly stamp cannot perturb chunk identity
or dedup. The signing vector populates both fields so the cross-language
fixture exercises them; schema and vector regenerated via `just contract`.

Refs: D-0001
Refs: D-0020
Spec: attestation-sync
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…and the flush-time runtime stamp

Cloud agent runtimes route all egress through a TLS-intercepting proxy
whose CA exists only inside the VM, and D-0011's bundled-roots-only trust
store fails every HTTPS call there. `DIRA_EXTRA_CA_CERTS` (DIRASH-0033)
names a PEM bundle whose certificates are *added* to the bundled roots by
`dira_core::httpclient::builder()`, the one construction site every
device→cloud client now uses. Never a store swap, never `SSL_CERT_FILE`;
a missing or malformed bundle degrades to the default client with a
warning, and a partially corrupt bundle keeps its valid certificates
(one whole-bundle build first, per-block salvage only on failure).

The sync debounce and backstop become clamped config knobs
(`DIRA_SYNC_DEBOUNCE_SECS`, `DIRA_SYNC_BACKSTOP_SECS`, defaults 3/90 so
the historical constants hold) so an ephemeral VM can flush eagerly and
lose at most seconds on an abrupt reclaim. `DIRA_IDENTITY_EMAIL`
overrides the attribution email for VMs whose git identity is the
platform bot; it must carry an `@` and a plausible length.
`dira_core::runtime::detect` reads the vendor marker or an explicit
`DIRA_RUNTIME`, clamps both fields to 64 chars, and takes the Claude
session ref only on the claude-web branch; the daemon stamps the result
onto every rollup leaving the process, and outside a cloud runtime the
keys stay off the wire, byte-identical to 1.3.

Refs: DIRASH-0033
Refs: D-0011
Refs: D-0009
Refs: D-0019
Refs: D-0008
Refs: D-0018
Refs: D-0020
Refs: DIRASH-0031
Refs: D-0001
Spec: daemon-lifecycle
Spec: attestation-sync
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
`dira cloud init` generates portable, repo-committed capture wiring:
`.dira/hook.sh` (run-time `dira` resolution, always exit 0),
`.dira/bootstrap.sh` (in a cloud VM: digest-verified install of the pinned
release, daemon start, runner-token device claim, then it forwards its own
SessionStart event; on a laptop: straight to the forward), a
`.gitattributes` pinning LF, and portable commands merged into the project
`.claude/settings.json` / `.cursor/hooks.json`, replacing machine-specific
`dira init` entries. It is repo-root anchored, idempotent, pins the release
digests at generation time (`--no-pin` to opt out, a failed fetch warns and
writes the unpinned form), refuses to overwrite an unparseable config when
wiring several harnesses, and warns on dev builds, gitignored outputs, and
Windows. Cursor cloud agents provision from `.cursor/environment.json` via
`--install-only` / `--provision-only`, since cloud agents never run
`sessionStart`.

`dira device link --runner-token` (env `DIRA_RUNNER_TOKEN`) is the
headless claim: same endpoint and invariants as the code claim, no TTY,
bounded timeouts, the client nonce persisted across retries, the token
never on argv. The committed portable hook yields to live user-scope
wiring only when the project config also carries the portable wrapper
(DIRASH-0037), so laptops with both never double-count and a stray env
marker cannot drop events; `dira init` in merge mode treats a portable
wrapper as already wired. `dira doctor` gains `cloud.runtime`,
`cloud.reachability` (probed only inside a cloud runtime or on explicit
`--check`, never a failure), `cloud.bootstrap`, and `hooks.scope_overlap`.
`dira status --json` / `dira sessions --json` are the scriptable capture
surface; errors are a JSON envelope on stdout.

The bootstrap isolates its provisioning subprocesses from the hook's stdin,
bounds every curl, prefers the embedded digest with the release `.sha256`
as fallback (sha256sum → shasum → openssl), probes flock support, logs
every skipped provisioning branch, and fails `--install-only` loudly while
hook mode always degrades to "not instrumented".

Refs: DIRASH-0022
Refs: DIRASH-0023
Refs: DIRASH-0029
Refs: DIRASH-0030
Refs: DIRASH-0033
Refs: DIRASH-0037
Refs: D-0003
Refs: D-0004
Refs: D-0006
Refs: D-0007
Spec: cloud-runtime
Spec: doctor
Spec: harness-sources
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…d runtime

Every other test covers half the story; this job wires a throwaway repo
with `dira cloud init --no-pin`, sets `CLAUDE_CODE_REMOTE=true` so the
bootstrap takes its real cloud branch, runs Claude Code headless, and
asserts on `dira status --json` that a session was captured and attributed
to the fixture's canonical repo ref, and on the store that a session-start
event landed. Needs a `CLAUDE_CODE_OAUTH_TOKEN` secret; without it (fork
PRs) it exits 0 with a SKIP. Bounded by `--max-budget-usd`, path-scoped,
GitHub-hosted, with the Claude Code CLI pinned instead of `latest`.

Refs: D-0012
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
Adds docs/cloud-runtimes.md and the `cloud-runtime` living spec (runner-token
endpoint contract for dirahq-cloud, the measured TLS and egress behaviour of
cloud runtimes, the yield rule, digest pinning, the doctor probe policy),
records DIRASH-0033 (extra CA roots are an explicit, additive opt-in; the
committed bootstrap may promote a runtime-declared CA file) and DIRASH-0037
(the portable hook yields to live user-scope wiring, two-sided, no cache,
no time window), refreshes the doctor spec for the new checks and the
D-0006 exemption, and regenerates the adapter files.

Refs: DIRASH-0033
Refs: DIRASH-0037
Refs: DIRASH-0022
Refs: D-0006
Spec: cloud-runtime
Spec: doctor
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
@azlekov
azlekov force-pushed the claude/dira-cli-cloud-runtime-3eb50e branch from 9eb6cdc to 0587797 Compare September 2, 2026 16:22
@azlekov
azlekov merged commit 92a85cf into develop Sep 2, 2026
9 checks passed
@azlekov
azlekov deleted the claude/dira-cli-cloud-runtime-3eb50e branch September 2, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant