Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
945a153
feat: add allowlisted provider-neutral agent tool adapter
lostlight530 Oct 19, 2026
fef4e44
feat: add JSON, terminal and offline HTML tool console
lostlight530 Oct 19, 2026
bbf3206
test: cover tool registry, agent dispatch, CLI and offline UI
lostlight530 Oct 19, 2026
21ad08a
docs: publish Tool, CLI, Agent API and offline console usage contract
lostlight530 Oct 19, 2026
d746f77
docs: align figure tools console, CLI UI and agent contract
lostlight530 Oct 19, 2026
70193ee
docs: align figure tools console, CLI UI and agent contract
lostlight530 Oct 19, 2026
dd7a2c0
contract: include tool adapter in active manifest and manual checks
lostlight530 Oct 19, 2026
84cdefb
contract: include tool adapter in active manifest and manual checks
lostlight530 Oct 19, 2026
e4de0dc
ci: add isolated optional PR smoke check for Tool CLI Agent integration
lostlight530 Oct 19, 2026
30e6ef4
ci: extend tool integration verification through native make test
lostlight530 Oct 19, 2026
3548cae
feat: add bounded approval-first multi-tool agent plan executor
lostlight530 Oct 19, 2026
abc4492
feat: CLI plan preview and explicit approval execution with UI status
lostlight530 Oct 19, 2026
267d3ab
fix: reject nonstring plan error policies without exceptions
lostlight530 Oct 19, 2026
33bc339
test: enforce plan approval, bounded steps and fail-fast semantics
lostlight530 Oct 19, 2026
bd5e539
fix: preserve structured Tool error for invalid figure recipe schema
lostlight530 Oct 19, 2026
6bbfb71
test: ensure invalid recipe keeps CLI JSON error and exit code
lostlight530 Oct 19, 2026
33a138c
test: include bounded agent plan suite
lostlight530 Oct 19, 2026
761ad4e
ci: execute agent approval harness regression tests alongside tool CLI
lostlight530 Oct 19, 2026
eac82ce
docs: show approval-first Agent plan invocation and UI status
lostlight530 Oct 19, 2026
40e05e1
feat(ui): preserve offline three-repository receipt viewer with nativ…
lostlight530 Oct 19, 2026
3f79a1d
test/docs(ui): verify and document offline console receipt contracts
lostlight530 Oct 19, 2026
bf58c6a
test/docs(ui): verify and document offline console receipt contracts
lostlight530 Oct 19, 2026
4cb712c
integrate: reconcile offline receipt UI, executable examples and auth…
lostlight530 Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/tool-cli-agent-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Tool CLI Agent smoke (optional)
on:
pull_request:
paths:
- agent_tools.py
- tool_cli.py
- tests/test_tool_cli.py
- agent_plan.py
- tests/test_agent_plan.py
- .github/workflows/tool-cli-agent-smoke.yml
workflow_dispatch:

permissions:
contents: read

jobs:
local-tool-contract:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout exact PR revision
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install bounded smoke dependencies
run: python -m pip install --disable-pip-version-check "PyYAML==6.0.3" "jsonschema==4.26.0" "numpy==2.3.5" "matplotlib==3.10.8"
- name: Compile tool and test modules
run: python -m compileall -q agent_tools.py agent_plan.py tool_cli.py tests/test_tool_cli.py tests/test_agent_plan.py
- name: Test registry, CLI, UI and native tool calls
run: python -m unittest tests.test_tool_cli -v
- name: Run complete repository manual test suite
run: make test
- name: Generate JSON and offline HTML catalog
run: |
python tool_cli.py list > /tmp/tools.json
python tool_cli.py list --format html --output /tmp/tools.html
python -c "import json; d=json.load(open('/tmp/tools.json')); assert len(d['result']['tools']) == 2"
test -s /tmp/tools.html
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ Metaphorical filenames are not scientific capability evidence. Experimental/impo

Render manifests, figure evidence, communication transfers, accessibility sidecars, maintenance reports, demonstrations, and canonical hash baselines never count as independent reproduction. R3 requires an actual separate rerun plus a declared comparison criterion.

## Optional Tool / CLI / Agent adapter

`agent_tools.py` provides an explicit local allowlist, argument contract and typed response envelope; `tool_cli.py` owns presentation in JSON, TTY and static offline HTML. [TOOL_CLI_AGENT.md](docs/02-examples-and-contracts/TOOL_CLI_AGENT.md) documents invocation, errors, UI and limits. It neither replaces `sci_render.py` nor supplies an MCP server, external model client, arbitrary shell or autonomous approvals.

A completed tool call does not imply figure-render success, publisher acceptance, accessible publication or research truth. Additions require explicit implementation, validation and documentation.

## Delivery boundary

For a confirmed maintenance repair:
Expand Down
31 changes: 31 additions & 0 deletions MANIFEST.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ recipes:
- bootstrap-interval
- heuristic-bound

tool_adapter:
profile: sci-render-kit/tool-adapter
registry: agent_tools.py
cli: tool_cli.py
test: tests/test_tool_cli.py
contract: docs/02-examples-and-contracts/TOOL_CLI_AGENT.md
tools: [figure_audit, communication_transfer]
argument_schema: JSON_Schema_object_allowlist
transport: local_synchronous_function_call
presentations: [json, text_tty, offline_html]
automatic_remote_model_call: false
arbitrary_shell_execution: false
automatic_write_to_repository: false
mcp_server_conformance_claim: false
scientific_validity_claim: false

backends:
matplotlib:
adapter: backends/matplotlib_adapter.py
Expand Down Expand Up @@ -451,3 +467,18 @@ scientific_boundaries:
# no A→H synthesis artifact is inferred or manufactured by this annotation.
# This is routing metadata only: calibrated=2026-09-22 is preserved, and no runtime capability,
# machine-contract semantics, scientific validation, reproduction status, or release authority is changed.

# Offline read-only presentation of three repository local receipts (2026-10-19)
offline_evidence_console:
owner: sci-render-kit/presentation
path: ui/tri_repo_console.html
style_contract: docs/02-examples-and-contracts/TOOL_UI_STYLE.md
tests: tests/test_tool_ui.py
accepted_primary_profile_suffix: tool-adapter
accepted_legacy_suffixes: [tool-execution, agent-plan, tool-catalog]
accepted_repository_prefixes: [auto-doc-engine, epistemic-pipeline, sci-render-kit]
local_json_import_only: true
invokes_tools: false
network_requests: false
browser_visual_accessibility_qa: NOT_EXECUTED
scientific_validity_claim: false
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help clean test test-core test-accessibility
.PHONY: help clean test test-core test-accessibility test-tools

