Skip to content

docs: the setup block is two commands, and the audit it needed is gone - #8

Merged
makseq merged 1 commit into
mainfrom
docs/setup-block-two-commands
Aug 10, 2026
Merged

docs: the setup block is two commands, and the audit it needed is gone#8
makseq merged 1 commit into
mainfrom
docs/setup-block-two-commands

Conversation

@makseq

@makseq makseq commented Aug 10, 2026

Copy link
Copy Markdown
Member

The setup block in docs/OPERATIONS.md was still the old shape: chmod 1777 on the
agent's state directory — world-writable, with the sticky bit — followed by roughly forty
lines of shell for the operator to run so they could find out whether that was exploitable
on their machine. The orchestrator prints two commands now, and nothing else.

What changed

  • chmod 1777chmod 700.

  • --user "$(id -u):$(id -g)" is the first flag of the docker run. The agent runs as
    the account that pasted the command rather than as the service account its image
    declares, which is what makes 0700 possible: its identity file and every running job's
    credentials belong to that account.

  • The second fenced block — the UNSAFE / INCOMPLETE / a=10001 audit program — is
    deleted, along with the paragraph that explained its verdicts. Every branch in it existed
    to make a world-writable parent safe, and it had to guess which uid the agent would run
    as, because it was printed before any agent existed.

  • The --group-add bullet now says that it grants a supplementary group, which is
    independent of the uid, so --user does not take the socket away. Without that sentence
    a reader assumes the two flags fight.

  • What --user does not fix is stated instead of checked, in the two sentences the
    orchestrator itself prints, word for word:

    On a machine other accounts use, nothing here checks who can replace the agent's state
    directory or anything on the path to it — including the directories holding any
    symbolic links in that path. Satisfy yourself of that before you start, and prefer a
    machine you do not share.

    No checker for that ships anywhere — not in the block, not in the agent's log. Several
    shipped and were withdrawn, all wrong in the same family of ways, because a short check
    cannot soundly resolve a chain of symbolic links: canonicalising a path is exactly what
    discards the directory that holds each link.

Labels

The mkdir line is BEHAVIOUR (skip it and the docker daemon creates the mount source
as root, and the agent exits at startup with "Operation not permitted"), not the
RECOMMENDATION it used to carry. The gap is BEHAVIOUR for what nothing checks plus
a RECOMMENDATION for what to do about it. Nothing here is a RULE: nothing in the
platform refuses anything on this path.

Provenance

Both the command and the two sentences were taken from
noderegistry/services.py::docker_run_command in the orchestrator at 7dcf95c5 and
compared against that commit mechanically, not retyped. The doc's block is a strict subset
of what the generator prints; the one line it does not carry is
-e LSPO_AGENT_MAX_CONCURRENT_JOBS=1, which was already absent before this change.

Tests

python -m pytest (this is CI, and it needs a real docker daemon): 128 passed, 1
skipped
— the expected skip is the verbatim-citation check, which needs a checkout of the
orchestrator.

What this does not fix, and why it is separate

Everything in this repository that describes a job's credentials directory as 0700 with
a 0600 file, and tells a node author to build their image as uid 10001 to match the
agent, rests on a premise the platform changed the day before this one: the credential file
is now 0444 inside a 0711 directory, re-applied on every write, precisely so an image
running as its own user can read its own credentials. This repository mentions neither mode
anywhere.

Run the suite with LSPO_ORCHESTRATOR_SRC pointed at an orchestrator checkout and the
verbatim-citation check names it exactly:

test_platform_rules.py:277: 'The directory is created 0700 and the file 0600 — on a shared
machine the credential must not be readable by other users' appears in none of
['agent/creds.py', 'agent/runner.py']

That correction spans docs/PROTOCOL.md, docs/AUTHORING.md, the uid coupling section
of docs/OPERATIONS.md, the Dockerfile comment, conformance/contract.py
(AGENT_UID, CREDENTIALS_DIR_MODE, CREDENTIALS_FILE_MODE), conformance/job.py and
one subject_is_platform test that measures the coupling with real containers. Correcting
half of it inside this PR would leave a document that contradicts itself, so it is left
whole for its own change.

🤖 Generated with Claude Code

OPERATIONS.md still told an operator to make the agent's state directory
world-writable (`chmod 1777`) and then handed them forty lines of shell to work
out whether that was exploitable on their machine. Neither is what the
orchestrator prints any more.

The agent now starts with `--user "$(id -u):$(id -g)"`, so it runs as the person
who pasted the command rather than as the service account its image declares. Its
state directory can therefore be `0700` and owned by them, nothing on the machine
is shared with anybody, and the audit has nothing left to audit: every branch of
it existed to make a world-writable parent safe, and it had to GUESS which uid the
agent would run as (`a=10001`, printed before any agent existed) — a guess an
operator can neither confirm nor act on. Its `UNSAFE` and `INCOMPLETE` verdicts
went with it, so the paragraph that explained them is gone too.

`--group-add` is unaffected, and the bullet now says why: it grants a supplementary
GROUP, which is independent of the uid, so overriding the user does not take the
socket away. A reader who does not know that assumes the two flags fight.

What `--user` does not fix is stated rather than checked. `0700` protects the
directory, not its NAME: an account that can write a directory ABOVE
`$HOME/lspo-agent` can rename it away and leave its own under that name, the
running agent never notices because its mount stays on the original inode, and the
docker daemon resolves the replacement on the host and as root when it mounts a
job's credentials. No checker for that ships anywhere — not in the printed block,
not in the agent's log. Several shipped and were withdrawn, all wrong in the same
family of ways, because a short check cannot soundly resolve a chain of symbolic
links: canonicalising a path is exactly what discards the directory that HOLDS each
link. The two sentences the orchestrator prints in their place are reproduced here
word for word, so an operator reads the same thing in both places.

The labels move with the facts. The `mkdir` line is BEHAVIOUR — skip it and the
daemon creates the mount source as root, and the agent exits at startup with
"Operation not permitted" — rather than the RECOMMENDATION it used to carry; and
the gap is BEHAVIOUR for what nothing checks plus a RECOMMENDATION for what to do
about it, never a RULE, since nothing refuses anything.

Taken from `noderegistry/services.py::docker_run_command` at 7dcf95c5 rather than
retyped: both the command and the two sentences were compared against that commit
mechanically. `python -m pytest` is 128 passed, 1 skipped — the expected skip.

Not touched, because it needs its own change: everything here that describes a
job's credentials directory as `0700` with a `0600` file and tells a node author to
build their image as uid 10001. The platform replaced that the day before (`0444`
file inside a `0711` directory, re-applied on every write, precisely so an image
running as its own user can read its own credentials), and this repository mentions
neither mode. It spans PROTOCOL.md, AUTHORING.md, the uid-coupling section of
OPERATIONS.md, the Dockerfile comment, `conformance/contract.py` and one platform
test, so correcting half of it here would leave a document contradicting itself.
Run with `LSPO_ORCHESTRATOR_SRC` set and the verbatim-citation check names it:
`test_platform_rules.py:277` quotes a sentence `agent/creds.py` no longer contains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@makseq
makseq merged commit 5650242 into main Aug 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant