Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,34 @@ BROWSERBASE_PROJECT_ID=
BELL_MODEL=gpt-5.6-sol
BELL_DEMO_URL=https://bell-stack-demo-site.vercel.app/protected
STAGEHAND_BROWSER=browserbase
# Disable network logging before any merchant credentials are presented.
BROWSIE_LOG_SESSION=false
# Optional shared-runtime features:
# Hosted browser logging and recording are always disabled.
# Optional browser runtime features:
# Local handoff only. For phone use, set this to Bell's deployed HTTPS origin.
BROWSIE_PUBLIC_URL=http://127.0.0.1:4320
BROWSIE_HANDOFF_SECRET=
# Clean production 1Password OAuth callback; no query parameters or fragment.
ONEPASSWORD_REDIRECT_URI=https://bell-stack.vercel.app/api/onepassword/callback
ONEPASSWORD_CLIENT_ID=
ONEPASSWORD_CLIENT_SECRET=
ONEPASSWORD_OAUTH_STATE_SECRET=
# Local desktop access: account name from the 1Password sidebar, not your email.
BELL_PUBLIC_URL=http://127.0.0.1:4320
BELL_HANDOFF_SECRET=
# Optional HTTP Basic access for one trusted operator; password must be at least 32 bytes.
BELL_OPERATOR_USERNAME=
BELL_OPERATOR_PASSWORD=
# 1Password uses the SDK by default. Select cli explicitly for existing CLI setups.
BELL_ONEPASSWORD_BACKEND=sdk
# SDK desktop access: sidebar account name or account UUID. CLI access: account UUID/address.
OP_ACCOUNT=
# Hosted access: a service-account token takes precedence over OP_ACCOUNT.
OP_SERVICE_ACCOUNT_TOKEN=
# Existing local encrypted vault files remain readable; no vault-writing API is provided.
BELL_VAULT_FILE=.bell/vault.enc.json
BELL_VAULT_MASTER_KEY=
# Planned per-user 1Password OAuth integration. The callback does not yet connect a vault.
# Set the registered HTTPS callback URL for your own Bell deployment.
ONEPASSWORD_REDIRECT_URI=
ONEPASSWORD_CLIENT_ID=
ONEPASSWORD_CLIENT_SECRET=
ONEPASSWORD_OAUTH_STATE_SECRET=
# Optional Linq messaging. Keep tokens and your personal number in .env.local.
LINQ_API_KEY=
LINQ_WEBHOOK_SECRET=
BELL_LINQ_PHONE_NUMBER=+12025550100
# Your Linq receiving line in E.164 format, starting with + and country code.
BELL_LINQ_PHONE_NUMBER=
# Comma-separated E.164 numbers. Empty accepts nobody.
BELL_LINQ_ALLOWED_SENDERS=

Expand All @@ -33,12 +42,21 @@ BASELAYER_API_KEY=
BASELAYER_PRINCIPAL_REF=
BASELAYER_ALLOWED_ORIGINS=https://bell-stack-demo-site.vercel.app

# Optional Link OAuth for allowlisted Linq senders. No static wallet token is used.
# Register <BROWSIE_PUBLIC_URL>/api/link/oauth/callback with your Link OAuth client.
# Optional database-free Link sandbox for one verified Linq principal.
# Inject the access token into the process environment; do not save it in this file.
LINK_SANDBOX=1
LINK_SANDBOX_ACCESS_TOKEN=
LINK_SANDBOX_PRINCIPAL_ID=

# Optional real Link OAuth for allowlisted Linq senders (LINK_SANDBOX=0).
# Register <BELL_PUBLIC_URL>/api/link/oauth/callback with your Link OAuth client.
# Apply migrations/001_link_oauth.sql before connecting a wallet. See docs/link.md.
DATABASE_URL=
LINK_CLIENT_ID=
LINK_CLIENT_SECRET=
STRIPE_PUBLISHABLE_KEY=
# Generate once: openssl rand -base64 32. Keep stable across instances and restarts.
LINK_TOKEN_ENCRYPTION_KEY=

