No uid coupling: the instruction to build as uid 10001 was false, and following it is what hurt - #9
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
This repository told node authors — in nine places, including the
Dockerfilea customer copies — that a job's credentials arrive as a0600file in a0700directory 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
0444inside a0711directory — 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.pysays 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/0600premise 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
0711grants traversal, not enumeration. Open the exact path inLSPO_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.mdrule 10,Dockerfile,conformance/contract.py,conformance/job.py,conformance/README.md,CONFORMANCE-BASELINE.md,tests/test_platform_rules.py.AGENT_UIDis 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 uid4242, deliberately not the agent image's10001, because a number shared with the agent is a number the next reader assumes has to match.conformance/job.pynow applies the real production modes to its own fixture instead of the looser0755/0644it 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_userasserted the coupling. It is replaced, not deleted, bytest_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 namingagent/creds.pyandagent/runner.pyand quoting four rules verbatim — checked by the harness's own verbatim-citation test.Its liveness was measured, not assumed
0600070007000755, so it can be listedTwo 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
10001and as uid1000both 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.pyno 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 inCONFORMANCE-BASELINE.mdas the second time that check earned itself.Verification
Against orchestrator
origin/masterat29341bd0, on a real docker daemon: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