Skip to content
Open
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
6 changes: 3 additions & 3 deletions s16code/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,19 +339,19 @@ async def complete_waiting_job(body: CompletionBody, request: Request):
"run": result, "channel_delivery": channel_delivery}


@router.post("/facts")
@router.post("/facts", dependencies=[Depends(require_control)])
async def fact(body: FactBody, request: Request):
return request.app.state.runtime.remember_fact(text=body.text, scope=body.scope(), source_uri=body.source_uri,
source_author=body.source_author, principal=Principal("gateway", "gateway"), supersedes_id=body.supersedes_id)


@router.post("/documents")
@router.post("/documents", dependencies=[Depends(require_control)])
async def document(body: IndexBody, request: Request):
return request.app.state.runtime.index_document(text=body.text, source_uri=body.source_uri,
scope=body.scope(), source_author=body.source_author)


@router.post("/memory/search")
@router.post("/memory/search", dependencies=[Depends(require_control)])
async def memory_search(body: SearchBody, request: Request):
hits = request.app.state.runtime.memory.recall(body.query, body.scope(),
kinds=body.kinds, limit=body.limit)
Expand Down
7 changes: 4 additions & 3 deletions s16code/ui/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
GET /v1/runs/{id}/events AG-UI event stream over SSE
GET /v1/runs/{id}/composed the interface the agent composed for a run
POST /v1/validate validate an arbitrary surface (injection wall)
POST /v1/action a validated user action (approve/reject/rerun)
POST /v1/action a validated user action (approve/reject/rerun); control token
GET /s/{id} the render client, pointed at a run

The data source is the runtime's own graph, read in-process off
Expand All @@ -20,7 +20,7 @@
import json
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from pydantic import BaseModel, Field

Expand All @@ -29,6 +29,7 @@
from .hitl import PendingAction, decide_resume
from .surface import build_run_surface
from .validator import validate_surface
from s16code.auth import require_control

router = APIRouter()
_CLIENT = Path(__file__).parent / "client" / "index.html"
Expand Down Expand Up @@ -152,7 +153,7 @@ class ActionBody(BaseModel):
pending_summary: str = ""


@router.post("/v1/action")
@router.post("/v1/action", dependencies=[Depends(require_control)])
async def action(body: ActionBody, request: Request):
try:
node = request.app.state.runtime.graph.snapshot(body.run_id).nodes[body.node_id]
Expand Down
10 changes: 9 additions & 1 deletion tests/test_control_plane_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
A subscription carries `allowed_side_effects` and a budget: it is the object the
whole session's security argument rests on. An unauthenticated write there hands
an anonymous caller the authority to decide what the agent may do and how much it
may spend. Starting a run and resuming a parked node both spend money too.
may spend. Starting a run and resuming a parked node both spend money too. So do
HITL approve/reject (`POST /v1/action`) and writing tenant memory
(`POST /v1/agent/facts`, `/documents`). Cross-tenant recall
(`POST /v1/agent/memory/search`) is a read, but the tenant lives in the JSON
body, so it is gated the same way.

The shape being guarded against is `if expected and not compare_digest(...)`,
which reads like a check and behaves like an open door whenever the variable is
Expand All @@ -20,6 +24,10 @@
"occurred_at": "2026-08-05T09:00:00Z"}),
("post", "/v1/agent/runs", {"prompt": "hello", "tenant_id": "t"}),
("post", "/v1/agent/runs/run-1/resume", {}),
("post", "/v1/action", {"run_id": "run-1", "node_id": "gate", "action": "approve"}),
("post", "/v1/agent/facts", {"tenant_id": "t", "text": "injected", "source_uri": "attacker://x"}),
("post", "/v1/agent/documents", {"tenant_id": "t", "text": "injected", "source_uri": "attacker://x"}),
("post", "/v1/agent/memory/search", {"tenant_id": "t", "query": "secrets"}),
]


Expand Down