# Exact trusted checkout/frame origins per approved merchant (JSON object).
LINK_CHECKOUT_ORIGINS={}
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ jobs:
with:
node-version: 24
- run: pnpm install --frozen-lockfile
- run: pnpm check:workspace
- run: pnpm check
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
node_modules/
.next/
.eve/
.browsie/
.bell/
contexts.json
*.enc.json
.output/
.vercel/
.env*
Expand Down
75 changes: 42 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

<img width="512" height="512" alt="pixel-bell-512" src="https://github.com/user-attachments/assets/f4c9363d-2ab0-4205-a3c7-7d7ad61ec6c6" />

**BELL is a reference architecture and sample app for developers building personal assistants. Developers should have a practical starting point for building their own versions that are truly performant on execution.**

**BELL is a reference architecture and sample app for developers building personal assistants. Developers should have a practical starting point for building their own versions that are truly performant on execution.**

Browserbase, Linq, 1Password, Baselayer, Visa, and Stripe power assistants that execute tasks on your behalf.
Browserbase, Linq, 1Password, Baselayer, and Stripe power assistants that execute tasks on your behalf.

Bell pairs an Eve agent with browser
automation powered by Browserbase and a compact web interface. This standalone repository owns
Expand All @@ -28,15 +27,16 @@ pnpm dev
```

Open `http://127.0.0.1:4320` and select **Open demo site**. Bell opens the configured `BELL_DEMO_URL`
in one persistent Browserbase session. Ask for a screenshot to update the capture panel. Follow-up
(the protected Baselayer demo in `.env.example`) in one persistent Browserbase session. Configure
Baselayer as described below for that demo, or set the URL to an ordinary website for browsing.
Ask for a screenshot to update the capture panel. Follow-up
messages at the same `/s/...` address reuse the conversation and browser. Stop cancels active work.
The panel is a screenshot viewer; use `human_handoff` when you need to interact with the browser.

Browser tools, hooks, and runtime code live directly in Bell. Both web apps use the root
`package.json` and lockfile; there is no separate browser package or library build.
The optional Browsie interface remains in `packages/browsie`; run it with `pnpm dev:studio`.
Its existing environment and data paths are unchanged. Restart the running app after runtime edits.
See [repository layout and source provenance](docs/repository.md).
Browser tools, hooks, and runtime code live directly in Bell. One Next.js app and one Eve agent
use the root `package.json` and lockfile. Restart Bell after runtime edits. See
[repository layout](docs/repository.md) and
[browser configuration and credential handling](docs/browser-runtime.md).

For phone use, point Linq directly at Bell's HTTPS deployment and use the same origin
for handoff links. Hosted browser launches keep `browserSettings.verified: true` and
Expand All @@ -46,8 +46,8 @@ managed proxies enabled. See [Linq setup](docs/linq.md).

Bell can mint a short-lived L2 credential, bind it to a merchant nonce, and present it on one
approved browser request using `merchant_access`. Configure the sandbox API key, verified
principal, and exact merchant origin in `.env.local`. Keep `BROWSIE_LOG_SESSION=false` before
starting the browser. [Follow the Baselayer setup and verification guide](docs/baselayer.md).
principal, and exact merchant origin in `.env.local`.
[Follow the Baselayer setup and verification guide](docs/baselayer.md).

The protected demo is `https://bell-stack-demo-site.vercel.app/protected`. Ordinary navigation
should be denied; successful `merchant_access` should return HTTP 200. Sandbox credentials
Expand All @@ -57,41 +57,50 @@ Best Buy browsing does not require Baselayer and does not demonstrate merchant v

## Optional integrations

