This is the clean-slate local runtime for customer machines. It is the target
production path for the daemon, CLI, macOS app, agent skill, and /apps
integration. The retired Python companion implementation has been removed from
the production repository path.
The public ottto repository is intended to be enough for an external
developer, support engineer, CI job, or coding agent to install, set up, verify,
repair, diagnose, update, and uninstall Ottto without private repository
knowledge.
Stable releases are distributed through the install owners advertised by the release manifest:
- Homebrew tap: installs
ottto, installsottto-service, and ownsbrew services start ottto. - Hosted native installer: downloads and verifies a signed, notarized DMG or PKG without installing mutable shell-managed payloads.
- GitHub Releases and release notes: publish immutable manifest-backed artifacts, checksums, SBOM/provenance metadata, and rollback instructions.
- Signed
Ottto.app: provides the native app path while staying aligned with the sameottto-serviceruntime and release manifest.
net.ottto.service is a single-owner user LaunchAgent. A normal install,
launch, upgrade, or restart may repair only the LaunchAgent it already owns:
Homebrew through brew services, the native app through its bundled helper,
and the hosted installer through its installed helper. Replacing a different
owner requires an explicit migration command; ordinary app launch must use a
Homebrew-owned service without rewriting it, and ordinary Homebrew service
start must not silently take over an app-bundle-owned job.
Final public-v1 closeout verifies that the public repository resolves as
ottto-ai/ottto, uses main as its default branch, and has that public main
branch head bound to the launch runtime commit. It also verifies that the exact
stable GitHub Release is non-draft, non-prerelease, has targetCommitish
bound to either main or the launch runtime commit, and contains only
canonical tag-scoped download assets for release-manifest.json, every stable
manifest artifact, and the CycloneDX SBOM before
stable_release_published can pass.
The same closeout requires a redacted public Public CI push-run report from
the public runtime commit whose public run URL matches the reported run ID,
with a unique workflow job inventory, canonical public GitHub job URLs for that
run, and the reviewed runtime, public-surface, and advisory jobs green.
The public repo, release, CI, and final closeout checked_at fields plus
GitHub release/CI timestamps must be parseable ISO-8601 UTC timestamps, so
reviewed evidence cannot carry ambiguous local time strings. Release and CI
access reports also require checked_at to be at or after the reported GitHub
event, and final closeout requires its checked_at to be at or after each
component access report.
Stable preflight also requires a passing, redacted internal stable-candidate RC
evidence record from a stable-candidate manifest built from the same commit,
so stable publication cannot skip candidate artifact trust, installer, setup,
diagnostics, update, rollback, and static install-owner checks. The
stable-candidate RC gate rejects candidate macOS artifacts that are not marked
signed, notarized, and Gatekeeper-assessed, and its evidence skeleton requires
explicit signature, notarization, Gatekeeper, and local-runtime pass/fail facts
bound to ottto-service, net.ottto.service, protocol v15, the
stable-candidate version/channel, and candidate release-manifest SHA-256.
Stable closeout then requires clean-machine evidence for each advertised install
owner, including setup, app detection, Codex verify/fix, diagnostics, logout,
update/upgrade, uninstall, reinstall, post-reinstall status, re-onboard after a
local Keychain wipe, and owner-specific trust checks before latest can be
treated as externally ready. Each owner entry must also bind the installed local
runtime to ottto-service,
net.ottto.service, protocol v15, the exact stable version/channel, install
owner, and release-manifest SHA-256. The closeout gate rejects extra required
install owners and unknown per-owner check names so the matrix cannot pass with
unreviewed launch evidence.
Setup is browser-claim-first. The CLI never collects an Ottto password in the
terminal. Agents and headless sessions use --json --no-browser --no-wait,
show the returned claim URL or claim code to the user, and resume through the
same daemon-owned setup flow.
The first public smoke commands are:
ottto status --json
ottto receipts --json --limit 20
ottto context --json
ottto setup --json
ottto apps detect --json
ottto verify --app codex --json
ottto verify --repair --app codex --json
ottto doctor --json
ottto fix --app codex --json
ottto diagnostics collect --json
ottto update check --jsonPrivacy defaults are part of the product contract. Local usage sync starts only
after claim approval, uses a six-month default backfill capped at 1000 files per
app/source, and must not upload raw prompts, raw output, absolute local paths,
cookies, credentials, or secret material. Live telemetry is source-level
opt-in, diagnostics upload is explicit-approval-only, and risky collectors are
excluded from default v1. Codex Cloud Sessions additionally requires the
user's versioned grant for the exact local device/user/organization scope plus
server policy approval; without both, its supervisor remains inert.
OTTTO_CODEX_CLOUD_SESSIONS_DISABLED=1 always wins.
This public repository owns the local runtime, installer/release tooling, public docs, tests, redaction, diagnostics, agent adapters, connector SDK/testkit, manifest schemas, and safe first-party source packages. The cloud application, backend, frontend, billing, auth, analytics, infrastructure, private operations, and planning docs remain private.
ottto-protocol: stable JSON models shared by the daemon, CLI, app, agent skill, backend event ingestion, and web setup flows.ottto-core: platform-neutral local logic such as source health summaries, repair planning primitives, diagnostics redaction, and stable CLI errors.ottto-service: the per-user daemon and future local authenticated control API owner.ottto-cli: theotttodeveloper, support, CI, and AI-agent interface.
Every customer-facing local action should cross the typed protocol boundary:
- status
- receipts
- setup
- doctor
- fix
- verify
- diagnostics collect
- update check
- uninstall
The SwiftUI app and agent skill should talk to ottto-service through this
protocol. The app must not shell out to the CLI for normal behavior, and the web
app must not duplicate local setup logic.
External install, setup, privacy, diagnostics, support, connector,
release-verification, agent-adapter, troubleshooting, and JSON automation examples live in
docs/. These docs are written to stand alone in a public
repository or release package without private repo links.
The future public ottto repository must be populated from the approved export
surface under this local-platform tree, not by copying arbitrary private
monorepo paths. The export manifest and checks live in
public-export/. Run
scripts/public_repo_export_check.sh --require-no-rewrites before any public
repo bootstrap or runtime move; it verifies export roots, fails private paths
and secret-like content, and fails private repo references that would need to be
rewritten to the public repository name. Current release attestation defaults already name
ottto-ai/ottto, so the expected pre-bootstrap bundle output is
rewrite-required references: 0; any new rewrite-required release/runtime
reference is a regression to fix before cutover.
scripts/public_repo_export_bundle.sh --force
materializes the root-shaped public staging tree, including connector
SDK/testkit crates, first-party source packages, the generated connector
registry, public manifest schemas, public GitHub Actions and Dependabot
configuration, and root Apache-2.0 license, NOTICE, security, contribution,
conduct, support, and trademark policy files. The bundle refuses rewrite-required
source by default; --allow-rewrites is reserved for legacy/test fixtures. The generated manifest records
every staged non-manifest file with SHA-256, size, mode, and executable-bit
metadata plus a deterministic content_sha256; run
scripts/public_repo_manifest_check.sh --staged-output dist/public-export/ottto
to verify that inventory before bootstrap. The generated tree must also pass
scripts/public_repo_skeleton_check.sh, which verifies the public repo shape
for runtime crates, connectors, docs, release tooling, policies, CI, and agent
adapters before bootstrap, and
scripts/public_repo_contract_check.sh --staged-output dist/public-export/ottto,
which verifies the generated root-shaped tree still carries the public CLI
JSON/NDJSON fixtures, local-control protocol fixtures, generated connector
registry, manifest schemas, setup claim fixtures, diagnostics redaction
fixture, release manifest schema, and private backend/frontend consumer
assumptions. When --private-repo-root is supplied, the same gate also checks
the private runtime pin at
backend/app/domain/local_platform/public_runtime_pin.json, so private
consumers only validate against the pinned public manifest digest. Once the
public repo is the runtime authority, run the same contract gate with
--require-public-authority; it requires the private pin to name a
public_repo_commit and verifies that the checked public root is a clean git
checkout at that commit. Before any public commit or runtime move, run
scripts/public_repo_secret_scan.sh --staged-output dist/public-export/ottto
to scan current export candidates, candidate path history, and the generated
bundle for private paths and secret-like content.
Run scripts/public_repo_surface_ci.sh --staged-output dist/public-export/ottto
to copy the generated root-shaped bundle into a temporary git checkout and run
the same public-surface gate that the exported GitHub Actions workflow uses:
schema/registry validation, shellcheck, export tests, manifest/skeleton,
secret-scan, contract checks, release/installer dry-run tests, and a
self-exported bundle verification. This includes release manifest,
stable-candidate RC, stable preflight, stable closeout, stable QA template,
Homebrew, hosted installer, and CycloneDX SBOM generator tests.
The skeleton gate also requires docs/support.md, the public support runbook
covering redacted triage, diagnostics, escalation, data boundaries, and
public-v1 closeout readiness.
Use scripts/public_repo_bootstrap_plan.sh --target-dir <clean-public-checkout> --report public-bootstrap-plan.json
to build and verify the bundle, check that the target checkout is a clean git
root outside the private monorepo with origin bound to ottto-ai/ottto,
print a dry-run add/change/delete plan, and write a machine-readable report
without absolute local filesystem paths. Only pass --apply after reviewing
that plan; apply mode copies the verified bundle into the target checkout and
reruns the public manifest, skeleton, secret-scan, and contract gates against
the applied tree.
After apply, run
scripts/public_repo_cutover_closeout.sh --target-dir <public-checkout> --source-repo-root <private-monorepo> --bootstrap-report public-bootstrap-apply.json --report public-cutover-closeout.json
before committing or enabling branch protection in the public checkout. The
closeout gate verifies the target manifest, skeleton, secret scan, public/private
contracts, and public-surface CI smoke against the applied checkout, then writes
a path-safe JSON report with the target origin repository binding, git status,
manifest digest, bootstrap report branch/head plus remote/digest consistency,
private source history/consumer-contract scope, per-check results, and
ready_to_commit=true.
The public CLI is the developer, support, CI, and AI-agent surface over
ottto-service. Help text for visible commands and options is frozen by
fixtures/cli/help-contract.txt and the ottto-cli unit test
cli_help_matches_frozen_contract; update both intentionally when changing the
public command surface.
Codex lifecycle automation uses .agents/skills/ottto/SKILL.md, which stays
thin over this contract: it calls ottto --json, uses public --app commands,
and does not duplicate setup, repair, diagnostics, or install-owner logic.
Claude Code lifecycle automation uses .claude/skills/ottto/SKILL.md, the
project skill equivalent, with no hooks, status-line monitor ownership, or
preapproved tool grants in v1.
MCP is deliberately not a public-v1 adapter. docs/agent-adapters.md records
the deferral: any future MCP server must wrap the stable CLI/local protocol and
must not own setup, repair, diagnostics upload, update, credential, or source
collection authority.
Current locked commands:
ottto status --json
ottto status --json --watch
ottto receipts --json
ottto receipts --json --limit 20 --since 2026-09-10T00:00:00Z --source codex
ottto status --refresh-agent-status --json
ottto context --json
ottto context --json --range today --source codex
ottto context --json --all-machines --max-tokens 4000
ottto costs --json
ottto costs --json --range today --source codex
ottto costs --json --all-machines --bucket day
ottto cloud materials --source vertex --json
ottto cloud plan --source vertex --config-file connector.json --json
ottto cloud register --source vertex --config-file connector.json --approve --json
ottto cloud test --source vertex --config-file connector.json --json
ottto cloud status --source vertex --json
ottto cloud sync --source vertex --days 30 --approve --json
ottto sessions --json
ottto sessions --json --limit 20 --source codex
ottto sessions --json --all-machines --sort-by cost --sort-dir desc
ottto recommendations --json
ottto provider-impact --json
ottto provider-impact --json --provider openai --app codex --kind quota --limit 25
ottto apps --json
ottto apps detect --json
ottto apps status --app codex --json
ottto setup --json
ottto setup --json --no-browser --no-wait
ottto setup --json --timeout 300
ottto setup --claim-code <code> --json
ottto login --json
ottto login --json --no-browser --no-wait
ottto account --json
ottto logout --json
ottto logout --local-only --json
ottto logout --local-only --forget-installation --confirm --json
ottto doctor --json
ottto fix --app codex --json
ottto verify --app codex --json
ottto verify --repair --app codex --json
ottto diagnostics collect --json
ottto diagnostics collect --upload --approve-upload --accept-retention-disclosure --support-claim <claim> --json
ottto update --json
ottto update check --json
ottto uninstall --backup-state ./ottto-state-backup --confirm --jsonAutomation should pass --json; JSON mode prints one final JSON object and no
human summary text. --json --watch prints newline-delimited JSON progress
events followed by one final event with ok, exit_code, and either payload
or error; setup can return a payload with a nonzero exit_code when it needs
user action or times out. Setup opens a browser claim by default when no local
setup-run binding exists, prints the URL/code fallback in human mode, and waits
up to --timeout seconds for browser approval and setup progress before
returning exit code 61. Agents and headless flows can pass --no-browser to
skip auto-open and --no-wait to return the fallback claim payload immediately
with exit code 60. --watch without --json exits as invalid_request.
Human mode prints short summaries only and is not a parsing contract. context,
costs, sessions, recommendations, and provider-impact are agent-only
JSON commands; omitting --json exits as invalid_request.
All commands call ottto-service through the local-control protocol except
local uninstall cleanup and the hidden Claude Code status-line helper.
Stable CLI exit codes are:
| Code | Meaning |
|---|---|
0 |
Success or setup complete |
2 |
Invalid request or arguments |
10 |
ottto-service unavailable |
14 |
Backend unreachable |
16 |
Backend unavailable |
50 |
Permission denied |
60 |
Setup needs user or browser action |
61 |
Setup timed out |
70 |
Internal error |
Public command nouns use apps and --app for local app lifecycle work. The
lower-level agent-status --source <source> selector remains available for
existing automation while protocol payloads continue to use SourceKind.
context --source <source> intentionally accepts backend source slugs, not only
local app values, because cloud context can include API and cloud-provider
sources in addition to Codex, Claude Code, and Pi.
This workspace contains the Phase 1 protocol/core foundation and the first Phase 2 daemon control primitives:
- authenticated local daemon status access
- daemon-owned relay and source state
- daemon-owned repair locks
- read-only Codex and Claude Code config drift verification with
sha256:fingerprints plusverify --repairWriteConfig repair - repair-plan proposal skeletons with required approval-boundary metadata: setup-safe config repairs may use terminal approval only when tied to an active setup-run binding, credential/auth-adjacent actions require browser approval, disconnected or stale local account state is browser-only, and old repair payloads without authority/approval metadata fail deserialization
- typed diagnostics bundles with explicit redaction reports
- typed local control request/response envelopes
- authenticated
claude_accounts_status,claude_account_prepare,claude_account_reconnect,claude_account_check,claude_account_stop_waiting,claude_account_set_upkeep_consent,claude_account_register_path, andclaude_account_removeoperations for one bounded, versioned machine-local registry. The true defaultCLAUDE_CONFIG_DIR-unset slot is always present; up to nine absolute config-dir strings are stored exactly as registered with stable opaque ids and explicitmanaged/externalownership, and one explicit consent bit governs future background upkeep for the whole registry. Advanced path registration is external ownership. Managed prepare creates a private0700directory and returns an exact Claude Code launch command; customer-owned/loginremains entirely inside official Claude Code.claude_account_prepareremains the managed first-connect operation; exact reconnect reuses one already-registered custom slot and its exact launch command without creating a directory or duplicate registration. Exact checks are single-flight, stop is durable until explicit prepare/reconnect replay, and these protocol-v18 operations perform no login automation, token refresh, inference, credential write, or Keychain write. Authenticated local status includes each exact slot's already-collected session, weekly, scoped-limit, and usage-credit values with captured/provider observation times plus typed fresh, stale, or partial state; this additive machine-local view does not change the backend snapshot wire. - consented background freshness upkeep for registered custom Claude slots.
On daemon startup, macOS wake/network restoration, and normal quota
collection opportunities, an expired access credential with a still-valid
absolute refresh deadline may run only the resolved installed
claudebinary with argvdoctor, exactCLAUDE_CONFIG_DIR, a shared cleared minimal environment, closed stdin, discarded output, and a bounded timeout. Success requires read-only proof thatexpiresAtadvanced; durable per-expiry claims and exponential backoff suppress restart/wake/process storms. The default slot is never probed. The existing subscription-usage off-switch pauses both reads and upkeep without clearing registrations, consent, or caches, while an absent-by-default operational sentinel stops only new upkeep commands. Ottto still performs no raw OAuth refresh, login, inference, credential write, second scheduling service, or Keychain mutation. - persistent Unix-socket serving with
--oncesmoke mode - CLI-to-daemon Unix-socket requests for status, setup, login, account, logout, setup claim codes, doctor, repair, verify, diagnostics, and uninstall
- CLI autostart recovery that kickstarts the standard per-user LaunchAgent when the default socket is unavailable, while leaving custom sockets deterministic
- macOS LaunchAgent plist generation and launchctl install-plan construction
- 0600 file-backed local control-token storage for CLI/agent access, avoiding macOS Keychain prompts on normal local-control calls
- daemon-owned account binding with non-secret account metadata persisted to
~/Library/Application Support/Ottto/account.json - daemon-owned setup-run binding metadata persisted to
~/Library/Application Support/Ottto/connection.json, allowing source verification to survive daemon restarts without exposing setup-run secrets to the app. The binding now persists the validatedapi_base_urlfrom the/appsdeep link so standalone Companion verify and setup-run polling keep talking to the same local or production backend that created the run; legacy binding files without the field default to the configured API base until the next setup deep link rewrites them. - daemon-owned persistent machine identity in
~/Library/Application Support/Ottto/machine.json, using a hashed macOS hardware UUID when available plus a generated installation id. Placeholder development ids are replaced on the next daemon startup. - daemon-owned setup-run and relay-device secret storage in the
net.ottto.serviceKeychain service, without exposing those secrets to the app. macOS Keychain operations are bounded by a timeout and mirrored to owner-only files under the Ottto support directory so dev/preview signature churn, unavailable Keychain access, or blocking Keychain calls do not strand verification after restart. - local-only logout moves the relay-device id and secret into a dormant, owner-only prior-credential slot. A later browser claim presents that exact predecessor proof, so emergency local cleanup does not strand an otherwise valid installation. The slot is never printed or logged and is deleted after a successful reattach, cloud logout, installation reset, or uninstall.
- native app auth commands:
auth_status,auth_start,auth_complete, andauth_reset. Reset/logout is cloud-first by default: the daemon records alocal-client/disconnectin Ottto before clearing local account, connection, and setup-run token state.local_only=trueis reserved for emergency local cleanup when the backend cannot be reached or the website state is stale. Neither mode clears account/device identity while Cloud sessions still needs an exact backend DELETE confirmation; revoke it in the Ottto app, wait for confirmation, then retry logout. Browser claim completion and setup-driven relay-device registration use the same guard before any backend identity rotation. While one accepted identity update is in flight, competing setup, logout, device writes, and Cloud-session mutations fail withidentity_mutation_in_progressand are safe to retry. Setup and login are idempotent no-ops while a bound user is already connected; they cannot create a pending-claim overlay over a healthy binding. Rejected or unauthorized setup-run polling is terminal locally instead of retrying a stale run. ottto logout --local-only --forget-installation --confirmis the explicit destructive reset path. It clears installation id, account scope, active and prior credentials, setup bindings, destination receipt namespaces, and first-sweep progress together, while leaving Codex, Claude Code, and Pi source data untouched. Restart the daemon before pairing so it creates a fresh installation identity. Because replacement credentials use a new destination namespace, ordinary replacement never reuses the old destination's receipts; it creates clean receipt and first-sweep state for the new destination while preserving the stableotm_…machine identity.ottto uninstallrequires--confirm, warns about receipt and first-sweep loss, and accepts--backup-state <directory>to export only those receipts and first-sweep state first (never relay credentials or account secrets). Uninstall also inventories and unregisters every Launch Services record for the production Companion bundle identifier before removal. Only stable packages declare the productionottto://URL scheme; development, preview, stable-candidate, QA, and runner bundles cannot become stale setup-link handlers.- native app account-state UX now treats browser login and local app binding as separate states: Verify returns a sign-in-specific message when no local account is bound, and the SwiftUI app keeps polling a pending browser claim so successful browser approval updates the local UI even if the deep link handoff is missed.
- real setup-run attach/resume for web-created
/appsruns, including local Codex/Claude/Pi scan publication, setup-action polling, bounded smoke verification commands, and redacted setup JSON shared by Companion and the CLI fallback. A fresh deep link withclaim_codereattaches the local binding to that run, while a mismatchedsetup_run_idwithout a claim code fails clearly instead of silently replacing the existing binding. - setup-run answers from trusted native clients, currently used by Companion to skip or disable a failed source through the setup-run token without exposing browser credentials or setup secrets to the app.
- setup-run actions from trusted native clients, currently used by Companion to
continue
ready_to_installsources by queueing and executing local-clientinstall_source/verify_sourcework through the daemon-owned setup-run token. - setup-run install completion payloads use backend-safe metadata keys, so successful Codex or Claude setup completion is not rejected by the setup-run secret/header guard.
- backend setup calls are routed through typed local-control backend errors
(
backend_unreachable,backend_rejected,backend_unavailable, andbackend_response_unexpected) with safe status/body excerpts, so CLI and Companion can distinguish network, rejection, service, and protocol failures. - setup-run polling sends backend heartbeats before asking for work, refreshing the short setup-run token and clearing disconnected state while the daemon is alive.
- SMAppService-oriented LaunchAgent packaging metadata with the
net.ottto.service.xpcMach service name - a real launchd-backed XPC listener for
ottto-service serve-xpc; it also serves the default per-user CLI/agent Unix socket for app-bundled launches, and a supplied debug socket overrides that fallback path - trusted native-app authorization that inspects the local peer process,
validates the Companion executable path, and can enforce a Developer ID code
requirement through
OTTTO_COMPANION_TEAM_IDorOTTTO_COMPANION_CODE_REQUIREMENT - a macOS packaging rehearsal script that builds the SwiftUI app bundle, embeds Rust CLI/daemon helpers, ad-hoc seals dev/preview bundles, writes a release manifest, and separates those internal builds from stable signed/notarized requirements
- a release gate that validates artifact presence, SHA-256 integrity, manifest shape, rollback metadata, macOS app bundle sealing, and stable-channel signing/notarization/Gatekeeper state
- a stable macOS preflight wrapper that blocks publish readiness unless the manifest is stable-only, artifact URLs and hashes match the rollback immutable prefix, Developer ID signing uses hardened runtime and timestamps, notarization profile access works, stapling validates, and Gatekeeper accepts the app/CLI/daemon artifacts. Dry-run mode validates non-secret manifest, rollback, and hash checks without requiring Apple credentials.
- a Homebrew formula generator that writes
Formula/ottto.rbfrom the stable release manifest only after Homebrew is listed as a supported install owner and the CLI/daemon artifacts are immutable-prefix pinned, signed, notarized, Gatekeeper-assessed, and SHA-256 pinned. The generated formula installs the public CLI asottto, installs the service executable asottto-service, owns Homebrew launchd labelnet.ottto.service, starts withbrew services start ottto, updates withbrew update && brew upgrade ottto, and uninstalls withbrew services stop ottto && brew uninstall ottto. The Homebrew service remains the LaunchAgent owner whenOttto.appis also present; the app talks to the running service instead of replacing the Homebrew plist. - a stable hosted native installer wrapper generator that writes
install-macos.shonly from a stable manifest whose native macOS app artifact is HTTPS, immutable-prefix pinned, SHA-256 pinned, signed, notarized, Gatekeeper-assessed, and advertised for thehosted_installerowner. The wrapper verifies and opens the DMG or PKG; it does not install mutable shell payloads, clear quarantine, or bootstrap launchd jobs. - a stable release closeout gate that validates redacted clean-machine QA
evidence before stable
latestpromotion. The evidence must match the exact manifest version, commit, and SHA-256 and prove every advertised install owner passed its install, service, status, update, uninstall, and trust checks while binding that owner to theottto-serviceruntime,net.ottto.service, protocol v15, stable channel/version, and release-manifest SHA-256. - a CycloneDX SBOM generator plus release-manifest
supply_chaincontract for SLSA v1.2 Build Track provenance. Stable release gates require the SBOM URL and SHA-256 under the immutable prefix, verified SBOM attestation with predicatehttps://cyclonedx.org/bom, SLSA provenance predicatehttps://slsa.dev/provenance/v1, Build L2 or better, and subject coverage for every artifact plusrelease-manifest.jsonand the SBOM. - a dispatch-only protected macOS stable release workflow that builds, signs, notarizes, attests, binds supply-chain metadata, CMS-signs the final manifest, and uploads artifacts without CDN write permissions or stable-channel promotion.
- a notarization helper that submits the generated macOS archives and updates manifest notarization flags only after Apple validation and local Gatekeeper assessment pass
- local and hosted preview installers that verify dev/preview artifacts,
install the app and Rust helpers to user-scoped locations, clear quarantine
for the explicit tester install path, and delegate LaunchAgent plist writing
to
ottto-service. Bootstrap refreshes only same-owner LaunchAgents by default and refuses cross-owner rewrites unless the operator supplies the explicit migration flag, so stale app-bundle helpers can be repaired without stealing Homebrew-owned or hosted-installer-owned jobs. Hosted installer readiness checks wait for the daemon without invoking CLI autostart, and the native app skips bundled SMAppService registration while the installer-owned user LaunchAgent exists. - an installed dev/preview E2E smoke that validates the app bundle, LaunchAgent, CLI-to-daemon protocol, real setup claim handoff when a claim is supplied, actionable Codex verification JSON, and diagnostics redaction
- a SwiftUI app flow that calls the daemon as a trusted local app client for account login, source verify/repair, and diagnostics collection without reading the daemon control token from Keychain. The native Companion keeps source rows compact and daemon-owned: brand marks, setup readiness, primary setup/verify/repair/skip actions, and disclosure details all render from the typed local-control status/setup/verify payloads instead of shelling out or scanning local files in SwiftUI.
- source verification that runs a bounded local smoke prompt, waits for fresh
backend verification evidence, uses the daemon-owned setup-run token to call
the backend local-client verification endpoint, updates local source health,
and reports
verified/no-fresh-telemetry/reconnect-required/local-smoke-failure states
instead of a placeholder pending result. Pi verification runs a real
non-interactive
pi --printprompt for every configured provider/model route from~/.pi/agent/settings.jsonplus the default route, falling back topi --list-modelswhen settings are unavailable. A Pi source isverifiedwhen every route passes,warningwhen at least one route passes and at least one route fails, and failed only when no route passes. Smoke diagnostics read both stderr and stdout through the same redaction/truncation path so CLIs that print authentication failures on stdout still produce actionable safe setup errors. - built-in local OTLP relay support in
ottto-serviceon127.0.0.1:43119for Claude Code and Codex, with deterministic per-user fallback ports when another macOS user account or stale Ottto service already owns the default loopback listener: approved setup-runinstall_sourceactions register a telemetry device, store the relay secret locally, patch~/.claude/settings.jsonor~/.codex/config.tomlwith logs, metrics, and traces endpoints for the active relay endpoint, expose/healthzfor local readiness, and expose a narrow browser CORS/controlendpoint for M3telemetry_controlenable/disable/status requests from the Apps page. The control endpoint returns the private-network CORS header required by Chromium for productionhttps://ottto.netto reach the loopback daemon, while the frontend marks the request target asloopback. The companion/local-healthendpoint returns the current relay-device id inside its canonical health payload only for an allowlisted browser origin. This lets the Apps page distinguish separate macOS-user installations on one physical Mac without exposing the relay secret, account identifiers, or hardware UUID. The origin gate is a browser CORS boundary, not native-client authentication, so the returned device id is a non-secret pseudonymous binding label and origin-less probes remain redacted. Claude Code setup also installs a localstatusLinewrapper that preserves an existing status line command while feeding documentedrate_limitsfields into the local quota cache. The relay uses a source header to route shared-port payloads to source-scoped relay-token exchange before forwarding OTLP payloads to the setup run's persisted backend API base URL. Manual edits to the managed Ottto fence are rejected withmanual_fence_review_required, leaving local keys in place until the user reviews the file instead of guessing at cleanup. Pi setup actions also register or merge the local telemetry device source grant so route-smoke session imports can issue Pi-scoped relay tokens, but they skip Codex/Claude OTLP config patching because Pi telemetry is imported from local session files. The local scan reports Pi as requiring install when the Pi CLI/session files are present but the device grant does not yet includepi. - local Claude Code reasoning-effort reduction at that loopback boundary:
Claude's documented
claude_code.api_requestlogs use OTLP/HTTP JSON while metrics and traces remain protobuf. The relay accepts both fixed-length and bounded HTTP/1.1 chunked request bodies, de-chunks before reduction and cloud forwarding, and rejects ambiguous or unsupported transfer framing. It stores only session id, request timestamp, model, applied effort, and token counters in owner-only hashed sidecars; prompts, responses, commands, paths, account identity, email, and raw OTLP are discarded. Snapshot sync partitions matching transcript model/hour rows by this evidence, leaves uncovered residual usage as unknown, and preserves exact transcript totals. Organization-disabled live telemetry still returns its normal OTLP success response after the local reduction; transient forwarding failures retain the existing exporter retry behavior. - source-local snapshot parsing and background sync in
ottto-servicefor Codex~/.codex/sessions/**/*.jsonl, Claude Code~/.claude/projects/**/*.jsonl, and Pi~/.pi/agent/sessions/**/*.jsonl, streamed line-by-line and reduced to safe usage/title/model/timestamp/selector hashes instead of response, tool output, command output, or raw local paths. The Codex parser versioncodex_jsonl:v11reads currentevent_msgtitle updates,payload.info.total_token_usagetotals, and selector fields such asservice_tier,actual_service_tier,fast_mode,batch_mode,inference_geo,context_bucket, and cache TTL aliases from direct or nestedselector_contextrows. Mutable~/.codex/config.tomldefaults are available to live status display but are not backdated onto cumulative historical usage rows; only session-native selector evidence enters snapshot semantics. During the semantic-sync cutover, a legacy Codex index that had a config file in scope receives one conservative corrective reconciliation: the old stat-only index cannot prove which rows inherited config guesses. This correction cannot recur after the index advances. The parser then fills missing titles from local Codex sidecars in this order:~/.codex/session_index.jsonlthread_name,~/.codex/state_5.sqlitethreads.title, and finally a short, filtered first-user-prompt fallback. The fallback rejects setup/system text, AGENTS and environment blocks, pasted plans, command-looking text, raw ids, generic labels, and tool/function names such asexec_commandorwrite_stdin. Claude Code parser versionclaude_code_jsonl:v4carriesmessage.usage.speed, service tier, residency, batch, and context aliases into selector context. The Pi parser accepts both the current nativesession.idplus nestedtype: "message"records and the historicalsession_idplusmessage_endrecords. It carriesottto-selector/ottto.selectorcustom entries and per-message selector aliases into subsequent assistant-message usage rows. Provider response time is the canonical usage clock even when a later envelope timestamp crosses an hour. An exact response id deduplicates every matchingmessage/message_endcompatibility occurrence; repeated same-shape or id-less responses remain distinct, while unmatched divergent cross-shape reuse makes the file retryable. All three parsers emit content-free hourly activity buckets from persisted request/usage event timestamps; polling cadence affects freshness only and no wall-clock activity is inferred. Parser build versions are provenance and do not trigger scans or historical replay. Incremental scan identity uses transcript size, nanosecond mtime, a frozen scan-derivation version, and only that session's title/state sidecar digest. A pre-cutover entry migrates without parsing only when transcript stats and the reconstructed legacy sidecar identity both match; uncertainty forces a one-time correctness parse. Parsed snapshots are compared by semantic component hashes (usage, lifecycle, latency, context, display, attribution, and artifacts); collection time, parser build, file identity, and evidence-lineage metadata cannot manufacture a new snapshot. Semantic no-ops are retained in the local index but omitted from upload. A fully read, valid file that truthfully parses to no snapshot joins the v2 index's explicit confirmed-empty set, so unchanged empty transcripts cost no repeated parse work and any file/scan-identity change reparses them. Positive allowlisted consumption/request/cost evidence that produces no snapshot, or a positive usage record with no valid activity bucket, instead increments content-free traversal diagnostics and keeps that file retryable. The current frozen generation retains those counters across bounded backoff; it never records the lossy file as empty or advances its manifest contribution. Healthy files continue through quarantine-not-fence. Snapshot audit and status requests expose confirmed-empty, positive-usage-evidence, and dropped-usage counts. Versions, limits, budgets, quotas, and unknown positive metadata do not qualify as usage evidence. Historical walks occur only for first bootstrap or an explicit reviewed replay revision. The daemon starts the snapshot sync loop beside the local OTLP relay, uses source-scoped relay tokens and backend activity hints, scans the last six months by default with stricter backend windows allowed, caps each app/source at 10,000 recent files, strips filtered session titles or path-free workspace labels before upload when the effective backend org/user activity hint disables them, uploads changed snapshots in schema-v5 batches, and reports collector status and cap-hit metadata without exposing local paths. Every collector-status receipt declares its own kind —checkinfor a liveness beat (the independent heartbeat and the cycle-start in-progress marker) orscan_statusfor a scan result (completed traversal, error, changed loss census, cap-hit change, enabled/disabled transition) — so the meaning of a receipt is stated rather than inferred from its shape. A liveness beat never discloses scan evidence: it is built without access to any scan outcome, and a receipt whose declared kind its own payload contradicts is refused before it reaches the network. Every scan result also discloses, beside each public loss counter, the share the daemon settled as non-progressing (last_terminal_*) and the residue witness as counts only, so a backend can prove class by class that the loss a census discloses is loss no retry can change. A census that completed with only such settled residue is reported as a success —last_success_atset, zero consecutive failures — under the named codecensus_residue, but only once the backend's activity hint advertisescensus_residue_status_contract: census_residue_status:v1; until then the receipt keeps the legacyparse_errorshape and still carries the additive counters. Schema-v5 activity buckets are generated from timestamped local usage events: Codex cumulative usage rows without explicit request totals count as one observed activity event, and top-level snapshotrequest_countmatches the uploaded bucket total when buckets are present. Policy-disabled metadata uses a policy-scoped scan index so later re-enabling titles or labels can re-upload unchanged local files with the newly allowed display metadata. Per-model usage is grouped by(model, selector_hash), so mixed fast and standard usage for one model is emitted as separate model-usage rows without duplicating canonical model records. - adaptive per-source local snapshot cadence primitives: debounced file watches
through
notify, hot/warm/idle/cold/failing/disabled scheduler states, backend activity-hint polling, and a persisted scan index under~/Library/Application Support/Otttowhose backend-visible fields are hashes. Multi-page scans also keep an atomic, policy- and relay-device-scoped upload checkpoint made only of accepted semantic snapshot fingerprints plus a one-way destination namespace hash. A timeout, restart, or one invalid snapshot therefore resumes at the unaccepted work instead of replaying every earlier page. Scan indexes and historical-bootstrap completion are also bound to that destination so an account switch cannot reuse another account's delivery cursor; the checkpoint is removed only after the final markers are durable. - source-scoped relay snapshot client helpers for relay-token exchange, batch upload, collector status, and backend activity-hints, with missing relay device credentials logged as a safe local skip instead of leaking paths or secrets
- local protocol version 12 and local snapshot schema version 5 as clean
cutovers. Local control requests must include
protocol_version: 12, and backend local snapshot batch/status endpoints reject stale internal schema versions instead of compatibility-mapping them. Protocol v11 adds owner-aware update gates, manifest minimum-version/protocol metadata, and required repair-plan authority fields that distinguish server-backed setup-safe terminal approval from browser-required account, stale binding, and credential-rotation cases; old repair payloads without these fields are rejected at Rust and macOS app protocol boundaries. Protocol v10 adds explicit diagnostics upload approval fields, local-only default upload reports, support-claim authorization, and retention disclosure acceptance before any diagnostics upload attempt. Protocol v9 adds typed diagnostics bundles and explicit redaction reports for local/support/agent/setup/command output surfaces. Protocol v8 added registry-backed source descriptor review tiers, maturity badges, and collector descriptors for Codex, Claude Code, and Pi without adding staticsupported_platforms; runtime availability remains represented by source operation state. Protocol v7 added display-safe billing identity hashes/evidence for accounts, model routes, plan observations, and Pi route verification results; protocol v6 added Pi route classifications, per-route smoke results, and source-level verification warnings while preserving protocol-v5 uninstall planning/execution and lifecycle diagnostics.SourceHealthcarries daemon-owned source descriptors and repair backup metadata,AgentStatusSnapshotemits protocol-v4quota_windowsinstead of legacylimits. Each quota window carriesscope,freshness, optional model/account label, reset time, percentage, and remaining/quota fields;rate_limitedis an explicit state. Local snapshots can include optional user-approvedworkspace_display_labeltext while keeping the hashed workspace identity as the only default project key; org and user telemetry settings can independently disable title and workspace-label upload. The same title/privacy choice gates optional attribution display labels: a sanitized prompt prefix (96 bytes maximum) or allowlisted skill name may accompany an opaque template/skill fact only when a compatible backend advertises support. Older backends keep that capability off, and no new polling, process inspection, local permission, full transcript, or path is introduced. - display-safe agent status snapshots attached to
SourceHealthfor Codex, Claude Code, and Pi, including account, subscription, model, context, quota-window, credit-balance, capability-gap, and diagnostic fields where each local CLI exposes safe metadata. Codex OAuth usage collection is an explicit setup-gated collector because it reads the local Codex credential only insideottto-serviceto call an undocumented ChatGPT usage endpoint, converts the returned primary and secondary windows into session/weekly quota windows withstarted_atandresets_at, and converts provider credits into display-safe balances without serializing access tokens.SourceHealthandAgentStatusSnapshotalso carryplan_observationswith last-seen, current marker, evidence method, optional session id, and normalized billing/model attribution so the backend can keep multiple known plans for one source/computer. When a resumed Claude Desktop session remains in more than one account bucket, the uniquely newest bucket owns the live session observation; equal or missing timestamps fail closed. Pi--list-modelsoutput is parsed as a provider table and exposed through structuredavailable_model_detailsso OpenAI Codex, OpenAI API-key, Google Vertex Gemini, Google Gemini API-key, AWS Bedrock, Azure OpenAI, gateway, and unknown Pi model routes keep their provider, model provider, billing provider, billing channel, auth mode, source category, context/output size, thinking, image-support metadata, and optional local billing identity evidence instead of flattening to one unknown model list. Locald now prefers$PI_CODING_AGENT_DIR/settings.jsonor~/.pi/agent/settings.jsonfordefaultProvider,defaultModel,defaultThinkingLevel, andenabledModels, reads$PI_CODING_AGENT_DIR/auth.jsonor~/.pi/agent/auth.jsonfor display-safe account/project/credential fingerprints, and falls back topi --list-modelsonly when settings are unavailable. - cached agent status in
ottto status --json, explicit refresh throughottto status --refresh-agent-status --jsonorottto apps detect --json, source-specific refresh throughottto agent-status --source <source> --jsonorottto apps status --app <app> --json, and locald collectors that redact raw command output, secrets, and absolute local paths before backend publication. Background source snapshot sync is source-isolated: a scan or upload failure for one local app logs that source and continues with the remaining granted sources, so a broken Codex session scan cannot prevent Claude Code agent status from reaching the backend. Sync logs report phase-level safe causes such as agent-status upload, activity-hint request, local scan, or snapshot upload failure without exposing raw response bodies or local paths. Public CLI affordances useappsand--app; typed daemon protocol payloads keepSourceHealth,SourceKind, andsourcefields. Codex keepscodex login statusas the local diagnostic source and can carry pasted or future structured/statusevidence when available; Claude usesclaude auth status --jsonas high-confidence account/plan evidence. - diagnostics collection returns a protocol-owned
DiagnosticsBundlewith sectioned runtime/version/install/repair/security facts plus aRedactionReportandDiagnosticsUploadReport. Collection is local-only by default;ottto diagnostics collect --uploadrequires--approve-upload,--accept-retention-disclosure, and either an active Ottto login or--support-claim <claim>. The redaction report names covered surfaces (diagnostics,support_output,agent_output,setup_error, andcommand_output), protected categories (local paths, secret tokens, account identifiers, machine identifiers, raw prompts, and command output), redacted fields, and intentionally preserved fields. - setup scan publication uses protocol-owned agent-status redaction: safe provider, auth, plan, billing, account email, confidence, and model-route metadata is kept, while raw account/org ids and labels, path-like diagnostics, and secret-shaped values are nulled before leaving the machine. Provider account hashes remain the stable grouping key when an email is unavailable.
- bounded local command execution resolves agent CLIs from launchd-safe macOS
candidate paths including
~/.local/bin, Homebrew,/usr/local/bin, and system directories in addition to the inheritedPATH; daemon-launched status and smoke commands also receive that augmentedPATHso/usr/bin/envshebangs can find interpreters such asnodeunder launchd. - setup and standalone verification use the persisted setup-run API base URL, run Pi smoke prompts for every configured provider/model route, record whether a local Pi session file was created for each route, filter backend verification by model/model-provider/billing-provider, and report route-level no-telemetry failures as command failure, no local session, local session not uploaded, or verification service unavailable.
- native update checks read the release manifest and report
current,update_available, orunknownplus a separate gate ofcurrent,soft_warn,hard_block, orunknown. Ordinary stale versions are soft warnings; only below-min-supported versions or protocol-incompatible manifests hard block. The parser requires public-v1 update metadata and rollback metadata before trusting a manifest. Update instructions route through the detected install owner (Homebrew, hosted installer, app bundle, or unknown) so Ottto does not self-overwrite owner-managed files. The default manifest URL follows the local release channel, whileOTTTO_RELEASE_MANIFEST_URLremains available for explicit operator tests.
The remaining production hardening work is live macOS lifecycle and release execution: SMAppService packaging, reboot/startup validation, sleep/wake and network-interruption soak, Developer ID signing, notarization, Gatekeeper assessment, rollback, and support-runbook rehearsal.
Preview builds currently register the app-bundled LaunchAgent and use the typed JSON command envelope over XPC. Native app requests are accepted only from a trusted Companion process, using peer PID plus macOS code-signing/path validation; CLI and agent requests still require the control token. Stable customer distribution remains blocked until Developer ID signing, notarization, and Gatekeeper assessment pass.