Skip to content

Security review: issue-358

Mechanism: the-loop's own checklist (reference/security.md), run against requirements.md § Security considerations and design.md § Security design.

Risk tier 4 — the change touches **/*schema* and assembles the argv of an unattended agent from an authorized human's reply. A named human sign-off is required and is the owner's approval of PR #359.

The one new trust boundary

Comment text → an argv. It is designed out rather than mitigated: a reply yields a token that is a key into the operator's declared list, and the argv is built from cli-config.yaml plus the harness adapter's own flag. There is no data path from a comment to a command line, so command injection here is absent rather than defended against.

Two guards hold that up, and both are tested:

  1. The token grammar. _CHECK_LINE admits no whitespace, quote or metacharacter, and a model name additionally matches ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$. That grammar is what makes it safe to pass a provider's own spelling verbatim into an argv.
  2. The lookup. What survives the grammar is matched against what the operator declared now. Anything else is no choice.

Abuse cases → mechanism → negative test

CaseMechanismTestResult
A1 an unauthorized user ticks and executes_authorized_comments, upstream of any parsetest_abuse_an_unauthorized_reply_freezes_nothingclosed
A2 a reply naming a flag, a path, a metacharactertoken grammar + lookup into the declared settest_abuse_a_reply_naming_a_flag_or_a_path_resolves_to_nothing, test_abuse_a_shell_fragment_after_a_real_name_is_not_smuggled_throughclosed
A3 a declared entry widens permissionsapplied as declared, and only from the declarationtest_abuse_the_argv_contains_only_what_config_and_the_adapter_producedclosed
A4 a hand-edited portable recordre-validated on read, three ways: declared set, operator narrowing, availabilitytest_abuse_a_forged_frozen_choice_is_ignored, test_abuse_a_declaration_withdrawn_stops_reaching_the_argv, test_abuse_a_model_narrowed_to_another_harness_cannot_be_forced_onto_this_oneclosed
A5 a label named after a modelno label is read anywhere in this pathtest_abuse_a_label_named_after_a_model_selects_nothingclosed
A6 the checklist cannot be readthe existing empty-body path resolves to no choicetest_abuse_an_unreadable_checklist_keeps_the_operators_argumentsclosed
A7 a forged availability verdictthe cache may withhold, never introducetest_abuse_a_forged_verdict_cannot_introduce_a_choiceclosed

One finding, found by writing these tests rather than by the design. A4's third form: harnesses: on a model is the operator's narrowing, and it was enforced only where the checklist is rendered. A frozen record is a state file an agent can write, so a hand-edited one could have put a cursor-only model onto claude. Now enforced where the argv is built as well — the gate decides what may be picked, resolution decides what may be run, and both consult the same declaration.

The new egress

modelprobe is the one component that starts a process and reaches a vendor. Bounded:

  • it sends a fixed the-loop constant as its prompt (PROBE_PROMPT) and the resolved arguments — never any text from a work item, a comment or a repository;
  • it runs off the delivery pathmodels check, daemon start, and one stale-verdict re-probe;
  • it inherits the daemon's environment exactly as a critic run does, adding no variable;
  • its cache lives in the machine-local state tree, so it is not agent-writable and never travels in a repository.

It does spend tokens. That is stated in the command's own documentation and in the design's cost section, because an operator should not discover it from a bill.

Fail-closed audit

Every ambiguity resolves to the operator's stated configuration — not to the narrowest or cheapest model, which would be a different and wrong reading of "closed": it would let a malformed reply silently downgrade a tier-5 work item.

SituationResolves to
no tick, several ticks, an unknown tokenno choice
an unreadable checklist commentno choice
an unreadable or absent frozen recordthe harness's own arguments
a model no longer declared, or narrowed away from this harnessthe harness's own arguments
a refused verdictthe harness's own arguments, plus one comment saying so
an unreadable verdict cacheofferable — a cache fault must not withhold a declared capability
a harness with no model flagno model section offered at all

Least privilege

A choice contributes only the arguments its declaration carries. the-loop adds no permission flag of its own, and there is deliberately no operator-written args on a model — a harness that cannot be handed a model is offered no model section rather than given a hand-written flag nothing validates.

Secrets

None involved. This work item reads and writes no secret, token or environment variable. A model name is not a credential.

Outcome

Pass, with the one finding above fixed in the same PR. Human sign-off: pending the owner's approval of PR #359.

Released under the MIT License.