A development report on SessionDock, based on the project state of September 5, 2026. SessionDock is not yet a released, end-to-end application.
T...
For further actions, you may consider blocking this person and/or reporting abuse
The epoch checks host-side state. So does process/argv verification, and conflict keys sit on the same side of the line. But expected_tenant is a claim about the remote service's state, and nothing in the design ever asks that service which identity the browser is actually holding. Your own line about "expected" points at this and then stops.
Manual login is where it opens up. Nothing binds the login a person actually performed to the binding's expected account, so signing into tenant B inside a slot bound to tenant A is a normal-looking action that produces no error. Persistence makes it durable. The profile preserves the wrong login across restarts through the same mechanism that makes the slot useful in the first place. Every later agent run passes every check and writes to the wrong tenant.
The epoch cannot catch that, because the epoch catches stale authority. This authority is current. It just points somewhere else, so no generation changes and no monotonic counter fires. Cookie-jar inspection misses it too when the tenant switch happens over SSO on one domain with the same cookie names.
What would close it is an identity probe against the server at lease grant. One authenticated request through the slot's session to the application's own "who am I" endpoint, compared against the binding's expected account and tenant, with a digest of the returned identity fields stored on the lease. Dispatch then compares that digest next to the epoch. A mismatch should fail the lease rather than the single action, since an identity error was never about one click. One request per grant, and "expected" starts meaning checked.
The probe is a sample, though, so it should not be sold as an invariant. A session can be revoked server-side mid-lease. An SSO reauth can quietly drop back to a default tenant. Give the probe result its own timestamp and let dispatch call identity stale past a bound, the way it already calls control stale. That gives you a maximum age for the evidence, which is a weaker and more honest claim than continuous correctness.
Yes, I think this identifies a real missing boundary.
expected_accountandexpected_tenantare currently binding claims, not verified remote state. A perfectly current lease can therefore still point at the wrong authenticated tenant.The direction you describe makes sense: verify the identity through the slot's authenticated session when granting the lease, bind that result to the lease, and fail the lease rather than an individual action if the identity does not match.
I also agree with the distinction in your last paragraph. Such a probe is evidence with an age, not a permanent invariant. Server-side revocation, SSO reauthentication, or tenant switching can invalidate it later.
So the useful contract is probably closer to “remote identity verified no more than X ago” than “this slot is continuously guaranteed to be tenant A.”
"Verified no more than X ago" makes sense, but X is still a free parameter. Nothing in the system derives it. The quantity that grows with X is the number of writes requiring review after a mismatch, so I'd pick X from the blast radius of a wrong-tenant write. A latency budget can't justify that exposure.
Before shrinking X, though, I'd stop treating identity verification as a separate request. Many authenticated applications already echo identity in ordinary responses, perhaps a tenant id in a header or an account slug in a JSON envelope. Where that marker reliably describes the context of the operation, the dispatcher can check responses it's already receiving. That gives near-continuous sampling at zero extra requests, and the grant-time probe only has to establish which marker to inspect and its expected value. A write response still arrives after the write. Detection there doesn't undo it.
Plenty of applications expose nothing usable. For those you really are sampling, and X comes back. Even an available marker has to describe the authenticated operation's tenant rather than some unrelated page context.
Either way, attach the last good probe digest to each dispatched write as a recorded precondition, together with the verification event that produced it. The event matters because repeated successful checks return the same digest. On mismatch, the potentially damaged set is exactly the writes recorded since the last successful verification, plus the write whose response exposed it. That's an enumerable review set bounded by the evidence window, rather than auditing everything since lease start. It makes a silent error traceable. Prevention stays the stronger claim.
Would you let an identity-failed lease recover through re-probing, or make it terminal, given that writes already made stay in that review set either way?
I like the idea of deriving X from the acceptable blast radius rather than from a latency budget. That gives the evidence window an actual risk meaning.
Reusing identity markers from normal application responses is also interesting. Where the marker genuinely represents the authenticated context of that operation, it could turn the grant-time probe into the starting point and subsequent responses into continuous evidence without additional requests.
And recording the verification event, not just the digest, makes the review set much cleaner: everything since the last known-good evidence becomes enumerable.
On your last question, I am leaning toward making an identity mismatch terminal for that lease. Re-probing may establish that the identity is correct again, but it cannot restore the broken chain of evidence or remove the writes already in the review set.
So recovery would mean reconciliation/quarantine followed by a fresh lease and fresh identity evidence, rather than silently reviving the existing one.
This is exactly the kind of distinction we are still working through in the design, so this input is very useful.
Terminal fits the broken evidence chain. It also creates an incentive problem, because termination is expensive while sampling frequency stays an operator-controlled dial.
Less sampling looks healthier. If the dashboard reports terminations per day, cutting probes improves that number while increasing exposure to wrong-tenant writes, since fewer mismatches ever become visible.
A better primary signal is the distribution of identity-evidence age at write dispatch: dispatch time minus the timestamp of the newest qualifying evidence available to that write. Record it for every dispatched write. When sampling slows the distribution shifts toward older evidence, so degraded observation shows up as a degraded number. Writes with no evidence at all need their own bucket rather than dropping out of the denominator.
That only works if the evidence classes stay separate. A passive marker establishes the identity associated with that response, subject to the marker's integrity. An active probe establishes the identity returned for a request the dispatcher constructed. Folding both into one
last_verifiedfield lets ordinary traffic keep refreshing evidence that answers a different question.Two timestamps, then. Dispatch can require an active probe within a longer bound and a passive marker within a shorter one, both bounds derived from the acceptable exposure, with each write referencing the specific evidence events that satisfied the predicates.
One wrinkle on the trust boundary. A misrouted or compromised proxy can rewrite a passive marker into agreement, because the marker rides inside the channel under suspicion. An active probe through that same proxy still needs an authenticated binding to the application's own answer. Constructing the request is not sufficient on its own.
The same distinction decides quarantine scope. If passive markers stay suspect they cannot advance the last-known-good boundary, so the review set should begin at the last trusted active probe and conservatively include writes whose ordering against that probe is uncertain. Otherwise the set stays enumerable while quietly excluding writes the evidence never cleared.
That distinction between evidence age and termination count is a very useful one. A low termination rate is meaningless if we can achieve it simply by looking less often.
I also agree that active and passive evidence should not collapse into a single
last_verifiedvalue. They answer different questions and have different trust properties, so each dispatched write should reference the actual evidence events that satisfied its policy.Your proxy example is particularly important. An active probe is only stronger if we can authenticate that the identity answer really belongs to the application/session we intended to verify. Simply constructing the request ourselves is not enough.
So I think the model is moving toward separate evidence classes, separate age bounds, and a conservative last-known-good boundary for quarantine. Passive markers can improve observation frequency where they are trustworthy, but they should not silently promote themselves to the same assurance level as an authenticated active probe.
And yes, measuring evidence age at write time seems like a much healthier operational signal than counting identity failures after the fact.
the delegated-action part is what cost me the most. two agents on one machine sharing a browser, and when control moved, the action that was already in flight had landed. it just never reported back.
so the retry wrote it twice. what I do now is read the field's own length before retrying anything, because the tool result is gone but the page still knows.
does a lease expiring mid-action tell you whether that action committed, or only that the lease ended? that's the gap I never closed, and I ended up giving each agent its own machine instead of solving it.
That is exactly the distinction we need to make explicit.
A lease expiring only tells us that the authority ended. It does not tell us whether an in-flight action committed.
So if the action already reached the browser but the result is lost, we have to treat it as potentially successful and move it to
OUTCOME_UNKNOWN. No automatic retry should happen until the actual application state has been reconciled.Your example is a good illustration of why “no result” must never be treated as “no effect.”
OUTCOME_UNKNOWN is the right name for it. the part I'd flag is what resolves it.
reconciling needs somewhere to look, and in the browser case the only thing that knew was the page itself. reading the field's length worked because the page was still sitting there. when the other agent had already navigated away, that evidence was gone and there was nothing left to reconcile against. the state just stayed unknown and something still had to decide.
I never got further than opening it myself and looking, which is slow and doesn't hold up past a couple of agents.
Yes, I think that exposes an important assumption in my use of “reconciliation”: there has to be an independent source of truth to reconcile against.
The DOM may provide one while the page is still there, but SessionDock cannot rely on that. After navigation, the useful evidence may simply be gone.
So I think reconciliation has to become an explicit capability of an action or application: perhaps re-read the record, query an API, find a resulting object or identifier, or use some other application-specific postcondition. The pre-action context can help with that, but it cannot prove that the write committed.
If no trustworthy reconciliation path exists,
OUTCOME_UNKNOWNhas to remain unknown and require human review rather than eventually turning into an automatic retry.That is a useful correction to the model: defining the unknown state is only half the problem; we also need to define what evidence is allowed to resolve it.
"What evidence is allowed to resolve it" is the sharper half, and I hadn't put it that way. Leaving it unknown when nothing can prove the write is the honest default, even though it's the one that costs a person.
Exactly. The human cost is uncomfortable, but I think that is preferable to inventing certainty where none exists.
If we cannot produce evidence that a write committed or did not commit, the system should preserve that uncertainty and escalate it rather than resolving it through an unsafe retry.
Your example helped sharpen that for us:
OUTCOME_UNKNOWNneeds explicit rules for what evidence may resolve it, not just a state-machine transition that eventually makes it disappear.Splitting this into three concerns — local browser data, the right to control, and conflicts over application data — is what makes the epoch check readable, so I'll poke at the third one.
Conflict keys only coordinate what the scheduler dispatches. A person who takes a lease and clicks through the viewer never declares a key, so nothing stands between them and agent A editing the same product record in another slot. The scheduler cannot see a click it did not dispatch.
Having a human lease hold the conflict keys of its binding for its whole duration would close that half, at the cost of blocking agents while someone is only looking.
Yes, that is another real boundary.
Conflict keys only protect work that goes through the scheduler. A human interacting directly with the viewer is otherwise invisible at the application-conflict layer.
Holding the binding's known conflict scope for an interactive human lease is probably the conservative default. It may over-block agents while the person is only inspecting something, but that is preferable to pretending we can infer arbitrary application-level conflicts from browser isolation.
I think we may need to distinguish read-only observation from interactive takeover here. The harder limitation remains: if the binding does not describe the affected application resource, SessionDock cannot magically discover that conflict from a human click either.
The epoch fencing at the action dispatcher is the cleanest part of this design. In human-agent handoffs, delayed event dispatch is where state corruption usually happens. An agent prepares a click three seconds before the operator grabs focus, and the action fires into whatever page the human just navigated to. Checking a monotonic epoch number right before the CDP event dispatches turns that race condition into a deterministic drop.
The edge that gets messy in practice with headless slots is in-flight requests that cleared the browser before the epoch bumped. The click was authorized when sent, but the server response and subsequent redirect land after the lease expired. If the agent loop watches DOM mutations to plan its next move, canceling pending fetch channels or tearing down the network listener on lease handover saves a lot of phantom state errors.
Yes, that is exactly the gap.
The epoch already fences actions that have not been dispatched yet. Once a request has left the browser, however, revoking the lease cannot undo its server-side effect.
So the handover also needs an observation fence: delayed DOM, network, redirect, and lifecycle events from the old generation must be discarded before they influence further planning.
If the outcome of an already dispatched request is no longer reliable, we should treat it as
OUTCOME_UNKNOWNand avoid blind retries.So I think your “phantom state” point is exactly right: stale outgoing actions and stale incoming observations are really the same handover problem.
The lease and the epoch fence the dispatcher, but they only cover actions that arrive through it. If a slot's Chromium is launched with a remote debugging port, that endpoint is unauthenticated by design: I ran a plain
GET /jsonagainst a running instance on this machine just now, no header and no token, and got back the full target list including page URLs, and a WebSocket from there drives the page directly. That is the same host-user boundary you already name for file access, except it lands on the browser itself, so a stale-epoch agent with file access never has to go near the dispatcher. The check that survives it is reading the identity of the browser actually being driven off the running process, the--user-data-dirof the PID holding the control port, rather than off the slot record, since those two can diverge with nothing in the daemon noticing.That is a fair point.
SessionDock does not currently launch Chromium with
--remote-debugging-portor--remote-debugging-pipe; the intended control path is the packaged extension usingchrome.debugger.But you are right that “we did not configure it” is not a sufficient security property. We need to verify the running browser, not just trust the slot record.
That means checking the live process argv and its actual
--user-data-dir, and treating any unexpected DevTools listener or/jsonendpoint as a hard failure.So the stronger invariant is: no externally reachable CDP control path, and the browser process must be cryptographically/operationally tied back to the daemon-owned slot state.
The argv check closes the case where a stale epoch starts its own browser, but Chrome's process singleton gives that actor a path that creates no process to inspect. Measured a few minutes ago on Chrome 152 / macOS: running the browser binary a second time with the live profile's
--user-data-dirprintedOpening in existing browser session.and exited in 0.07 s real, while the target list on the existing browser's port went from 4 to 5 with the new entry being a page. So an agent that only knows the slot's profile path can get a URL opened inside the logged-in browser owned by the current epoch, and there is no new argv and no new listener for the daemon to enumerate. It needs the same host-user access to that directory that you already treat as in scope, and it hands over a command line rather than DOM control, so the property worth pinning is exclusive ownership of the profile directory, not only the process list.Exactly. We are still in the architecture and validation phase, so this kind of input is genuinely useful.
We reproduced the ProcessSingleton behavior on Linux as well, which means we now have a concrete case to test against rather than just an architectural assumption.
At the moment we are looking at two levels of isolation: closing the profile/singleton boundary properly in the current Linux design, and evaluating a per-slot MicroVM as a possible higher-assurance execution domain later on.
The VM would not replace leases, epochs, identity checks, or conflict handling, but it could give us a much cleaner local boundary if the threat model includes arbitrary code running under the same host user.
That is exactly why I wanted to write about SessionDock before calling it finished. Comments like yours expose the places where an architecture still relies on an assumption instead of evidence.
The distinction between browser isolation and application isolation is especially important here. Two agents can have completely separate profiles, sessions, and workspaces while still competing over the same server-side record. That means the real coordination boundary often has to exist above the browser itself. I like the use of conflict keys for known shared resources, but it also highlights a harder problem: agents need some awareness of the resources their actions can affect, otherwise local isolation can create a false sense of safety.
Yes, that is exactly the distinction we are trying to keep explicit.
Browser isolation can tell us that two agents are using different local state. It cannot tell us that their actions are independent at the application level.
Conflict keys only work where the affected resource is known, so they depend on some application-aware description of the action. For a product update that might be a product ID; for a broader workflow it may need a larger conflict scope.
Where SessionDock cannot determine that scope reliably, the safe answer is not to assume independence.
That is also why I see conflict handling as a separate policy layer above the browser rather than another property of the slot itself.
The late-arriving action example is the part that bites in practice. We run several agents against a shared Chrome over CDP, and the recurring failure was exactly this: a click or Runtime.evaluate prepared against a tab that another agent had just navigated elsewhere. Target IDs stay valid across navigations, so the call still succeeds and lands on the wrong page. Our workaround is re-checking the target URL right before every input dispatch, a poor man's version of your authoritative ownership record. How does SessionDock revoke in-flight actions: a lease with expiry, or does a control handover explicitly cancel anything queued?
Yes, that is exactly the race we are trying to fence.
We do not want lease expiry to be the only mechanism. A control handover explicitly revokes the lease and cancels work that is still queued in the broker.
Immediately before an effective browser dispatch, the executor is also supposed to re-check the lease/epoch, target and document generation. So a target that survived a navigation is not enough by itself to authorize the action.
The hard boundary is anything already handed to Chromium. We cannot reliably “unsend” that. If control changes after dispatch and we lose a trustworthy result, the action has to become
OUTCOME_UNKNOWNrather than being retried automatically.Your URL re-check is essentially the same class of protection; we are trying to make it part of the authoritative dispatch contract rather than an agent-side convention.