Omniscio documentation
Browse all documentation
  1. Getting Started19
  2. Sessions & Agents140
  3. Inbox & Notifications72
  4. Projects & Tasks102
  5. Automation & Scheduling77
  6. Knowledge & Memory29
  7. AI Features75
  8. Integrations115
  9. Plugins & Marketplace44
  10. Cloud & Teams53
  11. Settings & Customization82
  12. Account & Billing29
  13. Troubleshooting86
  14. CLI & API Reference29
  15. Legal & Policies5
  16. Uncategorised22

Session Read Scoping — Who May See Which Session

A session's contents can only be read by a caller allowed to see that session. A phone or browser paired to your computer now has to pass that rule before it can read a session's full activity record, and a session lookup that names one session by its id must say, in code, whose scope it is reading under — a lookup that stays silent is refused at build time.

What it is

Every session belongs to exactly one person: the person signed in on this computer. Omniscio keeps that person's sessions in a database on their own machine, and a phone or browser paired to that machine is that same person's device. There is no second account in that database to leak to.

The risk is different, and real: anything that can name a session by its id can ask for it, and an id is easy to see in a URL or to guess. A lookup that answers for any id hands over a session the caller had no business reading. Session read scoping is the rule that stops that, plus the check that stops the rule being forgotten.

Two things narrow what a caller may read:

  • Who is asking. Your own desktop app may address any session it holds a live reference to — it is the application you are looking at. A device reaching in over the web bridge is held to a stricter rule, because a pairing token can leak: it may only read a session the app already treats as visible.
  • Which scope was declared. A lookup naming one session by id must declare its scope: either "the person who owns this install" or an explicit set of projects. There is no default. A scope of no projects at all is a mistake rather than a wildcard, and is treated as one.

Where to find it

There is nothing to switch on and no setting to change. The behaviour is always on, and it applies to the two places your data leaves the app:

  • a phone or browser paired over the web bridge, and
  • your own desktop app, which keeps its existing behaviour unchanged.

How it behaves

Your desktop app is unchanged. The owner scope adds no condition at all, so the database statement your desktop app runs is the statement it always ran — same rows, same timing, same statement cache.

A paired device is narrowed. Before this change, a device holding a valid pairing token could ask for any session's trajectory — the full record of every tool the agent ran, including raw tool input and output — just by naming its id, whether or not that session was one the device should see. A hidden session (a background recipe run, a parked session, a removed share) was readable at that one address. It is not any more.

A refusal tells you nothing. A refused id answers with the same empty result a session with no transcript gives, so the answer is identical whether the session does not exist or the caller may not see it. That matters as much as the refusal: an error saying "not allowed" would confirm the id is real.

The wider picture, honestly. A pairing token is still one token for the whole install, so a phone sees your sessions, not a subset of them chosen per device. This change narrows the specific per-id reads that can be reached by guessing an id; it does not turn the bridge into a per-device permission system. Several list and search reads remain unscoped and are named as such in the code, so the remaining work is a readable list rather than a guess.

For agents

  • The scope vocabulary and the fail-closed predicate live in session-read-scope.ts: SessionReadScope is install-owner or projects, INSTALL_OWNER_SCOPE is the desktop scope, and an empty project list THROWS rather than widening to every row.
  • sessionVisibleForWebRemoteAsync(id, lane, scope) takes its scope as a REQUIRED argument, and getSessionInScope(scope, id) is the scoped point read. A new point read composes sessionScopePredicate rather than hand-writing a clause.
  • The build-failing census is session-read-scope-chokepoint.test.ts: every FROM sessions … WHERE id = ? under src/main must either carry a project_id IN (…) predicate or appear in session-read-scope-register.ts with a count and a reason. The register's kind is the honest part — internal (nothing outside the machine supplies the id), wire-gap (a caller can supply it and nothing narrows it yet), or broad (the general reader most of the app calls).
  • Adding a new point read: either compose the scope, or add a register row with a TRUE reason. The build fails on a new unregistered read, on a count that grew, and on a row that went stale.

Related

Last verified 2026-10-25