Skip to content

No uid coupling: the instruction to build as uid 10001 was false, and following it is what hurt - #9

Merged
makseq merged 1 commit into
mainfrom
fix/uid-coupling
Aug 10, 2026
Merged

No uid coupling: the instruction to build as uid 10001 was false, and following it is what hurt#9
makseq merged 1 commit into
mainfrom
fix/uid-coupling

Conversation

@makseq

@makseq makseq commented Aug 10, 2026

Copy link
Copy Markdown
Member

The defect

This repository told node authors — in nine places, including the Dockerfile a customer copies — that a job's credentials arrive as a 0600 file in a 0700 directory owned by the agent's uid, and that a customer's image therefore had to run as uid 10001. Every part of that had stopped being true.

It is the worst kind of false statement a public repository can carry: an instruction about how to build your image, where obeying it is the thing that breaks you.

What is actually true

Two independent platform changes, and correcting only one of them would have made things worse.

Orchestrator #250 made the credential FILE 0444 inside a 0711 directory — traversable by anyone, listable by nobody but the agent — and re-applies both modes on every write, credential refreshes included. Confidentiality comes from the agent's own working directory above the mounted leaf (owner-only, bind-mounted nowhere), not from the leaf's mode. agent/creds.py says why in as many words: the leaf is deliberately open so that "an image that declares any user other than the one the agent happens to run as" can open its own credentials, because the old shape left root as the only workaround a customer could find — the platform punishing the careful choice.

Orchestrator #268 starts the agent with --user "$(id -u):$(id -g)", so a deployed agent runs as the operator, not as the account its image declares. "The agent is uid 10001" was not what an operator got either.

Net effect: there is no uid coupling. Your image may run as whatever user it likes.

A half-fix that corrected only the second half — "the agent is the invoking user, so build as uid 1000" — would have been worse than the original text: it keeps the false 0700/0600 premise alive and sends the author chasing a number that changes per machine. So the premise went first, everywhere.

The one residual constraint, which replaces the false one

0711 grants traversal, not enumeration. Open the exact path in LSPO_CREDENTIALS_FILE; never list the directory it sits in. Labelled RULE, with the qualifier that the kernel enforces it rather than any check the platform makes on your node.

Also still live, and unchanged: in local demo mode the agent forces your container to its own uid and gid with no supplementary groups, so your image's declared user is ignored there — the opposite problem, and it is called out as still live rather than folded into the good news.

Where it was corrected

docs/OPERATIONS.md (section retitled Which user your image runs as, with a subsection stating what it replaced rather than quietly deleting it, plus the troubleshooting row), docs/PROTOCOL.md §1.3, docs/AUTHORING.md (step 3 and the ship checklist), docs/CONFORMANCE.md (the level-3 claim), docs/README.md (routing row + anchor), CLAUDE.md rule 10, Dockerfile, conformance/contract.py, conformance/job.py, conformance/README.md, CONFORMANCE-BASELINE.md, tests/test_platform_rules.py.

AGENT_UID is deleted, not corrected — a number written down as "the agent's uid" is a number somebody builds an image around. The example image moves to an arbitrary uid 4242, deliberately not the agent image's 10001, because a number shared with the agent is a number the next reader assumes has to match.

conformance/job.py now applies the real production modes to its own fixture instead of the looser 0755/0644 it used while the agent's modes would have locked the harness out of its own directory. That buys something concrete: the directory belongs to whoever ran pytest and the container runs as somebody else, so every container test in the suite now reads its credentials through the same permission class a customer's image uses.

The platform test

test_a_0700_credentials_directory_is_unreadable_to_any_other_user asserted the coupling. It is replaced, not deleted, by test_a_workload_running_as_any_uid_can_read_its_own_credentials, which measures that the coupling is gone — and that is the stronger of the two tests, because the old one could pass on a platform that had a defect while the new one goes red on the regression that would reintroduce it.

It reads a job's credentials from inside the image as three users (the image's own, a uid present in no passwd file anywhere, and the directory's owner), then once more as that stranger across an atomic replacement modelling a credential refresh, and finally confirms the stranger still cannot LIST the directory. Real containers throughout; the kernel gives the verdict.

Labelling follows this repository's discipline: subject_is_platform, basis_contract, with a citation naming agent/creds.py and agent/runner.py and quoting four rules verbatim — checked by the harness's own verbatim-citation test.

Its liveness was measured, not assumed

Mutation Result
credential file narrowed back to 0600 red — "a 0600 file in a 0711 directory owned by uid 1000 was NOT readable as the image's own user"
directory narrowed back to 0700 red — the same, naming 0700
directory widened to 0755, so it can be listed red — "a 0755 directory was listable by a uid that does not own it"
mode applied at job start but not to the refreshed inode red — "the replaced credential file was not readable by an arbitrary uid"

Two negative controls stay green, since the whole claim is that the image's uid is nobody's business: rebuilding this repository's image as uid 10001 and as uid 1000 both pass.

The first draft of the test failed that battery, instructively. It opened with assert CREDENTIALS_FILE_MODE == 0o444, so narrowing the constant failed on a literal in this repository disagreeing with a constant in this repository — and the container never started. A guard marking its own homework. Removing it is what turned the mutations into the four honest failures above, and there is now a comment in the test saying so.

How it was found

