Harden Sigil: sigil/sessions.py

This commit is contained in:
bigpickle 2026-10-05 21:24:43 +08:00
parent 854db7c6a7
commit 128a8db763

View File

@ -17,6 +17,8 @@ Two properties worth stating out loud:
from __future__ import annotations
import hashlib
import json
import secrets
import time
from typing import List, Optional
@ -40,6 +42,16 @@ AGENT_SCOPES = (AGENT_ACT, "view:read", "view:write", "secret:use")
DEFAULT_AGENT_TTL = 900 # 15 minutes. Short enough to be boring.
def hash_token(token: str) -> str:
"""Digest a bearer token for storage.
Session tokens are stored hashed, like one-time codes. A stolen database
must not hand an attacker live sessions; the plaintext token exists only in
the caller's hands, which is where a bearer token belongs.
"""
return hashlib.sha256(token.encode("utf-8")).hexdigest()
def _token() -> str:
return secrets.token_urlsafe(32)
@ -59,7 +71,8 @@ def issue_agent_session(
token = _token()
expires = int(time.time()) + ttl
store.put_session(
token, AGENT, agent, agent, granted, expires, granted_by=None
hash_token(token), AGENT, agent, agent, granted, expires,
granted_by=None, fingerprint=fingerprint,
)
store.record(
"session.issued",
@ -88,6 +101,11 @@ def issue_view_session(
) -> dict:
"""Mint a human_via_agent session.
Plumbing primitive, not an entry point: it trusts whoever calls it. Code
reached from outside the process must authenticate first. issue_code()
enforces that by requiring an agent session, and redeem_code() may present a
one-time code as its credential.
`granted_by` is the agent that approved the code. It is recorded so the
audit trail can answer "who let this human in" and not merely "which
agent's things did they see".
@ -101,7 +119,7 @@ def issue_view_session(
token = _token()
expires = int(time.time()) + ttl
store.put_session(
token,
hash_token(token),
HUMAN_VIA_AGENT,
f"{agent}'s human",
agent,
@ -134,22 +152,22 @@ def issue_view_session(
def resolve(store: Store, token: str) -> dict:
"""Look up a session and reject it if revoked or expired."""
row = store.get_session(token)
row = store.get_session(hash_token(token))
if row is None:
raise errors.SessionUnknown("no such session")
if row["revoked_at"] is not None:
raise errors.SessionExpired("session revoked")
if time.time() >= row["expires_at"]:
raise errors.SessionExpired("session expired")
import json as _json
return {
"token": token,
"kind": row["kind"],
"subject": row["subject"],
"on_behalf_of": row["on_behalf_of"],
"granted_by": row["granted_by"],
"scopes": _json.loads(row["scopes"]),
"fingerprint": row["fingerprint"],
"revoked_reason": row["revoked_reason"],
"scopes": json.loads(row["scopes"]),
"expires_at": row["expires_at"],
}
@ -179,5 +197,5 @@ def assert_may_act_as_agent(session: dict) -> str:
def revoke(store: Store, token: str) -> None:
store.revoke_session(token)
store.revoke_session(hash_token(token))
store.record("session.revoked", actor="system", detail={"token_prefix": token[:8]})