Skip to content

research platform assistant

sinrimin edited this page Oct 14, 2026 · 2 revisions

Research: platform assistant (#52) — S0 spike findings

Status: verified 2026-10-14 on feat/52-platform-assistant (packages/server/test/assistant-spike.test.ts, offline, no network). Ground truth for the seams the M1 implementation rides on.

Superseded by the Plan B decision (same day): the user chose the full service-layer refactor (#53) — assistant tools call principal-carrying services directly. §1 below (ALS disproven, internal credential proposed) is the HISTORICAL RECORD of why in-process identity could not ride ambient context or self-HTTP; the mechanism itself is no longer part of the design. §2–§4 (pi-ai/pi-agent-core seams) remain current.

1. Identity: AsyncLocalStorage is DEAD through app.inject() — internal credential instead

The design's first sketch (ALS actor context opened around app.inject()) was disproven: light-my-request dispatches outside the caller's async context — the ALS store is empty inside onRequest hooks. Verified in bare Node (not just vitest):

const r = await als.run({ userId: 'u-1' }, () => app.inject({ url: '/w' }));
// hook sees: no store. (outside run: also no store — correct, but the
// inside-run case is the one that matters and it fails.)

A real loopback socket would not carry ALS either (server-side handlers run from the event loop). Settled mechanism (test-verified):

  • boot-time random secret (randomBytes(32).base64url), held in server memory only; restart rotates it;
  • tool executors call app.inject() with headers x-hnx-internal-auth: <secret> + x-hnx-actor: <userId>;
  • the root auth hook's leading branch: timingSafeEqual on the secret, then a FRESH findUserById (disabled/demoted takes effect next call, matching the PAT path), then req.user + a viaAssistant audit marker; mismatch/absent → anonymous → the normal Bearer path.

Network clients cannot know the secret; header names are not the gate. Cryptographically equivalent to a minted JWT but per-process, no TTL juggling, nothing to revoke, and it survives any future transport change.

2. pi-ai gateway provider — createProvider is the models.json factory

createProvider({id, name, baseUrl, auth, models, api}) builds a Provider from parts (the same path models.json custom providers take). Verified shape for our LlmProvider rows:

  • auth.apiKey.resolve: async () => ({ auth: { apiKey: decrypted }, source: 'credential' }) — resolution is deferred per request (the decrypt happens here, not at build time);
  • registry models map to ProviderModel entries: api mapped 1:1 (anthropic→anthropic-messages, openai-chat→openai-completions, openai-responses→openai-responses), name/contextWindow/maxTokens from the #51 registry; reasoning: false default; input: ['text','image'] when vision;
  • api: takes {stream, streamSimple} from the per-wire modules — import as @earendil-works/pi-ai/api/openai-completions (subpath; the ROOT index does NOT export the api factory functions; the .lazy variants export openAICompletionsApi()-style factories that wrap the same thing).

3. Agent loop + tool harness — fauxProvider is the test wire

  • fauxProvider({models, tokensPerSecond}) + setResponses([...]) scripts assistant messages (text/thinking/toolCall blocks via fauxAssistantMessage) with ZERO I/O — the AssistantService unit tests never touch the network (repo rule);
  • new Agent({initialState: {systemPrompt, model, tools, messages}, streamFn: models.streamSimple.bind(models), beforeToolCall, afterToolCall}) — verified end-to-end: scripted tool call → tool_execution_* → scripted conclusion → turn_end ×2 → agent_end; both audit hooks fire in order; tools are pi-native typebox schemas (piAi.Type.Object);
  • Agent.state.messages is the rebuild-from-DB seam: assign per turn, pi owns nothing durable (platform-owned session persistence holds).

4. Environment

@earendil-works/{pi-agent-core,pi-ai}@1.1.0 (pinned exact) import cleanly on Node 22.22 (engines >=22.19; the #50 floor). pi-ai re-exports typebox Type — tool schemas need no extra dependency.

Clone this wiki locally