Access drawer and inbound gate honor the persona connector grant

effective_connectors intersects the declared allowlist, so the drawer no longer
renders undeclared connectors as live sources and inbound delivery is gated too.
Builder-based builtins (no manifest) stay unrestricted for channel bindings.
This commit is contained in:
Rohit C Prasad
2026-08-15 10:51:46 -07:00
committed by Rohit P
parent 78fc2bd8f7
commit 0a18d72657
2 changed files with 64 additions and 2 deletions
+26 -2
View File
@@ -643,11 +643,27 @@ class SessionManager:
record = self.session_store.load(session_id)
return (record.agent if record else None) or self.personas.default_id()
def _persona_connector_grant(self, persona_id: str) -> Optional[set[str]]:
"""The persona's declared connector allowlist (OPE-93). None = unrestricted (the
`all` sentinel of general builtins); a set = only these ids can ever be effective
for its sessions — the empty set means no connector access at all."""
entry = self.personas.get(persona_id)
if entry is None or entry.manifest is None:
# Builder-based builtins (Chat/Code/Cowork/Ops) predate the allowlist: their
# `connectors` trait gates TOOLS only, while their sessions legitimately use
# the drawer/inbound path (channel bindings). No manifest → no restriction.
return None
declared = entry.manifest.connectors
if declared is True:
return None
return set(declared or ())
def effective_connectors(
self, session_id: str, persona_id: Optional[str] = None
) -> set[str]:
"""The connectors effectively enabled for this session (§4.1): connected AND not muted by
the session override / persona default. Drives the engine's connector-tool gating; seeds the
the session override / persona default AND within the persona's declared grant (OPE-93).
Drives the engine's connector-tool gating and the inbound delivery gate; seeds the
persona defaults from the manifest on first read using the full connected set.
"""
persona = self._persona_of(session_id, persona_id)
@@ -658,13 +674,15 @@ class SessionManager:
persona, manifest, connected=connected
)
session_overrides = self.session_connections.get(session_id)
return set(
effective = set(
effective_connections(
connected=connected,
persona_defaults=persona_defaults,
session_overrides=session_overrides,
)
)
grant = self._persona_connector_grant(persona)
return effective if grant is None else effective & grant
def _inbound_connector_allowed(self, session_id: str, connector: str) -> bool:
"""Whether an inbound message on `connector` should be DELIVERED to `session_id` (§4.3).
@@ -823,6 +841,12 @@ class SessionManager:
connectors = connector_list(self.secrets)
by_name = {c["name"]: c for c in connectors}
connected_names = {c["name"] for c in connectors if c["connected"]}
# OPE-93 (owner-hit 2026-08-15): the drawer must show the persona's world, not the
# account's. An undeclared connector is not a mutable source of this session — it
# was rendering as toggled-ON while the engine (correctly) refused its tools.
grant = self._persona_connector_grant(persona)
if grant is not None:
connected_names &= grant
effective = self.effective_connectors(session_id, persona)
connected = [
{
+38
View File
@@ -283,3 +283,41 @@ def test_declared_connector_allowlist_gates_session_tools(tmp_path):
none = names_for(False)
assert not any(n.startswith(("linear_", "box_")) for n in none)
def test_allowlist_persona_drawer_and_effective_set_exclude_undeclared(
tmp_path, monkeypatch
):
"""OPE-93, owner-hit 2026-08-15: a fresh Cloud Posture session rendered Browser and
Slack as toggled-ON sources while the engine (correctly) refused their tools the
drawer and the inbound gate read the pre-allowlist hierarchy. All three surfaces now
share the persona grant, so an undeclared connector neither renders nor delivers."""
mgr = _mgr(tmp_path, monkeypatch)
_connect_github(mgr)
_connect_slack(mgr)
mgr.session_store.save(
SessionRecord(
session_id="sp",
workspace=str(mgr.default_workspace),
model="m",
mode="interactive",
agent="security", # manifest declares connectors: [github]
)
)
assert mgr.effective_connectors("sp", "security") <= {"github"}
assert mgr._inbound_connector_allowed("sp", "slack") is False
names = {
c["connector"]
for c in mgr.session_connections_view("sp", "security")["connected"]
}
assert names <= {"github"} # browser (always connected) and slack are absent
# Builder-based personas keep the unrestricted view — their sessions use the
# drawer/inbound path (channel bindings) even though they expose no connector tools.
_ops_session(mgr, "sg")
ops_names = {
c["connector"] for c in mgr.session_connections_view("sg", "ops")["connected"]
}
assert "slack" in ops_names # undeclared for security, present for ops