Skip to content
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
8 changes: 6 additions & 2 deletions sage/gateway/conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,13 @@ def run(member: str, instance: str, host_agent: str = "sage-conformance",

# 5. ruling mid-session — an operator decision visible to the SAME session ----------
live = scope.get("live_grants") or []; standing = scope.get("standing_grants") or []
# a ruling that narrowed a grant is still a ruling this session can see (#523): since hestia
# #1370 such a grant appears only in grants_by_mode. Step 4 keeps the two lists, since a
# narrowed grant admits nothing at a mode-blind gate and so cannot explain an allow.
by_mode = scope.get("grants_by_mode") or []
pend = [r for r in (scope.get("requests") or []) if r.get("status") == "pending"]
if live or standing:
out.append(_step("ruling_mid_session", "PASS", grants=live + standing))
if live or standing or by_mode:
out.append(_step("ruling_mid_session", "PASS", grants=(live + standing) or by_mode))
else:
out.append(_step("ruling_mid_session", "NOT_YET", pending_requests=[r.get("request_id") or r.get("id") for r in pend],
needs="operator ruling (dashboard) — then this session sees the grant without reconnect"))
Expand Down
31 changes: 27 additions & 4 deletions sage/gateway/heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,27 @@ def fleet_digest(hours: float, forum_dir: Path, repos: list[str]) -> str:
return "\n\n".join(out) if out else "(nothing new in the window)"


def scope_grant_lines(st: dict) -> list:
"""The beat's "granted paths", from hestia_scope_status. Spelled with the gate's own suffix:
`<root>/**` reaches the subtree, bare is EXACT.

WITH ITS MODES, AND NARROWED GRANTS KEPT (#523): since hestia #1370 a grant narrowed below
rwed appears only in `grants_by_mode`, so reading the two old lists made it vanish from the
beat while its request still read "granted". An older daemon has no such key, and its two
lists are read as before.

A NARROWED GRANT IS RECORDED, NOT NECESSARILY ADMITTED (Nova on #532): a mode-blind gate
admits nothing for it (hestia #1345 part B). The line says so in those words rather than
guess which gate is installed; hestia://reach/<path> gives the effective answer."""
if st.get("grants_by_mode") is not None:
return [f"{g.get('path')}{'/**' if g.get('recursive') else ''} ({g.get('modes') or '-'}"
+ ("" if (g.get("modes") or "") == "rwed" else
"; narrowed: recorded, may not be admitted yet, see hestia://reach") + ")"
for g in st.get("grants_by_mode") or []]
return [f"{g.get('path')}{'/**' if g.get('recursive') else ''}"
for g in (st.get("live_grants") or []) + (st.get("standing_grants") or [])]


def decided_requests(reqs):
"""The settled subset of hestia_scope_status `requests[]`, as (request_id, path, decision).
hestia's `ScopeRequest::status` emits granted | refused | pending | expired, and its
Expand Down Expand Up @@ -4112,9 +4133,7 @@ def main(argv=None) -> int:
if disp is not None and hasattr(disp, "_call"):
try:
st = disp._call("hestia_scope_status", {"plugin_id": args.member})
# spelled with the gate's own suffix: `<root>/**` reaches the subtree, bare is EXACT
grants = [f"{g.get('path')}{'/**' if g.get('recursive') else ''}"
for g in (st.get("live_grants") or []) + (st.get("standing_grants") or [])]
grants = scope_grant_lines(st)
reqs = [(r.get("request_id"), r.get("path"), r.get("decision") or r.get("status"))
for r in (st.get("requests") or [])]
who_ruled = {r.get("request_id"): r.get("decided_by") for r in (st.get("requests") or [])
Expand Down Expand Up @@ -4149,7 +4168,11 @@ def main(argv=None) -> int:
+ (f". Their note: \"{str(why_ruled[i]).strip()[:400]}\"" if why_ruled.get(i) else "")
for i, p_, d in new_decisions) + "\n"
if new_decisions else "")
+ "(live grants die when the daemon restarts; only standing grants persist)")
+ "(live grants die when the daemon restarts; only standing grants persist)\n"
# SAGE #530: the being can ask, instead of acting to find out
+ "(modes: r read, w write, e run, d delete. To see what you hold with who "
"granted it, or to ask whether one path is inside it: memory_read "
"{\"path\": \"hestia://reach\"} or {\"path\": \"hestia://reach/<absolute path>\"})")
except Exception as e:
scope = f"(scope status unavailable: {type(e).__name__})"
# the being's own appeals and any ruling new since the last beat (see appeals_block)
Expand Down
191 changes: 190 additions & 1 deletion sage/gateway/hestia_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ def __call__(self, intent: BeingIntent, verdict: GatewayVerdict) -> ResultEnvelo
# before the local dispatcher tries to open it as a path (see _resolve_pointer).
if intent.effector == "memory_read":
try:
resolved = self._resolve_pointer(str(intent.args.get("path", "")))
raw = str(intent.args.get("path", ""))
resolved = (self._reach_pointer(raw, verdict) if _is_reach_pointer(raw)
else self._resolve_pointer(raw))
except Exception as e: # a broken lookup must not eat the read
resolved = (f"[could not resolve that pointer: {type(e).__name__}]"
if "://" in str(intent.args.get("path", "")) else None)
Expand Down Expand Up @@ -534,6 +536,172 @@ def _resolve_pointer(self, raw: str) -> Optional[str]:
return (f"[a message of yours was not delivered: {frag or 'no detail recorded'} "
f"(egress row {ident}). The row is retired; nothing of yours is queued behind it.]")

# -- what the member holds, on request (SAGE #530) -------------------------------
# cbp-being, 2026-10-24, to dp: "What files or paths are in-scope for me to read? ... I want
# to narrow my request to something I can actually read." It held the file it wanted under a
# recursive grant the whole time, and had no way to ask except to act and read the refusal.
# The beat's "granted paths" line is a set of roots with no modes, beside every request ever
# filed; whether a path falls inside one is left to the being. These two reads answer it:
# hestia://reach every grant it holds now, with modes and provenance, and the floor
# hestia://reach/<path> does anything it holds cover this path, in which modes, via what
# Both read the law (hestia_scope_status, and the reach the gate itself resolved on this
# verdict); neither touches the path, judges an act, or opens a request.
def _reach_rows(self) -> tuple:
"""(grants, floor) from hestia_scope_status.

Each grant: {path, recursive, modes, channel, granted_by, request_id, expires_at}.
`grants_by_mode` is preferred (hestia #1370: it alone carries narrowed grants); an older
daemon's two lists are read as all-modes.

Each floor entry: {path, recursive, modes, served, why}. `modes` is what the floor
RECORDS; `served` is what this member's gate is actually handed: "rwed" for an entry in
`society_floor`, "r" for one in `society_floor_read` (#1381, read acts only), "" for one
in `society_floor_withheld` (with its `why`). A daemon without those keys serves every
`society_floor` entry as all-modes, as its gate does."""
st = self._call("hestia_scope_status", {"plugin_id": self.member}) or {}
if not isinstance(st, dict) or "_hestia_error" in st:
raise RuntimeError(str((st or {}).get("_hestia_error") if isinstance(st, dict) else st))
grants = []
if st.get("grants_by_mode") is not None:
for g in st.get("grants_by_mode") or []:
grants.append({**g, "modes": str(g.get("modes") or "")})
else:
for chan in ("live", "standing"):
for g in st.get(f"{chan}_grants") or []:
grants.append({**g, "channel": chan, "modes": "rwed"})

def key(f):
return (os.path.realpath(os.path.expanduser(str(f.get("path") or ""))), bool(f.get("recursive")))
served = {key(f): "rwed" for f in st.get("society_floor") or []}
served.update({key(f): "r" for f in st.get("society_floor_read") or []})
withheld = {os.path.realpath(os.path.expanduser(str(w.get("path") or ""))): str(w.get("why") or "")
for w in st.get("society_floor_withheld") or []}
recorded = (st.get("floor_by_mode") if st.get("floor_by_mode") is not None
else [{**f, "modes": "rwed"} for f in st.get("society_floor") or []])
floor = []
for f in recorded or []:
k = key(f)
floor.append({**f, "modes": str(f.get("modes") or ""), "served": served.get(k, ""),
"why": withheld.get(k[0], "" if k in served else "not served to you")})
return grants, floor

@staticmethod
def _covers(row: dict, p: str) -> bool:
"""The gate's own reading of reach (hestia #1002): a grant spelled with /** covers its
subtree, a bare one exactly itself. Both sides canonicalized."""
root = os.path.realpath(os.path.expanduser(str(row.get("path") or "")))
if not root or root == "/":
return False
return p == root or (bool(row.get("recursive")) and p.startswith(root.rstrip("/") + "/"))

@staticmethod
def _modes_words(modes: str) -> str:
names = [w for c, w in (("r", "read"), ("w", "write"), ("e", "run"), ("d", "delete")) if c in modes]
return ", ".join(names) or "nothing"

@staticmethod
def _union(modes) -> str:
return "".join(sorted(set("".join(modes)), key="rwed".index))

def _reach_pointer(self, raw: str, verdict) -> str:
"""The answer to `hestia://reach` or `hestia://reach/<path>` (see the block above).

WHAT THE GATE ADMITS NOW IS NOT WHAT IS RECORDED (Nova, HOLD on #532). A member asks this
to know which acts will go through, so the first line is always the EFFECTIVE answer:
the member's grants the gate itself resolved as admitted on this verdict, plus the floor
entries this member's gate is served. A recorded grant the gate does not admit (a
narrowed grant on a mode-blind gate, hestia #523/#1345) and a floor entry withheld from
this member are listed under it, labelled as such, and never counted as "yes". Where
the verdict says nothing about reach, admission is reported as unconfirmed."""
try:
grants, floor = self._reach_rows()
except Exception as e:
return (f"[what you hold could not be read just now: hestia did not answer "
f"({type(e).__name__}). That is an outage, not a change in what you hold. "
f"Your beat's 'granted paths' line is the last list it gave.]")
# what the gate itself resolved as admitted on this verdict, when it says
admitted = tuple(_granted_reach_of(verdict)) if verdict is not None else ()

def gate_admits(row) -> Optional[bool]:
if not admitted:
return None
root = os.path.realpath(os.path.expanduser(str(row.get("path") or "")))
return any(os.path.realpath(str(r)) == root and bool(rec) == bool(row.get("recursive"))
for r, rec, *_ in admitted)

def spell(row) -> str:
return f"{row.get('path')}{'/**' if row.get('recursive') else ' (exactly this path)'}"

def provenance(row) -> str:
bits = [str(row.get("channel") or "").strip() or None,
f"granted by {row['granted_by']}" if row.get("granted_by") else None,
f"request {row['request_id']}" if row.get("request_id") else None,
f"expires {row['expires_at']}" if row.get("expires_at") else None]
return "; ".join(b for b in bits if b)

def grant_status(row) -> str:
a = gate_admits(row)
if a is True:
return "the gate admits it now"
if a is None:
return "whether the gate admits it right now could not be confirmed"
if row.get("modes") != "rwed":
return ("recorded, but the gate does NOT admit it now: a grant narrowed to fewer "
"than all four modes admits nothing until hestia #1345 part B")
return "recorded, but the gate does NOT admit it now"

def floor_status(f) -> str:
if f["served"]:
return f"served to your gate now: {self._modes_words(f['served'])}"
return f"NOT served to you now ({f['why'] or 'withheld'})"

rest = raw.strip()[len("hestia://reach"):].lstrip("/")
if not rest:
lines = ["[what you hold, read from hestia at this moment]"]
lines += [f"- {spell(g)}: {self._modes_words(g['modes'])} ({provenance(g)}). {grant_status(g)}."
for g in grants] or ["- no grants"]
if floor:
lines.append("The society floor (every member's, where none of their own grants reach):")
lines += [f"- {spell(f)}: {self._modes_words(f['modes'])}. {floor_status(f)}." for f in floor]
lines.append('To ask about one path: memory_read {"path": "hestia://reach/<absolute path>"}. '
"This read looked at the law only: nothing was judged and no request was opened.]")
return "\n".join(lines)

shown = rest if rest.startswith(("/", "~")) else "/" + rest
p = os.path.realpath(os.path.expanduser(shown))
own = [g for g in grants if self._covers(g, p)]
fl = [f for f in floor if self._covers(f, p)]
own_now = [g for g in own if gate_admits(g) is True]
unknown = [g for g in own if gate_admits(g) is None]
floor_now = [f for f in fl if f["served"]]
now = self._union([g["modes"] for g in own_now] + [f["served"] for f in floor_now])
lines = [f"[does what you hold reach {p}? read from hestia at this moment]"]
if now:
lines.append(f"now: yes, {self._modes_words(now)}.")
elif unknown:
lines.append(f"now: unconfirmed. You hold a grant here recording "
f"{self._modes_words(self._union(g['modes'] for g in unknown))}, but whether "
f"the gate admits it right now could not be confirmed.")
elif own or fl:
lines.append("now: no. Something recorded reaches this path, but the gate does not admit "
"it at the moment (see below), so an act on it would be refused.")
else:
lines.append("now: no. Nothing you hold reaches this path, so an act on it would be refused.")
for g in own:
lines.append(f"- your grant {spell(g)}: {self._modes_words(g['modes'])} ({provenance(g)}). "
f"{grant_status(g)}.")
for f in fl:
lines.append(f"- floor entry {spell(f)}: {self._modes_words(f['modes'])}. {floor_status(f)}.")
if now and not os.path.lexists(p):
lines.append("Nothing exists at that path yet.")
if not now:
copies = same_path_in_reach(p, admitted)
if copies:
lines.append(copy_alternative(p, copies))
lines.append("This read looked at the law only: nothing was judged, nothing was read at "
"that path, and no request was opened.]")
return "\n".join(lines)

def _escalation_pointer(self, ident: str) -> str:
"""What a `review_request` invitation is actually about (SAGE #109).

Expand Down Expand Up @@ -645,6 +813,21 @@ def _say_instead(self, to: str) -> Optional[str]:
base = (to or "").split("/", 1)[0].strip().lower()
if not base:
return None
# "SEAT" IS WHAT EVERY RECEIPT CALLS THE SEAT. cbp-being sent peer_ask to="seat" seven
# times (to 2026-10-24) and each time got only "not on the hub roster" and a list of
# boxes, nothing leading back: the seat's own conversation title says "(the seat)" and
# receipts say "hand the seat", but the name resolved only through a per-instance
# `also_known_as`, which no seat had declared. The seat's conversation answers to it here.
if base in ("seat", "the seat", "my seat"):
seat_conv = self._seat_conversation()
if seat_conv:
return (f"'{to}' is your seat, and you are already in a conversation with it: "
f"\"{seat_conv}\". The hub is the wrong door and nothing was sent. Use say "
f"with the conversation id \"{seat_conv}\"; it reaches the seat directly "
f"and is not rate-limited the way asks are. This did not count against any "
f"limit. If what you want is for one of your files to be RUN, call "
f"request_run with the path: a message asking for a run is not a run "
f"request.")
try:
from sage.gateway import conversations as _conv
for m in _conv.listing(self.memory_root):
Expand Down Expand Up @@ -4526,6 +4709,12 @@ def _release_stage_lock(lock_path: str, fd: int) -> None:
os.close(fd)


def _is_reach_pointer(raw: str) -> bool:
"""`hestia://reach` or `hestia://reach/<path>` (SAGE #530)."""
s = (raw or "").strip()
return s == "hestia://reach" or s.startswith("hestia://reach/")


def _granted_reach_of(verdict) -> tuple:
"""((root, recursive), ...) from a verdict. A verdict built without reach (older client,
hand-made in tests) keeps the pre-#1002 prefix reading of its bare `granted` roots."""
Expand Down
16 changes: 16 additions & 0 deletions sage/gateway/reference_f1a.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,22 @@ def _do_memory_read(self, intent: BeingIntent) -> ResultEnvelope:
+ " Nothing was read this time.]"),
witness_id=self._witness(
f"memory_read {p.name} (not in home; exists at {above}; copy in reach)"))
if self._reach_pairs():
# AND WITH NO COPY, IT IS STILL NOT A READ TO OFFER (SAGE #530). Falling through
# to "To read it:" hands the member a path its grants do not cover, as one it
# can read. Say what is true and suggest no door; asking stays its decision.
# Only when the verdict said what the member holds: a verdict with no grants
# on it (hand-made, or a failed policy fetch) cannot tell, and keeps the old line.
return ResultEnvelope(
ok=True,
result=(f"[no such path in your home: a path without a leading / is read "
f"inside your home, and '{shown}' is not there. A file exists at "
f"'{above}', but it is outside what you hold, so a read of it "
f"would be refused, and nothing you hold has a copy of it. The "
f"paths you hold are in your beat under 'granted paths'. Nothing "
f"was read this time.]"),
witness_id=self._witness(
f"memory_read {p.name} (not in home; exists only outside reach at {above})"))
if above is not None:
# A RELATIVE PATH IS READ INSIDE THE HOME, AND THE BEING DID NOT KNOW THAT.
# Measured 2026-10-20/21 on cbp-being: it read the gate source by its absolute
Expand Down
Loading
Loading