- Link: connect a wallet from an allowlisted Linq conversation using OAuth. Bell sends a
- Link sandbox: test purchases with an operator-injected token, no database or saved wallet
credentials. Real Link: connect a wallet from an allowlisted Linq conversation using OAuth. Bell sends a
consent URL and resumes in the same chat. PostgreSQL stores encrypted per-sender grants;
checkout retains Eve confirmation and Link purchase approval. Follow [Link setup](docs/link.md).
spend requests retain Eve confirmation and Link approval. Approved payment entry uses the server-side checkout tool;
card details and payment tokens are not returned to the model. Follow [Link setup](docs/link.md).
- 1Password: for local desktop access, set `OP_ACCOUNT="Your account name"` in `.env.local`,
using the account name shown in the 1Password sidebar. In the desktop app, enable
**Settings > Developer > Integrate with other apps** under the SDK options, then approve
the request when prompted. Bell must run on the same computer as the desktop app.
For hosted access, set `OP_SERVICE_ACCOUNT_TOKEN` instead; it takes precedence over `OP_ACCOUNT`.
Existing CLI setups can select `BELL_ONEPASSWORD_BACKEND=cli` with an explicit account or
service token; see the [credential setup and limits](docs/browser-runtime.md#credentials-and-otp).
Save a Login item with the destination website, username, and password before asking Bell to log in.
Add a one-time password field if the site uses TOTP. The shared vault tools enforce the saved
website host and HTTPS before filling credentials. Keep `BROWSIE_LOG_SESSION=false` before
starting a hosted login session. Credentials are resolved on the server; a URL alone does not
Add a one-time password field if the site uses TOTP. The vault tools enforce the saved
website host and HTTPS before filling credentials. Credentials are resolved on the server; a URL alone does not
provide a login.
- Linq: text Bell at **+12025550100** after configuring your Linq API key, webhook signing
secret, and personal sending number. Follow the [Linq setup and phone demo](docs/linq.md).
The planned per-user OAuth routes are retained, but they do not yet connect an account to
the vault tools. Use the configured SDK or CLI access for working logins.
- Local vault: existing encrypted files remain readable using `BELL_VAULT_FILE` and
`BELL_VAULT_MASTER_KEY`. No vault administration UI or writing API is included. Protect the
file and its website mappings as trusted operator configuration.
- Linq: configure your own Linq receiving line, API key, webhook signing secret, and allowed
personal sending number, then text that line. Follow the [Linq setup and phone demo](docs/linq.md).
Each Linq chat keeps its own conversation and browser. Baselayer is not required for browsing.
- Handoff: set a random `BROWSIE_HANDOFF_SECRET` of at least 32 characters and
`BROWSIE_PUBLIC_URL` to the app's reachable origin. Configure this before asking for a handoff.
- Handoff: set a random `BELL_HANDOFF_SECRET` of at least 32 characters and
`BELL_PUBLIC_URL` to the app's reachable origin. Configure this before asking for a handoff.

The shared runtime retains its `BROWSIE_*` configuration names. Bell does not copy the Context or
Vault administration screens. Their agent tools remain available. Link checkout is available
through the private Linq flow after configuration and approval. Visa is not integrated.
Browser logging and recording are disabled for hosted sessions. Context and vault tools run
directly in Bell. Visa is not integrated. Before updating an existing deployment, follow the
[configuration and session migration notes](docs/browser-runtime.md#updating-an-existing-deployment).

## Check the code

```sh
pnpm check:workspace
pnpm check
```

This checks Browsie and Bell, including both standalone production builds. Browsie's live tests
remain opt-in and skip without their required configuration. The default suite makes no live
provider calls. Set `TEST_LINK_DATABASE_URL` to a disposable PostgreSQL database to include
Link persistence tests; CI runs these automatically. A successful build verifies compilation only.
Keep this demo local until you configure application authentication and durable storage. Eve's
default HTTP channel rejects unauthenticated production traffic. The shared vault/Context data
uses local files and is scoped to a single demo operator. See
[`packages/browsie/README.md`](packages/browsie/README.md) for runtime
settings and [the vault guide](packages/browsie/docs/vault-and-otp.md) for the credential boundary.
This runs tests, TypeScript, lint, and the Bell production build. Browser runtime tests live in
`tests/browser`; live provider tests remain opt-in and skip without their required configuration.
The default suite makes no live provider calls. Set `TEST_LINK_DATABASE_URL` to a disposable
PostgreSQL database to include Link persistence tests; CI runs these automatically. A successful build verifies compilation only.
Keep this demo local until you configure application authentication and durable storage. Bell's
HTTP channel rejects unauthenticated production traffic. Context metadata and optional local
vault files are scoped to one trusted operator; Context writes are serialized within one process.
See the [browser runtime guide](docs/browser-runtime.md)
for storage, authentication, and credential boundaries and [provider checks](docs/provider-tests.md)
for live regression commands.
6 changes: 3 additions & 3 deletions agent/channels/eve.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";
import { vercelOidc } from "eve/channels/auth";

import { browserChannelEvents } from "../lib/browser-runtime";
import { studioAuth } from "../../server/browser/studio-auth";
import { localOperatorAuth, operatorAuth } from "../../server/operator-auth";

export default eveChannel({
auth: [studioAuth, vercelOidc(), localDev()],
auth: [operatorAuth, vercelOidc(), localOperatorAuth],
events: browserChannelEvents,
});
109 changes: 109 additions & 0 deletions agent/extensions/link/lib/spend-requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { z } from "zod";
import { isLinkSandbox, isSandboxRequest } from "../../../../server/link/sandbox";

export const spendRequestId = z.string().min(1).max(255)
.regex(/^[A-Za-z0-9_-]+$/, "Invalid Link spend-request ID.");
export const spendRequestInput = z.strictObject({ id: spendRequestId });

// Link's SDK accepts arbitrary IDs and interpolates them into request paths.
// Keep every mutation on its intended resource, including direct execute calls.
export function validateSpendRequestId(input: { id: string }): void {
spendRequestId.parse(input.id);
}

export async function linkMetadataRequest<T>(request: () => T | Promise<T>): Promise<T> {
try {
return await request();
} catch (error) {
// The SDK includes provider response bodies in its errors. Do not persist
// those bodies, while preserving Eve's separate authorization pause signals.
if (error instanceof Error && ["LinkApiError", "LinkResponseError", "LinkTransportError", "LinkConfigurationError"].includes(error.name)) {
const status = "status" in error && typeof error.status === "number" && Number.isInteger(error.status)
&& error.status >= 100 && error.status <= 599 ? ` (HTTP ${error.status})` : "";
throw new Error(`Link request failed${status}. Check the existing request before retrying.`);
}
throw error;
}
}

// Construct metadata-only results before Eve can persist them. The SDK's loose
// response objects can include credentials, including fields added in the future.
const total = z.object({
type: z.string(),
display_text: z.string(),
amount: z.number(),
});

const metadata = z.object({
id: spendRequestId,
status: z.string(),
amount: z.number().optional(),
currency: z.string().optional(),
merchant_name: z.string().optional(),
merchant_url: z.string().optional(),
context: z.string().optional(),
line_items: z.array(z.object({
name: z.string(),
url: z.string().optional(),
image_url: z.string().optional(),
description: z.string().optional(),
sku: z.string().optional(),
totals: z.array(total).optional(),
quantity: z.number().optional(),
unit_amount: z.number().optional(),
product_url: z.string().optional(),
})).optional(),
totals: z.array(total).optional(),
approval_url: z.string().optional(),
activity_url: z.string().optional(),
expires_at: z.number().optional(),
recurring: z.object({
interval: z.string(),
interval_count: z.number().optional(),
}).nullable().optional(),
payment_status_details: z.object({
outcome: z.enum(["success", "failure"]),
code: z.string().nullable().optional(),
decline_code: z.string().nullable().optional(),
amount: z.number(),
currency: z.string(),
created: z.number().nullable().optional(),
refund_details: z.object({
amount: z.number(),
currency: z.string(),
state: z.string(),
created: z.number(),
}).nullable().optional(),
}).nullable().optional(),
status_details: z.object({
requires_action: z.object({
failure_code: z.string().optional(),
next_action: z.object({
type: z.string().optional(),
display_message: z.string().optional(),
action_url: z.string().nullable().optional(),
resolution: z.string().optional(),
expires_at: z.number().nullable().optional(),
}).optional(),
}).optional(),
}).nullable().optional(),
created_at: z.string(),
updated_at: z.string(),
});

export function spendRequestMetadata(value: unknown) {
const result = metadata.safeParse(value);
if (!result.success) throw new Error("Unexpected Link spend-request response.");
return isLinkSandbox() ? { ...result.data, test: true as const } : result.data;
}

export function spendRequestListMetadata(value: unknown) {
if (!Array.isArray(value)) throw new Error("Unexpected Link spend-request list.");
return value.map(spendRequestMetadata).filter(item => isSandboxRequest(item.id));
}

export function spendApprovalMetadata(value: unknown) {
const result = z.object({ id: spendRequestId, approval_url: z.string() }).safeParse(value);
if (!result.success) throw new Error("Unexpected Link approval response.");
return result.data;
}
Loading
Loading