Replace read_database_table with typed object tools #85

Closed
opened 2026-10-08 09:19:35 +02:00 by EduarD3V · 14 comments
Owner

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:

Context burn. Every call re-injects megabytes of immutable metadata (sha256, signature) that no decision depends on.
Ordering is newest-first and undocumented, so "find the last execution of plan X" is a guessing game, not a query.
No pagination contract. offset exists but nothing reports total pages.
Error messages are dead ends. unknown filter 'planId' gives no valid set, so recovery is trial and error inside a paid context window.

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

read_database_table removed from the tool surface for all agent profiles.
Each of the object tools above exists, or a documented equivalent with a clear reason.
plan_read without id returns summaries only (no sha256, no signature, no requirements).
executions_for_plan supports filtering by status and returns a cursor.
Any filter/field error lists the valid values.
Pagination returns total count and page cursor.
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: Context burn. Every call re-injects megabytes of immutable metadata (sha256, signature) that no decision depends on. Ordering is newest-first and undocumented, so "find the last execution of plan X" is a guessing game, not a query. No pagination contract. offset exists but nothing reports total pages. Error messages are dead ends. unknown filter 'planId' gives no valid set, so recovery is trial and error inside a paid context window. 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 read_database_table removed from the tool surface for all agent profiles. Each of the object tools above exists, or a documented equivalent with a clear reason. plan_read without id returns summaries only (no sha256, no signature, no requirements). executions_for_plan supports filtering by status and returns a cursor. Any filter/field error lists the valid values. Pagination returns total count and page cursor.
gotoi-agent added reference issue/85-typed-object-tools 2026-10-08 09:31:25 +02:00
Collaborator

Claimed — branch issue/85-typed-object-tools. Doing: replace read_database_table with typed object tools (plan_read/create/update/delete/test, rule_read/update/delete, executions_for_plan, execution_read, incident_read, machine_read) in middleware/src/control-agent.ts.

Claimed — branch `issue/85-typed-object-tools`. Doing: replace `read_database_table` with typed object tools (plan_read/create/update/delete/test, rule_read/update/delete, executions_for_plan, execution_read, incident_read, machine_read) in `middleware/src/control-agent.ts`.
Collaborator

Issue #85 — Replace read_database_table with typed object tools

Restated goal

read_database_table is a raw table browser: it re-injects immutable plan metadata
(sha256, signature, requirements) on every call, has no schema, no valid-value
enumeration in its errors (unknown filter 'planId'), no pagination contract, and no
object 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 table
reader 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, …), every
filter/field error enumerates the valid values, and plan_read without an id returns
summaries with no sha256/signature/requirements.

Scope

Changed

  • middleware/src/control-agent.ts — delete the read_database_table tool; add the
    typed object tools; update PLAN_MODE_TOOLS and both CONTROL_SYSTEM_PROMPT tool
    strings; declare ControlRowVisibility locally (moved out of the deleted module).
  • middleware/src/routes/control.ts — accessFor() supplies the incident row
    predicate, 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 — the readDatabaseTable honors visibility test becomes a plan_read visibility test through controlTools.
  • 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 and
    JSON summary shape (pretty-print assertions kept).
  • docs/use-cases/middleware/UC-MW-136-ai-read-table.md — rewritten as the typed
    object 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 and
    related-link text only.

Not touched

  • plans/ (product templates), middleware/src/routes/plans.ts and
    middleware/src/routes/rules.ts (read-only reuse of their helpers),
    scripts/agent-pipeline/quarantine.py (its read_table is unrelated),
    middleware/src/ai-table-reader.ts callers other than those listed (none exist).
  • No new files, no new dependencies, no new config knobs. middleware/src/control-agent.ts
    keeps 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 for