The repository's own verbatim-citation check, pointed at the orchestrator, went red on the single quotation that had gone stale: "The directory is created 0700 and the file 0600 — on a shared machine the credential must not be readable by other users", a sentence agent/creds.py no longer contains. One red assertion was the only thread leading to nine wrong statements, none of which any test could have contradicted, because they were prose. Recorded in CONFORMANCE-BASELINE.md as the second time that check earned itself.

Verification

Against orchestrator origin/master at 29341bd0, on a real docker daemon:

python -m pytest                                            → 128 passed, 1 skipped
LSPO_ORCHESTRATOR_SRC=… LSPO_ORCHESTRATOR_REF=origin/master  → 129 passed

The skip in the first run is the citation check declining to pass with nothing to read, which is its designed behaviour. Before this change, the second command failed on tests/test_platform_rules.py.

🤖 Generated with Claude Code

…and following it is what hurt

This repository told node authors, in nine places, that a job's credentials arrive
as a `0600` file in a `0700` directory owned by the agent's uid, and that a
customer's image therefore had to run as **uid 10001**. Every part of that had
stopped being true, and it is the worst kind of false: a public document telling
somebody how to build their image, where obeying it is the thing that breaks them.

Both halves changed on the platform, and both had to be corrected together.

`agent/creds.py` now makes the credential FILE `0444` inside a `0711` directory —
traversable by anyone, listable by nobody but the agent — and re-applies both on
every write, refreshes included (orchestrator #250). Confidentiality comes from the
agent's own workdir ABOVE the mounted leaf, which is owner-only and bind-mounted
nowhere; the leaf is open precisely so that an image running as ANY user can open
its own credentials. That shape exists to remove the exact defect this repository
was still documenting as a requirement: an image declaring any other user got a
permission error on its own credentials, and the only repair a customer could find
was to run their container as root — the platform punishing the careful choice.

Separately, the agent is started `--user "$(id -u):$(id -g)"` (orchestrator #268),
so it runs as the operator, not as the account its image declares. "The agent is
10001" was not what a deployed agent gave you either.

Correcting only the second — "the agent is the invoking user, so build as uid 1000"
— would have been WORSE than the original text: it keeps the false `0700`/`0600`
premise alive and sends the author chasing a number that changes per machine. The
premise had to go first, so it did, everywhere: OPERATIONS.md (section retitled,
with what it replaced stated rather than quietly deleted), PROTOCOL.md §1.3,
AUTHORING.md's step 3 and its checklist, CLAUDE.md rule 10, the Dockerfile,
CONFORMANCE.md's level-3 claim, docs/README.md's routing row, and the harness's
own constants.

What replaces it is a real constraint rather than nothing. `0711` grants traversal,
not enumeration: open the exact path in `LSPO_CREDENTIALS_FILE`, never list the
directory it sits in. That is labelled RULE with the qualifier that the kernel
enforces it, not a check on your node.

`conformance/contract.py` loses `AGENT_UID` entirely — a number written down as
"the agent's uid" is a number somebody builds an image around — and carries the two
real modes instead. `conformance/job.py` now applies those real modes to its
fixture rather than the looser 0755/0644 it used while the agent's own modes would
have locked the harness out. That buys something: the directory belongs to whoever
ran pytest and the container runs as somebody else, so every container test in the
suite now reads its credentials through the same permission class a customer's
image uses. The image itself moves to an arbitrary uid 4242, deliberately NOT the
agent image's 10001, because a number shared with the agent is a number the next
reader assumes has to match.

The platform test is replaced rather than deleted, and the replacement is stronger
than the original: instead of asserting the coupling, it measures that the coupling
is GONE. `test_a_workload_running_as_any_uid_can_read_its_own_credentials` reads a
job's credentials from inside the image as three users — the image's own, a uid in
no passwd file anywhere, and the directory's owner — then again as that stranger
across an atomic replacement, and finally confirms the stranger still cannot LIST
the directory. Real containers; the kernel answers.

Its liveness was measured, not assumed. Four mutations, each red for the right
reason: file narrowed to 0600, directory narrowed to 0700, directory widened to
0755 so it can be listed, and the mode applied at job start but not to the
refreshed inode. Two negative controls stay green — rebuilding this image as uid
10001 and as uid 1000 — because the whole claim is that the image's uid is nobody's
business. The first draft FAILED that battery instructively: it opened with
`assert CREDENTIALS_FILE_MODE == 0o444`, so narrowing the constant failed on a
literal in this repository disagreeing with a constant in this repository and the
container never started. A guard marking its own homework. Removing it is what
turned the mutations into honest failures.

How this was found: the repository's own verbatim-citation check, pointed at the
orchestrator, went red on the one quotation that had gone stale — "The directory is
created 0700 and the file 0600 — on a shared machine the credential must not be
readable by other users", a sentence `agent/creds.py` no longer contains. One red
assertion was the only thread leading to nine wrong statements, none of which any
test could have contradicted, because they were prose. Recorded in
CONFORMANCE-BASELINE.md as the second time that check earned itself.

Verified against `origin/master` at 29341bd0: `python -m pytest` (what CI runs)
128 passed, 1 skipped; and with LSPO_ORCHESTRATOR_SRC/REF set, 129 passed with the
citation check green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@makseq
makseq merged commit 1421acd 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