Replace read_database_table with typed object tools #85
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Labels: api, tools, breaking, priority/high
read_database_table is a raw SQL-shaped escape hatch exposed as a first-class tool. It should not exist. There are no object tools — no plan_read, plan_edit, rule_update, executions_for_plan. The agent is pushed into hand-rolled table browsing for everything.
What it cost in one session (2026-10-08):
Call Result
read_database_table("executions", limit=50) 3881 rows, truncated: true. ~60 KB of JSON pulled into context, 95% of it evidence-gather noise from unrelated hosts. The8 executions that mattered were in the first 5 rows, found by luck.
read_database_table("plans", limit=50) 22 rows, each carrying sha256, signature, requirements, ownerId, organizationId, runBranch. ~15 KB to read 10 step strings.
read_database_table("plans", filters={"planId":"…"}) {"error":"unknown filter 'planId'"} — no schema, no enumeration of valid filter fields, no error listing what is allowed. I guessed twice.
Additional harm:
Proposed API:
code
plan_read(id?, fields?, limit?, cursor?) # no id -> summary list only
plan_create(...) # existing
plan_update(id, steps?/verify?/enabled?/risk?/...) # PATCH semantics
plan_delete(id) # or plan_archive
plan_test(id, host) # dry-run against one host, no execution record
rule_read(id?)
rule_update(id, planId?/enabled?/conditions?/...)
rule_delete(id)
executions_for_plan(plan_id, status?, limit?, cursor?)
execution_read(id)
incident_read(id?)
machine_read(hostname) # supersedes read_machine_context
fields projection is the key part: plan_read(id, fields=["steps","verify","enabled"]) returns ~400 bytes instead of 15 KB.
Acceptance criteria
Claimed — branch
issue/85-typed-object-tools. Doing: replaceread_database_tablewith typed object tools (plan_read/create/update/delete/test, rule_read/update/delete, executions_for_plan, execution_read, incident_read, machine_read) inmiddleware/src/control-agent.ts.Issue #85 — Replace
read_database_tablewith typed object toolsRestated goal
read_database_tableis a raw table browser: it re-injects immutable plan metadata(
sha256,signature,requirements) on every call, has no schema, no valid-valueenumeration in its errors (
unknown filter 'planId'), no pagination contract, and noobject tools for the things the control-chat agent actually needs (read a plan, patch a
plan, list a plan's executions). Replace it with typed object tools that take object
ids, support field projection, and return
total+ a cursor, then delete the tablereader entirely.
Done for the operator: the control-chat agent can no longer browse tables; it
queries named objects (
plan_read,plan_update,executions_for_plan, …), everyfilter/field error enumerates the valid values, and
plan_readwithout an id returnssummaries with no
sha256/signature/requirements.Scope
Changed
middleware/src/control-agent.ts— delete theread_database_tabletool; add thetyped object tools; update
PLAN_MODE_TOOLSand bothCONTROL_SYSTEM_PROMPTtoolstrings; declare
ControlRowVisibilitylocally (moved out of the deleted module).middleware/src/routes/control.ts—accessFor()supplies the incident rowpredicate, and the runtime deps literal supplies
hubRemove.middleware/src/ai-table-reader.ts— deleted.middleware/tests/uc-mw-136-ai-read-table.test.ts— rewritten for the new tools(same faux-AI end-to-end shape, ≥11 assertions).
middleware/tests/uc-mw-permissions-audit.test.ts— thereadDatabaseTable honors visibilitytest becomes aplan_readvisibility test throughcontrolTools.middleware/tests/uc-mw-169-tool-scoping.test.ts—read_machine_context→machine_read.dashboard/src/lib/control-chat.test.ts— display-only rename of the tool name andJSON summary shape (pretty-print assertions kept).
docs/use-cases/middleware/UC-MW-136-ai-read-table.md— rewritten as the typedobject tools UC.
docs/use-cases/middleware/UC-MW-151-control-plan-mode.md,UC-MW-169-control-tool-scoping.md,UC-MW-115,UC-MW-120,UC-MW-137,docs/use-cases/README.md,docs/permissions-audit-plan.md— tool-name andrelated-link text only.
Not touched
plans/(product templates),middleware/src/routes/plans.tsandmiddleware/src/routes/rules.ts(read-only reuse of their helpers),scripts/agent-pipeline/quarantine.py(itsread_tableis unrelated),middleware/src/ai-table-reader.tscallers other than those listed (none exist).middleware/src/control-agent.tskeeps being the one seam; the new tools reuse
createPlanFromBody,applyPlanPatch,assertConcreteCommit,stampPlan,createRuleFromBody,rulePatchSchema,buildRule,validateRule,compatibility,KillSwitch.blocked,clampLimit,executionSummary,newestFirst,machineContextText,removeRepo,inScope.UC decision
Rewrite UC-MW-136 in place. Its subject is exactly "what the control chat may read
from the stores"; renaming the tools does not change the subject. A new id would force
bidirectional
related:edits across UC-MW-115, UC-MW-120, UC-MW-137 and UC-MW-169 forno behavioural gain.
Implementation:middleware/src/control-agent.tscontrolTools(plusmiddleware/src/routes/control.tsregisterfor the access wiring).Tests:middleware/tests/uc-mw-136-ai-read-table.test.ts(kept name; the filetext carries
UC-MW-136).related:unchanged: UC-MW-115, UC-MW-120, UC-MW-137, UC-MW-169.UC-MW-151 and UC-MW-169 get tool-name text updates only (their stories do not change);
their
version:bumps are not needed because their behaviour is unchanged — only thenames of the tools they list.
Tool surface (final)
Reads (allowed in plan mode):
plan_readid?,fields?,limit?,cursor?id→ summaries only (nosha256/signature/requirements); withid→ that planrule_readid?,fields?,limit?,cursor?executions_for_planplan_id,status?,limit?,cursor?total+nextCursorexecution_readid,fields?signature/jobStepsunless asked forincident_readid?,limit?,cursor?machine_readhostnameread_machine_contextWrites (blocked in plan mode, operator-request gate where the operator must ask):
create_planplan_updatetitle?,steps?,enabled?,risk?,verify?,rollback?,groupId?,phrase?,autonomyCeiling?,incidentPolicy?,incidentCreationDelaySeconds?,agentPickingTimeoutSeconds?,approvalTimeoutSeconds?plan_deleteremoveRepowhenformat === 'repo'+ audit + hub removeplan_testcreate_match_rulerule_updatetitle?,planId?,alertNameContains?,sources?,hostnames?,groupId?(s),enabled?,conditions?,combinator?,chain?rule_deleteRemoved:
read_database_table,read_machine_context(renamed tomachine_read),and
middleware/src/ai-table-reader.ts.Shared contracts:
total(matchingrows before paging) and
nextCursor(nullwhen the page is the last).unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...,unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval.deps.access.rows; rows the caller cannot see areabsent, not redacted. Writes require the row to be visible and in the caller's org
(
inScope).machine_readandplan_testauthorize their host through the existingdispatch-time
hosts: [...]gate (UC-MW-169 unchanged).plan_testnever enqueues: it callscompatibility(plan, agent),kill.blocked({planId, host})andassertConcreteCommit(plan.commit)and reports theresult. It deliberately does not call
decide(), which acquires a host lease thatis only released on a non-execute decision and would leak for a
productionplan.Ordered tasks (each with its verifying command)
Verify:
test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md.ControlRowVisibility(plan/rule/alert/execution/incident)to
control-agent.ts; wireaccessFor()+hubRemoveinroutes/control.ts.Verify:
cd middleware && bunx tsc --noEmit -p tsconfig.json(orbun test tests/uc-mw-169-tool-scoping.test.tsfor a real compile).read_database_table+ delete the module.Verify:
cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts.plan_update,plan_delete,plan_test,rule_update,rule_delete) + prompt/plan-mode lists. Verify:cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts tests/uc-mw-137-ai-create-plan-rule.test.ts tests/uc-mw-control-hardening.test.ts.uc-169, dashboard control-chat). Verify:
cd middleware && bun testandcd dashboard && bun run test.python3 docs/use-cases/_check.py.python3 scripts/check-ai-headers.py;python3 scripts/check-type-mirror.py;cd middleware && bun test;cd dashboard && bun run test && bun run check;rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'.buildApp, drivesplan_read/executions_for_plan/plan_test/rule_updatedirectly against PGlite, prints the JSON, and is deleted afterwards.
Verify: script output +
git status --shortclean of the throwaway.test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md.middleware: …,docs: …,Refs #85).Open questions and the assumption taken
plan_deleteneedhub.publish({entity:'plan',op:'remove'})?ControlDepshas no hub. Assumption: add one optional
hubRemove?: (entity, id) => voidandwire it from
routes/control.ts; without it the operator console would keep a stalerow until the next snapshot. Fallback (if the wiring fights the tests): drop the
field and document that the console reconciles on the next
GET /events.plan_updatefor repo plans. The HTTP PATCH also pinscommitforrunBranchand re-syncs
source, which needs the git helpers and the plans dir.Assumption: the tool patches the same fields
applyPlanPatchaccepts and doesnot change
runBranch/source; the tool description says so and the reportrecords it. The boring fallback is the console.
rule_updategroupId. Rules storegroupIds: string[].Assumption: expose
groupIds(array), matchingrulePatchSchema.incident_readvisibility.VisibilityhasrowVisiblebut not an incidentpredicate. Assumption: add
incident: (i) => booleantoControlRowVisibilitysourced from
incidentVisible(i, vis).in-memory maps with no stable key order across writes and an offset is honest about
that.
read_machine_contextrename. UC-MW-169 and UC-MW-151 name the old tool.Assumption: rename to
machine_readas the issue asks and update both UCs'text; the UC ids stay valid because the tests keep their names and ids.
Risk and the boring fallback
middleware/src/control-agent.tsand a test file whose text containsUC-MW-136.Fallback: keep the test file name and the
UC-MW-136string in it; citecontrolTools.scripts/check-test-tampering.pycountsexpect(/assert(). Current counts: uc-136 = 11, permissions-audit = 43, uc-169 = 35,dashboard control-chat = 119. Fallback: new tests assert more, never fewer; the
rewritten uc-136 will run ≥14 expectations.
bun testdiag in an unrelated suite. Fallback: run the four touchedfiles first, then the full suite; a red unrelated suite is reported, not silenced.
hubRemovewiring point accepts the new field. Fallback per openquestion 1.
Type.Objectonfields?: string[]misbehaves in the pi harness.Fallback: declare
Type.Optional(Type.Array(Type.String()))and validate the valuesin the handler.
PARTIAL — branch
issue/85-typed-object-tools, not pushed.Commits:
2afb6942middleware: typed control-chat object tools replace read_database_table1542a814test: cover typed control-chat object tools and gate write tools by operator request3d3e31adocs: UC-MW-136 covers typed control-chat object tools8907bb3test: pin plan_read summary totals and cursor to the store size934bc705test: use value-pinning matchers in the control object tool testsShipped:
read_database_tableandmiddleware/src/ai-table-reader.tsdeleted;plan_read,rule_read,incident_read,executions_for_plan,execution_read,machine_readreplace it with field projection and{total,rowCount,nextCursor,rows}paging;plan_update,plan_delete,plan_test,rule_update,rule_deleteadded with plan-mode + operator-request + per-row inScope gates. Errors enumerate valid fields/statuses.Gates:
python3 docs/use-cases/_check.py— PASS (359 UCs)python3 scripts/check-ai-headers.py— PASS (576 ok)python3 scripts/check-type-mirror.py— PASS (exit 0)python3 scripts/check-test-tampering.py— PASS (clean)cd dashboard && bun run test— PASS (254/254)cd dashboard && bun run check— PASS (0 errors, 7 pre-existing warnings)bun teston the four touched middleware suites — uc-136 13 pass, uc-137 6 pass, uc-169 8 pass, uc-mw-control-hardening 11 pass;uc-mw-permissions-audit.test.ts6 pass / 5 fail (pre-existing, reproduced at merge base78b750ein a scratch worktree)cd middleware && bun test— NOT OBSERVED, exceeded 300 s in this environmentrg -n 'Showing lines'— PASSReport:
/home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md. Refs #85REVIEW: FIXED 3 — findings: (1) ControlScope lost its denial member in
2afb694while four call sites still use scope.denial, so tsc reported TS2339/TS2353 on control-agent.ts — restored, commitaa1bd25; (2) UC-MW-136 claimed every listed write runs blockInPlanMode + requireOperatorRequest, but plan_test runs neither — story/main flow now separate it from mutating writes, commit6369ff0; (3) docs/permissions-audit-plan.md still told the reader to edit ai-table-reader.ts / readDatabaseTable, both deleted by this issue — Task 3a and test step 5 now describe ControlRowVisibility and the plan_read visibility check, commit6369ff0. All six issue acceptance criteria verified present; checkers (_check, ai-headers, type-mirror, test-tampering, dashboard 254/254, svelte-check 0 errors) reproduce the report claims; no test skipped or weakened (53 expects added vs 14 removed, all removed ones target the deleted surface). Deviations kept: docs/control-agent-hardening-plan.md was touched without being in the plan scope (stale tool list otherwise); plan-listed text updates to UC-MW-115/120/137 were no-ops (no stale names). UNFIXED: docs/plans/2026-09-27-org-ownership-and-source-hardening.md:117 still names AiRowVisibility (dated historical plan, out of scope); plan_read list order is insertion order and undocumented in the tool description; repo-wide tsc noise is pre-existing broken moduleResolution config, not this change; one transient 500 in a uc-136 + uc-169 combined run was not reproducible in two later runs (flake). Refs #85FIX INCOMPLETE: compacted the 9 new tool descriptions in middleware/src/control-agent.ts (8 edits, -146 chars), commit
086f5aa. Re-run: bun test tests/uc-mw-115-control-chat.test.ts -> still 11 pass / 1 fail.Root cause is NOT description length. Measured the real prompt the harness sends at the second turn: tools JSON 9040 chars vs 5034 at base
78b750e; system prompt 2978 vs 2878 (byte-identical body, only the tool-name list grew). Over a 2048-token window the faux provider's char/4 estimator reads ~3051 tokens for the request, so it reports 'Assistant request exceeded the context window'. Stripping ALL tool descriptions still left tools at 7669 chars and still 422 — the growth is the 9 extra tools' JSON Schemas (plan_update 1020, rule_update 1077, create_match_rule 1038, create_plan 1124 chars), not their prose. Trimming prose cannot close a ~1.9k-token gap.Options needing your call (any of): (a) raise the test's contextWindow above 2048; (b) budget-gate the control tool surface (fewer/wider tools, or lazily expose write tools only in plan-mode); (c) drop the heavy write-tool schemas from the always-on prompt. Not touched: system prompt, test, window. Stopping per time-box.
NEEDS-DECISION: UC-MW-115 tiny-context harness cannot fit the 9 new typed object tools | options: (a) raise the test's contextWindow above 2048 tokens / (b) budget-gate the control tool surface (fewer or wider tools, write tools lazily exposed in plan-mode only) / (c) drop the heavy write-tool schemas (plan_update/rule_update/create_plan/create_match_rule) from the always-on prompt | I recommend (b) because the 422 is real context pressure for small-window profiles, the issue's goal is context economy, and (a) only moves the ceiling while (c) hides write capability | state: 7 commits on issue/85-typed-object-tools (compaction commit reverted as useless), all other gates green, no PR opened | Refs #85
DECISSION: Option B
STATUS: branch issue/85-typed-object-tools pushed (9 commits: typed object tools + tests + docs + review fixes aa1bd25/6369ff0, compaction attempt
086f5aareverted in355a5f3as ineffective). Gates: uc-catalog/ai-headers/type-mirror/dashboard 254/254/svelte-check 0 errors PASS; middleware full suite 597/599 with UC-MW-19 pre-existing red and UC-MW-115 422 regression (see NEEDS-DECISION). No PR while blocked-on-operator.issue/85-typed-object-toolsto issue/85-typed-object-toolsDECISION APPLIED — Option B: budget-gate the control tool surface. Implementing now on branch issue/85-typed-object-tools: write tools (plan_update/plan_delete/plan_test/rule_update/rule_delete/create_plan/create_match_rule) exposed lazily — plan-mode / operator-request gates decide schema inclusion — so the UC-MW-115 tiny-context harness fits without raising the window or dropping capability.
Claimed (continuation) — branch
issue/85-typed-object-tools. Doing: Option B, budget-gate the write-tool schemas out of the model-facing tool list so the 2048-token UC-MW-115 harness fits; read tools stay always-on.Issue #85 — Replace
read_database_tablewith typed object toolsRestated goal
read_database_tableis a raw table browser: it re-injects immutable plan metadata(
sha256,signature,requirements) on every call, has no schema, no valid-valueenumeration in its errors (
unknown filter 'planId'), no pagination contract, and noobject tools for the things the control-chat agent actually needs (read a plan, patch a
plan, list a plan's executions). Replace it with typed object tools that take object
ids, support field projection, and return
total+ a cursor, then delete the tablereader entirely.
Done for the operator: the control-chat agent can no longer browse tables; it
queries named objects (
plan_read,plan_update,executions_for_plan, …), everyfilter/field error enumerates the valid values, and
plan_readwithout an id returnssummaries with no
sha256/signature/requirements.Scope
Changed
middleware/src/control-agent.ts— delete theread_database_tabletool; add thetyped object tools; update
PLAN_MODE_TOOLSand bothCONTROL_SYSTEM_PROMPTtoolstrings; declare
ControlRowVisibilitylocally (moved out of the deleted module).middleware/src/routes/control.ts—accessFor()supplies the incident rowpredicate, and the runtime deps literal supplies
hubRemove.middleware/src/ai-table-reader.ts— deleted.middleware/tests/uc-mw-136-ai-read-table.test.ts— rewritten for the new tools(same faux-AI end-to-end shape, ≥11 assertions).
middleware/tests/uc-mw-permissions-audit.test.ts— thereadDatabaseTable honors visibilitytest becomes aplan_readvisibility test throughcontrolTools.middleware/tests/uc-mw-169-tool-scoping.test.ts—read_machine_context→machine_read.dashboard/src/lib/control-chat.test.ts— display-only rename of the tool name andJSON summary shape (pretty-print assertions kept).
docs/use-cases/middleware/UC-MW-136-ai-read-table.md— rewritten as the typedobject tools UC.
docs/use-cases/middleware/UC-MW-151-control-plan-mode.md,UC-MW-169-control-tool-scoping.md,UC-MW-115,UC-MW-120,UC-MW-137,docs/use-cases/README.md,docs/permissions-audit-plan.md— tool-name andrelated-link text only.
Not touched
plans/(product templates),middleware/src/routes/plans.tsandmiddleware/src/routes/rules.ts(read-only reuse of their helpers),scripts/agent-pipeline/quarantine.py(itsread_tableis unrelated),middleware/src/ai-table-reader.tscallers other than those listed (none exist).middleware/src/control-agent.tskeeps being the one seam; the new tools reuse
createPlanFromBody,applyPlanPatch,assertConcreteCommit,stampPlan,createRuleFromBody,rulePatchSchema,buildRule,validateRule,compatibility,KillSwitch.blocked,clampLimit,executionSummary,newestFirst,machineContextText,removeRepo,inScope.UC decision
Rewrite UC-MW-136 in place. Its subject is exactly "what the control chat may read
from the stores"; renaming the tools does not change the subject. A new id would force
bidirectional
related:edits across UC-MW-115, UC-MW-120, UC-MW-137 and UC-MW-169 forno behavioural gain.
Implementation:middleware/src/control-agent.tscontrolTools(plusmiddleware/src/routes/control.tsregisterfor the access wiring).Tests:middleware/tests/uc-mw-136-ai-read-table.test.ts(kept name; the filetext carries
UC-MW-136).related:unchanged: UC-MW-115, UC-MW-120, UC-MW-137, UC-MW-169.UC-MW-151 and UC-MW-169 get tool-name text updates only (their stories do not change);
their
version:bumps are not needed because their behaviour is unchanged — only thenames of the tools they list.
Tool surface (final)
Reads (allowed in plan mode):
plan_readid?,fields?,limit?,cursor?id→ summaries only (nosha256/signature/requirements); withid→ that planrule_readid?,fields?,limit?,cursor?executions_for_planplan_id,status?,limit?,cursor?total+nextCursorexecution_readid,fields?signature/jobStepsunless asked forincident_readid?,limit?,cursor?machine_readhostnameread_machine_contextWrites (blocked in plan mode, operator-request gate where the operator must ask):
create_planplan_updatetitle?,steps?,enabled?,risk?,verify?,rollback?,groupId?,phrase?,autonomyCeiling?,incidentPolicy?,incidentCreationDelaySeconds?,agentPickingTimeoutSeconds?,approvalTimeoutSeconds?plan_deleteremoveRepowhenformat === 'repo'+ audit + hub removeplan_testcreate_match_rulerule_updatetitle?,planId?,alertNameContains?,sources?,hostnames?,groupId?(s),enabled?,conditions?,combinator?,chain?rule_deleteRemoved:
read_database_table,read_machine_context(renamed tomachine_read),and
middleware/src/ai-table-reader.ts.Shared contracts:
total(matchingrows before paging) and
nextCursor(nullwhen the page is the last).unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...,unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval.deps.access.rows; rows the caller cannot see areabsent, not redacted. Writes require the row to be visible and in the caller's org
(
inScope).machine_readandplan_testauthorize their host through the existingdispatch-time
hosts: [...]gate (UC-MW-169 unchanged).plan_testnever enqueues: it callscompatibility(plan, agent),kill.blocked({planId, host})andassertConcreteCommit(plan.commit)and reports theresult. It deliberately does not call
decide(), which acquires a host lease thatis only released on a non-execute decision and would leak for a
productionplan.Ordered tasks (each with its verifying command)
Verify:
test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md.ControlRowVisibility(plan/rule/alert/execution/incident)to
control-agent.ts; wireaccessFor()+hubRemoveinroutes/control.ts.Verify:
cd middleware && bunx tsc --noEmit -p tsconfig.json(orbun test tests/uc-mw-169-tool-scoping.test.tsfor a real compile).read_database_table+ delete the module.Verify:
cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts.plan_update,plan_delete,plan_test,rule_update,rule_delete) + prompt/plan-mode lists. Verify:cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts tests/uc-mw-137-ai-create-plan-rule.test.ts tests/uc-mw-control-hardening.test.ts.uc-169, dashboard control-chat). Verify:
cd middleware && bun testandcd dashboard && bun run test.python3 docs/use-cases/_check.py.python3 scripts/check-ai-headers.py;python3 scripts/check-type-mirror.py;cd middleware && bun test;cd dashboard && bun run test && bun run check;rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'.buildApp, drivesplan_read/executions_for_plan/plan_test/rule_updatedirectly against PGlite, prints the JSON, and is deleted afterwards.
Verify: script output +
git status --shortclean of the throwaway.test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md.middleware: …,docs: …,Refs #85).Open questions and the assumption taken
plan_deleteneedhub.publish({entity:'plan',op:'remove'})?ControlDepshas no hub. Assumption: add one optional
hubRemove?: (entity, id) => voidandwire it from
routes/control.ts; without it the operator console would keep a stalerow until the next snapshot. Fallback (if the wiring fights the tests): drop the
field and document that the console reconciles on the next
GET /events.plan_updatefor repo plans. The HTTP PATCH also pinscommitforrunBranchand re-syncs
source, which needs the git helpers and the plans dir.Assumption: the tool patches the same fields
applyPlanPatchaccepts and doesnot change
runBranch/source; the tool description says so and the reportrecords it. The boring fallback is the console.
rule_updategroupId. Rules storegroupIds: string[].Assumption: expose
groupIds(array), matchingrulePatchSchema.incident_readvisibility.VisibilityhasrowVisiblebut not an incidentpredicate. Assumption: add
incident: (i) => booleantoControlRowVisibilitysourced from
incidentVisible(i, vis).in-memory maps with no stable key order across writes and an offset is honest about
that.
read_machine_contextrename. UC-MW-169 and UC-MW-151 name the old tool.Assumption: rename to
machine_readas the issue asks and update both UCs'text; the UC ids stay valid because the tests keep their names and ids.
Risk and the boring fallback
middleware/src/control-agent.tsand a test file whose text containsUC-MW-136.Fallback: keep the test file name and the
UC-MW-136string in it; citecontrolTools.scripts/check-test-tampering.pycountsexpect(/assert(). Current counts: uc-136 = 11, permissions-audit = 43, uc-169 = 35,dashboard control-chat = 119. Fallback: new tests assert more, never fewer; the
rewritten uc-136 will run ≥14 expectations.
bun testdiag in an unrelated suite. Fallback: run the four touchedfiles first, then the full suite; a red unrelated suite is reported, not silenced.
hubRemovewiring point accepts the new field. Fallback per openquestion 1.
Type.Objectonfields?: string[]misbehaves in the pi harness.Fallback: declare
Type.Optional(Type.Array(Type.String()))and validate the valuesin the handler.
Continuation — Option B: budget-gate the control tool surface
Restated goal
The typed object tools shipped in the first session (
355a5f3) blew the UC-MW-115 tinywindow: the 9 new tools add ~4006 chars of JSON Schema to the tool payload the harness
sends (9040 vs 5034 at base
78b750e), and the faux provider'schars/4estimate thenreads ~3051 tokens against a 2048-token
contextWindow, so the second chat turn failswith 422
Assistant request exceeded the context window. Compacting prose did not closethe gap (
086f5aa, reverted in355a5f3) because the weight is the schemas.Done for the operator = the UC-MW-115 test passes with its 2048 window untouched, the
read tools stay always-on, and a session whose model cannot afford the write schemas no
longer gets them injected.
What I measured (probe against the real harness, not inference)
tools9040 chars,systemPrompt2978 chars,messages ~235 → ~3051 tokens. Base was 8096/8174 chars → ~2024 tokens, i.e. base
passed with 17 chars of headroom. The window is a hard char budget of 8192.
toolsat 7669 chars and still422 — prose cannot bridge it.
r2 == 200.So the payload size is the mechanism, confirmed empirically.
Decision
Budget-gate the write tools (
create_plan,plan_update,plan_delete,plan_test,create_match_rule,rule_update,rule_delete) out of the tool list handed to themodel when the full surface cannot fit the model's context window; the read tools stay
always-on. The gate lives in the harness seam (
createControlHarness), not incontrolTools().Why not the operator-request gate the briefing also mentions: the UC-MW-137 suite drives
create_plan/create_match_rulerefusal through HTTP and asserts the tool resulttext
… explicitly asks …(i.e. the model must still be able to call the tool and getthe refusal). An unadvertised tool never reaches
requireOperatorRequest— the harnessrejects the call as unknown first — so gating by
operatorAskedat the harness wouldbreak
uc-mw-137tests 116/146, which the task forbids ("do not delete or weaken anytest"). The budget gate is also what the briefing's own headline asks for, and it keeps
requireOperatorRequest/blockInPlanMode/inScopeexactly as the issue shipped.Scope
Changed
middleware/src/control-agent.ts— addWRITE_TOOLS+fitControlTools(...); call itfrom
createControlHarness; §6Lastbump.docs/use-cases/middleware/UC-MW-136-ai-read-table.md— one Story/Notes line + theTests/versionhousekeeping for the budget gate.docs/use-cases/middleware/UC-MW-115-control-chat.md— only if its text contradictsthe gate.
Not touched
middleware/tests/uc-mw-115-control-chat.test.ts(2048 window stays), theCONTROL_SYSTEM_PROMPTprose, every gate the first session added (requireOperatorRequest,blockInPlanMode,inScope, field projection,{total,rowCount,nextCursor,rows}),middleware/src/ai-table-reader.ts(already deleted),plans/,routes/plans.ts,routes/rules.ts, the dashboard.UC decision
No new UC.
UC-MW-136("control chat reads and updates objects through typed tools") isthe subject that gains a clause;
UC-MW-115continues to own the harness/compactionbehaviour. Implementation cite stays
middleware/src/control-agent.tscontrolTools.Ordered tasks
test -f …/issue-85.plan.md.WRITE_TOOLS+fitControlTools+ wire intocreateControlHarness; §6Lastbump.Verify:
cd middleware && bun test tests/uc-mw-115-control-chat.test.ts(must be 12/12)and
bun test tests/uc-mw-136-ai-read-table.test.ts tests/uc-mw-137-ai-create-plan-rule.test.ts.python3 docs/use-cases/_check.py.python3 scripts/check-ai-headers.py,python3 scripts/check-type-mirror.py,the four middleware suites above,
rg -n 'Showing lines'.Open questions and assumptions taken
both. Assumption: budget gating, because the operator-request variant breaks the
shipped UC-MW-137 refusal tests (evidence above). Recorded in the report.
systemPrompt + serializedTools + RESERVE <= 4 * contextWindow(the faux estimator ischars/4; the reserve stands in for the transcript). Constant, no env knob.
leave the prompt byte-identical rather than change the signature the UC-MW-136 /
hardening tests call with two arguments; a model that calls a gated tool gets a
harness tool error and adapts. Recorded as a known limitation.
scope — this change only removes the write tier.
Risk and the boring fallback
create_planfrom the model on small windows. Fallback:the gate is budget-derived, so anything with a normal window (≥8k) keeps the full
surface; the tiny window is a test fixture.
Fallback: grep for
contextWindowin tests before committing; if one exists, gate onlyabove the smallest such window.
versiononly.DONE — budget gate shipped; UC-MW-115 is 12/12.
Commit:
aabfcbdmiddleware: budget-gate the control write tools out of a tiny-window harness (Refs #85).What: createControlHarness now drops the 7 write tools (create_plan, plan_update, plan_delete, plan_test, create_match_rule, rule_update, rule_delete) from the model's tool list when systemPrompt + schema JSON + reserve exceeds contextWindow*4; reads stay always-on; controlTools() and every existing gate untouched.
Why not operator-request gating: uc-mw-137 tests 116/146 drive create_plan/create_match_rule over HTTP and assert the tool result text 'explicitly asks' — an unadvertised tool never reaches requireOperatorRequest, so that variant would break shipped tests.
Gates:
Known limitation (in the report): the system prompt still names the write tools when they are gated out on a tiny window.
REVIEW: FIXED 2 — findings: budget gate leaves the system prompt promising the 7 write tools it just dropped (fixed
90625c2: prompt now states the dropped tier; smoke shows 14 tools + note at window 2048, 21 tools + no note at default); fitControlTools had no direct contract test, only the end-to-end UC-MW-115 chat (fixed24623ed: tiny-window drops exactly the 7 writes and keeps all 6 reads, fit/undefined/0/-1/NaN windows return the list untouched); UNFIXED: gate holds a fixed 1024-char transcript reserve and only one tier (reads overflowing is documented out-of-scope), docs/control-agent-hardening-plan.md tool-name line was outside the plan's Scope but its text is correct, so it staysDONE — branch issue/85-typed-object-tools, PR #94 → agent-develop (operator decision Option B applied: budget-gated write tier).
Commits: 12 on origin/agent-develop (typed tools + tests + docs: 2afb694…934bc705; review round 1:
aa1bd25,6369ff0; budget gate:aabfcbd; review round 2:90625c2,24623ed).Option B implementation: fitControlTools() drops the 7 write tools (create_plan, plan_update, plan_delete, plan_test, create_match_rule, rule_update, rule_delete) from the model surface when systemPrompt + serialized schemas + 1024-char reserve exceed contextWindow*4; 6 read tools always-on; controlTools() and all write gates untouched. Review round 2 fixed: system prompt now names the dropped tier when writes are gated out (
90625c2); direct contract test for fitControlTools (24623ed).Orchestrator gates (run on final reviewed tree):
PR: #94 — Refs #85