How gstack Manages Scoped Token Authorization for Remote Agents: A Deep Dive into the Token Registry Architecture
gstack isolates remote agents by minting short-lived scoped tokens derived from a long-lived root secret, enforcing least-privilege access through JWT-based scope validation and automatic revocation on rotation.
The gstack browser daemon implements a hardened scoped token authorization for remote agents architecture that prevents privilege escalation by never exposing the permanent root credential to helper processes. Instead of sharing a single master key, the system maintains a hierarchical token registry in browse/src/token-registry.ts that issues cryptographically bound session tokens with granular capabilities, ensuring each remote agent operates within strict identity and resource boundaries.
The Root Token Foundation
When the browse daemon initializes, it generates a cryptographically secure root token that serves as the ultimate trust anchor. This long-lived secret persists in the server environment and is retrievable via the POST /token HTTP endpoint for authorized clients. According to the implementation in browse/src/token-registry.ts, this root token acts as the signing key for all descendant credentials, but it is never transmitted to remote agent processes.
The root token's lifecycle is managed through the rotateRoot() function. When administrators trigger a rotation, the registry immediately invalidates its internal signing material, causing all previously issued scoped tokens to fail validation with a 401 missing_scoped_token response. This guarantees that a compromise at the session level cannot survive a root-key refresh.
Minting Scoped Tokens via the Registry
Clients possessing the root token can request limited-capability credentials by specifying desired restrictions. The registry validates the root bearer, then issues a signed JWT containing the agent ID, scope restrictions, and expiration timestamp.
Direct Token Creation
For local tool invocations, the daemon exposes the /token endpoint. Clients POST a scope description—such as type: 'write' combined with a specific tabId—and receive a short-lived bearer token. As implemented in browse/src/token-registry.ts, this flow supports direct minting via CLI or HTTP, allowing automation to request exactly the permissions required without exposing broader access.
import fetch from 'node-fetch';
const ROOT_TOKEN = process.env.GSTACK_ROOT_TOKEN;
// Request a write-scoped token limited to tab 1234
const resp = await fetch('http://localhost:8123/token', {
method: 'POST',
headers: { Authorization: `Bearer ${ROOT_TOKEN}` },
body: JSON.stringify({ scope: { type: 'write', tabId: '1234' } })
});
const { token: scopedToken } = await resp.json();
Remote Agent Exchange Flow (/pair → /connect)
Remote agents—such as the pair-agent helper that runs secondary Claude or OpenAI instances—use a two-step exchange to obtain credentials without handling the root token. The process, verified in browse/test/pair-agent-e2e.test.ts, works as follows:
- Pairing: The agent presents the root token to
POST /pair. The daemon validates the root token and returns a one-time setup key. - Connection: The agent exchanges the setup key via
POST /connect. The daemon issues a fresh scoped token bound to the specific session the agent is about to initialize.
// Step 1: Pair to receive setup key
const pairResp = await fetch('http://localhost:8123/pair', {
method: 'POST',
headers: { Authorization: `Bearer ${ROOT_TOKEN}` }
});
const { setupKey } = await pairResp.json();
// Step 2: Connect to exchange for scoped token
const connResp = await fetch('http://localhost:8123/connect', {
method: 'POST',
body: JSON.stringify({ setupKey })
});
const { token: scopedToken } = await connResp.json();
Scope Enforcement and Validation
Every authorized request passes through getTokenInfo() in browse/src/server.ts, which extracts the bearer token, validates its cryptographic signature, checks expiration, and returns the parsed scope object. Command handlers such as /command consult this scope before execution.
Owner-only scopes prevent a token from acting on browser tabs it does not own, while write-scoped tokens can issue commands but cannot read sensitive session data. This enforcement happens at the middleware layer, ensuring that even if an agent process is compromised, the stolen credential cannot escape its original permission boundaries.
// Using a scoped token for targeted automation
await fetch('http://localhost:8123/command', {
method: 'POST',
headers: { Authorization: `Bearer ${scopedToken}` },
body: JSON.stringify({ command: 'click', args: ['#submit'] })
});
Security Guarantees: Rotation and Revocation
The token registry provides atomic revocation capabilities through root token rotation. When rotateRoot() is invoked, the registry clears its internal signing map, immediately invalidating every scoped token in circulation. This design ensures that credential rotation does not require tracking individual session tokens—a critical feature for high-churn agent environments.
Testing coverage in browse/test/server-auth.test.ts confirms that the middleware rejects root-only requests that omit a valid scoped token. Additionally, browse/test/pair-agent-tunnel-eval.test.ts verifies that scoped tokens crossing network tunnels are rejected for commands outside their allowance, such as an owner-only token attempting to create a new tab in another agent's context.
Summary
- gstack implements scoped token authorization for remote agents through a hierarchical registry in
browse/src/token-registry.tsthat separates long-lived root secrets from short-lived session credentials. - Remote agents obtain tokens via a pair/connect exchange that never exposes the root token to the agent process, as tested in
browse/test/pair-agent-e2e.test.ts. - The
getTokenInfo()function inbrowse/src/server.tsenforces granular scopes (owner-only, write-scoped) at the request middleware layer. - Root token rotation atomically revokes all descendant scoped tokens, preventing session hijacking from surviving a key compromise.
- Each skill spawn receives a unique scoped token that is never persisted to user-visible artifacts, confirmed by
browse/test/skill-token.test.ts.
Frequently Asked Questions
How does gstack prevent a compromised remote agent from accessing other agents' data?
Each remote agent receives a scoped token containing an agent ID and resource scope embedded in a signed JWT. The getTokenInfo() middleware in browse/src/server.ts validates this scope before executing commands. Owner-only tokens are cryptographically restricted to specific tab IDs, ensuring they cannot interact with sessions belonging to other agents even if the bearer token is exfiltrated.
What happens to active agent sessions when the root token is rotated?
All active sessions are immediately terminated. The rotateRoot() function in browse/src/token-registry.ts clears the internal signing key map, causing validation to fail for any request bearing a scoped token signed with the previous root. Clients receive a 401 missing_scoped_token response and must re-authenticate with the new root secret to obtain fresh credentials.
Can a scoped token be used to mint additional sub-tokens?
No. Only the root token can authorize the /token endpoint to create new scoped tokens. Scoped tokens themselves lack the signing authority to derive descendant credentials. This constraint maintains the principle of least privilege by ensuring that a compromised session token cannot be used to create siblings or children with different permissions.
How does the pair-agent workflow validate the setup key exchange?
The exchange is protected as a one-time use mechanism. When the daemon receives a valid root token at /pair, it generates a cryptographically random setup key stored in ephemeral state. The subsequent call to /connect must present this exact key to receive a scoped token. As verified in browse/test/pair-agent-e2e.test.ts, replaying a consumed setup key or presenting an invalid key results in authentication failure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →