Repository navigation
Harness: run and apply for the review Intent, and the model holds no credential that can write #194
Copy link
Copy link
Open
Description
Part of the epic "Epic: slice 1, first approval. A Release on next reviews a pull request to this repo, and the machine user's approval stands".
Why. The first commit of harness/ and roles/: run and apply for the review Intent (§2.3 steps 7 to 13). Forty-six rows and eight lessons wait on harness/; nine on roles/. FR-10, "the model process holds no credential that can write", is held by nothing until this exists.
Done when
-
harness/task.pybuildstask/1from a Decision and the facts, every text labelled by its trust class, saying what was left out and how much (L-56);harness/engine.pyruns the pinned CLI (L-35) on thefactory-runenvironment with tools disallowed, hands itresult/1with references resolved and the Harness's fields removed, and writesresult/1, or afailedone withresult_invalidfor an answer that does not validate (Schemas for Task, Result, Effect and Disposition #19 ruling 9). -
harness/apply.pyonfactory-applyasks the Kernel again with its own Run left out (I-38), re-reads the item and refuses if held, paused, closed or changed (I-21), refuses a Result whoserunorreleaseis not the Task's, an Effect the Task does not permit, a duplicate id, a Finding posted other than once or a non-gating one on a thread, a Disposition with neither half or both or a half its Role may not make, and a push to a protected path or a Lens's input with no model review (Harness: apply refuses a Role's push to a protected path or a Lens's input that no model Lens has reviewed #139, closed here); submits the review as a comment on the admitted commit, never an approval; mints a token narrowed to the requested Effects through the identity port; reads each Effect back; writesapplied/1andoutcome/1. -
roles/pr-review.jsonvalidates againstrole/1, namesevals/pr-review, and §5.4's mechanical checks run over it intools/check_authority.py(one owner per category; Effects within the App's ceiling; an Intent of classgatehas an eval set). -
tests/test_harness.pydrives both jobs against a stand-in CLI and a fake effects port (as Trial scripts: P-13's script hardened before it runs again #178 does for the trial script), including: no job holds both a model credential and a write credential (L-43, held by the workflow's environments andtools/check_workflows.py); every refusal above; a claim is written only from what was read back (L-41). Every row waiting onharness/androles/that this holds is re-pointed. - No Run has a shell or a network tool until P-26 passes (Trial P-26: the model CLI's sandbox hides the parent's environment and limits connections #188, Design: the CLI's sandbox limits connections but passes the parent's environment through (P-26 failed); the run job relies on the scrub and --tools "" #287): the run job starts the CLI with
--tools ""(P-13 at 2.1.306 shows structured output still works), setsCLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1, keeps the model credential only underCLAUDE_CODE_OAUTH_TOKENand holds no other secret in its environment;tools/check_workflows.pyholds all four. (Added 2026-10-21 by the orchestration session, owed by Trial P-26: the model CLI's sandbox hides the parent's environment and limits connections #188.)
Needs. The two ports, the eval set, #19 and #28 on main. The keys are stored; tests read recorded responses only.
Design. §2.2, §2.3, §3.5 to §3.8, §3.11, §4.9; decisions 0004, 0005, 0029, 0033, and #19's record; L-14, L-35, L-41, L-43, L-56.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
don-petry commentedon Oct 21, 2026
Working rulings (overturnable in review)
Solution Architect, delegated. On main at 95b6adb. Read from:
runandapply(lane s). The workflows", waiting on the ports and the eval set) and its rule that the first story in a reserved directory re-points every row waiting on it;applied.json,outcome.json, the act Run), §3.11 (the Role manifest), §4.3, §4.9 (the threats, the credentials table) and §5.1;harness/), 0005 (each Role writes through its own App), 0020 (a change to a Lens's inputs is reviewed by a model Lens), 0029 (an uncovered criterion has not reported), 0033 (the model credential is, for now, the Maintainer's own subscription; slice 1's CI credential reopens it), 0035 (each key lives in the environment of the one job that holds it), 0038 (a Result names no one;applywrites the Finding) and 0039 (the envelope);schemas/task.schema.json,result,effect,disposition,applied,outcome,roleandfinding, as Schemas: Task, Result, Effect and Disposition (#19) #210 and Schemas: the Role manifest, the pass record, the feed, and one envelope for all (#28) #214 merged them;config/apps.json(the reader App onfactory-run,pr-reviewonfactory-apply);tools/check_authority.py;evals/pr-review/cases.json(the review Intent's set,count: 0);trials/P-13.jsonandtools/trial_p13_cli.shas Trial scripts: P-13's script hardened before it runs again #242, Hook tests: a budget measures the hook, not the machine #257 and Trial scripts: a reason is the Task's one code, every check message is hidden, and the after-Task version's exit is read #263 left them (the pinned CLI,--json-schema,structured_output, the allowlist of what is kept,--tools ""not yet run against the real CLI), andtools/trial_as_app.sh(an App's JWT signed byopensslfrom the key's path);harness/and 10 onroles/;What this story decides. What the two jobs that carry a Run are, as code: what
rungives the model and takes back, and whatapplychecks, writes and records. It is the first commit ofharness/androles/, and the first place FR-10, "the model process holds no credential that can write", is held by code rather than waited on.1. Three pull requests, in order.
runjob: the Task from a Decision and the facts, the CLI's call, the Resultharness/task.py,harness/engine.py,harness/run.pyapplyjob: the re-read, every refusal, the token's mint, the Effects and their read-back,applied/1andoutcome/1harness/apply.py,adapters/identity.pyroles/pr-review.json, its prompt, and §5.4's checks intools/check_authority.py; the job boundary intools/check_workflows.pyroles/, two checksWhy:
runholds the model credential andapplythe write credentials; reviewing them apart is reviewing the boundary FR-10 draws. Each PR is testable with no I/O beyond a stand-in, as #178 tested the trial script.2. PR 1: the Task.
harness/task.pybuildstask/1from the pass record's Decision and the item's facts, and nothing else (§3.5: "a Run started any other way finds no Decision and does nothing").maintainerfor the Maintainer,rolefor a Factory App,advisoryfor a third-party reviewer listed in the configuration,outsidefor anyone else.task/1has no field for that today, so it gains an optionalomitted: for each kind of text cut, the count cut and the characters cut, in codes and numbers. An optional field keeps the major version (0038: "a field added later is optional, or the major version goes up"). An example and a counter-example come with it.limits,tierandpermitted_effectscome from the Role's manifest for the Intent;releasefrom the Release that runs;content_idanditemfrom the facts.3. PR 1: the Engine.
harness/engine.pyruns the CLI astools/trial_p13_cli.shdoes, and reuses what #242, #257 and #263 hardened there rather than writing it again:--tools "": no tool at all, so a Task that holdsoutsideoradvisorytext has no shell and no network tool (I-35, 0004's last row), and the review Intent needs none;--json-schemawithresult/1as the CLI is handed it: references resolved, the Harness's fields (release,usage) removed,statusnarrowed tocompleted,incompleteandrefused,effectsnarrowed to the Task'spermitted_effects(§3.5);structured_outputand the fieldsusageneeds, by allowlist; the model's prose is never kept or logged.An answer whose
structured_outputdoes not validate is not a Result: the Harness writes the whole Result itself,failed, reasonresult_invalid, empty lists andusage(0038).usageis filled from the CLI's own answer; the model and the session are named before the call, so both are known even when the CLI never answers.4. PR 1: what
runuploads.result.jsonwith its sha256, and nothing else from the model. The log carries no field of the Result (§3.10: "nothing is handed from job to job through the log").5. PR 2: the re-read and the refusals.
harness/apply.py, onfactory-apply, takes the Result by the artifact id and digestrunreported, never by name (§3.6). Before any Effect, in this order, it writes nothing and ends the Run when:content_idis no longer the Task's: the Run endssuperseded(I-21);runorreleaseis not the Task's;idtwice;minororinfoone of any category butsecurity) is posted on a thread (§3.7);securitythread ondeclinedordeferred(§3.8);submit_reviewwould be anything but a comment on the commit the Decision admitted (§3.6).Each refusal is a reason code in
applied/1and inoutcome/1, whose fault isitemfor a Result the Role wrote andsupersededfor a change under the Run.6. PR 2: the token.
adapters/identity.pymints an installation token for the one repo the Run is about, narrowed to the permissions the requested Effects need, asconfig/apps.json'sneedsnames them.openssl, astools/trial_as_app.shsigns it: the standard library cannot sign RS256, and the repo installs nothing. The key is written to a file of mode 0600 in the runner's temporary directory, read only byopenssl, and removed in afinally.applyends.7. PR 2: the Effects and the record. Each Effect is written through the effects port in
effect/1's order, recovered by its key where one is already applied (§3.6), and read back.applied/1lists each with its state (applied,already_applied,refused,failed), the node id read back and a reason code, and each Finding posted, written whole asfinding/1from what was read back (0038). A claim is only ever what was read back (L-41).outcome/1is the last step's.8. PR 3: the Role.
roles/pr-review.jsonvalidates againstrole/1: kindreviewer, owner of the model-judged categories (§5.1), its Appcombsmith-sf-pr-review, its Intentreview-changeof budget classgatenamingevals/pr-review. Its prompt lives beside it inroles/.tools/check_authority.pygains §5.4's three checks over every manifest: one owner per category, every permitted Effect within its App's ceiling inconfig/apps.json, and an Intent of classgatenames an eval set that exists. That the review Intent may serve only once its set has cases stays the cutover's (tests/test_evals.py::test_the_review_intents_eval_set_has_its_cases, not held yet, #183).9. PR 3: the job boundary.
tools/check_workflows.pyholds, for any workflow: no job names both an environment holding a model credential and one holding a write credential, and no job that holds a write credential checks out a Work item's branch (L-43; 0004's first two rows waiting onharness/). The Factory's own workflows are the workflows story's (#196); this PR adds the rule and counter-examples, so #196's files are held by it from their first commit.10. What waits.
effect/1has nopush_commitsuntil the fix Intent (§3.6: "the other types come with the Intents that need them"). The refusal waits for that Intent; see the rulings on Harness: apply refuses a Role's push to a protected path or a Lens's input that no model Lens has reviewed #139.11. Tests and mutants.
tests/test_harness.pydrives both jobs against a stand-in CLI and a fake effects port, astests/test_tools.py::TrialScriptdrives the trial script: the environment the CLI is given,--tools "", a Result that does not validate becomingresult_invalid, every refusal of ruling 5 with its code, the token narrowed and never written, and a claim written only from what was read back. Each rule gets a mutant naming its test, checked as the shards run them and by the capped filtered run on each path. Every row waiting onharness/androles/that a PR holds is re-pointed in that PR; the rest name the narrower file that will hold them.12. Record. One, numbered at enqueue with PR 1: "The model is given a Task and no tool, and
applywrites only what it reads back". Its table holds rulings 2 to 9. PR 2 and PR 3 add rows to it.13. Gates. Export verify; a fresh Claude review and a security pass on each PR, the security pass reading the Engine's environment (PR 1) and the token's mint (PR 2) first. AGY on Argon, since this is the boundary FR-10 rests on and PR 2 asks the Kernel again. "Muse: not run (Maintainer, 2026-10-20: Kernel/hook only)". The capped filtered mutant run on each new path.
14. Out of scope. The workflows and their environments (#196). The approving act, a Run with no Engine (#195). The effects port's real writes (#193 PR 2). The author Intents and their bundle, preflight and
push_commits. FR-12's injection suite (slice 2).Needs the Maintainer? Not for building it. Three points are his:
runjob's secret onfactory-runis either a token from his own subscription (claude setup-token) or a metered key under FR-73's cap. Nothing in this story uses either; the first live Run does. The decision should be made, and 0033 reopened, before Workflows: the frozen entry shim, the poller, the Run's three jobs and the nudge, each held to its shape #196 runs a Run.--tools ""with--json-schema(risk). P-13 proved structured output with the CLI's default tools. That it still works with none is unverified until the next real run oftools/trial_p13_cli.sh(Trial scripts: what Muse's later rounds asked of trial_p13_cli.sh #247's fourth item waits on the same run). If it does not, the fallback is--disallowedToolsnaming every built-in tool, which the issue's own wording allows.roles/pr-review.jsonexists, and should land before PR 3 or in it. See their rulings.don-petry commentedon Oct 21, 2026
Working rulings: the Maintainer's answers (2026-10-21)
The orchestrator relayed the Maintainer's answers to the points the rulings above left to him. The rulings file is updated to match.
claude setup-tokenunder his login, stored as therunjob's environment secret onfactory-run. A metered API key under FR-73's cap is 0033's fallback. This story's record carries it, and reopens 0033 as that decision asked (ruling 12).--tools ""with--json-schema. Joined to "Trial scripts: what Muse's later rounds asked of trial_p13_cli.sh" (issue 247), whose fourth item waits on the same real P-13 run at his terminal. Until then the Engine is built and tested against the stand-in; the fallback, if structured output does not hold with no tools, is--disallowedToolsnaming every built-in tool.opensslthrough a pipe, as-sign /dev/fd/N, and is never written to a file; tried with a throwaway key on 2026-10-21, it signs. A file of mode 0600 removed in afinallyis the fallback only where a runner'sopensslcannot read a descriptor.The re-pin's fix round: 0033's row names no version, the test keeps t…
don-petry commentedon Oct 21, 2026
Ruling 4, the CLI's call:
--tools ""with--json-schema, settled (2026-10-21)The real P-13 run at the CLI's 2.1.306 shows that
--tools ""leaves--json-schemaworking:permission_denialswas empty;stop_reasonwastool_use, because the CLI delivers structured output through a tool of its own, which--tools ""does not remove.So the run job can disable every tool and still get its Result, and the
--disallowedToolsfallback is not needed. The fixture istrials/P-13.v2-1-306.json, landing in the re-pin pull request.don-petry commentedon Oct 21, 2026
Working rulings, amended: the run job's environment (2026-10-21)
P-26 at the CLI's 2.1.306 failed. The CLI's sandbox limits connections and writes, but it hands a tool the parent's environment. Only
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1removes names on its own list,CLAUDE_CODE_OAUTH_TOKENamong them. Filed as "Design: the CLI's sandbox limits connections but passes the parent's environment through (P-26 failed); the run job relies on the scrub and --tools """ (issue 287).Ruling 3 (the Engine) and ruling 9 (the job boundary) gain these rules, and the record carries them as rows with ruling 12's. They bind from PR 1:
runjob setsCLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1explicitly, in the environment the Engine gives the CLI. It is never left to a default.CLAUDE_CODE_OAUTH_TOKEN, the name the scrub removes.outsideoradvisorytext runs with--tools "". P-13 at 2.1.306 shows structured output still works, so the review Intent runs so always.tools/check_workflows.pyholds the scrub variable, and the absence of any other secret, in therunjob, with counter-examples. It lands with ruling 9's job boundary in PR 3, and PR 1's Engine test holds rules 1, 2 and 4 in the environment it builds.The rewording of design §4.9, and the Linux
/proc/<pid>/environtrial, are issue 287's own small pull request.don-petry commentedon Oct 21, 2026
Trial P-26 (#188) failed at 2.1.306; its fixture and design rows are on
trials/p-26-sandbox. The model CLI's sandbox limited where a tool connected, but it passed the parent's environment to the tool.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1, took out the one listed credential name that was tried./procand the CLI's configuration files were not tried.#188's "if no" asks that this story's Done when gain one line, until P-26 passes:
The Maintainer or the story's owner adds it; this comment does not edit the issue.
Harness: the fix round of #194 PR 1, after the Claude review, the sec…
Harness: one description for harness/ on the list (#194, PR 1)
Harness: the run job reads and writes only inside the directory it ru…
Harness: #194's record takes 0050, by the orchestrator's order (#194,…
Harness: config/harness.json is pr-review's own input, and its pin is…
don-petry commentedon Oct 22, 2026
Working rulings, amended: the change reaches the model, and the credential is the org's (2026-10-22)
The session building the eval job (#309) found a gap:
task/1has no field for the change, and the model runs with--tools "", so neither a live review Run nor an eval run can see what it judges.Ruling 2 gains the diff. For a pull request, the Task carries the change as one text, the first:
diff:<the content id's hex>.outside, whoever pushed it, since the change is the text a review judges and an injection can arrive in it.config/harness.json'stexts.diff_characters, counted escaped as the other texts are. Up to it the diff is kept whole; over it, it is cut at a file's boundary, never inside one. The Task'somittedsays, for kinddiff, how many files were left out and how many characters.task/1keeps its major version, and the Engine hands the diff to the model as data in the Task, like every text.The run job is given the diff's bytes, and builds no Task unless their sha256 is the content id the Decision admitted. That is the hash the facts port takes of the same bytes (decision 0044), so a Run can only be given the content it was admitted for. A pull request with no diff, or one that is not text, gets no Task; an issue has none.
The credential is the org's (the Maintainer, 2026-10-22). The Claude credential is an org-level secret. The
runjob reads it from the org, underCLAUDE_CODE_OAUTH_TOKEN, not from thefactory-runenvironment. Ruling 9's check intools/check_workflows.pyholds that only therunstep names it. This replaces the 2026-10-21 wording "therunjob's environment secret onfactory-run".Built in "Harness: the Task, the Engine and the run job (#194, PR 1)" (#304), with its record.
Harness: a pull request's Task carries its change, held to the conten…