Skip to content

feat(infra): default-off hosted-v2 control signer wiring and validator trust registration - #1901

Open
ai-mountain wants to merge 28 commits into
agent/coding-hosted-validator-controlfrom
agent/coding-hosted-control-signer-wiring
Open

ai-mountain wants to merge 28 commits into
agent/coding-hosted-validator-controlfrom
agent/coding-hosted-control-signer-wiring

Conversation

@ai-mountain

@ai-mountain ai-mountain commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1896. This is B4 from the canary blocker decisions: default-off Platform control signer wiring and the validator trust-registration path. No seed is generated, transported, templated, copied or read. Seed creation, backup, placement and production trust activation stay a separate protected ceremony. Nothing is enabled or run.

Platform app (platform_app role)

  • Defaults and rendering:

    • platform_coding_hosted_control_enabled: false; the env renders DITTO_CODING_HOSTED_CONTROL_ENABLED=false.
    • When enabled it renders only true, the fixed literal seed path /etc/ditto-platform/coding-hosted-signer/seed (a variable can't move it) and the reviewed public SS58 hotkey.
    • The seed file is owned by deploy, the pm2 user running ditto-api, which read_private requires.
  • When enabled, before any host change, metadata-only checks (follow: false, get_checksum: false):

    • fixed paths; valid 48-char SS58 hotkey, distinct from the screener hotkey; non-root API user;
    • ancestors are real directories owned by root or deploy, with no group/world write;
    • the directory is a deploy-owned 0700;
    • the seed is a regular single-link 0600 32-byte file owned by deploy.

    A missing or unsafe seed fails the converge with a pointer to the ceremony doc.

  • Disabled: the check tasks aren't included, so the path is never touched, matching the Python loader's disabled no-read behaviour.

Validator trust (validator_stack role and compose)

  • validator_stack_coding_hosted_control_enabled: false and validator_stack_coding_hosted_platform_hotkey: "". They render VALIDATOR_CODING_HOSTED_CONTROL_ENABLED and VALIDATOR_CODING_HOSTED_PLATFORM_HOTKEY (feat(validator): default-off hosted-v2 control command for signed evaluate, status and acknowledge #1896's command reads only these).
  • When enabled, a pre-change assert requires a valid SS58 that differs from validator_stack_hotkey.
  • docker-compose.yml ditto-subnet passes both through as false and empty by default, so community validators are unaffected.

Docs: new infra/docs/coding-hosted-control-signer-v2.md covers:

  • Ceremony properties: generated on an offline/protected machine; never in CI, Git, chat/Telegram, tickets or workflow artifacts; fresh randomness, distinct from the curator, validator, screener and wallet keys; only the public address recorded; placed directly with the required owner/mode; custodians hold the backup.
  • Enable order: seed placed → reviewed host_vars → Platform converge and restart → hosted_control_configured=true → reviewed validator trust change → validator converge.
  • Rotation and revocation.

Tests

  • The Ansible test playbook renders both env templates for real (disabled, disabled with staged values, enabled). It runs the real asserts against bad inputs and the seed check against 11 temporary trees:

    • missing, 0640, symlink, hard link, 64 bytes;
    • 0750 directory, symlinked directory, group-writable parent, foreign owners.

    Deliberately breaking the link check or the disabled template fails it.

  • Root: 369 pass. Platform: 42 pass. Ruff and mypy are clean.

  • Syntax checks pass for gcp-platform-app.yml, gcp-validator-prod.yml and gcp-validator.yml. The CI compose config check passes, resolving to false and empty.

Questions for Peyton

  1. The loader fails closed at startup, so a later-missing seed stops ditto-api at the next deploy. Should scripts/update.sh run the same metadata check before reload?
  2. The relay processes don't read the three DITTO_CODING_HOSTED_* keys. Blanking them in ecosystem.config.js/deploy-relay-release.sh would trigger a relay rollout. Add it anyway?
  3. Is Secret Manager ruled out as the custodian backup location, or allowed?
  4. The Ansible ancestor rule forbids group/world write even on root-owned sticky dirs the Python loader accepts. Is stricter OK?
  5. Merging marks validator_stack (compose) and platform_api (test/doc) for release, with no runtime behaviour change. Validators need a stack release containing both feat(validator): default-off hosted-v2 control command for signed evaluate, status and acknowledge #1896 and this PR.

Review fixes (rebased onto #1896 c9057350d; head 057abae9c)

  • Seed exposure (unresolved; needs a decision): any process running as deploy can read the seed: both relays, the cleanup job, deploy-time npm ci/uv sync and sudo -iu deploy users. deploy is also effectively root: it is in the docker group and has a sudo journalctl * grant with a known pager escape. The doc now says this plainly and gives options A–D for a dedicated ditto-api user. The flag stays off.
  • Rotation and revocation: they now disable, redeploy through update.sh (pm2 … --update-env), and confirm hosted_control_configured=false before touching the seed. pm2 autorestarts reuse the saved environment.
  • Deploy-time preflight: python -m ditto.api_server.coding_hosted_signer_preflight --check-metadata reuses read_private's checks with lstat only, and a test proves the seed is never opened. update.sh runs it before migrations and reload whenever the flag is on. This is now a real platform_api runtime change.
  • Other fixes:
    • hotkeys are compared only after an exact 48-char SS58 match;
    • import_tasks for the guards, with the fixture running the real entry point and new negative cases;
    • no configurable seed path variables;
    • activation tests fail only on truthy flags that aren't in the reviewed table;
    • platform-ci triggers on the two infra inputs;
    • fixture cleanup always runs.
  • Results: fixture 0 failed / 28 expected rejections; root 88 and Platform 110 focused tests pass; 19 mutations caught.
  • Questions for Peyton: the seed-exposure option (A–D); whether narrowing deploy's docker/journalctl access should happen regardless; recovery if a reviewed hotkey is wrong; Secret Manager as the backup location.

Update 2026-09-15: Peyton's isolation decision implemented (16 commits)

Decision: a dedicated locked-down ditto-api identity; only the signer reads the online seed; narrow deploy's docker and journalctl access regardless. All 14 findings from an independent review of the first 8 commits are fixed in the last 8.

  • Identity:
    • ditto-api and ditto-api-build are system users with no login, locked passwords and no supplementary groups. The converge refuses them in docker, sudo, adm, systemd-journal or google-sudoers.
    • The seed and its 0700 directory are owned by ditto-api, with parents owned only by root or ditto-api.
    • The signer requires both new switches, and update.sh refuses to deploy an enabled signer under pm2.
  • Supervision: ditto-platform-api.service runs as ditto-api through a root-owned launcher, which resolves current once, runs the metadata preflight from that release and execs the server.
    • Hardening: NoNewPrivileges, empty capabilities, ProtectSystem=strict, the deploy checkout and Docker socket inaccessible, and namespace and syscall filters.
    • systemd-analyze security: 1.4.
  • Sealed releases: a root installer fetches a main-reachable revision into root-only repos. It builds as ditto-api-build in a sandbox, with hash-pinned build backends (--require-hashes --no-build, then --no-build-isolation).
    • Links are resolved hop by hop, and any that escape the release are refused.
    • The installer seals the tree and records a manifest digest, re-checked at activation.
    • Deploy gets exactly four sudo commands (install, activate, stop, logs), with requests on stdin.
  • Journal and sudo (ungated, per "regardless"):
    • Removed sudo journalctl * and sudo systemctl status ditto-* (pager or cursor-file root escapes).
    • The team rules are now exact systemctl start|stop|restart <unit> for 15 services and 6 timers, as (root) only, applied to %ditto,!deploy.
    • No automation used the removed rules.
  • Docker (gated by platform_pylon_root_unit_enabled): a root-owned Pylon unit runs compose from root-owned inputs with an empty root-owned wallet mount, and deploy leaves docker. A live root-run probe refuses the signer (converge, install/activate and update.sh) if deploy, or any deploy-UID process, still has the docker group, or if the socket isn't root-only.
  • Deploy scripts: start.sh, stop.sh, profile-python.sh and update.sh follow the supervisor setting.
    • activate runs reset-failed first.
    • Activation failures go through fail_deploy, printing unit state and journal.
    • pm2 save runs before activate.
  • Default path: unchanged with all switches off.
  • Tests:
    • root 127 passed;
    • Platform update.sh harness 53 passed, signer tests 54 passed;
    • the Ansible fixture passed with failed=0;
    • 22 guard mutations caught.
  • Residual risks:
    • OS Login admins and the deploy workflow's osAdminLogin identity are root;
    • deploy still influences what is signed (migrations, DB, env), but can't copy the key;
    • any main revision can be installed;
    • the builder's uv cache persists between builds;
    • GitHub's host key is trusted on first use.
  • Questions for Peyton:
    1. Should release-build-requirements.txt hashes (compiled offline) be regenerated online in review?
    2. Please review the team unit list.
    3. Can you confirm production Pylon has no signing identity in deploy's wallet directory?
    4. A rootless Docker started by deploy is out of scope. Is that OK?
    5. Release tags vs any main revision?
    6. Should the deploy workflow move off osAdminLogin?
    7. Is building releases on the host (disk and time cost) OK?
    8. Should the unit start at boot?

🤖 Generated with Claude Code

Note

Add default-off hosted-v2 control signer wiring, dedicated ditto-api identity, and validator trust registration

  • Adds default-off hosted-v2 control signer with fixed seed paths, stat-only metadata preflight, and live Docker-access probe that blocks deployment if platform_owner can still reach the Docker daemon
  • Introduces a dedicated ditto-api system user and systemd-supervised sealed-release pipeline (ditto-platform-api.service), replacing pm2-managed deploys when platform_api_service_identity_enabled is true; deploy scripts (start.sh, stop.sh, update.sh) gain a systemd branch alongside the default pm2 path
  • Adds a root-owned Pylon unit (ditto-platform-pylon.service) that runs the sidecar via root-owned Docker Compose and removes platform_owner from the docker group when enabled
  • Registers validator-side hosted-control trust: validates the Platform control-signer hotkey is a distinct 48-char SS58 address from the validator's own hotkey, rendered through validator.env.j2
  • Hardens team sudoers in users.yml: replaces wildcard systemctl/journalctl rules with exact command aliases, restricts root-only commands, and adds systemd-journal group membership to team accounts
  • Risk: enabling platform_api_service_identity_enabled or platform_pylon_root_unit_enabled changes the API process owner to ditto-api, removes platform_owner from the docker group, and shifts supervision from pm2 to systemd — existing deployments relying on pm2-managed ditto-api or deploy-user Docker access will break unless the migration path in update.sh is followed

Macroscope summarized d8b0961.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

No dashboard preview for this commit. This PR resolved to preview profiles stack with no dashboard profile: it changes no apps/platform/dashboard/ path, so there is no dashboard build to publish. Profile selection fails closed rather than guessing, which is why a PR touching no recognized runtime path still reads as stack.

Stack previews are never automatic. A maintainer can provision one for this PR:

gh workflow run preview-stack.yml -f pr=1901                    # provision
gh workflow run preview-stack.yml -f pr=1901 -f action=retire   # free the slot

It runs on the default branch, resolves the head commit from the API, and comments dashboard, Platform, and isolated Backroom URLs once readiness succeeds. profile defaults to auto, which picks stack-copy when the PR touches apps/platform/alembic/. Eight slots cap every active preview; a re-dispatch reuses the slot this PR already holds. Reconciliation keeps it while the PR is open, up to a 24-hour lease cap, and retires it four hours after the PR closes.

Exact SHA: d8b09614da806960feda127a3a87fd888fb32b62

ai-mountain and others added 10 commits September 14, 2026 17:51
…nly seed guard

The platform_app role can now render the existing hosted-v2 signer settings.
Every switch ships off and no key is generated, transported or activated.

- Disabled (default) renders DITTO_CODING_HOSTED_CONTROL_ENABLED=false and
  includes no signer task, so the seed path is never inspected.
- Enabled renders true, the literal fixed seed path
  /etc/ditto-platform/coding-hosted-signer/seed and the reviewed public hotkey.
- Straight after preflight, before anything on the host is touched, the role
  asserts the fixed path and a 48-character SS58 hotkey distinct from the
  screener hotkey, then stat-verifies (follow: false, get_checksum: false)
  root/deploy-owned ancestors without group or world write, a deploy-owned
  0700 directory and a deploy-owned single-link 0600 32-byte seed. deploy is
  the pm2 user that runs ditto-api, which read_private requires.
- No task creates, copies, templates, fetches, slurps, hashes or deletes the
  seed. A missing or unsafe seed fails the converge with a pointer to the
  protected ceremony.

infra/docs/coding-hosted-control-signer-v2.md records the fixed placement, the
properties the separate protected ceremony must meet, enabling order,
rotation and revocation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ation

Validators can now register the public Platform control signer address that
python -m ditto.validator.coding_hosted_control trusts. Both values default
off and empty, including for community validators using docker-compose.yml.

- docker-compose.yml passes VALIDATOR_CODING_HOSTED_CONTROL_ENABLED
  (default false) and VALIDATOR_CODING_HOSTED_PLATFORM_HOTKEY (default empty)
  into the ditto-subnet container. The worker never reads either.
- validator_stack renders both from new role defaults; the hotkey is empty
  while disabled.
- When enabled, a pure-variable guard runs before any host mutation and
  refuses an address that is not a 48-character SS58 string or equals
  validator_stack_hotkey.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and fails closed

- infra/ansible/tests/coding-hosted-control-signer.yml renders platform.env.j2
  and validator.env.j2 with real Ansible: disabled, staged-but-disabled and
  enabled (including an ignored path override). It drives the real validator
  trust guard and the Platform profile guard through invalid and
  self-referential values, and the stat-only seed guard through synthetic
  temporary trees: missing, group-readable, symlinked, hard-linked, wrong
  size, unsafe directory, unsafe ancestor and foreign owner. Infrastructure
  CI runs it without --check because the trees must exist.
- Root tests parse every converge task file so none reads, creates, copies
  or hashes the seed, pin the stat flags, gate the include on the enable flag
  right after preflight, pin compose defaults and env-key parity with the
  control command, and refuse any inventory or workflow that enables either
  side.
- A Platform test parses the rendered lines with
  parse_hosted_signer_config_from_env and loads the bound synthetic public
  key, and proves disabled rendering never reads a seed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…undary

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…loy reload

ditto-api fails closed at startup on a missing or unsafe control signer seed,
and pm2 then restarts it into `errored`. scripts/update.sh now runs
`python -m ditto.api_server.coding_hosted_signer_preflight --check-metadata`
after sourcing the environment pm2 will receive, unless that environment
disables the signer, and refuses the deploy before Pylon, migrations or pm2
are touched. The checkout rolls back to the revision still serving.

The check reuses read_private's own location and file predicates, extracted
unchanged, through lstat only. It never opens the seed, so a hotkey mismatch
or zero seed is still caught only at API startup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tkeys

- platform_app imports the signer guard statically, so syntax checks of
  gcp-platform-app.yml parse it and the stat guard; a disabled converge
  skips every imported task without a stat.
- The seed path is no longer a role default. coding_hosted_signer.yml passes
  the literal paths platform.env.j2 renders as import vars, and the stat
  guard reads only those import-only names.
- The Platform hotkey is compared with the trimmed screener hotkey, and
  validator trust requires both addresses to be exact 48-character SS58
  strings before comparing them, so padding cannot hide an equal key.
- Comments say "before any other platform_app/validator_stack task": both
  playbooks run the base role first. The defaults state that every deploy
  process can read the deploy-owned seed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…inputs

test_coding_hosted_signer_host_wiring.py parses the rendered signer env and
the Ansible hotkey guard with the real Platform loader, so infra-only edits to
platform.env.j2 or coding_hosted_signer.yml must trigger Platform CI. Every
other infra assertion lives in the root suite, which runs on all pull requests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lues

- The fixture runs coding_hosted_signer.yml as tasks/main.yml does: disabled
  skips every stat, and enabled reaches the imported stat guard on the fixed
  paths. New negative cases cover the screener hotkey (plain and padded), a
  root or empty API user, and padded or malformed validator hotkeys.
- Seed cases create and arrange their tree inside the block, so cleanup
  always runs and setup failures surface as the wrong failed task. A root
  run skips the foreign-ancestor case with an explicit message. The fixture
  user is resolved once.
- The root activation guard parses YAML and workflow strings and fails only
  on truthy flags outside a reviewed REVIEWED_ACTIVATIONS table, so staging a
  hotkey or revoking is not blocked. Seed-path variables may appear nowhere.
- Infra-only assertions move out of the Platform test, which keeps only
  the loader cross-checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rt order

- Say plainly that any process running as deploy can read the seed: both
  relays, the cleanup job, deploy-time package installs, sudo -iu deploy.
  deploy is also root-equivalent through docker and sudo journalctl.
- Rotation and revocation now disable and restart through the Deploy
  Platform workflow before the seed is touched. update.sh refreshes the
  pm2 env with --update-env and pm2 save; a pm2 autorestart reuses the saved
  env.
- Add a Risks and open decisions section: what a dedicated ditto-api user
  would take (ownership, process manager, update.sh, code integrity, root
  paths, Caddy), with options A to D.
- Document the update.sh metadata preflight, the reviewed-activation table
  and the base-role-first ordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…environment

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ai-mountain
ai-mountain force-pushed the agent/coding-hosted-control-signer-wiring branch from 891ce71 to 057abae Compare September 14, 2026 15:52
ai-mountain and others added 14 commits September 15, 2026 01:33
… for ditto

Every ditto group member, including the deploy service account, could run
`sudo /bin/journalctl *` and `sudo /bin/systemctl status ditto-*`. The first
accepts any option as root (--cursor-file= writes a root-owned file at any
path, --vacuum-*, --setup-keys) and both run a pager as root, which has a
shell escape wherever systemd's secure pager mode is off.

Remove both rules. Team members join systemd-journal and read logs and unit
status as themselves; deploy gets no journal access. No automation used either
rule, so this is not gated: it applies on the next converge of any playbook
that runs the base role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cker

The docker group is root-equivalent, and deploy is in it only so update.sh can
run `docker compose up -d --wait pylon`. A root unit over the checkout's
compose file or deploy's .env would still hand deploy root, so
platform_pylon_root_unit_enabled (default false) installs root-owned inputs
instead: the pylon service copied from apps/platform/docker-compose.yml, a
0600 environment with the three values compose interpolated from .env, and
an empty Docker client config. ditto-platform-pylon.service runs the same
`up --detach --wait pylon` under the pinned project name and is never enabled
or started by Ansible. One exact sudoers rule lets deploy restart it, .env
renders DITTO_PLATFORM_PYLON_UNIT, and deploy is removed from docker.

Gated because container adoption and the pm2 daemon's cached groups can only
be proven on a live host. With the switch off nothing changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eases

Implements the #1901 decision that only the component that signs may read the
online control-signer seed. platform_api_service_identity_enabled (default
false) prepares ditto-api to run as the locked-down `ditto-api` system user
(nologin, locked password, no supplementary group; the converge refuses
docker, sudo, adm, systemd-journal, google-sudoers and deploy's groups) through
ditto-platform-api.service, with no root pm2 daemon. Relays and the cleanup
job stay on deploy's pm2. Nothing is enabled or started.

- Code integrity: ditto-api runs only from sealed root-owned releases. The
  root installer (/usr/local/sbin/ditto-platform-api-release) fetches a
  revision reachable from main from GitHub into fresh root-only repositories,
  extracts git archive, copies the dashboard as deploy-owned plain data
  without following links, builds the environment as ditto-api-build in a
  sandboxed transient unit, seals the tree root-owned and records a manifest
  digest that activation re-verifies.
- deploy reaches it only through four exact sudo argument vectors
  (install, activate, stop, logs); requests arrive on stdin and are strictly
  validated, never echoed.
- The unit runs a root-owned launcher that sources only root-owned
  environment files and exports the sealed revision, with NoNewPrivileges,
  an empty capability bounding set, ProtectSystem=strict, the deploy
  checkout and Docker socket inaccessible, PrivateTmp/Devices, namespace,
  address-family and system-call restrictions.
- The signer profile now requires both isolation switches and the ditto-api
  process user, and the stat guard requires the seed and its directory to be
  owned by ditto-api (ancestors root or ditto-api, never deploy). The Hippius
  evidence authority files and spool follow the API process user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… unit

update.sh reads two role-rendered switches and keeps today's path by default:
pm2 as deploy for ditto-api, docker compose as deploy for Pylon, and no sudo.

- DITTO_PLATFORM_API_SUPERVISOR=systemd: before Pylon and migrations, pipe
  the revision and the exact .env.deploy keys on stdin to
  `sudo -n ditto-platform-api-release install`, which seals the release and
  runs the signer metadata preflight as ditto-api. Skip ditto-api in the pm2
  plan, retire a pm2 copy that still holds the port, then `activate` after
  migrations. Verify through `systemctl show` plus /health on the revision,
  and on failure print the unit's journal through the installer's `logs`.
- Returning to pm2 stops and disables a leftover enabled or active unit first.
- DITTO_PLATFORM_PYLON_UNIT=ditto-platform-pylon.service restarts the root
  unit through its one sudo rule instead of calling docker.
- An enabled signer while ditto-api would run under pm2 is refused at
  signer-preflight before anything serving changes. deploy never runs the
  seed preflight itself, so it never stats or opens the seed.

start.sh leaves ditto-api to the unit in systemd mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ploy access

- Installer: request parsing refuses anything but the revision and the exact
  deploy keys without echoing values; only the four argument vectors run.
  A real local Git repository drives end-to-end install and activate:
  unmerged or unknown revisions are refused, the tree is sealed root-owned
  with the builder and preflight in sandboxed transient units, tampering
  with content, modes or links blocks activation, the running release is
  never deleted, pruning keeps the previous release. Dashboard links, hard
  links, special files and foreign-owned files (with a control showing a
  following copy would publish the seed) are refused. A subprocess audit
  hook, with a positive control, shows install, activate, logs and stop
  never touch the seed directory.
- Roles: exact sudoers for the team, the Pylon unit and the four installer
  commands, each parsed by visudo; locked-down users; unit hardening; Pylon
  compose parity with apps/platform/docker-compose.yml; the launcher's
  exact invocations and its refusal for any user but ditto-api. The two
  isolation switches join the reviewed activation flags.
- update.sh harness: default path uses no sudo; the signer is refused under
  pm2; systemd mode installs before Pylon and migrations with only the
  deploy keys on stdin, activates after, retires the pm2 copy, verifies the
  unit and prints its journal on failure; returning to pm2 stops the unit;
  the Pylon unit replaces docker.
- Loader: a process whose effective UID does not own the placement is
  refused without opening the seed.
- Ansible fixture: both supervision renders, the sealed-release env copy,
  unit and Pylon renders, profile refusals for each missing switch, and a
  seed tree still owned by deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion design

Replace the options A-D analysis with what is built: who can read the seed
(ditto-api only, plus root), the three reviewed activation switches and the
immediate journal narrowing, Pylon without the docker group, the dedicated
unit and its hardening, and sealed releases with their trade-offs. Enabling,
rotation, revocation and the return to pm2 now follow the unit. The online
control-signer seed stays completely separate from the offline curator key,
and neither ever enters CI, Git, Telegram, workflow artifacts, command
arguments or logs. Residual risks are listed: root (including OS Login
admins and the deploy workflow identity), the signing oracle, any main
revision, the builder cache, trust-on-first-use for GitHub, availability,
changed operations and the missing host rehearsal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s narrowing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mutation runs showed two guards were only covered by an earlier layer:

- a foreign-owned file inside a deploy-owned dashboard directory was caught
  by the directory owner check, so removing the per-file check went
  unnoticed. A new test reports only regular files as foreign.
- the non-owner loader test was refused by ancestor ownership, because the
  synthetic tree's ancestors belong to the test user. It now reports the
  ancestors as root-owned, as on a host, with a control that the owner
  accepts the placement. Removing the placement's owner checks fails it
  without the seed ever being opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The remaining %ditto rules were `(ALL) systemctl start|stop|restart ditto-*`.
In sudoers `*` matches further arguments, so they allowed extra units
(`stop ditto-x docker.service`), pager-opening options, stopping isolation
guards and any run-as user, and deploy is a ditto member.

Replace them with one alias of exact `/bin/systemctl start|stop|restart <unit>`
argument vectors for 15 reviewed service units (with and without .service)
and 6 timers, plus daemon-reload and reload caddy, granted as (root) to
`%ditto,!deploy`. Isolation guards (coding executor and hosted egress, sandbox
firewall, IMDS guard, egress proxy, rootless Docker daemons) are not listed.
No automation used the wildcard rules. Parsed by visudo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…unit

The root Pylon unit bind-mounted /home/deploy/.bittensor/wallets. dockerd
resolves a bind source as root, so deploy could have replaced it with a symlink
to /etc/ditto-platform or / and had it mounted into a third-party image.

The Platform's Pylon only serves reads (PYLON_IDENTITY_TOKEN is empty), so the
unit now mounts /etc/ditto-platform/pylon/wallets, an empty root-owned
directory. The converge refuses when deploy's old wallet path is a link, not
a directory, or not empty, checked without following links. The Pylon sudoers
file gains one exact, pager-free journal read of this unit, so update.sh can
show why it failed without the release installer being installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s Docker

The signer activation guard trusted platform_pylon_root_unit_enabled, but
switches only change files. The deploy pm2 daemon and its children keep the
docker group until pm2-deploy restarts, and Docker access is root-equivalent:
`pm2 start -- docker run -v /etc/ditto-platform:...` would read the seed.

files/deploy-docker-access.py checks the live host as root and fails closed:
for the docker group and whichever group owns /run/docker.sock, deploy is
neither a member nor has it as primary group; no process whose real,
effective, saved or filesystem UID is deploy holds such a GID (every
/proc/<pid>/status); the socket is root-owned, grants nothing to other users
and has no POSIX ACL. The signer converge guard runs it right after the stat
guard and refuses with the offending processes named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng gaps

- Build backends: the sealed venv was built with hatchling, hatch-vcs and
  editables resolved from PyPI with no hash. Builds now run in three sandboxed
  steps as ditto-api-build: uv venv; `uv pip install --require-hashes
  --no-build` of apps/platform/release-build-requirements.txt; `uv sync
  --frozen --no-dev --no-build-isolation`. Rehearsed locally against a
  read-only source tree: builds succeed, nothing outside .venv is written,
  and sync removes the backends again.
- Docker: install and activate run the root-owned live Docker probe whenever
  the root-owned environment enables the signer (anything but false/0).
- Links: seal resolves each link hop by hop and refuses any that leaves the
  release at any step, so chained relative links cannot escape.
- Values: any printable ASCII that stays literal in single quotes, covering
  the URLs upsert_env accepts (& | ; ~); refusals name a known key, never a
  value.
- activate runs `systemctl reset-failed` before restart so a start-limit
  failure cannot block a rollback. The manifest is hashed once when sealed
  and verified at activation, no longer re-verified straight after sealing.
- Launcher: `serve` resolves `current` once, runs the metadata preflight from
  that release, then execs the server from it; the unit drops ExecStartPre.
- Ordering: the ditto-api and ditto-api-build accounts are created right
  after the signer guard, before the Hippius evidence tasks chown to
  ditto-api, so one converge with the identity and evidence both on works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- update.sh stops an enabled signer early when this user can still reach the
  Docker daemon (advisory; the root installer repeats the check).
- After retiring the pm2 copy of ditto-api it runs `pm2 save` before
  activating, so a failed activation cannot leave a dump that resurrects the
  pm2 copy next to the enabled unit after a reboot. An activation failure now
  goes through fail_deploy with the unit state and journal; the state and
  failure helpers are defined before anything touches the serving process.
- A failed Pylon unit prints its journal through the exact sudo rule.
- The final hint names the unit's logs in systemd mode.
- start.sh honours DITTO_PLATFORM_PYLON_UNIT instead of always running
  docker compose; stop.sh stops the unit through the installer in systemd
  mode; profile-python.sh takes ditto-api's PID from the unit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Docker probe: membership, primary group, a process that kept the GID in any
  UID/GID field, the socket's own group when docker is missing, socket owner,
  other access and ACLs, bounded output and exit codes.
- Installer: hash-pinned three-step build argv, a failure at each step,
  signer-enabled install and activate refused by the probe (and allowed when
  clean), reset-failed before restart, chained link escape and loop
  refusals (with a control showing the textual rule accepted the chain),
  URL characters accepted and key-only refusal messages, one manifest hash
  per step. The audit-hook run now includes the probe.
- Roles: exact team sudo alias parsed by visudo with no wildcard, deploy
  excluded and guards absent; the Pylon journal rule and wallet refusal;
  account creation before the Hippius ownership tasks; the probe install;
  no ExecStartPre; launcher preflight-then-exec from one resolution; build
  requirements pinned with hashes and covering the project backends.
- update.sh harness: probe refusal and skip, pm2 save before activation
  and fail_deploy on activation failure, Pylon journal on unit failure,
  final hint; start.sh and stop.sh follow the switches.
- Ansible fixture: the real Docker-access guard on synthetic process tables
  and the new Pylon and unit renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ai-mountain and others added 2 commits September 15, 2026 02:42
…ned builds

Correct the claim that build backends are resolved at build time, and
describe the exact team rules, the root-owned Pylon wallet mount and journal
rule, the live Docker check, single-resolution launcher, reset-failed, the
account ordering and the updated residual risks and rehearsal scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… imports

A mutation adding `when: false` to the live Docker-access import went
unnoticed. Both imports inside the enabled guard must now carry only a name,
the task file and their literal vars.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…form_api component

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Peyton-Spencer Peyton-Spencer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated finding from Open Code Review through the Ditto Code Review endpoint.

@Peyton-Spencer Peyton-Spencer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open Code Review pass completed at c5d3911bd7472cda0f24e70c5fb8639d35fcdf59 using z-ai/glm-5.3-flash through Ditto’s Code Review endpoint. Coverage: 49/49 added, modified, renamed, or type-changed files in the PR diff.

Model candidates were checked against the source; independently confirmed actionable findings, if any, are posted separately as inline review comments. This was a static review; no PR code or test suite was executed. This comment is not a merge approval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ai-mountain added a commit that referenced this pull request Sep 16, 2026

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto Review · Beta

Review completed for d8b09614da806960feda127a3a87fd888fb32b62.

No actionable findings in the reviewed files. This is not an approval or proof that the PR is bug-free.

Configure Ditto Review. Inference is billed through your Ditto Router endpoint; targeted verification is included during beta.

This branch was successfully deployed

1 active deployment
preview — d8b09614 Deployed Sep 16, 2026 by ai-mountain via dashboard-publish / retire #2625
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.

2 participants