Repository navigation
feat(infra): default-off hosted-v2 control signer wiring and validator trust registration - #1901
ai-mountain wants to merge 28 commits into
Conversation
|
No dashboard preview for this commit. This PR resolved to preview profiles 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 slotIt runs on the default branch, resolves the head commit from the API, and comments dashboard, Platform, and isolated Backroom URLs once readiness succeeds. Exact SHA: |
…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>
891ce71 to
057abae
Compare
… 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>
…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
left a comment
There was a problem hiding this comment.
Validated finding from Open Code Review through the Ditto Code Review endpoint.
Peyton-Spencer
left a comment
There was a problem hiding this comment.
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>
…8b0961 # Conflicts: # .github/workflows/infra-ci.yml
There was a problem hiding this comment.
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.
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_approle)Defaults and rendering:
platform_coding_hosted_control_enabled: false; the env rendersDITTO_CODING_HOSTED_CONTROL_ENABLED=false.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.deploy, the pm2 user runningditto-api, whichread_privaterequires.When enabled, before any host change, metadata-only checks (
follow: false,get_checksum: false):deploy, with no group/world write;deploy-owned0700;060032-byte file owned bydeploy.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_stackrole and compose)validator_stack_coding_hosted_control_enabled: falseandvalidator_stack_coding_hosted_platform_hotkey: "". They renderVALIDATOR_CODING_HOSTED_CONTROL_ENABLEDandVALIDATOR_CODING_HOSTED_PLATFORM_HOTKEY(feat(validator): default-off hosted-v2 control command for signed evaluate, status and acknowledge #1896's command reads only these).validator_stack_hotkey.docker-compose.ymlditto-subnetpasses both through asfalseand empty by default, so community validators are unaffected.Docs: new
infra/docs/coding-hosted-control-signer-v2.mdcovers:hosted_control_configured=true→ reviewed validator trust change → validator converge.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:
0640, symlink, hard link, 64 bytes;0750directory, 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.ymlandgcp-validator.yml. The CI compose config check passes, resolving tofalseand empty.Questions for Peyton
ditto-apiat the next deploy. Shouldscripts/update.shrun the same metadata check before reload?DITTO_CODING_HOSTED_*keys. Blanking them inecosystem.config.js/deploy-relay-release.shwould trigger a relay rollout. Add it anyway?validator_stack(compose) andplatform_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; head057abae9c)deploycan read the seed: both relays, the cleanup job, deploy-timenpm ci/uv syncandsudo -iu deployusers.deployis also effectively root: it is in thedockergroup and has asudo journalctl *grant with a known pager escape. The doc now says this plainly and gives options A–D for a dedicatedditto-apiuser. The flag stays off.update.sh(pm2 … --update-env), and confirmhosted_control_configured=falsebefore touching the seed. pm2 autorestarts reuse the saved environment.python -m ditto.api_server.coding_hosted_signer_preflight --check-metadatareusesread_private's checks with lstat only, and a test proves the seed is never opened.update.shruns it before migrations and reload whenever the flag is on. This is now a realplatform_apiruntime change.import_tasksfor the guards, with the fixture running the real entry point and new negative cases;platform-citriggers on the two infra inputs;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-apiidentity; 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.ditto-apiandditto-api-buildare system users with no login, locked passwords and no supplementary groups. The converge refuses them in docker, sudo, adm, systemd-journal or google-sudoers.ditto-api, with parents owned only by root orditto-api.update.shrefuses to deploy an enabled signer under pm2.ditto-platform-api.serviceruns asditto-apithrough a root-owned launcher, which resolvescurrentonce, runs the metadata preflight from that release and execs the server.systemd-analyze security: 1.4.main-reachable revision into root-only repos. It builds asditto-api-buildin a sandbox, with hash-pinned build backends (--require-hashes --no-build, then--no-build-isolation).sudo journalctl *andsudo systemctl status ditto-*(pager or cursor-file root escapes).systemctl start|stop|restart <unit>for 15 services and 6 timers, as(root)only, applied to%ditto,!deploy.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 leavesdocker. A live root-run probe refuses the signer (converge, install/activate andupdate.sh) if deploy, or any deploy-UID process, still has the docker group, or if the socket isn't root-only.start.sh,stop.sh,profile-python.shandupdate.shfollow the supervisor setting.activaterunsreset-failedfirst.fail_deploy, printing unit state and journal.pm2 saveruns before activate.update.shharness 53 passed, signer tests 54 passed;osAdminLoginidentity are root;mainrevision can be installed;release-build-requirements.txthashes (compiled offline) be regenerated online in review?mainrevision?osAdminLogin?🤖 Generated with Claude Code
Note
Add default-off hosted-v2 control signer wiring, dedicated
ditto-apiidentity, and validator trust registrationplatform_ownercan still reach the Docker daemonditto-apisystem user and systemd-supervised sealed-release pipeline (ditto-platform-api.service), replacing pm2-managed deploys whenplatform_api_service_identity_enabledis true; deploy scripts (start.sh, stop.sh, update.sh) gain a systemd branch alongside the default pm2 pathditto-platform-pylon.service) that runs the sidecar via root-owned Docker Compose and removesplatform_ownerfrom thedockergroup when enabledsystemctl/journalctlrules with exact command aliases, restricts root-only commands, and addssystemd-journalgroup membership to team accountsplatform_api_service_identity_enabledorplatform_pylon_root_unit_enabledchanges the API process owner toditto-api, removesplatform_ownerfrom thedockergroup, and shifts supervision from pm2 to systemd — existing deployments relying on pm2-managedditto-apior deploy-user Docker access will break unless the migration path inupdate.shis followedMacroscope summarized d8b0961.