feat: executor v0.2.0: agent-owned credential relay — creds ride the request envelope from agent KBs

This commit is contained in:
2026-10-04 00:27:25 +00:00
parent 04aa3a8b82
commit e06c8724e6
+26 -11
View File
@@ -1,7 +1,8 @@
"""coreci-executor v0.1.4 — governed write rail (Design v1.0, approved 2026-10-03).
"""coreci-executor v0.2.0 — governed write rail (Design v1.0, approved 2026-10-03).
Tool endpoints (Authorization: Bearer <EXECUTOR_API_KEY> or X-Executor-Key):
POST /submit_request {agent, kind, targets[], content, justification}
POST /submit_request {agent, kind, targets[], content, justification,
credentials: {gitea_user, gitea_token} (optional)}
GET /request_status?request_id=...
GET /health
Web UI (passcode session — EXECUTOR_PASSCODE): / (queue), /ui/request/{id},
@@ -12,8 +13,12 @@ Audit: JSONL to logs/ (+ optional Discord webhook). Secrets stay in env.
v0.1.2: CORS for browser-origin schema fetch; Bearer auth support (Open WebUI).
v0.1.3: never-500 login; OpenAPI securitySchemes (Swagger Authorize button).
v0.1.4: allow_headers wildcard — Open WebUI's tool layer sends X-Session-Id
(and possibly other headers); the strict list 400'd the CORS preflight.
v0.1.4: allow_headers wildcard — Open WebUI's tool layer sends X-Session-Id.
v0.1.5: allow_origins wildcard — the code sandbox sends Origin: null.
v0.2.0: agent-owned credential relay — credentials {gitea_user, gitea_token}
ride the request envelope (never the pack content); Gitea enforces
the relayed identity's own permissions; falls back to the executor
identity when the envelope is absent.
"""
import hashlib
import json
@@ -52,12 +57,12 @@ TIMEOUT_S = int(ENV.get("REQUEST_TIMEOUT_S", "900"))
COOKIE_NAME = "exec_session"
CRED_RE = re.compile(r"https?://[^/\s@]+:[^@\s]+@")
app = FastAPI(title="coreci-executor", version="0.1.5")
app = FastAPI(title="coreci-executor", version="0.2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # add other origins you use for Open WebUI here
allow_origins=["*"], # v0.1.5: sandbox iframe sends Origin: null
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"], # v0.1.4: Open WebUI tool calls carry X-Session-Id; strict lists 400 the preflight
allow_headers=["*"], # v0.1.4: Open WebUI tool calls carry X-Session-Id
)
REQS: dict[str, dict] = {}
@@ -77,6 +82,7 @@ def audit(evt: str, r: dict, **extra) -> None:
rec = {"ts": now(), "event": evt, "id": r.get("id"),
"agent": r.get("agent"), "kind": r.get("kind"),
"targets": r.get("targets"), "decision": r.get("decision"),
"auth": ("relay:" + r.get("relay_user")) if r.get("relay_token") else "executor",
"actor": extra.pop("actor", None), **extra}
line = json.dumps(rec, default=str)
(AUDIT_DIR / f"audit-{datetime.now(timezone.utc):%Y-%m-%d}.jsonl").open("a").write(line + "\n")
@@ -95,17 +101,22 @@ def classify(agent: str, kind: str, targets: list) -> str:
def run_pack(rid: str) -> None:
r = REQS[rid]
r["status"] = "running"; audit("started", r)
relay_user, relay_token = r.get("relay_user") or "", r.get("relay_token") or ""
git_user = relay_user or GITEA_USER
git_token = relay_token or GITEA_TOKEN
home = Path(tempfile.mkdtemp(dir=str(WORK_ROOT), prefix="req-"))
(home / ".git-credentials").write_text(
f"https://{GITEA_USER}:{GITEA_TOKEN}@{GITEA_HOST}\n")
f"https://{git_user}:{git_token}@{GITEA_HOST}\n")
script = "git config --global credential.helper store\n" + r["content"]
env = {"PATH": "/usr/local/bin:/usr/bin:/bin", "HOME": str(home),
"GIT_TERMINAL_PROMPT": "0", "GITEA_URL": GITEA_URL,
"GITEA_TOKEN": GITEA_TOKEN, "GITEA_USER": GITEA_USER, "TZ": "UTC"}
"GITEA_TOKEN": git_token, "GITEA_USER": git_user, "TZ": "UTC"}
try:
p = subprocess.run(["bash", "-c", script], capture_output=True,
text=True, timeout=TIMEOUT_S, cwd=home, env=env)
tail = (p.stdout[-MAX_TAIL:] + "\n" + p.stderr[-MAX_TAIL:]).strip()
if relay_token:
tail = tail.replace(relay_token, "***")
r.update(status="completed" if p.returncode == 0 else "failed",
exit_code=p.returncode,
output=tail.replace(GITEA_TOKEN, "***"), finished_at=now())
@@ -139,7 +150,7 @@ def page(title: str, body: str) -> str:
@app.get("/health")
def health():
return {"status": "ok", "version": "0.1.5", "requests": len(REQS)}
return {"status": "ok", "version": "0.2.0", "requests": len(REQS)}
@app.post("/submit_request")
async def submit(request: Request):
@@ -149,6 +160,9 @@ async def submit(request: Request):
agent, kind = d.get("agent", ""), d.get("kind", "bash_pack")
targets = d.get("targets") or []
content, just = d.get("content", ""), d.get("justification", "")
creds = d.get("credentials") or {}
relay_user = (creds.get("gitea_user") or "").strip()
relay_token = (creds.get("gitea_token") or "").strip()
if kind != "bash_pack":
raise HTTPException(400, "v0 supports kind=bash_pack only")
if not (agent and content and just):
@@ -160,7 +174,8 @@ async def submit(request: Request):
rid = uuid.uuid4().hex[:12]
r = {"id": rid, "agent": agent, "kind": kind, "targets": targets,
"content": content, "justification": just, "submitted_at": now(),
"status": "pending_approval", "decision": classify(agent, kind, targets)}
"status": "pending_approval", "decision": classify(agent, kind, targets),
"relay_user": relay_user, "relay_token": relay_token}
with LOCK:
REQS[rid] = r
audit("submitted", r)