Repository navigation
Conversation
| let service = api.url; | ||
| const advertised = new Set([...Object.keys(company.scopes), ...(company.oauth?.scopes_supported ?? [])]); | ||
| if (api.type === "openapi") { | ||
| const description = await this.describe(owner, conversation, url, apiIndex, signal); |
There was a problem hiding this comment.
are we able to leveragedescribePoppyApi() directly here so reconnecting does not depend on the existing grant? after a failing refresh, the saved grant essentially becomes uncertain. this call to describe() reaches accessToken(), which rejects that grant before device authorization can start. the user is told to reconnect, however reconnecting will j produce the same error.
i'd recommend reproing this w/ a mocked refresh failure followed by client.connect(). also worth noting - pls fetch the API metadata w/o using the existing grant, and you'll prob want to add a regression test confirming that a failed token refresh does not prevent the user from starting sign in again.
There was a problem hiding this comment.
The old path was a real reconnect trap. The SDK refactor removed connect() → authenticated describe()/accessToken(): connection now validates public discovery metadata and calls DeviceSignInClient.begin() directly.
Added a regression in d4fd356 for failed refresh → explicit disconnect → fresh device sign-in. It passes with the old refresh endpoint still failing and asserts that fresh sign-in performs no MCP call or refresh-token exchange. Bell intentionally continues to require disconnect before replacing a saved or uncertain grant, so recovery does not silently discard access that has not been revoked.
| async withCompany<T>(owner: string, company: string, operation: (state: PoppyCompanyState, save: () => Promise<void>) => Promise<T>): Promise<T> { | ||
| if (!z.string().min(1).max(2048).safeParse(owner).success || !httpsUrl.safeParse(company).success) | ||
| throw new Error("Poppy state requires an authenticated owner and company identifier."); | ||
| const work = (pending.get(this.#path) ?? Promise.resolve()).then(async () => { |
There was a problem hiding this comment.
perhaps we should scope the network-spanning lock to the owner/company, and keep shared file updates in a separate, shorter section?
this queue is keyed only by the file path and stays locked throughout operation(), including remote API and oauth requests. therefore, a stalled provider therefore blocks unrelated users' reads, status checks, and disconnects.
u can double check this by holding one user's read open: another user's API inspection wont be able to finish until that read was released. you'll also prob want to ensure that serialization is preserved for the same owner/company, and keep the shared file read/merge/write atomic to prevent lost updates.
There was a problem hiding this comment.
Addressed in d4fd356. Reproduced the file-wide blockage with a held provider operation; the regression failed before the fix and now passes.
Network work is serialized by file + owner + company using the SDK's public InProcessRefreshTokenLock. A separate, short file lock rereads current encrypted state, merges only the checkpointed record, and saves atomically. Tests verify that another owner and another company complete while the first request is held, the same account stays serialized across store instances, concurrent updates survive, and corrupt shared state is not overwritten. The store remains single-process.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
shriyatheunicorn
left a comment
There was a problem hiding this comment.
few comments abt failure handling, other lgtm! lmk when rereview and ill stamp
| await save(); | ||
| if (!token) return { error: "auth_required", message: "Reconnect this account before reading." }; | ||
| } | ||
| const auth = new McpSessionAuth(api.url, { ...runtime, session, token }); |
There was a problem hiding this comment.
once a provider ends a session, the subsequent reads keep using the rejected session ID. McpSessionAuth signs the requests, but it doesnt start a replacement session, so the user will j keep getting a generic API error. maybe we can use the Poppy SDK's recovery handling to retry once (w a new session ID) and save it for future reads? this was reproducible across two consecutive reads after a session_ended response
| let token; | ||
| if (state.grant) { | ||
| // A process/storage failure during credential mutation must not replay an old grant. | ||
| state.blocked = true; |
There was a problem hiding this comment.
okie here, a temporary metadata outage currently forces the user to disconnect and sign in again. basically,blocked is saved before tokenFor() fetches the API's resource metadata. if that fetch returns a 503, the account remains blocked even though no token exchange happened. i'd fetch the metadata before setting this checkpoint, while keeping the checkpoint before the actual token exchange
| } | ||
| const body = request.body ? await beforeAbort(request.arrayBuffer(), deadline) : undefined; | ||
| if (body && body.byteLength > MAX_REQUEST_BYTES) throw new Error("Poppy request exceeds Bell's size limit."); | ||
| const response = await beforeAbort(pinnedFetch(request.url, { |
There was a problem hiding this comment.
the rate limit regression here returned 429 with Retry-After: 60, but the MCP client immediately sent another request to the same endpoint: server/discover was followed by initialize. the fallback isn't prevented by maxRetries: 0. so we prob need to enforce the provider's wait time in the shared request path (including across separate tool calls) and return the retry timing instead of a generic API error
Bell can discover a company's
/.well-known/poppy.json, inspect its MCP tools, and perform explicitly read-only operations using published clients.@datalayer/personal-agent-protocolowns discovery, protocol versions, Session signing, device authorization, resource-bound tokens, refresh, and revocation.Compatibility and setup
The pinned Poppy SDK uses 0.4 by default. Providers serving 0.3 are unsupported; Bell contains no protocol translation or compatibility patches. This integration exposes MCP reads only. OpenAPI execution, writes, purchases, and browser handoff are excluded. Only uniquely named tools with
readOnlyHint: trueand withoutdestructiveHint: trueare exposed. These annotations are provider claims, not verified behavior: use providers trusted with the requested data. Bell does not infer permissions from tool or parameter names.The encrypted file store requires one agent process with durable storage. SDK-provided locks serialize each owner/company independently; short file checkpoints reread, merge, and atomically save current state without holding a file-wide lock during provider requests. Setup is in docs/poppy.md; the repeatable production smoke is in docs/provider-tests.md.
E2E Test Matrix
--onceflower query completed three tool calls in 12 seconds. Both exited 0 and confirmed session retirement.sites:readapproval. A scripted Cancel produced a persisteddeniedevent, no successful connection or sign-in result, and a normal agent reply. Unauthenticated session creation returned HTTP 401.sites:readpassed through Bell's SDK-backed client: API inspection, two successfullist_sitesreads, and SDK disconnect. Both reads returned an empty list.pnpm audit --prodreports zero known vulnerabilities after a scoped override updates Reactor’s transitive TOON package to 2.3.1. Thirty-two temporary compatibility/security checks passed against TOON and the installed Reactor formatter/runner.pnpm check: 518 passed, 18 opt-in tests skipped; typecheck, lint, and production build passed in an isolated directory with PostgreSQL. CI and both Socket checks passed.The repeatable production demo is documented in provider tests. Authenticated model conversations and live iMessage delivery remain separate verification steps.
Code flow
Before this PR, all channels reach the existing Eve agent and its browser/integration tools:
flowchart LR Message["Web/API or iMessage via Linq"] --> Eve["Eve conversation and model"] Eve --> Existing["Existing browser and integration tools"] Existing --> Results["Tool results"] Results --> Eve Eve --> Reply["Reply in the same conversation"]The PR adds a Poppy tool path alongside those existing tools. Green nodes are new Bell code; blue nodes are published packages.
flowchart TD Message["Existing message entrypoints"] --> Eve["Same Eve conversation and model"] Eve --> Existing["Existing browser and integration tools"] Existing --> Eve Eve --> Tools["New Poppy tools: owner identity and account approvals"] Tools --> App["client.ts: account orchestration"] App <--> Store["store.ts: encrypted owner/company state"] App --> SDK["Poppy SDK: discovery, signing and account authorization"] SDK --> Catalog["Official MCP client: fetch tool catalog"] Catalog --> Policy["apis.ts: provider read-only hints and known-credential input check"] Policy --> MCP["Official MCP client: schema validation and tool call"] MCP --> Provider["Company API"] Provider --> Output["apis.ts: filter known credential echoes and limit output"] Output --> Eve Eve --> Reply["Reply in the same conversation"] classDef added fill:#e6f4ea,stroke:#137333,color:#123 classDef package fill:#e8f0fe,stroke:#1967d2,color:#123 class Tools,App,Store,Policy,Output added class SDK,Catalog,MCP packageAccount connection additionally pauses for Bell approval and the provider’s human consent flow. Public agent metadata/JWKS routes let providers verify Bell’s signing identity. The diagram shows logical control/data flow; the SDKs own their HTTP requests and responses.