help:
@echo "sci-render-kit manual maintenance commands"
Expand All @@ -12,10 +12,17 @@ clean:
rm -rf __pycache__ .pytest_cache output/*
@echo "Cleaned local caches and generated artifacts"

test: test-core test-accessibility
test: test-core test-accessibility test-tools test-tool-ui

test-core:
python3 tests/test_all.py

test-accessibility:
python3 -m unittest tests.test_accessibility -v

test-tools:
python3 -m unittest tests.test_tool_cli tests.test_agent_plan -v

.PHONY: test-tool-ui
test-tool-ui:
python3 -m unittest tests.test_tool_ui -v
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ Project identifiers are unversioned

Real external/runtime versions remain evidence when genuinely known, including WCAG 2.2 references, CFF 1.2.0, Matplotlib/NumPy/Python/ggplot2/R versions, and pinned Observable Plot 0.6.17

## Tool / CLI / Agent integration (2026-10-19)

The additional [offline three-repository Evidence Console](ui/tri_repo_console.html) consumes selected JSON files for local, read-only comparison. It accepts this repository's native `sci-render-kit/tool-adapter` envelopes (including approval previews and completed agent plan steps) plus explicitly named legacy receipt profiles. See the [UI contract](docs/02-examples-and-contracts/TOOL_UI_STYLE.md). This file-import viewer is distinct from `tool_cli.py --format html` (a static single-run report). Neither surface checks current GitHub status, executes code, nor establishes WCAG/scientific validity.

To view a result, create a JSON receipt with `python3 tool_cli.py list --output output/tools.json` or `python3 tool_cli.py plan --plan-file examples/tool_plan.json --output output/plan-preview.json`, then open the console HTML file locally and select that JSON. Actual execution requires explicit `--approve` on the CLI; the browser never approves or runs tools.


`agent_tools.py` exposes allowlisted, nonrendering `figure_audit` and `communication_transfer` tools. `tool_cli.py` presents structured JSON, consistent terminal status and an offline HTML report. Actual scientific figures still use the repository-owned `sci_render.py` pipeline.

```bash
python3 tool_cli.py list
python3 tool_cli.py invoke figure_audit --args '{"recipe":"recipes/accessible-line-chart.yaml","profile":"nature"}' --format text
python3 tool_cli.py list --format html --output output/tool-catalog.html
python3 -m unittest tests.test_tool_cli -v
```

[Tool / CLI / Agent and UI contract](docs/02-examples-and-contracts/TOOL_CLI_AGENT.md). Pre-render project checks do not assert publisher acceptance, actual rendering, WCAG conformance, independent reruns or truth.

## Capability map

| Surface | Status | Boundary |
Expand Down
106 changes: 106 additions & 0 deletions agent_plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Bounded, explicit-approval execution of caller-provided agent tool plans.

A plan is declarative tool-call data, not autonomous reasoning. The caller
controls authorization, durable storage and any model-provider integration.
Preview never invokes a tool; approval is a caller flag, not authenticated IAM.
"""
from __future__ import annotations

from typing import Any
from agent_tools import PROFILE, TOOL_SPECS, _check_arguments, execute_tool_call

MAX_PLAN_STEPS = 16


def _plan_error(code: str, message: str) -> dict:
return {"profile": PROFILE, "tool": "agent_plan", "status": "error",
"result": None, "error": {"code": code, "message": message},
"approval_granted": False, "scientific_validity_claim": False,
"remote_provider_called": False}


def _preflight(plan: Any) -> dict:
if not isinstance(plan, dict):
raise ValueError("plan must be an object")
allowed = {"steps", "purpose", "on_error"}
if set(plan) - allowed:
raise ValueError("unexpected plan keys: " + ", ".join(sorted(set(plan)-allowed)))
purpose = plan.get("purpose")
if purpose is not None and (not isinstance(purpose, str) or not purpose.strip()):
raise ValueError("purpose must be a nonempty string when present")
policy = plan.get("on_error", "stop")
if not isinstance(policy, str) or policy not in {"stop", "continue"}:
raise ValueError("on_error must be stop or continue")
steps = plan.get("steps")
if not isinstance(steps, list) or not 1 <= len(steps) <= MAX_PLAN_STEPS:
raise ValueError("steps must contain 1 to 16 tool calls")
normalized = []
for index, step in enumerate(steps):
if not isinstance(step, dict) or set(step) != {"tool", "arguments"}:
raise ValueError(f"steps[{index}] must contain exactly tool and arguments")
name = step["tool"]
if not isinstance(name, str) or name not in TOOL_SPECS:
raise ValueError(f"steps[{index}] tool is not allowlisted")
try:
arguments = _check_arguments(name, step["arguments"])
except ValueError as exc:
raise ValueError(f"steps[{index}]: {exc}") from exc
normalized.append({"tool": name, "arguments": dict(arguments)})
return {"purpose": purpose, "on_error": policy, "steps": normalized}


def _domain_failed(response: dict) -> bool:
if response.get("status") != "ok":
return True
result = response.get("result")
return isinstance(result, dict) and (
result.get("valid") is False
or result.get("diagnostic_exit_code", 0) != 0
or result.get("check_status") == "failed"
)


def run_plan(plan: dict, *, approved: bool = False) -> dict:
"""Preview by default; execute no more than 16 calls after explicit approval.

No automatic parallelism, hidden retry, state mutation or tool discovery.
An unapproved plan never reads tool-target files.
"""
if not isinstance(approved, bool):
return _plan_error("INVALID_ARGUMENT", "approved must be a boolean")
try:
normalized = _preflight(plan)
except ValueError as exc:
return _plan_error("INVALID_PLAN", str(exc))
metadata = {"purpose": normalized["purpose"], "on_error": normalized["on_error"],
"step_count": len(normalized["steps"])}
if not approved:
return {"profile": PROFILE, "tool": "agent_plan",
"status": "approval_required",
"result": {**metadata, "proposed_steps": normalized["steps"],
"executed": False},
"error": {"code": "APPROVAL_REQUIRED",
"message": "review the plan and set approved=True before any tool call"},
"approval_granted": False,
"scientific_validity_claim": False, "remote_provider_called": False}

results = []
failed_at = None
for index, step in enumerate(normalized["steps"]):
response = execute_tool_call(step["tool"], step["arguments"])
failed = _domain_failed(response)
results.append({"index": index, "tool": step["tool"],
"domain_check_failed": failed, "response": response})
if failed and failed_at is None:
failed_at = index
if normalized["on_error"] == "stop":
break
return {"profile": PROFILE, "tool": "agent_plan",
"status": "error" if failed_at is not None else "ok",
"result": {**metadata, "executed": True, "completed_steps": len(results),
"failed_at": failed_at, "step_results": results},
"error": {"code": "STEP_FAILED", "message": f"tool step {failed_at} failed"}
if failed_at is not None else None,
"approval_granted": True,
"scientific_validity_claim": False, "remote_provider_called": False}
Loading
Loading