CRITICAL: chat-originated tool dispatch (Python local_tool_runtime.py) had ZERO mode awareness — a chat write_file in CHAT mode executed ungated (the Go serve surface gated but the chat path never touched it). Closed: mode authorization at request intake in local_tool_ runtime, authorize seam in tool_protocol dispatch, approve() resume door gated, hatch-exposure filtered. Typed ModeRefusal feeds the model as tool-error data; failure breaker trips on repeat. T1: ONE mode matrix artifact (schemas/mode/v1/mode_matrix.json) read by BOTH Go (embed, byte-identical, 16-case battery) and Python (mode_gate.py, per-dispatch read 0.011ms, RUNE_MODE env wins). T2: model only SEES mode-allowed tools at capability selection. T3: per-dispatch mode re-check (mid-loop switch pinned). T4: tool events persist as channel-scoped role=tool conversation records — results land in the chat lane through the bridge poll. Main-session verify: mode dispatch gate + tool authority suites green own runs. 31/31 + 3438/0 new-mode-gating failures.
258 lines
11 KiB
Python
258 lines
11 KiB
Python
"""Admission dispatch for wave wave-tools-live-build: B-TOOLS-LIVE —
|
||
the tool loop wired through chat, gated twice, made visible (T1–T4 + the
|
||
two-door bypass fix).
|
||
|
||
classify + activate + admit (the headless scaffolding entry law,
|
||
tests/test_construction_admission.py pattern), admitted through the
|
||
research gate on the chain research/factory-tools-live/07-qualify.json
|
||
(verdict PROCEED — the chain's terminal QUALIFY artifact).
|
||
|
||
The critical defect first (adversarial A1, red-first): the chat tool
|
||
dispatch (Python local_tool_runtime) had ZERO mode awareness while the Go
|
||
serve surface gates — a chat-originated write_file in CHAT executed
|
||
ungated. Artifacts this wave admits:
|
||
|
||
- T1 ONE matrix artifact: schemas/mode/v1/mode_matrix.json (mode -> tool
|
||
classes -> concrete executor tools, honest 501s for pending classes),
|
||
read by BOTH doors — the Go authority embeds the byte-identical copy
|
||
(scripts/gen_mode_matrix.py + --check drift gate) and derives its
|
||
frozen profiles from it; Python reads the canonical file.
|
||
- T3 dispatch gate: tool_protocol.dispatch_validated_tool gained the
|
||
authorize seam; LocalToolRuntime gates at request intake (before the
|
||
approval branch), on the approval/resume door (approve()), and the
|
||
hatch exposure filter (A4 half). Typed ModeRefusal rides the tool lane
|
||
as data; mode read per-dispatch (A3 mid-loop switch). src/rune/
|
||
mode_gate.py mirrors the Go readModeState law (absent/corrupt/unknown
|
||
-> CHAT; RUNE_MODE env wins).
|
||
- T2 SEE gate: operator._capability_selection intersects the broker
|
||
selection (and the full-toolset degrade) with the active mode's
|
||
allowed set — the model never sees unreachable tools.
|
||
- T4 tool transcript: operator persists each tool_event as a
|
||
channel-scoped role="tool" conversation record (bounded render,
|
||
deterministic idempotent msg_ids; the bridge poll renders it with zero
|
||
new machinery) and the runtime preserves pre-tool narration into the
|
||
user_text assembly.
|
||
|
||
Red-first battery: tests/test_mode_dispatch_gate.py (28 RED against the
|
||
unfixed code — the bypass proven — then green).
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||
|
||
from rune import construction_activation as cact # noqa: E402
|
||
from rune import construction_admission as cadm # noqa: E402
|
||
from rune import construction_mode as cmode # noqa: E402
|
||
from rune import scaffold as sc # noqa: E402
|
||
|
||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||
|
||
SLUG = "wave-tools-live-build"
|
||
RESEARCH_SLUG = "factory-tools-live"
|
||
|
||
OBJECTIVE = (
|
||
"B-TOOLS-LIVE T1-T4 + the two-door bypass fix. CRITICAL first: chat "
|
||
"tool dispatch (Python local_tool_runtime) had ZERO mode awareness "
|
||
"while the Go serve surface gates — a chat-originated write_file in "
|
||
"CHAT executed ungated (adversarial A1). T1: the frozen CHAT/TASK/"
|
||
"PLAN matrix moves to ONE JSON artifact schemas/mode/v1/"
|
||
"mode_matrix.json (mode classes -> concrete executor tools; "
|
||
"read->{the 13 read-family executor tools}, shell->{run_command}, "
|
||
"build/edit->{write_file,edit_file,format_code,run_tests,"
|
||
"git_commit}, pending classes honestly 501), read by BOTH doors — "
|
||
"toolexec embeds "
|
||
"the byte-identical copy (scripts/gen_mode_matrix.py, --check drift "
|
||
"gate) and generates its profiles from it; Python reads the "
|
||
"canonical file through the new src/rune/mode_gate.py. T3: "
|
||
"tool_protocol.dispatch_validated_tool authorizes against the matrix "
|
||
"BEFORE execute (authorize seam; every dispatch incl. "
|
||
"find_capability, the approval prepare path, and the approve/resume "
|
||
"door); LocalToolRuntime gates at request intake before the approval "
|
||
"branch; typed ModeRefusal result feeds the model as tool-error "
|
||
"data and the loop records a tool_event; mode read per-dispatch (A3 "
|
||
"mid-loop switch); active-mode law mirrors readModeState "
|
||
"(absent/corrupt/unknown -> CHAT default, RUNE_MODE env wins); the "
|
||
"find_capability hatch exposure is mode-filtered (A4 half). T2: "
|
||
"operator._capability_selection intersects the broker selection "
|
||
"AND the full-toolset degrade with the active mode's allowed set — "
|
||
"the model never SEES unreachable tools. T4: the operator persists "
|
||
"each tool_event as a channel-scoped role='tool' conversation record "
|
||
"(bounded one-line render, deterministic idempotent msg_ids so "
|
||
"resume replays no-op) — the landed bridge poll renders results in "
|
||
"the chat lane with zero new machinery — and the runtime preserves "
|
||
"pre-tool narration into the final user_text assembly (gap 6). "
|
||
"Red-first frozen battery tests/test_mode_dispatch_gate.py: 28 RED "
|
||
"against unfixed code (the bypass proven: write_file executed / "
|
||
"approval offered in CHAT), then green after the gates."
|
||
)
|
||
AFFECTED = [
|
||
"schemas/mode/v1/mode_matrix.json",
|
||
"scripts/gen_mode_matrix.py",
|
||
"scripts/admit_wave_tools_live_build.py",
|
||
"src/rune/mode_gate.py",
|
||
"src/rune/tool_protocol.py",
|
||
"src/rune/local_tool_runtime.py",
|
||
"src/rune/rune_operator.py",
|
||
"src/rune/canary_p1_01.py",
|
||
"src/go/tool-executor/internal/toolexec/modegate.go",
|
||
"src/go/tool-executor/internal/toolexec/mode_matrix.json",
|
||
"tests/test_mode_dispatch_gate.py",
|
||
"tests/test_local_tool_runtime.py",
|
||
"tests/test_p1_01_capability_authority.py",
|
||
"tests/test_tool_authority.py",
|
||
"tests/test_orchestration_conversation_state.py",
|
||
"tests/test_p0_05_acceptance.py",
|
||
"docs/factory/domains.json",
|
||
]
|
||
|
||
EXPECTED_NEW = [
|
||
"schemas/mode/v1/mode_matrix.json",
|
||
"scripts/gen_mode_matrix.py",
|
||
"scripts/admit_wave_tools_live_build.py",
|
||
"src/rune/mode_gate.py",
|
||
"src/go/tool-executor/internal/toolexec/mode_matrix.json",
|
||
"tests/test_mode_dispatch_gate.py",
|
||
]
|
||
|
||
|
||
def record_research_gate(marker: str) -> int:
|
||
"""Post-mint: pin the gate evidence on the contract's ext.research_gate
|
||
record so the contract resolves through the tools-live chain, never
|
||
through a registry glob. Idempotent."""
|
||
store_dir = os.path.join(ROOT, "store", "scaffold")
|
||
scaffold = sc.Scaffold(store_dir, "rune")
|
||
doc = scaffold.load()
|
||
c = scaffold.contract(doc, f"ADMIT-{SLUG}")
|
||
if c is None:
|
||
print("contract ADMIT-<slug> not found — nothing to record")
|
||
return 1
|
||
ext = c.setdefault("ext", {})
|
||
rg = ext.get("research_gate") or {}
|
||
rg.update({
|
||
"chain": f"research/{RESEARCH_SLUG}/07-qualify.json",
|
||
"verdict": "PROCEED",
|
||
"marker": marker,
|
||
})
|
||
ext["research_gate"] = rg
|
||
scaffold.commit(doc, what=f"record research_gate evidence for {SLUG}")
|
||
print("research_gate record committed on ADMIT-" + SLUG)
|
||
return 0
|
||
|
||
|
||
def main() -> int:
|
||
gate = subprocess.run(
|
||
[sys.executable, os.path.join("scripts", "check_research_gate.py"),
|
||
"admit", "--slug", SLUG, "--objective", OBJECTIVE,
|
||
"--research-slug", RESEARCH_SLUG],
|
||
capture_output=True, text=True, cwd=ROOT,
|
||
)
|
||
print(gate.stdout.strip())
|
||
if gate.returncode != 0:
|
||
print("research gate REFUSED — the law holds")
|
||
return 1
|
||
marker = ""
|
||
for line in gate.stdout.splitlines():
|
||
if line.startswith("research-gate:ALLOW"):
|
||
marker = line.strip()
|
||
break
|
||
if not marker:
|
||
print("no ALLOW marker in gate output — refusing to mint unrecorded")
|
||
return 1
|
||
|
||
store = os.path.join(ROOT, "store", "scaffold")
|
||
scaffold = sc.Scaffold(store, "rune")
|
||
|
||
md = cmode.classify_mode(
|
||
OBJECTIVE,
|
||
target_project="rune",
|
||
project_exists=True,
|
||
affected_surface=AFFECTED,
|
||
)
|
||
print("classified mode:", md.mode)
|
||
|
||
need = {
|
||
"need_id": f"need-{SLUG}",
|
||
"objective": OBJECTIVE,
|
||
"origin": "worker",
|
||
"project": "rune",
|
||
"task_ref": SLUG,
|
||
"affected_surface": AFFECTED,
|
||
"evidence_refs": [
|
||
marker,
|
||
"research/factory-tools-live/07-qualify.json",
|
||
"research/factory-tools-live/06-synthesis.json",
|
||
"research/factory-tools-live/05-adversarial.json",
|
||
],
|
||
}
|
||
act = cact.activate(md, need=need, scaffold=scaffold,
|
||
builder=SLUG)
|
||
print("activated:", act.get("activated"), act.get("mode"))
|
||
|
||
env = cadm.admit(
|
||
act,
|
||
objective=OBJECTIVE,
|
||
builder=SLUG,
|
||
affected_surface=AFFECTED,
|
||
evidence_refs=need["evidence_refs"],
|
||
origin="worker",
|
||
task_ref=SLUG,
|
||
expected_new_entities=EXPECTED_NEW,
|
||
expected_runtime_effects=[
|
||
"a chat-originated write_file in CHAT (default state) is "
|
||
"refused at the dispatch boundary with a typed mode_refusal "
|
||
"result; the executor is never invoked; the refusal rides "
|
||
"the tool lane and the loop continues (A1 closed)",
|
||
"an approval prepared under TASK does not execute after a "
|
||
"switch to CHAT — approve() refuses through the same matrix "
|
||
"(the resume door gated)",
|
||
"a mid-loop mode switch changes the next dispatch's verdict "
|
||
"(mode read per-dispatch, A3)",
|
||
"the model's tool set in CHAT contains only read-family "
|
||
"tools (broker selection and the full-toolset degrade both "
|
||
"intersected); find_capability cannot widen the seen set "
|
||
"past the mode (A4 half)",
|
||
"tool events land as role='tool' conversation records in "
|
||
"the channel (bounded one-line render) so the bridge poll "
|
||
"shows results in the chat lane; pre-tool narration "
|
||
"survives into the final user_text",
|
||
"the Go authority battery stays green byte-identical from "
|
||
"the ONE artifact (embedded copy drift-gated by "
|
||
"scripts/gen_mode_matrix.py --check)",
|
||
],
|
||
verification_obligations=[
|
||
"pytest tests/test_mode_dispatch_gate.py green (31 cases: "
|
||
"matrix parity, mode-state law, A1/A3/A4 dispatch gates, "
|
||
"SEE gate, tool transcript)",
|
||
"pytest tests/test_local_tool_runtime.py "
|
||
"tests/test_tool_protocol.py "
|
||
"tests/test_p1_01_capability_authority.py "
|
||
"tests/test_tool_authority.py "
|
||
"tests/test_orchestration_conversation_state.py green "
|
||
"(migrated batteries pin TASK explicitly)",
|
||
"python3 src/rune/canary_p0_05.py + "
|
||
"python3 src/rune/canary_p1_01.py print their PASSED lines",
|
||
"go test ./... green in src/go/tool-executor and "
|
||
"src/go/rune-entry (frozen 16-case mode battery + serve "
|
||
"surface byte-identical from the artifact)",
|
||
"python scripts/gen_mode_matrix.py --check exits 0 "
|
||
"(embedded copy byte-identical)",
|
||
],
|
||
approval_ref="user:wave-tools-live-build dispatch",
|
||
contract_id=f"ADMIT-{SLUG}",
|
||
project_store=store,
|
||
project_id="rune",
|
||
)
|
||
print(json.dumps(env, indent=2, default=str)[:2400])
|
||
rc = 0 if env.get("success") else 1
|
||
if record_research_gate(marker) != 0:
|
||
return 1
|
||
return rc
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|