PhysicsBase docs
PhysicsBase gives every AI agent a company runs its own identity, with scoped access to the APIs it uses, a full audit trail and a kill switch. The policy is set once, and no human has to approve each step.
Agents generate their own key, register themselves and get scoped five-minute tokens for each app. APIs that have never heard of PhysicsBase (Stripe, GitHub, your vendors) are reached through the credential vault, so agents never hold their secrets. Operators set the rules once with an enrollment key; apps verify agents and people with the same call.
Nothing to install: hosted MCP and proxy
The quickest way to connect an agent. Create an enrollment key in the dashboard, then give the agent one of these. PhysicsBase creates the agent on first use and signs for it, with the same policy, limits, activity log, alerts and kill switch as any other agent.
| Your agent speaks… | Give it |
|---|---|
| MCP (Claude Code, Cursor, the OpenAI Agents SDK, any MCP client) | Server URL https://www.physicsbase.ai/mcp with the header Authorization: Bearer pbk_… |
| HTTP (any language, script or no-code tool) | Put https://www.physicsbase.ai/proxy/ in front of the URL and send the same Authorization header |
curl -X POST https://www.physicsbase.ai/proxy/https://api.stripe.com/v1/refunds \
-H 'Authorization: Bearer pbk_…' \
-H 'X-PhysicsBase-Agent: support-bot-1' \
-H 'X-PhysicsBase-Reason: refund for order 8812' \
-d 'charge=ch_123'
| Header | Meaning |
|---|---|
Authorization: Bearer pbk_… | Required. The enrollment key decides the organisation and the policy. |
X-PhysicsBase-Agent | Optional agent name (default hosted-agent). Each name is a separate agent with its own identity, limits and kill switch. |
X-PhysicsBase-Reason | Why. Stored in the activity log. |
X-PhysicsBase-Scope | Actions to ask a PhysicsBase app for, e.g. notes:write. |
What happens: if the URL is an app that uses PhysicsBase, the call carries the agent's own five-minute, key-bound token. Any other API goes through your vault with the stored credential and a Web Bot Auth signature. The MCP endpoint offers the tools physicsbase_request, physicsbase_status and physicsbase_discover.
Clients that sign in: MCP clients that support OAuth need only the URL. They register themselves, send you to PhysicsBase to approve, and get a one-hour token that refreshes for 30 days. You pick the agent name and policy when you approve. Discovery: /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.
The trade-off: in hosted mode PhysicsBase keeps the agent's private key (encrypted in the vault), and the enrollment key works like a password. Anyone who has it can act as your agents within the key's policy until you revoke it. That is the same as with self-registration, where a leaked key lets someone register agents. Keep the key in your MCP client's or server's secret settings. When agents must hold their own keys, use the SDKs or the local gateway below. A revoked hosted agent stays revoked; use a new agent name to start over.
How it works: an API at the core, MCP on top
Authentication has to be an HTTP API, because the check happens on every request an agent makes to an app, and apps speak HTTP. MCP is how LLMs pick up tools, so PhysicsBase ships MCP as an adapter on top of the API, not instead of it. Every path below ends at the same token endpoint and the same one-call check in your app.
- RegisterThe agent makes an Ed25519 key and calls
POST /agent/register, signed by that key. With an enrollment key it acts for you; without one it acts as itself. - Get a tokenFor each app it calls
POST /agent/token(or the standard OAuth/tokenJWT-bearer grant). The server checks the policy and returns a five-minute token bound to the agent's key. - Call the appEvery request carries the token and a fresh DPoP proof signed by the key. Your app calls
pb.authenticate(req)and gets the caller, the agent and its scopes.
| Your agent is… | Use |
|---|---|
| Anything, with nothing installed | Hosted: MCP at /mcp or HTTP at /proxy/<URL> with Authorization: Bearer pbk_… (above) |
| Any agent, any language, no SDK | npx physicsbase-agent gateway, then call http://127.0.0.1:7777/<any URL> or MCP at http://127.0.0.1:7777/mcp |
| Node or TypeScript code | physicsbase-agent: agent.fetch(url) |
| Python code (LangChain, CrewAI, OpenAI or Anthropic SDK…) | pip install physicsbase: Agent.load().fetch(url) |
| An LLM in an MCP client calling HTTP APIs | npx physicsbase-agent mcp: tools physicsbase_request and friends |
| An LLM using a remote MCP server | npx physicsbase-agent connect https://…/mcp: no browser, no consent screen |
| Any other language | The HTTP API: OpenAPI spec, or any OAuth library that supports JWT-bearer and DPoP |
The local gateway
The fastest way to connect an existing agent. It holds the key, registers on first use and signs every request, so the agent only needs plain HTTP or MCP. It listens on loopback only and refuses requests from web pages; set PHYSICSBASE_GATEWAY_TOKEN to also require a bearer token.
PHYSICSBASE_ISSUER=https://www.physicsbase.ai PHYSICSBASE_ENROLLMENT_KEY=pbk_… npx -y physicsbase-agent gateway
curl http://127.0.0.1:7777/https://crm.example.com/api/tickets
curl -X POST -H 'X-PhysicsBase-Scope: tickets:write' -d '{"title":"Refund"}' \
http://127.0.0.1:7777/https://crm.example.com/api/tickets
LLM-readable instructions for all of this live at /llms.txt.
Connect your agent in two minutes
Each recipe needs only two values from the dashboard: your server address and an enrollment key (Enrollment keys → Create). The agent registers itself the first time it runs.
Claude.ai and Claude Desktop (sign in, no key to paste)
- Open Settings → Connectors → Add custom connector.
- Name it PhysicsBase and enter the URL
https://www.physicsbase.ai/mcp. Leave the OAuth fields empty. - Click Connect, sign in to PhysicsBase, choose the agent name and policy, and click Allow.
Claude then appears in your dashboard as an agent with its own activity log and kill switch. The same sign-in works for any MCP client that supports OAuth (ChatGPT connectors, Cursor, VS Code): give it the URL and no headers. Revoking the agent or its policy disconnects it.
Claude Code
# hosted, nothing to install
claude mcp add --transport http physicsbase https://www.physicsbase.ai/mcp \
--header "Authorization: Bearer pbk_…"
# or keep the agent's key on this machine
claude mcp add physicsbase \
-e PHYSICSBASE_ISSUER=https://www.physicsbase.ai \
-e PHYSICSBASE_ENROLLMENT_KEY=pbk_… \
-- npx -y physicsbase-agent mcp
Cursor and other clients that take a URL
{
"mcpServers": {
"physicsbase": {
"url": "https://www.physicsbase.ai/mcp",
"headers": { "Authorization": "Bearer pbk_…", "X-PhysicsBase-Agent": "cursor-agent" }
}
}
}
Claude Desktop and clients that launch a local command
Add this to the client's MCP settings, for example claude_desktop_config.json for Claude Desktop. The agent's key stays on the machine.
{
"mcpServers": {
"physicsbase": {
"command": "npx",
"args": ["-y", "physicsbase-agent", "mcp"],
"env": {
"PHYSICSBASE_ISSUER": "https://www.physicsbase.ai",
"PHYSICSBASE_ENROLLMENT_KEY": "pbk_…"
}
}
}
}
The model gets a physicsbase_request tool. It calls PhysicsBase apps with its own token and any other API through your vault, so you never paste an API key into a prompt.
OpenAI Agents SDK (Python), hosted MCP
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async with MCPServerStreamableHttp(name="physicsbase", params={
"url": "https://www.physicsbase.ai/mcp",
"headers": {"Authorization": "Bearer pbk_…", "X-PhysicsBase-Agent": "billing-bot"},
}) as pb:
agent = Agent(name="billing-bot", instructions="…", mcp_servers=[pb])
print((await Runner.run(agent, "Refund order 8812")).final_output)
OpenAI Agents SDK (Python), agent keeps its key
from agents import Agent, Runner, function_tool
from physicsbase import Agent as PhysicsBase
pb = PhysicsBase.load() # reads PHYSICSBASE_ISSUER and PHYSICSBASE_ENROLLMENT_KEY
@function_tool
def call_api(method: str, url: str, body: str = "", reason: str = "") -> str:
"""Call an HTTP API as this agent. Never ask for API keys: PhysicsBase signs the request
or uses the organisation's vault. Always give a short reason."""
r = pb.fetch(url, method=method, body=body or None, reason=reason or None,
headers={"Content-Type": "application/json"} if body else None)
return f"HTTP {r.status}\n{r.text()[:4000]}"
agent = Agent(name="billing-bot", instructions="…", tools=[call_api])
print(Runner.run_sync(agent, "Refund order 8812").final_output)
LangChain (Python)
from langchain_core.tools import tool
from physicsbase import Agent as PhysicsBase
pb = PhysicsBase.load()
@tool
def call_api(method: str, url: str, body: str = "", reason: str = "") -> str:
"""Call an HTTP API as this agent. PhysicsBase handles sign-in and API keys."""
r = pb.fetch(url, method=method, body=body or None, reason=reason or None)
return f"HTTP {r.status}\n{r.text()[:4000]}"
# pass call_api in your agent's tools list
Node (any framework)
import { PhysicsBaseAgent } from 'physicsbase-agent';
const pb = PhysicsBaseAgent.load();
const res = await pb.fetch('https://api.stripe.com/v1/refunds', {
method: 'POST', body: 'charge=ch_123', reason: 'Refund order 8812' });
Anything else: the local gateway
For any language or tool that can make HTTP requests, including shell scripts, n8n, Zapier code steps and Go or Rust services:
PHYSICSBASE_ISSUER=https://www.physicsbase.ai PHYSICSBASE_ENROLLMENT_KEY=pbk_… npx -y physicsbase-agent gateway
curl -H 'X-PhysicsBase-Reason: nightly sync' http://127.0.0.1:7777/https://api.example.com/things
Concepts
| Person | Signs up with an email code once, then signs in with a passkey. |
|---|---|
| Agent | An LLM, script or assistant with its own Ed25519 key. It never holds a password or a long-lived secret. |
| Independent agent | An agent that registered itself with no owner. It acts as itself and gets its own account at apps that allow agent sign-up. |
| Enrollment key | A pbk_ key an operator creates once with a policy. Agents started with it register themselves and act for the operator, with no human involved. |
| Mandate | The policy an agent runs under: which apps, which actions, optional actions that pause for a person, and when its identity ends. |
| Organisation | A team that owns agents, enrollment keys, apps and connections. Members are owners, admins or viewers. Everyone also has a personal organisation. |
| Connection | A credential for a third-party API (bearer token, custom header or basic auth), stored encrypted in the vault. Agents use it through the broker and never see it. |
| Limits | Per-agent caps on requests per minute and per day, set on the enrollment key. Tokens and vault calls both count. |
| App | Your product. You register it to get a client ID and declare the actions (scopes) agents can request. |
| Agent token | A five-minute token for one app, bound to the agent's key. Useless without that key. |
Add it to your app
1. Register the app
Open the dashboard, choose Your apps, and enter your app's URL and the actions agents may request, such as notes:read notes:write notes:delete. You get a client ID.
2. Install the package
npm install physicsbase-site
3. Wire it up (Express)
import express from 'express';
import { PhysicsBase, PhysicsBaseAuthError } from 'physicsbase-site';
const pb = new PhysicsBase({
issuer: process.env.PHYSICSBASE_ISSUER, // your PhysicsBase server
clientId: process.env.PHYSICSBASE_CLIENT_ID,
appUrl: 'https://notes.example.com',
secret: process.env.SESSION_SECRET, // 32+ random characters
name: 'Notes',
scopes: { 'notes:read': 'Read your notes', 'notes:write': 'Add notes', 'notes:delete': 'Delete notes' },
});
const app = express();
app.use(pb.middleware()); // adds /auth/login, /auth/callback, /auth/logout, /.well-known/physicsbase.json
app.get('/api/notes', (req, res) => {
const who = pb.require(req.physicsbase, 'notes:read');
res.json(listNotes(who.sub)); // same sub for the person and their agents
});
app.use((err, req, res, next) =>
err instanceof PhysicsBaseAuthError ? pb.reject(res, err) : next(err));
Link people to /auth/login to sign in. After sign-in your app has its own session cookie; PhysicsBase is not in the request path again.
What you get on each request
req.physicsbase = {
kind: 'agent', // or 'human'
sub: 'q3m0cTl7hNf8…', // stable, and different at every app
email: 'amina@example.com', // if the person chose to share it with agents
agent: { id: 'agt_…', name: 'Claude on my laptop' },
scopes: ['notes:read', 'notes:write'], // null for people: full access to their own account
can(scope) { … }
}
Without Express
Call await pb.handle(req, res) first; it returns true when it served a PhysicsBase route. Then await pb.authenticate(req) returns a principal or null and throws PhysicsBaseAuthError for bad credentials. authenticate also accepts a standard Web Request, so it works in Next.js route handlers, Hono, Bun and Deno.
Other languages
Agent requests are standard: verify the access token (EdDSA JWT, typ: at+jwt) against /.well-known/jwks.json, check iss, aud equals your client ID and exp; then verify the DPoP proof per RFC 9449: signed by the key whose thumbprint is cnf.jkt, matching method and URL, ath equal to the token's SHA-256, a fresh iat and an unused jti. Reject a second use of any token that carries "once": true.
Build an agent
npm install physicsbase-agent
Zero setup: register on first use
Give the agent two environment variables. On its first request it generates a key, registers itself and starts signing requests. No human sees it.
PHYSICSBASE_ISSUER=https://www.physicsbase.ai
PHYSICSBASE_ENROLLMENT_KEY=pbk_… # omit to register as an independent agent
PHYSICSBASE_AGENT_NAME=support-bot-1 # optional
import { PhysicsBaseAgent } from 'physicsbase-agent';
const agent = PhysicsBaseAgent.load();
const res = await agent.fetch('https://crm.example.com/api/tickets'); // registers, gets a token, signs the request
Or register explicitly: await agent.register({ issuer, name, enrollmentKey }), or from the shell: npx physicsbase-agent register https://www.physicsbase.ai --key pbk_….
Optional: act as one specific person
When an agent must act inside one consumer's account, that person approves it once with a code. After that it runs unattended.
import { PhysicsBaseAgent } from 'physicsbase-agent';
const agent = PhysicsBaseAgent.load(); // key stored in ~/.physicsbase/agent.json
if (!agent.linked) {
await agent.link({
issuer: 'https://www.physicsbase.ai',
name: 'Research assistant',
onCode: ({ url, fingerprint }) => console.log(`Approve me at ${url} (key ${fingerprint})`),
});
}
const res = await agent.fetch('https://notes.example.com/api/notes', {
method: 'DELETE',
scope: 'notes:delete',
reason: 'You asked me to clear last week\'s notes', // shown to the person if approval is needed
onApproval: ({ url }) => console.log(`Waiting for approval at ${url}`),
});
fetch reads the app's /.well-known/physicsbase.json, gets a token for that app, signs a DPoP proof for the exact request, and waits for the person's approval when the mandate requires it. Omit scope to receive every action the mandate allows without approval.
Command line
npx physicsbase-agent link https://www.physicsbase.ai --name "My agent"
npx physicsbase-agent status
npx physicsbase-agent fetch https://notes.example.com/api/notes
npx physicsbase-agent fetch https://notes.example.com/api/notes -d '{"text":"hi"}' --scope notes:write
Python agents
pip install physicsbase
from physicsbase import Agent
agent = Agent.load() # registers itself on first use from PHYSICSBASE_ISSUER / PHYSICSBASE_ENROLLMENT_KEY
res = agent.fetch("https://crm.example.com/api/tickets", scope="tickets:read")
print(res.status, res.json())
Wrap agent.fetch as a tool in any framework. The command line works too: python -m physicsbase register|status|fetch.
MCP
Give an LLM authenticated HTTP
Add the MCP server to Claude Desktop, Claude Code or any MCP client:
{ "mcpServers": { "physicsbase": { "command": "npx", "args": ["-y", "physicsbase-agent", "mcp"] } } }
Add "env": { "PHYSICSBASE_ISSUER": "…", "PHYSICSBASE_ENROLLMENT_KEY": "pbk_…" } and the model registers itself the first time it calls an app. Tools: physicsbase_request, physicsbase_discover, physicsbase_status, physicsbase_register and the optional physicsbase_link.
Protect your own MCP server, and let agents in without a browser
Remote MCP servers normally send every client through a browser OAuth consent screen. With PhysicsBase, your MCP server verifies agent tokens like any API, and agents connect with no person involved.
// your MCP server (Streamable HTTP)
if (await pb.handle(req, res)) return; // serves /.well-known/oauth-protected-resource (RFC 9728)
const who = await pb.authenticate(req); // the agent, its owner type and scopes
if (!who) return pb.reject(res); // 401 with resource_metadata, as MCP clients expect
// the LLM client's config: a local bridge that signs every request
{ "mcpServers": { "helpdesk": { "command": "npx",
"args": ["-y", "physicsbase-agent", "connect", "https://helpdesk.example.com/mcp"],
"env": { "PHYSICSBASE_ISSUER": "…", "PHYSICSBASE_ENROLLMENT_KEY": "pbk_…" } } } }
A complete example lives in examples/mcp-server: three tools, each gated by a scope.
HTTP API
The full API is described in OpenAPI 3.1. Standard OAuth clients can use the JWT-bearer grant instead of /agent/token:
POST /token
DPoP: <proof signed by the agent key, htm=POST, htu=<issuer>/token>
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<physicsbase-agent+jwt>&resource=https://crm.example.com&scope=tickets:read
Management API
Everything you do in the dashboard for agents, keys and the vault, you can do from code: scripts, CI, Terraform, your own admin tools. Create a token under Dashboard → API tokens, then send it as a bearer token. A token belongs to one organisation.
export PHYSICSBASE_TOKEN=pbm_…
# who am I?
curl -H "Authorization: Bearer $PHYSICSBASE_TOKEN" https://www.physicsbase.ai/api/token
# store a credential in the vault
curl -X POST https://www.physicsbase.ai/api/connections \
-H "Authorization: Bearer $PHYSICSBASE_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Stripe (live)","host":"api.stripe.com","auth_type":"bearer","secret":"sk_live_…"}'
# an enrollment key for a fleet: 30 calls a minute, 500 a day, Stripe only
curl -X POST https://www.physicsbase.ai/api/enrollment-keys \
-H "Authorization: Bearer $PHYSICSBASE_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Billing bots","connections":["con_…"],"rate_per_min":30,"daily_limit":500}'
# kill switch
curl -X DELETE -H "Authorization: Bearer $PHYSICSBASE_TOKEN" https://www.physicsbase.ai/api/agents/agt_…
| Endpoint | What it does |
|---|---|
GET /api/token | The token's name, role and organisation |
GET /api/agents | Agents with their policy and today's usage |
PATCH /api/agents/:id | Change an agent's policy: connections, allowed_apps, allowed_scopes, ask_scopes, rate_per_min, daily_limit, expires_in_days |
DELETE /api/agents/:id | Revoke an agent. Takes effect on its next call. |
GET / POST /api/enrollment-keys | List or create keys. Same policy fields as agents, plus name and max_agents. The new key is returned once, as secret. |
DELETE /api/enrollment-keys/:id | Revoke a key and every agent it registered |
GET / POST /api/connections | List or add vault credentials: name, host, auth_type (bearer, header, basic), header_name, secret. Secrets are never returned. |
POST /api/connections/:id/rotate | Replace the secret; every agent uses the new one at once |
DELETE /api/connections/:id | Remove a credential |
GET /api/activity, /api/activity.csv | Search the activity log: q, agent, type, since, limit, before |
GET /api/usage, /api/overview | This month's agents and requests against the free plan |
GET / PUT /api/alerts | Alert settings: email, webhook_url, events |
GET / POST /api/apps, DELETE /api/apps/:id | Apps that use PhysicsBase for sign-in |
GET /api/approvals | Checkpoints waiting for a person (read only) |
Limits of a token, on purpose: a token cannot create or list tokens, manage the team, export or delete the organisation, approve an MCP connection or decide a checkpoint. Those stay with a signed-in person. A read-only token can only read. A token never does more than the person who made it can do today: if they leave the organisation, it stops working. Tokens expire after 90 days unless you choose otherwise, are limited to 600 requests a minute, and every change they make is in the activity log as api-token:<name>. Errors are JSON: {"error": "…", "error_description": "…"}.
Scopes and mandates
Scopes are resource:action strings your app declares. Mandates match them with patterns:
| Pattern | Matches |
|---|---|
* | Everything |
notes:* | Every notes action |
*:read | Reading anything, in any app |
notes:read | Exactly that |
By default nothing pauses for a person. If you want a human checkpoint on a few actions, list them under "Pause for a person", for example *:purchase. A token issued on such an approval covers one request, lasts 60 seconds and is rejected if reused. Independent agents never pause: there is no one to ask.
Apps decide whether independent agents may sign up. Turn off "Let agents sign up on their own" for an app and only agents acting for a person or organisation get tokens for it.
Vault, limits and Web Bot Auth
Most APIs an agent needs will never support PhysicsBase. Add each one under Connections in the dashboard: the host (for example api.stripe.com) and its credential. Then choose which connections an enrollment key may use. Agents call the API as usual; the SDKs, the gateway and the MCP tools send anything that isn't a PhysicsBase app through the broker automatically.
// Node: same call as for a PhysicsBase app
await agent.fetch('https://api.stripe.com/v1/refunds', { method: 'POST', body: 'charge=ch_123' });
# gateway: same URL scheme
curl -X POST -d 'charge=ch_123' http://127.0.0.1:7777/https://api.stripe.com/v1/refunds
For every brokered call PhysicsBase checks that the agent is active and acts for an organisation, that its policy allows the connection and that it is within its limits. It then injects the credential, adds Web Bot Auth headers, makes the request (https only, private networks blocked, redirects not followed, 30 second timeout, 5 MB cap) and records it in the activity log. Independent agents have no vault.
Web Bot Auth. Brokered requests are signed per the IETF Web Bot Auth draft (HTTP Message Signatures, RFC 9421): Signature-Agent names your PhysicsBase server, and the public key is published at /.well-known/http-message-signatures-directory. Sites and CDNs that verify bot signatures can see the request came from your operator rather than an anonymous scraper. examples/vendor-api shows a server verifying it.
Limits. Set "requests per minute" and "requests per day" on an enrollment key. Over the limit, token and broker calls return HTTP 429 with rate_limited or daily_limit, and the event appears in the activity log.
Kill switch. Revoke an agent, an enrollment key (every agent it created) or a connection (every agent loses that API at once). Tokens already issued expire within five minutes; broker calls stop immediately.
Audit trail and alerts
Activity records every registration, token, vault call, refusal, limit hit and change, tied to the agent (or person) and the policy behind it. Agents can say why they're making a call: pass reason to agent.fetch(), --reason to the CLI, X-PhysicsBase-Reason to the gateway or reason to the MCP tool. The reason is stored with the call.
Search by agent, person, app, API host, path or reason, and filter to refusals and limits, vault calls, tokens, agents or team changes. Download any search as CSV (GET /api/activity.csv?q=…&type=…). Under Team, owners and admins can export everything an organisation holds as JSON (GET /api/org/export). Secrets are never included.
Alerts go out when an agent is refused, hits a limit, or someone uses the kill switch. Choose the events, email to owners and admins, and an optional webhook. Slack incoming-webhook URLs get a plain message. Any other URL gets JSON signed in the Standard Webhooks format:
// verify a PhysicsBase webhook (Node)
const key = Buffer.from(secret.replace('whsec_', ''), 'base64');
const expected = 'v1,' + createHmac('sha256', key)
.update(`${headers['webhook-id']}.${headers['webhook-timestamp']}.${rawBody}`).digest('base64');
if (headers['webhook-signature'] !== expected) throw new Error('bad signature');
Repeats of the same event for the same agent are grouped: at most one alert per five minutes, with a count of the ones held back.
Teams
Create an organisation under Team and invite people by email. Owners and admins can change keys, apps, connections and agents; viewers can see everything, including the activity log, but change nothing. Invites are accepted automatically when the person signs in. Switch organisations from the menu at the top of the dashboard.
Under Team you also see this month's active agents and requests, can export the organisation's data, and, as an owner, can delete the organisation. Anyone can delete their own account under Account. That deletes their personal organisation and removes them from teams; an organisation's last owner must hand over ownership first.
Agents enrolled in a team organisation act for the organisation, not for the person who created the key: their tokens carry sub_type: "organization" and org_name, and nobody's email. In the site SDK, who.actingFor is "organization", "person" or "self".
Protocol
| Endpoint | Purpose |
|---|---|
GET /.well-known/openid-configuration | Discovery, including the agent endpoints |
GET /.well-known/jwks.json | Public signing keys |
GET /authorize, POST /token | Sign-in for people: authorization code with PKCE (S256), returns an ID token |
POST /agent/register | Agent sends a physicsbase-register+jwt signed by its own key (with the public key in the header, and optionally enrollment_key); gets an agent ID immediately |
POST /agent/link | Agent sends {name, jwk}; gets a code for the person and a link ID to poll |
GET /agent/link/:link_id | Poll until approved; returns the agent ID |
POST /agent/token | Agent sends a signed assertion naming the app (resource) and scope; gets a DPoP-bound token, or approval_required |
GET /agent/approvals/:id | Poll an approval |
POST /agent/whoami | Agent reads its own mandate |
POST /agent/broker | Agent sends {assertion, method, url, headers, body} (body base64); PhysicsBase calls the API with the vault credential and returns {status, headers, body} |
GET /.well-known/http-message-signatures-directory | Web Bot Auth key directory, used by sites to verify brokered requests |
Agent assertions are EdDSA JWTs with typ: physicsbase-agent+jwt, iss and sub set to the agent ID, aud set to the issuer, a random jti and a lifetime of at most 120 seconds. Each is accepted once.
sub_type is "agent" for an independent agent, "organization" when it acts for a team and "person" when it acts for one person.
// an agent access token, decoded
{ "iss": "https://www.physicsbase.ai", "aud": "app_…", "sub": "q3m0cTl7…",
"act": { "sub": "agt_…", "name": "Claude on my laptop" },
"sub_type": "person", "scope": "notes:read notes:write", "cnf": { "jkt": "…" },
"email": "amina@example.com", "iat": 1792000000, "exp": 1792000300, "jti": "…" }
Security model
- No shared secrets with agents. The agent generates its key locally. PhysicsBase stores only the public half.
- Stolen tokens don't work. Every request needs a fresh proof signed by the agent's private key, tied to the method, URL and token.
- Registration proves key possession. Every registration is signed by the key being registered, and each assertion is accepted once.
- Enrollment keys are policy, not power. A leaked
pbk_key can only create agents inside its policy and agent limit. Revoking it revokes every agent it created. - Linking is phishing-resistant by design. The person approves while signed in and sees the agent's key fingerprint, which the agent also shows.
- Agents never hold API secrets. Third-party credentials stay in the vault, encrypted with AES-256-GCM. The encryption key is kept apart from the database, so a copy of the database alone can't open the vault. Rotating one connection's secret updates every agent at once.
- The broker can't be pointed inside your network. In production the address each outbound connection actually reaches is checked, so private ranges, cloud metadata addresses and DNS-rebinding tricks are refused. Redirects are never followed.
- Revocation is fast. Revoked agents can't get tokens; tokens they hold expire within five minutes.
- Pairwise IDs. Apps can't correlate a person across sites by user ID.
- Prompt injection is still the main risk. A manipulated agent can misuse what its policy allows. Keep policies narrow; use "pause for a person" only where a human checkpoint is worth the wait.