The console tells an auditor "nothing can change anything" from a field that does not mean that
store/console/src/lib/toolPolicy.ts:106:
export function writesOwnData(policy: readonly ToolPolicyEntry[]): boolean {
return listedTools(policy).some((t) => t.scope === "write" && !t.connector);
}
and :77:
export function mayWrite(t: ToolPolicyEntry): boolean {
return t.scope === "write" || t.writeConsent === "per_call";
}
writesOwnData decides which sentence toolScopeSummary renders above the tool switches —
including the read-only claim that :116's own comment calls "the one #525 made dangerous."
#563 established what scope actually is: the write-consent trigger, not a mutation label.
FIRST_PARTY_TOOLS omit it and instance-tool-policy.ts defaults it to "read", so nine
mutating tools — start_work, stop_work, end_coding_session, set_behaviour,
set_stats_card, run_pipeline, record_feedback, create_ticket, dedupe_upsert — report
scope: "read" with no connector. writesOwnData cannot see any of them.
Why nobody has seen it
BASE always contains write_memory, which is scope:"write" with no connector, so the
predicate is true for every agent and the wrong sentence never renders. The claim is correct
today because of an invariant that exists for unrelated reasons and is declared nowhere as
load-bearing. Remove write_memory from BASE, or ship an agent that declares its tools without
it, and a Coder holding start_work and run_pipeline is described to its owner as unable to
change anything.
What changed that makes this fixable
#563 shipped a mutates field on the listing precisely because scope could not answer this
question — 104 rows, 52 mutates:true, measured in production. writesOwnData is asking the
mutates question and reading the scope field.
mayWrite is a separate call and should be looked at, not assumed: its own comment argues
scope plus per_call is right for the connector-grant decision, because http_request is
scope:"read" yet the caller names the method. That reasoning may survive mutates intact.
Decide it explicitly rather than changing both because they sit together.
Acceptance criteria
writesOwnData derives from mutates, or a recorded reason why scope is correct there.
mayWrite is decided separately and the decision stated — its existing comment is a real
argument and must be answered, not overwritten.
- A test pins the case that is currently masked: a policy containing a
mutates:true,
scope:"read", connector-less tool and no write_memory renders the "can change things"
sentence. It must fail on today's code.
- The
BASE-contains-write_memory dependency is either removed or declared where it is
relied on, so the next person does not rediscover it as a bug.
Verified vs inferred
The console tells an auditor "nothing can change anything" from a field that does not mean that
store/console/src/lib/toolPolicy.ts:106:and
:77:writesOwnDatadecides which sentencetoolScopeSummaryrenders above the tool switches —including the read-only claim that
:116's own comment calls "the one #525 made dangerous."#563 established what
scopeactually is: the write-consent trigger, not a mutation label.FIRST_PARTY_TOOLSomit it andinstance-tool-policy.tsdefaults it to"read", so ninemutating tools —
start_work,stop_work,end_coding_session,set_behaviour,set_stats_card,run_pipeline,record_feedback,create_ticket,dedupe_upsert— reportscope: "read"with no connector.writesOwnDatacannot see any of them.Why nobody has seen it
BASEalways containswrite_memory, which isscope:"write"with no connector, so thepredicate is true for every agent and the wrong sentence never renders. The claim is correct
today because of an invariant that exists for unrelated reasons and is declared nowhere as
load-bearing. Remove
write_memoryfromBASE, or ship an agent that declares its tools withoutit, and a Coder holding
start_workandrun_pipelineis described to its owner as unable tochange anything.
What changed that makes this fixable
#563 shipped a
mutatesfield on the listing precisely becausescopecould not answer thisquestion — 104 rows, 52
mutates:true, measured in production.writesOwnDatais asking themutatesquestion and reading thescopefield.mayWriteis a separate call and should be looked at, not assumed: its own comment arguesscopeplusper_callis right for the connector-grant decision, becausehttp_requestisscope:"read"yet the caller names the method. That reasoning may survivemutatesintact.Decide it explicitly rather than changing both because they sit together.
Acceptance criteria
writesOwnDataderives frommutates, or a recorded reason whyscopeis correct there.mayWriteis decided separately and the decision stated — its existing comment is a realargument and must be answered, not overwritten.
mutates:true,scope:"read", connector-less tool and nowrite_memoryrenders the "can change things"sentence. It must fail on today's code.
BASE-contains-write_memorydependency is either removed or declared where it isrelied on, so the next person does not rediscover it as a bug.
Verified vs inferred
toolScopeSummary's use ofwritesOwnData; the nine mis-scoped tools and the?? "read"default (list_instance_tools reports nine mutating tools asscope: "read"— one field carries two incompatible meanings, and its sibling deliberately fails the other way #563); the 104/52production measurement;
write_memory's presence inBASE.write_memory— inspection ofBASE, not anenumeration of every shipped agent's resolved tool set. If one does, this is observed rather
than latent and should be re-rated.