Skip to content
This repository was archived by the owner on Oct 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 103 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,109 @@ jobs:
- name: Run the trust release gate
run: scripts/trust_release_gate.sh

# Constant test loop: proves the local trust-plane loop (self-provision path).
# Runs on pull_request and push to main. Production smoke (api.thisisatest.tech)
# runs only on push to main when CI_SMOKE_AGENT_KEY is set.
constant_test_loop:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Comment thread
PetrefiedThunder marked this conversation as resolved.

- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install httpx

- name: Run local trust-plane constant test loop (self-provision)
# Always run: proves the local loop with self-provisioned credentials.
# Uses quickstart path with ephemeral SQLite and dev key self-provision.
# Registers two tools (dogfood + harmless second) to prove out-of-scope denial.
env:
ENABLE_DEV_KEY_SELF_PROVISION: "true"
TRUST_MODE_ENABLED: "true"
ALLOW_LEGACY_UNPERMITTED_MCP: "false"
ENABLE_PROOF_SURFACES: "false"
ENABLE_DOGFOOD_TOOL: "true"
Comment thread
PetrefiedThunder marked this conversation as resolved.
ENABLE_DOGFOOD_SECOND_TOOL: "true"
TRUST_SIGNING_PRIVATE_KEY_B64: dGVzdC1zaWduaW5nLWtleS1tYXRlcmlhbC0zMmJ5dGU=
Comment thread
PetrefiedThunder marked this conversation as resolved.
DATABASE_URL: sqlite+aiosqlite:///./constant-test-loop.db
STATE_BACKEND: sqlite
SQLITE_URL: ./constant-test-loop-kv.db
run: |
# Boot server in background
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 &
SERVER_PID=$!

# Wait for health endpoint
for i in {1..60}; do
if curl -f http://127.0.0.1:8000/health >/dev/null 2>&1; then
echo "Server ready"
break
fi
if [ $i -eq 60 ]; then
echo "Server failed to start"
kill $SERVER_PID 2>/dev/null || true
exit 1
fi
sleep 1
done

# Run constant test loop (self-provision, no CI_SMOKE_AGENT_KEY)
python scripts/constant_test_loop.py
TEST_EXIT=$?

# Clean up server
kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true

exit $TEST_EXIT
Comment thread
PetrefiedThunder marked this conversation as resolved.

- name: Run production constant test loop (api.thisisatest.tech)
# SECURITY: Only run on push to default branch (main), never on pull_request.
# A PR from the same repo can inject code into constant_test_loop.py that
# exfiltrates CI_SMOKE_AGENT_KEY. Push to main is trusted (requires review).
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
API_URL: https://api.thisisatest.tech
CI_SMOKE_AGENT_KEY: ${{ secrets.CI_SMOKE_AGENT_KEY }}
CI_SMOKE_WALLET_ID: ${{ secrets.CI_SMOKE_WALLET_ID }}
CI_SMOKE_KEY_ID: ${{ secrets.CI_SMOKE_KEY_ID }}
Comment thread
PetrefiedThunder marked this conversation as resolved.
CI_SMOKE_TOOL: ${{ vars.CI_SMOKE_TOOL }}
CI_SMOKE_TOOL_ARGS: ${{ vars.CI_SMOKE_TOOL_ARGS }}
run: |
# Genuinely opt-in. A secret cannot be tested in a step-level "if",
# so the skip has to happen here. Both the credential and the tool
# must be configured: without the key the loop tries to
# self-provision against production, where that endpoint is
# disabled, so the run would fail rather than skip.
if [ -z "$CI_SMOKE_AGENT_KEY" ] || [ -z "$CI_SMOKE_TOOL" ]; then
echo "Skipping production smoke: set the CI_SMOKE_AGENT_KEY secret"
echo "and the CI_SMOKE_TOOL variable to enable it."
exit 0
fi

# --tool and --tool-args are both mandatory off loopback: which tool
# a monitor invokes on every push, and with what payload, is an
# operator's decision rather than one discovery makes for them. An
# empty payload is a valid answer but still has to be given.
TOOL_ARGS="$CI_SMOKE_TOOL_ARGS"
if [ -z "$TOOL_ARGS" ]; then
TOOL_ARGS='{}'
fi

python scripts/constant_test_loop.py \
--tool "$CI_SMOKE_TOOL" \
--tool-args "$TOOL_ARGS"
Comment thread
PetrefiedThunder marked this conversation as resolved.

lint:
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down
6 changes: 6 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class Settings(BaseSettings):
# /mcp/tools.json is non-empty for permit→invoke→receipt dogfood.
# Default false: no stub pollution. Independent of ENABLE_PROOF_SURFACES.
ENABLE_DOGFOOD_TOOL: bool = False

# --- Second dogfood tool (opt-in read-only partner.notes.count) ---
# When true, registers a second harmless read-only tool for CI to test
# out-of-scope denial (permit allows first tool, denies second).
# Default false: only enable in test/CI environments.
ENABLE_DOGFOOD_SECOND_TOOL: bool = False
Comment thread
PetrefiedThunder marked this conversation as resolved.

# --- Governed upstream MCP partner tool ---
# One explicitly configured Streamable HTTP server/tool for the
Expand Down
1 change: 1 addition & 0 deletions app/core/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ async def gather_dependency_report() -> dict[str, Any]:
"simulation_modes": sim_modes,
"enable_proof_surfaces": bool(settings.ENABLE_PROOF_SURFACES),
"enable_dogfood_tool": bool(settings.ENABLE_DOGFOOD_TOOL),
"enable_dogfood_second_tool": bool(settings.ENABLE_DOGFOOD_SECOND_TOOL),
"runtime_degradation": runtime_degradation,
"metric_scopes": _METRIC_SCOPES,
"unhealthy": unhealthy,
Expand Down
72 changes: 70 additions & 2 deletions app/services/dogfood_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
When ``ENABLE_DOGFOOD_TOOL=true``, registers ``partner.notes.write`` as a
local MCP tool that appends a note to a JSONL file under permit control.

When ``ENABLE_DOGFOOD_SECOND_TOOL=true``, also registers ``partner.notes.count``
as a second harmless tool that returns the current note count (read-only, no
side effects). This second tool is used in CI to prove out-of-scope denial when
the permit only allows the first tool.

Default is off so production discovery stays empty of demo tools unless
ops explicitly enables the flag. This is independent of
``ENABLE_PROOF_SURFACES`` (which gates Phase9 / marketplace stubs).
Expand All @@ -24,12 +29,15 @@

DOGFOOD_TOOL_ID = "partner.notes.write"
DOGFOOD_TOOL_NAME = "Partner Notes Write"
DOGFOOD_SECOND_TOOL_ID = "partner.notes.count"
DOGFOOD_SECOND_TOOL_NAME = "Partner Notes Count"
DOGFOOD_NOTES_PATH = Path("data") / "dogfood_partner_notes.jsonl"
DOGFOOD_CREDITS_PER_UNIT = 2.0
DOGFOOD_MAX_TEXT_CHARS = 2_000
DOGFOOD_MAX_NOTES = 1_000

_registered = False
_second_registered = False
_registration_lock = Lock()
_note_count: int | None = None

Expand Down Expand Up @@ -82,6 +90,22 @@ def _write_note(text: str = "hello") -> dict[str, Any]:
}


def _count_notes() -> dict[str, Any]:
"""Count dogfood notes (read-only, no side effects; for out-of-scope denial test)."""
global _note_count
with _registration_lock:
if _note_count is None:
_note_count = _load_note_count()
count = _note_count

return {
"status": "ok",
"tool": DOGFOOD_SECOND_TOOL_ID,
"note_count": count,
"notes_path": str(DOGFOOD_NOTES_PATH),
}


def register_dogfood_tool() -> None:
"""Register the executable dogfood tool in the local MCP registry."""
global _registered
Expand Down Expand Up @@ -110,6 +134,34 @@ def register_dogfood_tool() -> None:
logger.info("Registered dogfood MCP tool: %s", DOGFOOD_TOOL_ID)


def register_dogfood_second_tool() -> None:
"""Register the second harmless dogfood tool (read-only note counter)."""
global _second_registered
if _second_registered:
return

with _registration_lock:
if _second_registered:
return
registry = get_service_registry()
registry.register_local(
service_id=DOGFOOD_SECOND_TOOL_ID,
name=DOGFOOD_SECOND_TOOL_NAME,
description=(
"Harmless read-only tool that returns the current dogfood note count. "
"Used in CI to test out-of-scope denial when permit only allows the first tool. "
"Opt-in via ENABLE_DOGFOOD_SECOND_TOOL; not a production partner tool."
),
category=ServiceCategory.AGENT_COMMS,
func=_count_notes,
credits_per_unit=1.0,
unit_name="call",
require_permit=True,
)
_second_registered = True
logger.info("Registered second dogfood MCP tool: %s", DOGFOOD_SECOND_TOOL_ID)


def unregister_dogfood_tool() -> None:
"""Remove the dogfood tool from the local registry."""
global _registered, _note_count
Expand All @@ -120,16 +172,32 @@ def unregister_dogfood_tool() -> None:
_note_count = None


def unregister_dogfood_second_tool() -> None:
"""Remove the second dogfood tool from the local registry."""
global _second_registered
registry = get_service_registry()
with _registration_lock:
registry.unregister_local(DOGFOOD_SECOND_TOOL_ID)
_second_registered = False


def sync_dogfood_tool_registration() -> None:
"""Register or unregister the dogfood tool to match ENABLE_DOGFOOD_TOOL.
"""Register or unregister the dogfood tools to match ENABLE_DOGFOOD_TOOL / ENABLE_DOGFOOD_SECOND_TOOL.

When the flag is off, only remove the tool if *this* module registered it.
Scripts (``dogfood_trust_plane``) and tests may register the same id
independently; discovery sync must not delete those registrations.
"""
from ..core.config import get_settings

if get_settings().ENABLE_DOGFOOD_TOOL:
settings = get_settings()

if settings.ENABLE_DOGFOOD_TOOL:
register_dogfood_tool()
elif _registered:
unregister_dogfood_tool()

if settings.ENABLE_DOGFOOD_SECOND_TOOL:
register_dogfood_second_tool()
elif _second_registered:
unregister_dogfood_second_tool()
Loading