feat: executor v0.2.0: agent-owned credential relay — creds ride the request envelope from agent KBs
This commit is contained in:
+26
-11
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user