no behavioural gain.

  • Title: "Control chat reads and updates objects through typed tools".
  • Implementation: middleware/src/control-agent.ts controlTools (plus
    middleware/src/routes/control.ts register for the access wiring).
  • Tests: middleware/tests/uc-mw-136-ai-read-table.test.ts (kept name; the file
    text 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 the
names of the tools they list.

Tool surface (final)

Reads (allowed in plan mode):

Tool Parameters Notes
plan_read id?, fields?, limit?, cursor? no id → summaries only (no sha256/signature/requirements); with id → that plan
rule_read id?, fields?, limit?, cursor?
executions_for_plan plan_id, status?, limit?, cursor? returns total + nextCursor
execution_read id, fields? drops signature/jobSteps unless asked for
incident_read id?, limit?, cursor?
machine_read hostname supersedes read_machine_context

Writes (blocked in plan mode, operator-request gate where the operator must ask):

Tool Semantics
create_plan unchanged
plan_update PATCH: title?, steps?, enabled?, risk?, verify?, rollback?, groupId?, phrase?, autonomyCeiling?, incidentPolicy?, incidentCreationDelaySeconds?, agentPickingTimeoutSeconds?, approvalTimeoutSeconds?
plan_delete delete + removeRepo when format === 'repo' + audit + hub remove
plan_test dry run against one session host: no execution row, no lease
create_match_rule unchanged
rule_update PATCH: title?, planId?, alertNameContains?, sources?, hostnames?, groupId?(s), enabled?, conditions?, combinator?, chain?
rule_delete delete + hub remove

Removed: read_database_table, read_machine_context (renamed to machine_read),
and middleware/src/ai-table-reader.ts.

Shared contracts:

  • Cursor: opaque decimal offset string. Response always carries total (matching
    rows before paging) and nextCursor (null when the page is the last).
  • Errors: every filter/field/status error enumerates the valid values, e.g.
    unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...,
    unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval.
  • Tenancy: reads filter through deps.access.rows; rows the caller cannot see are
    absent, not redacted. Writes require the row to be visible and in the caller's org
    (inScope). machine_read and plan_test authorize their host through the existing
    dispatch-time hosts: [...] gate (UC-MW-169 unchanged).
  • plan_test never enqueues: it calls compatibility(plan, agent),
    kill.blocked({planId, host}) and assertConcreteCommit(plan.commit) and reports the
    result. It deliberately does not call decide(), which acquires a host lease that
    is only released on a non-execute decision and would leak for a production plan.

Ordered tasks (each with its verifying command)

  1. Plan + claim. Write this file; post the claim and the plan to issue #85.
    Verify: test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md.
  2. Access plumbing. Add ControlRowVisibility (plan/rule/alert/execution/incident)
    to control-agent.ts; wire accessFor() + hubRemove in routes/control.ts.
    Verify: cd middleware && bunx tsc --noEmit -p tsconfig.json (or bun test tests/uc-mw-169-tool-scoping.test.ts for a real compile).
  3. Typed read tools + delete read_database_table + delete the module.
    Verify: cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts.
  4. Typed write tools (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.
  5. Rewrite the tests that referenced the removed surface (uc-136, permissions-audit,
    uc-169, dashboard control-chat). Verify: cd middleware && bun test and
    cd dashboard && bun run test.
  6. UC + docs text. Verify: python3 docs/use-cases/_check.py.
  7. All gates. 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'.
  8. Smoke run the changed path: a throwaway script that builds the app through
    buildApp, drives plan_read/executions_for_plan/plan_test/rule_update
    directly against PGlite, prints the JSON, and is deleted afterwards.
    Verify: script output + git status --short clean of the throwaway.
  9. Report + Done comment. Verify: test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md.
  10. Commit after each of tasks 2–6 (middleware: …, docs: …, Refs #85).

Open questions and the assumption taken

  1. Does plan_delete need hub.publish({entity:'plan',op:'remove'})? ControlDeps
    has no hub. Assumption: add one optional hubRemove?: (entity, id) => void and
    wire it from routes/control.ts; without it the operator console would keep a stale
    row 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.
  2. plan_update for repo plans. The HTTP PATCH also pins commit for runBranch
    and re-syncs source, which needs the git helpers and the plans dir.
    Assumption: the tool patches the same fields applyPlanPatch accepts and does
    not change runBranch/source; the tool description says so and the report
    records it. The boring fallback is the console.
  3. rule_update groupId. Rules store groupIds: string[].
    Assumption: expose groupIds (array), matching rulePatchSchema.
  4. incident_read visibility. Visibility has rowVisible but not an incident
    predicate. Assumption: add incident: (i) => boolean to ControlRowVisibility
    sourced from incidentVisible(i, vis).
  5. Cursor shape. Assumption: decimal offset string, because the stores are
    in-memory maps with no stable key order across writes and an offset is honest about
    that.
  6. read_machine_context rename. UC-MW-169 and UC-MW-151 name the old tool.
    Assumption: rename to machine_read as 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

  • Risk: UC checker failures. The checked-in UC must cite a real symbol in
    middleware/src/control-agent.ts and a test file whose text contains UC-MW-136.
    Fallback: keep the test file name and the UC-MW-136 string in it; cite
    controlTools.
  • Risk: assertion-count gate (scripts/check-test-tampering.py counts
    expect(/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.
  • Risk: bun test diag in an unrelated suite. Fallback: run the four touched
    files first, then the full suite; a red unrelated suite is reported, not silenced.
  • Risk: no hubRemove wiring point accepts the new field. Fallback per open
    question 1.
  • Risk: Type.Object on fields?: string[] misbehaves in the pi harness.
    Fallback: declare Type.Optional(Type.Array(Type.String())) and validate the values
    in the handler.
# Issue #85 — Replace `read_database_table` with typed object tools ## Restated goal `read_database_table` is a raw table browser: it re-injects immutable plan metadata (`sha256`, `signature`, `requirements`) on every call, has no schema, no valid-value enumeration in its errors (`unknown filter 'planId'`), no pagination contract, and no object 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 table reader 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`, …), every filter/field error enumerates the valid values, and `plan_read` without an id returns summaries with no `sha256`/`signature`/`requirements`. ## Scope **Changed** - `middleware/src/control-agent.ts` — delete the `read_database_table` tool; add the typed object tools; update `PLAN_MODE_TOOLS` and both `CONTROL_SYSTEM_PROMPT` tool strings; declare `ControlRowVisibility` locally (moved out of the deleted module). - `middleware/src/routes/control.ts` — `accessFor()` supplies the incident row predicate, 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` — the `readDatabaseTable honors visibility` test becomes a `plan_read` visibility test through `controlTools`. - `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 and JSON summary shape (pretty-print assertions kept). - `docs/use-cases/middleware/UC-MW-136-ai-read-table.md` — rewritten as the typed object 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 and related-link text only. **Not touched** - `plans/` (product templates), `middleware/src/routes/plans.ts` and `middleware/src/routes/rules.ts` (read-only reuse of their helpers), `scripts/agent-pipeline/quarantine.py` (its `read_table` is unrelated), `middleware/src/ai-table-reader.ts` callers other than those listed (none exist). - No new files, no new dependencies, no new config knobs. `middleware/src/control-agent.ts` keeps 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 for no behavioural gain. - Title: "Control chat reads and updates objects through typed tools". - `Implementation`: `middleware/src/control-agent.ts` `controlTools` (plus `middleware/src/routes/control.ts` `register` for the access wiring). - `Tests`: `middleware/tests/uc-mw-136-ai-read-table.test.ts` (kept name; the file text 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 the names of the tools they list. ## Tool surface (final) Reads (allowed in plan mode): | Tool | Parameters | Notes | |---|---|---| | `plan_read` | `id?`, `fields?`, `limit?`, `cursor?` | no `id` → summaries only (no `sha256`/`signature`/`requirements`); with `id` → that plan | | `rule_read` | `id?`, `fields?`, `limit?`, `cursor?` | | | `executions_for_plan` | `plan_id`, `status?`, `limit?`, `cursor?` | returns `total` + `nextCursor` | | `execution_read` | `id`, `fields?` | drops `signature`/`jobSteps` unless asked for | | `incident_read` | `id?`, `limit?`, `cursor?` | | | `machine_read` | `hostname` | supersedes `read_machine_context` | Writes (blocked in plan mode, operator-request gate where the operator must ask): | Tool | Semantics | |---|---| | `create_plan` | unchanged | | `plan_update` | PATCH: `title?`, `steps?`, `enabled?`, `risk?`, `verify?`, `rollback?`, `groupId?`, `phrase?`, `autonomyCeiling?`, `incidentPolicy?`, `incidentCreationDelaySeconds?`, `agentPickingTimeoutSeconds?`, `approvalTimeoutSeconds?` | | `plan_delete` | delete + `removeRepo` when `format === 'repo'` + audit + hub remove | | `plan_test` | dry run against one session host: no execution row, no lease | | `create_match_rule` | unchanged | | `rule_update` | PATCH: `title?`, `planId?`, `alertNameContains?`, `sources?`, `hostnames?`, `groupId?`(s), `enabled?`, `conditions?`, `combinator?`, `chain?` | | `rule_delete` | delete + hub remove | Removed: `read_database_table`, `read_machine_context` (renamed to `machine_read`), and `middleware/src/ai-table-reader.ts`. Shared contracts: - **Cursor**: opaque decimal offset string. Response always carries `total` (matching rows before paging) and `nextCursor` (`null` when the page is the last). - **Errors**: every filter/field/status error enumerates the valid values, e.g. `unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...`, `unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval`. - **Tenancy**: reads filter through `deps.access.rows`; rows the caller cannot see are absent, not redacted. Writes require the row to be visible *and* in the caller's org (`inScope`). `machine_read` and `plan_test` authorize their host through the existing dispatch-time `hosts: [...]` gate (UC-MW-169 unchanged). - **`plan_test` never enqueues**: it calls `compatibility(plan, agent)`, `kill.blocked({planId, host})` and `assertConcreteCommit(plan.commit)` and reports the result. It deliberately does **not** call `decide()`, which acquires a host lease that is only released on a non-execute decision and would leak for a `production` plan. ## Ordered tasks (each with its verifying command) 1. **Plan + claim.** Write this file; post the claim and the plan to issue #85. *Verify*: `test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md`. 2. **Access plumbing.** Add `ControlRowVisibility` (plan/rule/alert/execution/incident) to `control-agent.ts`; wire `accessFor()` + `hubRemove` in `routes/control.ts`. *Verify*: `cd middleware && bunx tsc --noEmit -p tsconfig.json` (or `bun test tests/uc-mw-169-tool-scoping.test.ts` for a real compile). 3. **Typed read tools + delete `read_database_table` + delete the module.** *Verify*: `cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts`. 4. **Typed write tools (`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`. 5. **Rewrite the tests that referenced the removed surface** (uc-136, permissions-audit, uc-169, dashboard control-chat). *Verify*: `cd middleware && bun test` and `cd dashboard && bun run test`. 6. **UC + docs text.** *Verify*: `python3 docs/use-cases/_check.py`. 7. **All gates.** `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'`. 8. **Smoke run** the changed path: a throwaway script that builds the app through `buildApp`, drives `plan_read`/`executions_for_plan`/`plan_test`/`rule_update` directly against PGlite, prints the JSON, and is deleted afterwards. *Verify*: script output + `git status --short` clean of the throwaway. 9. **Report + Done comment.** *Verify*: `test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md`. 10. **Commit after each of tasks 2–6** (`middleware: …`, `docs: …`, `Refs #85`). ## Open questions and the assumption taken 1. *Does `plan_delete` need `hub.publish({entity:'plan',op:'remove'})`?* `ControlDeps` has no hub. **Assumption**: add one optional `hubRemove?: (entity, id) => void` and wire it from `routes/control.ts`; without it the operator console would keep a stale row 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`. 2. *`plan_update` for repo plans.* The HTTP PATCH also pins `commit` for `runBranch` and re-syncs `source`, which needs the git helpers and the plans dir. **Assumption**: the tool patches the same fields `applyPlanPatch` accepts and does **not** change `runBranch`/`source`; the tool description says so and the report records it. The boring fallback is the console. 3. *`rule_update` `groupId`.* Rules store `groupIds: string[]`. **Assumption**: expose `groupIds` (array), matching `rulePatchSchema`. 4. *`incident_read` visibility.* `Visibility` has `rowVisible` but not an incident predicate. **Assumption**: add `incident: (i) => boolean` to `ControlRowVisibility` sourced from `incidentVisible(i, vis)`. 5. *Cursor shape.* **Assumption**: decimal offset string, because the stores are in-memory maps with no stable key order across writes and an offset is honest about that. 6. *`read_machine_context` rename.* UC-MW-169 and UC-MW-151 name the old tool. **Assumption**: rename to `machine_read` as 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 - **Risk: UC checker failures.** The checked-in UC must cite a real symbol in `middleware/src/control-agent.ts` and a test file whose text contains `UC-MW-136`. *Fallback*: keep the test file name and the `UC-MW-136` string in it; cite `controlTools`. - **Risk: assertion-count gate** (`scripts/check-test-tampering.py` counts `expect(`/`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. - **Risk: `bun test` diag in an unrelated suite.** *Fallback*: run the four touched files first, then the full suite; a red unrelated suite is reported, not silenced. - **Risk: no `hubRemove` wiring point accepts the new field.** *Fallback* per open question 1. - **Risk: `Type.Object` on `fields?: string[]` misbehaves in the pi harness.** *Fallback*: declare `Type.Optional(Type.Array(Type.String()))` and validate the values in the handler.
Collaborator

PARTIAL — branch issue/85-typed-object-tools, not pushed.

Commits:

  • 2afb6942 middleware: typed control-chat object tools replace read_database_table
  • 1542a814 test: cover typed control-chat object tools and gate write tools by operator request
  • 3d3e31a docs: UC-MW-136 covers typed control-chat object tools
  • 8907bb3 test: pin plan_read summary totals and cursor to the store size
  • 934bc705 test: use value-pinning matchers in the control object tool tests

Shipped: read_database_table and middleware/src/ai-table-reader.ts deleted; plan_read, rule_read, incident_read, executions_for_plan, execution_read, machine_read replace it with field projection and {total,rowCount,nextCursor,rows} paging; plan_update, plan_delete, plan_test, rule_update, rule_delete added 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 test on 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.ts 6 pass / 5 fail (pre-existing, reproduced at merge base 78b750e in a scratch worktree)
  • whole-suite cd middleware && bun test — NOT OBSERVED, exceeded 300 s in this environment
  • rg -n 'Showing lines' — PASS

Report: /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md. Refs #85

PARTIAL — branch `issue/85-typed-object-tools`, not pushed. Commits: - `2afb6942` middleware: typed control-chat object tools replace read_database_table - `1542a814` test: cover typed control-chat object tools and gate write tools by operator request - `3d3e31a` docs: UC-MW-136 covers typed control-chat object tools - `8907bb3` test: pin plan_read summary totals and cursor to the store size - `934bc705` test: use value-pinning matchers in the control object tool tests Shipped: `read_database_table` and `middleware/src/ai-table-reader.ts` deleted; `plan_read`, `rule_read`, `incident_read`, `executions_for_plan`, `execution_read`, `machine_read` replace it with field projection and `{total,rowCount,nextCursor,rows}` paging; `plan_update`, `plan_delete`, `plan_test`, `rule_update`, `rule_delete` added 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 test` on 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.ts` 6 pass / 5 fail (pre-existing, reproduced at merge base `78b750e` in a scratch worktree) - whole-suite `cd middleware && bun test` — NOT OBSERVED, exceeded 300 s in this environment - `rg -n 'Showing lines'` — PASS Report: `/home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md`. Refs #85
Collaborator

REVIEW: FIXED 3 — findings: (1) ControlScope lost its denial member in 2afb694 while four call sites still use scope.denial, so tsc reported TS2339/TS2353 on control-agent.ts — restored, commit aa1bd25; (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, commit 6369ff0; (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, commit 6369ff0. 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 #85

REVIEW: FIXED 3 — findings: (1) ControlScope lost its denial member in 2afb694 while four call sites still use scope.denial, so tsc reported TS2339/TS2353 on control-agent.ts — restored, commit aa1bd25; (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, commit 6369ff0; (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, commit 6369ff0. 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 #85
Collaborator

FIX 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.

FIX 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.
Collaborator

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

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
Author
Owner

DECISSION: Option B

DECISSION: Option B
Collaborator

STATUS: branch issue/85-typed-object-tools pushed (9 commits: typed object tools + tests + docs + review fixes aa1bd25/6369ff0, compaction attempt 086f5aa reverted in 355a5f3 as 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.

STATUS: branch issue/85-typed-object-tools pushed (9 commits: typed object tools + tests + docs + review fixes aa1bd25/6369ff0, compaction attempt 086f5aa reverted in 355a5f3 as 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.
gotoi-agent changed reference from issue/85-typed-object-tools to issue/85-typed-object-tools 2026-10-08 13:00:04 +02:00
Collaborator

DECISION 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.

DECISION 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.
Collaborator

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.

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.
Collaborator

Issue #85 — Replace read_database_table with typed object tools

Restated goal

read_database_table is a raw table browser: it re-injects immutable plan metadata
(sha256, signature, requirements) on every call, has no schema, no valid-value
enumeration in its errors (unknown filter 'planId'), no pagination contract, and no
object 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 table
reader 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, …), every
filter/field error enumerates the valid values, and plan_read without an id returns
summaries with no sha256/signature/requirements.

Scope

Changed

  • middleware/src/control-agent.ts — delete the read_database_table tool; add the
    typed object tools; update PLAN_MODE_TOOLS and both CONTROL_SYSTEM_PROMPT tool
    strings; declare ControlRowVisibility locally (moved out of the deleted module).
  • middleware/src/routes/control.ts — accessFor() supplies the incident row
    predicate, 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 — the readDatabaseTable honors visibility test becomes a plan_read visibility test through controlTools.
  • 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 and
    JSON summary shape (pretty-print assertions kept).
  • docs/use-cases/middleware/UC-MW-136-ai-read-table.md — rewritten as the typed
    object 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 and
    related-link text only.

Not touched

  • plans/ (product templates), middleware/src/routes/plans.ts and
    middleware/src/routes/rules.ts (read-only reuse of their helpers),
    scripts/agent-pipeline/quarantine.py (its read_table is unrelated),
    middleware/src/ai-table-reader.ts callers other than those listed (none exist).
  • No new files, no new dependencies, no new config knobs. middleware/src/control-agent.ts
    keeps 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 for
no behavioural gain.

  • Title: "Control chat reads and updates objects through typed tools".
  • Implementation: middleware/src/control-agent.ts controlTools (plus
    middleware/src/routes/control.ts register for the access wiring).
  • Tests: middleware/tests/uc-mw-136-ai-read-table.test.ts (kept name; the file
    text 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 the
names of the tools they list.

Tool surface (final)

Reads (allowed in plan mode):

Tool Parameters Notes
plan_read id?, fields?, limit?, cursor? no id → summaries only (no sha256/signature/requirements); with id → that plan
rule_read id?, fields?, limit?, cursor?
executions_for_plan plan_id, status?, limit?, cursor? returns total + nextCursor
execution_read id, fields? drops signature/jobSteps unless asked for
incident_read id?, limit?, cursor?
machine_read hostname supersedes read_machine_context

Writes (blocked in plan mode, operator-request gate where the operator must ask):

Tool Semantics
create_plan unchanged
plan_update PATCH: title?, steps?, enabled?, risk?, verify?, rollback?, groupId?, phrase?, autonomyCeiling?, incidentPolicy?, incidentCreationDelaySeconds?, agentPickingTimeoutSeconds?, approvalTimeoutSeconds?
plan_delete delete + removeRepo when format === 'repo' + audit + hub remove
plan_test dry run against one session host: no execution row, no lease
create_match_rule unchanged
rule_update PATCH: title?, planId?, alertNameContains?, sources?, hostnames?, groupId?(s), enabled?, conditions?, combinator?, chain?
rule_delete delete + hub remove

Removed: read_database_table, read_machine_context (renamed to machine_read),
and middleware/src/ai-table-reader.ts.

Shared contracts:

  • Cursor: opaque decimal offset string. Response always carries total (matching
    rows before paging) and nextCursor (null when the page is the last).
  • Errors: every filter/field/status error enumerates the valid values, e.g.
    unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...,
    unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval.
  • Tenancy: reads filter through deps.access.rows; rows the caller cannot see are
    absent, not redacted. Writes require the row to be visible and in the caller's org
    (inScope). machine_read and plan_test authorize their host through the existing
    dispatch-time hosts: [...] gate (UC-MW-169 unchanged).
  • plan_test never enqueues: it calls compatibility(plan, agent),
    kill.blocked({planId, host}) and assertConcreteCommit(plan.commit) and reports the
    result. It deliberately does not call decide(), which acquires a host lease that
    is only released on a non-execute decision and would leak for a production plan.

Ordered tasks (each with its verifying command)

  1. Plan + claim. Write this file; post the claim and the plan to issue #85.
    Verify: test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md.
  2. Access plumbing. Add ControlRowVisibility (plan/rule/alert/execution/incident)
    to control-agent.ts; wire accessFor() + hubRemove in routes/control.ts.
    Verify: cd middleware && bunx tsc --noEmit -p tsconfig.json (or bun test tests/uc-mw-169-tool-scoping.test.ts for a real compile).
  3. Typed read tools + delete read_database_table + delete the module.
    Verify: cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts.
  4. Typed write tools (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.
  5. Rewrite the tests that referenced the removed surface (uc-136, permissions-audit,
    uc-169, dashboard control-chat). Verify: cd middleware && bun test and
    cd dashboard && bun run test.
  6. UC + docs text. Verify: python3 docs/use-cases/_check.py.
  7. All gates. 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'.
  8. Smoke run the changed path: a throwaway script that builds the app through
    buildApp, drives plan_read/executions_for_plan/plan_test/rule_update
    directly against PGlite, prints the JSON, and is deleted afterwards.
    Verify: script output + git status --short clean of the throwaway.
  9. Report + Done comment. Verify: test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md.
  10. Commit after each of tasks 2–6 (middleware: …, docs: …, Refs #85).

Open questions and the assumption taken

  1. Does plan_delete need hub.publish({entity:'plan',op:'remove'})? ControlDeps
    has no hub. Assumption: add one optional hubRemove?: (entity, id) => void and
    wire it from routes/control.ts; without it the operator console would keep a stale
    row 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.
  2. plan_update for repo plans. The HTTP PATCH also pins commit for runBranch
    and re-syncs source, which needs the git helpers and the plans dir.
    Assumption: the tool patches the same fields applyPlanPatch accepts and does
    not change runBranch/source; the tool description says so and the report
    records it. The boring fallback is the console.
  3. rule_update groupId. Rules store groupIds: string[].
    Assumption: expose groupIds (array), matching rulePatchSchema.
  4. incident_read visibility. Visibility has rowVisible but not an incident
    predicate. Assumption: add incident: (i) => boolean to ControlRowVisibility
    sourced from incidentVisible(i, vis).
  5. Cursor shape. Assumption: decimal offset string, because the stores are
    in-memory maps with no stable key order across writes and an offset is honest about
    that.
  6. read_machine_context rename. UC-MW-169 and UC-MW-151 name the old tool.
    Assumption: rename to machine_read as 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

  • Risk: UC checker failures. The checked-in UC must cite a real symbol in
    middleware/src/control-agent.ts and a test file whose text contains UC-MW-136.
    Fallback: keep the test file name and the UC-MW-136 string in it; cite
    controlTools.
  • Risk: assertion-count gate (scripts/check-test-tampering.py counts
    expect(/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.
  • Risk: bun test diag in an unrelated suite. Fallback: run the four touched
    files first, then the full suite; a red unrelated suite is reported, not silenced.
  • Risk: no hubRemove wiring point accepts the new field. Fallback per open
    question 1.
  • Risk: Type.Object on fields?: string[] misbehaves in the pi harness.
    Fallback: declare Type.Optional(Type.Array(Type.String())) and validate the values
    in 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 tiny
window: 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's chars/4 estimate then
reads ~3051 tokens against a 2048-token contextWindow, so the second chat turn fails
with 422 Assistant request exceeded the context window. Compacting prose did not close
the gap (086f5aa, reverted in 355a5f3) 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)

  • Turn-2 request the fake provider sees: tools 9040 chars, systemPrompt 2978 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.
  • Stripping every tool description in a probe left tools at 7669 chars and still
    422 — prose cannot bridge it.
  • Filtering the tool list down to base's 12 tools (4528 chars) in a probe → 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 the
model 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 in
controlTools().

Why not the operator-request gate the briefing also mentions: the UC-MW-137 suite drives
create_plan / create_match_rule refusal through HTTP and asserts the tool result
text … explicitly asks … (i.e. the model must still be able to call the tool and get
the refusal). An unadvertised tool never reaches requireOperatorRequest — the harness
rejects the call as unknown first — so gating by operatorAsked at the harness would
break uc-mw-137 tests 116/146, which the task forbids ("do not delete or weaken any
test"). The budget gate is also what the briefing's own headline asks for, and it keeps
requireOperatorRequest / blockInPlanMode / inScope exactly as the issue shipped.

Scope

Changed

  • middleware/src/control-agent.ts — add WRITE_TOOLS + fitControlTools(...); call it
    from createControlHarness; §6 Last bump.
  • docs/use-cases/middleware/UC-MW-136-ai-read-table.md — one Story/Notes line + the
    Tests/version housekeeping for the budget gate.
  • docs/use-cases/middleware/UC-MW-115-control-chat.md — only if its text contradicts
    the gate.

Not touched

  • middleware/tests/uc-mw-115-control-chat.test.ts (2048 window stays), the
    CONTROL_SYSTEM_PROMPT prose, 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") is
the subject that gains a clause; UC-MW-115 continues to own the harness/compaction
behaviour. Implementation cite stays middleware/src/control-agent.ts controlTools.

Ordered tasks

  1. Plan addendum + claim comment. Verify: test -f …/issue-85.plan.md.
  2. WRITE_TOOLS + fitControlTools + wire into createControlHarness; §6 Last bump.
    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.
  3. UC text + version. Verify: python3 docs/use-cases/_check.py.
  4. Gates: python3 scripts/check-ai-headers.py, python3 scripts/check-type-mirror.py,
    the four middleware suites above, rg -n 'Showing lines'.
  5. Report + Done comment.

Open questions and assumptions taken

  1. Is "budget-gate" meant as operator-request gating? The briefing's own sentence names
    both. Assumption: budget gating, because the operator-request variant breaks the
    shipped UC-MW-137 refusal tests (evidence above). Recorded in the report.
  2. Gate threshold. Assumption: include the write tools only when
    systemPrompt + serializedTools + RESERVE <= 4 * contextWindow (the faux estimator is
    chars/4; the reserve stands in for the transcript). Constant, no env knob.
  3. The system prompt still lists the 7 names when writes are gated out. Assumption:
    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.
  4. Tiny windows. Assumption: a model that cannot hold the read surface either is out of
    scope — this change only removes the write tier.

Risk and the boring fallback

  • Risk: dropping writes hides create_plan from 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.
  • Risk: some other suite builds a harness with a small window and asserts a write tool.
    Fallback: grep for contextWindow in tests before committing; if one exists, gate only
    above the smallest such window.
  • Risk: UC checker on the edited UC. Fallback: keep cite/symbol/test unchanged, bump
    version only.
# Issue #85 — Replace `read_database_table` with typed object tools ## Restated goal `read_database_table` is a raw table browser: it re-injects immutable plan metadata (`sha256`, `signature`, `requirements`) on every call, has no schema, no valid-value enumeration in its errors (`unknown filter 'planId'`), no pagination contract, and no object 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 table reader 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`, …), every filter/field error enumerates the valid values, and `plan_read` without an id returns summaries with no `sha256`/`signature`/`requirements`. ## Scope **Changed** - `middleware/src/control-agent.ts` — delete the `read_database_table` tool; add the typed object tools; update `PLAN_MODE_TOOLS` and both `CONTROL_SYSTEM_PROMPT` tool strings; declare `ControlRowVisibility` locally (moved out of the deleted module). - `middleware/src/routes/control.ts` — `accessFor()` supplies the incident row predicate, 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` — the `readDatabaseTable honors visibility` test becomes a `plan_read` visibility test through `controlTools`. - `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 and JSON summary shape (pretty-print assertions kept). - `docs/use-cases/middleware/UC-MW-136-ai-read-table.md` — rewritten as the typed object 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 and related-link text only. **Not touched** - `plans/` (product templates), `middleware/src/routes/plans.ts` and `middleware/src/routes/rules.ts` (read-only reuse of their helpers), `scripts/agent-pipeline/quarantine.py` (its `read_table` is unrelated), `middleware/src/ai-table-reader.ts` callers other than those listed (none exist). - No new files, no new dependencies, no new config knobs. `middleware/src/control-agent.ts` keeps 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 for no behavioural gain. - Title: "Control chat reads and updates objects through typed tools". - `Implementation`: `middleware/src/control-agent.ts` `controlTools` (plus `middleware/src/routes/control.ts` `register` for the access wiring). - `Tests`: `middleware/tests/uc-mw-136-ai-read-table.test.ts` (kept name; the file text 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 the names of the tools they list. ## Tool surface (final) Reads (allowed in plan mode): | Tool | Parameters | Notes | |---|---|---| | `plan_read` | `id?`, `fields?`, `limit?`, `cursor?` | no `id` → summaries only (no `sha256`/`signature`/`requirements`); with `id` → that plan | | `rule_read` | `id?`, `fields?`, `limit?`, `cursor?` | | | `executions_for_plan` | `plan_id`, `status?`, `limit?`, `cursor?` | returns `total` + `nextCursor` | | `execution_read` | `id`, `fields?` | drops `signature`/`jobSteps` unless asked for | | `incident_read` | `id?`, `limit?`, `cursor?` | | | `machine_read` | `hostname` | supersedes `read_machine_context` | Writes (blocked in plan mode, operator-request gate where the operator must ask): | Tool | Semantics | |---|---| | `create_plan` | unchanged | | `plan_update` | PATCH: `title?`, `steps?`, `enabled?`, `risk?`, `verify?`, `rollback?`, `groupId?`, `phrase?`, `autonomyCeiling?`, `incidentPolicy?`, `incidentCreationDelaySeconds?`, `agentPickingTimeoutSeconds?`, `approvalTimeoutSeconds?` | | `plan_delete` | delete + `removeRepo` when `format === 'repo'` + audit + hub remove | | `plan_test` | dry run against one session host: no execution row, no lease | | `create_match_rule` | unchanged | | `rule_update` | PATCH: `title?`, `planId?`, `alertNameContains?`, `sources?`, `hostnames?`, `groupId?`(s), `enabled?`, `conditions?`, `combinator?`, `chain?` | | `rule_delete` | delete + hub remove | Removed: `read_database_table`, `read_machine_context` (renamed to `machine_read`), and `middleware/src/ai-table-reader.ts`. Shared contracts: - **Cursor**: opaque decimal offset string. Response always carries `total` (matching rows before paging) and `nextCursor` (`null` when the page is the last). - **Errors**: every filter/field/status error enumerates the valid values, e.g. `unknown field 'shah'; valid fields: id, title, steps, enabled, risk, ...`, `unknown status 'ok'; valid statuses: enqueued, pending, started, succeeded, failed, timeout, cancelled, awaiting_approval`. - **Tenancy**: reads filter through `deps.access.rows`; rows the caller cannot see are absent, not redacted. Writes require the row to be visible *and* in the caller's org (`inScope`). `machine_read` and `plan_test` authorize their host through the existing dispatch-time `hosts: [...]` gate (UC-MW-169 unchanged). - **`plan_test` never enqueues**: it calls `compatibility(plan, agent)`, `kill.blocked({planId, host})` and `assertConcreteCommit(plan.commit)` and reports the result. It deliberately does **not** call `decide()`, which acquires a host lease that is only released on a non-execute decision and would leak for a `production` plan. ## Ordered tasks (each with its verifying command) 1. **Plan + claim.** Write this file; post the claim and the plan to issue #85. *Verify*: `test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.plan.md`. 2. **Access plumbing.** Add `ControlRowVisibility` (plan/rule/alert/execution/incident) to `control-agent.ts`; wire `accessFor()` + `hubRemove` in `routes/control.ts`. *Verify*: `cd middleware && bunx tsc --noEmit -p tsconfig.json` (or `bun test tests/uc-mw-169-tool-scoping.test.ts` for a real compile). 3. **Typed read tools + delete `read_database_table` + delete the module.** *Verify*: `cd middleware && bun test tests/uc-mw-136-ai-read-table.test.ts`. 4. **Typed write tools (`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`. 5. **Rewrite the tests that referenced the removed surface** (uc-136, permissions-audit, uc-169, dashboard control-chat). *Verify*: `cd middleware && bun test` and `cd dashboard && bun run test`. 6. **UC + docs text.** *Verify*: `python3 docs/use-cases/_check.py`. 7. **All gates.** `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'`. 8. **Smoke run** the changed path: a throwaway script that builds the app through `buildApp`, drives `plan_read`/`executions_for_plan`/`plan_test`/`rule_update` directly against PGlite, prints the JSON, and is deleted afterwards. *Verify*: script output + `git status --short` clean of the throwaway. 9. **Report + Done comment.** *Verify*: `test -f /home/eduardez/workspace/gotoi-wt/meta/issue-85.report.md`. 10. **Commit after each of tasks 2–6** (`middleware: …`, `docs: …`, `Refs #85`). ## Open questions and the assumption taken 1. *Does `plan_delete` need `hub.publish({entity:'plan',op:'remove'})`?* `ControlDeps` has no hub. **Assumption**: add one optional `hubRemove?: (entity, id) => void` and wire it from `routes/control.ts`; without it the operator console would keep a stale row 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`. 2. *`plan_update` for repo plans.* The HTTP PATCH also pins `commit` for `runBranch` and re-syncs `source`, which needs the git helpers and the plans dir. **Assumption**: the tool patches the same fields `applyPlanPatch` accepts and does **not** change `runBranch`/`source`; the tool description says so and the report records it. The boring fallback is the console. 3. *`rule_update` `groupId`.* Rules store `groupIds: string[]`. **Assumption**: expose `groupIds` (array), matching `rulePatchSchema`. 4. *`incident_read` visibility.* `Visibility` has `rowVisible` but not an incident predicate. **Assumption**: add `incident: (i) => boolean` to `ControlRowVisibility` sourced from `incidentVisible(i, vis)`. 5. *Cursor shape.* **Assumption**: decimal offset string, because the stores are in-memory maps with no stable key order across writes and an offset is honest about that. 6. *`read_machine_context` rename.* UC-MW-169 and UC-MW-151 name the old tool. **Assumption**: rename to `machine_read` as 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 - **Risk: UC checker failures.** The checked-in UC must cite a real symbol in `middleware/src/control-agent.ts` and a test file whose text contains `UC-MW-136`. *Fallback*: keep the test file name and the `UC-MW-136` string in it; cite `controlTools`. - **Risk: assertion-count gate** (`scripts/check-test-tampering.py` counts `expect(`/`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. - **Risk: `bun test` diag in an unrelated suite.** *Fallback*: run the four touched files first, then the full suite; a red unrelated suite is reported, not silenced. - **Risk: no `hubRemove` wiring point accepts the new field.** *Fallback* per open question 1. - **Risk: `Type.Object` on `fields?: string[]` misbehaves in the pi harness.** *Fallback*: declare `Type.Optional(Type.Array(Type.String()))` and validate the values in 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 tiny window: 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's `chars/4` estimate then reads ~3051 tokens against a 2048-token `contextWindow`, so the second chat turn fails with 422 `Assistant request exceeded the context window`. Compacting prose did not close the gap (`086f5aa`, reverted in `355a5f3`) 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) - Turn-2 request the fake provider sees: `tools` 9040 chars, `systemPrompt` 2978 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. - Stripping **every** tool description in a probe left `tools` at 7669 chars and still 422 — prose cannot bridge it. - Filtering the tool list down to base's 12 tools (4528 chars) in a probe → `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 the model 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** in `controlTools()`. Why not the operator-request gate the briefing also mentions: the UC-MW-137 suite drives `create_plan` / `create_match_rule` refusal through HTTP and asserts the tool *result* text `… explicitly asks …` (i.e. the model must still be able to call the tool and get the refusal). An unadvertised tool never reaches `requireOperatorRequest` — the harness rejects the call as unknown first — so gating by `operatorAsked` at the harness would break `uc-mw-137` tests 116/146, which the task forbids ("do not delete or weaken any test"). The budget gate is also what the briefing's own headline asks for, and it keeps `requireOperatorRequest` / `blockInPlanMode` / `inScope` exactly as the issue shipped. ## Scope **Changed** - `middleware/src/control-agent.ts` — add `WRITE_TOOLS` + `fitControlTools(...)`; call it from `createControlHarness`; §6 `Last` bump. - `docs/use-cases/middleware/UC-MW-136-ai-read-table.md` — one Story/Notes line + the `Tests`/`version` housekeeping for the budget gate. - `docs/use-cases/middleware/UC-MW-115-control-chat.md` — only if its text contradicts the gate. **Not touched** - `middleware/tests/uc-mw-115-control-chat.test.ts` (2048 window stays), the `CONTROL_SYSTEM_PROMPT` prose, 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") is the subject that gains a clause; `UC-MW-115` continues to own the harness/compaction behaviour. Implementation cite stays `middleware/src/control-agent.ts` `controlTools`. ## Ordered tasks 1. Plan addendum + claim comment. *Verify*: `test -f …/issue-85.plan.md`. 2. `WRITE_TOOLS` + `fitControlTools` + wire into `createControlHarness`; §6 `Last` bump. *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`. 3. UC text + version. *Verify*: `python3 docs/use-cases/_check.py`. 4. Gates: `python3 scripts/check-ai-headers.py`, `python3 scripts/check-type-mirror.py`, the four middleware suites above, `rg -n 'Showing lines'`. 5. Report + Done comment. ## Open questions and assumptions taken 1. *Is "budget-gate" meant as operator-request gating?* The briefing's own sentence names both. Assumption: budget gating, because the operator-request variant breaks the shipped UC-MW-137 refusal tests (evidence above). Recorded in the report. 2. *Gate threshold.* Assumption: include the write tools only when `systemPrompt + serializedTools + RESERVE <= 4 * contextWindow` (the faux estimator is chars/4; the reserve stands in for the transcript). Constant, no env knob. 3. *The system prompt still lists the 7 names when writes are gated out.* Assumption: 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. 4. *Tiny windows.* Assumption: a model that cannot hold the read surface either is out of scope — this change only removes the *write* tier. ## Risk and the boring fallback - **Risk: dropping writes hides `create_plan` from 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. - **Risk: some other suite builds a harness with a small window and asserts a write tool.** Fallback: grep for `contextWindow` in tests before committing; if one exists, gate only above the smallest such window. - **Risk: UC checker on the edited UC.** Fallback: keep cite/symbol/test unchanged, bump `version` only.
Collaborator

DONE — budget gate shipped; UC-MW-115 is 12/12.

Commit: aabfcbd middleware: 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:

  • tests/uc-mw-115-control-chat.test.ts: 12 pass / 0 fail (was 11/1)
  • uc-136 + uc-137 + uc-169 + uc-mw-control-hardening + uc-mw-plan-chat: 49 pass / 0 fail
  • docs/use-cases/_check.py: 359 conform; check-ai-headers.py: 576 ok; check-type-mirror.py: exit 0
  • rg 'Showing lines': only the rule's own mentions
  • uc-mw-permissions-audit: 5 pass / 6 fail (pre-existing, order-dependent; the 6th passes in isolation) — unchanged by this diff
  • whole middleware suite, dashboard, agent: not run (diff touches control-agent.ts only; those suites do not exercise it)

Known limitation (in the report): the system prompt still names the write tools when they are gated out on a tiny window.

DONE — budget gate shipped; UC-MW-115 is 12/12. Commit: aabfcbd middleware: 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: - tests/uc-mw-115-control-chat.test.ts: 12 pass / 0 fail (was 11/1) - uc-136 + uc-137 + uc-169 + uc-mw-control-hardening + uc-mw-plan-chat: 49 pass / 0 fail - docs/use-cases/_check.py: 359 conform; check-ai-headers.py: 576 ok; check-type-mirror.py: exit 0 - rg 'Showing lines': only the rule's own mentions - uc-mw-permissions-audit: 5 pass / 6 fail (pre-existing, order-dependent; the 6th passes in isolation) — unchanged by this diff - whole middleware suite, dashboard, agent: not run (diff touches control-agent.ts only; those suites do not exercise it) Known limitation (in the report): the system prompt still names the write tools when they are gated out on a tiny window.
Collaborator

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 (fixed 24623ed: 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 stays

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 (fixed 24623ed: 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 stays
Collaborator

DONE — 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):

  • docs/use-cases/_check.py PASS (359 conform); check-ai-headers.py PASS (576 ok); check-type-mirror.py exit 0
  • middleware full suite: 601/602 — UC-MW-115 12/12 (422 fixed), UC-MW-19 PASS this run; sole red UC-MW-81-84 seed test passes isolated (14s, contention artifact — hidden set ran concurrently, rerun green)
  • hidden-tests.sh (authoritative set): PASS isolated after re-run (first run overlapped full suite: timeout artifact, baseline agent-develop also red under same contention, green isolated)
  • dashboard: vitest 254/254, svelte-check 0 errors / 7 pre-existing warnings
  • agent go test: 4-6 FAILs (TestUCAG50*, TestUCAG49*, TestAgentBinaryIntegration) — environmental (/etc/gotoi/agent.json permission denied on this host), identical at agent-develop baseline
  • rg 'Showing lines': only rule mentions

PR: #94 — Refs #85

DONE — 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): - docs/use-cases/_check.py PASS (359 conform); check-ai-headers.py PASS (576 ok); check-type-mirror.py exit 0 - middleware full suite: 601/602 — UC-MW-115 12/12 (422 fixed), UC-MW-19 PASS this run; sole red UC-MW-81-84 seed test passes isolated (14s, contention artifact — hidden set ran concurrently, rerun green) - hidden-tests.sh (authoritative set): PASS isolated after re-run (first run overlapped full suite: timeout artifact, baseline agent-develop also red under same contention, green isolated) - dashboard: vitest 254/254, svelte-check 0 errors / 7 pre-existing warnings - agent go test: 4-6 FAILs (TestUCAG50*, TestUCAG49*, TestAgentBinaryIntegration) — environmental (/etc/gotoi/agent.json permission denied on this host), identical at agent-develop baseline - rg 'Showing lines': only rule mentions PR: https://git.eduard3v.com/EduarD3V/gotoi/pulls/94 — Refs #85
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
EduarD3V/gotoi#85
No description provided.