@guuey/host 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/options.js DELETED
@@ -1,248 +0,0 @@
1
- import { GUUEY_DEFAULT_SYSTEM_PROMPT, defaultModelFor } from "@guuey/config";
2
- /**
3
- * Default Claude model — only used when the snapshot omits `model`. Derived
4
- * from the `@guuey/config` registry (single source of truth per the
5
- * model-release playbook §8 item A) rather than a bare literal, so a
6
- * registry default change propagates here automatically.
7
- */
8
- const DEFAULT_MODEL = defaultModelFor("claude-agent-sdk");
9
- /** Default cap on agent-loop turns per user message (matches the SDK/runner default). */
10
- const DEFAULT_MAX_TURNS = 25;
11
- /**
12
- * Env-var names the Router injects so agent code reaches the home/app layers
13
- * portably. Host-owned copies of `@guuey/fs`'s `ENV_HOME_DIR`/`ENV_APP_DIR`
14
- * (trivial string literals — not imported, to keep this package OSS-legal).
15
- */
16
- export const ENV_HOME_DIR = "GUUEY_HOME_DIR";
17
- export const ENV_APP_DIR = "GUUEY_APP_DIR";
18
- /**
19
- * File tools enabled when GuueyFS layers are bound. `Bash` is added separately
20
- * (see {@link BASH_TOOL}) so the two are independently testable.
21
- */
22
- const FS_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
23
- /**
24
- * Real shell exec, enabled alongside the file tools when GuueyFS layers are
25
- * bound. Unlike the source runner — which enabled `Bash` only when the SDK's OWN
26
- * `sandbox:{}` block was on — this host runs entirely INSIDE the Router's
27
- * bubblewrap jail, so the OS isolation is always present whenever fs is bound.
28
- * The SDK `sandbox:{}` block is therefore NOT set here (it would spawn a nested
29
- * bubblewrap inside the Router's bwrap); the Router's bwrap IS the isolation.
30
- */
31
- const BASH_TOOL = "Bash";
32
- /**
33
- * Build the Claude Agent SDK `Options` for one invoke. Pure: all disk/env access
34
- * is injected via {@link BuildOptionsContext}. Throws on an unresolved `{file}`
35
- * system prompt or a missing API key (the same loud failures the source had).
36
- */
37
- export function buildOptions(snapshot, ctx) {
38
- const apiKey = ctx.apiKey;
39
- const baseUrl = ctx.baseUrl;
40
- const authToken = ctx.authToken;
41
- // Require either the loopback proxy credentials (hosted/broker path) or a
42
- // direct API key (local-dev fallback). Neither → fail loudly.
43
- if (!((baseUrl !== undefined && authToken !== undefined) || apiKey)) {
44
- throw new Error("@guuey/host: either (baseUrl + authToken) for the managed-LLM proxy, " +
45
- "or ANTHROPIC_API_KEY for local dev, is required.");
46
- }
47
- // Snapshots reach the worker fully resolved — the CLI's loader inlines any
48
- // `{file}` system prompt before upload. A `{file}` reaching the worker means
49
- // someone hit the API directly with an un-resolved snapshot; reject loudly.
50
- if (snapshot.systemPrompt !== undefined && typeof snapshot.systemPrompt !== "string") {
51
- throw new Error(`@guuey/host: snapshot.systemPrompt must be a resolved string (got ${JSON.stringify(snapshot.systemPrompt)}). The CLI inlines {file} references before upload; workers never read the filesystem.`);
52
- }
53
- const mcpServers = resolveMcpServers(ctx);
54
- const allowedTools = buildAllowedTools(snapshot, Object.keys(mcpServers), Boolean(ctx.fs));
55
- const systemPrompt = withContextPreamble(snapshot.systemPrompt ?? GUUEY_DEFAULT_SYSTEM_PROMPT, ctx.history, ctx.priorMemory, ctx.priorState);
56
- const model = snapshot.model ?? DEFAULT_MODEL;
57
- const maxTurns = snapshot.runtime?.maxTurns ?? DEFAULT_MAX_TURNS;
58
- const fs = ctx.fs;
59
- // Build the subprocess env. Two mutually-exclusive paths:
60
- //
61
- // - Proxy path (baseUrl + authToken present): route the Claude CLI
62
- // subprocess through the managed-LLM broker. baseUrl + authToken are
63
- // spread LAST so a builder's snapshot.env cannot override them.
64
- // ANTHROPIC_API_KEY is intentionally absent — the proxy owns auth.
65
- // CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC suppresses telemetry pings
66
- // that would bypass the proxy.
67
- //
68
- // - Local-dev fallback (only apiKey present): pass the real key directly.
69
- // The guard above guarantees apiKey is non-null on this branch.
70
- // Explicit Record<string, string> annotation prevents TypeScript from widening
71
- // the ternary to a union `{ K: string } | {}`, which would make spread targets
72
- // produce optional-undefined keys that conflict with Record<string, string>.
73
- const fsEnv = fs
74
- ? { [ENV_HOME_DIR]: fs.home, [ENV_APP_DIR]: fs.app }
75
- : {};
76
- let env;
77
- if (baseUrl !== undefined && authToken !== undefined) {
78
- env = {
79
- ...(snapshot.env ?? {}),
80
- ...fsEnv,
81
- ANTHROPIC_BASE_URL: baseUrl,
82
- ANTHROPIC_AUTH_TOKEN: authToken,
83
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
84
- };
85
- }
86
- else {
87
- // apiKey is guaranteed non-null by the guard; the if-check narrows the type.
88
- if (!apiKey)
89
- throw new Error("unreachable: apiKey guard should have prevented this path");
90
- env = {
91
- ANTHROPIC_API_KEY: apiKey,
92
- ...(snapshot.env ?? {}),
93
- ...fsEnv,
94
- };
95
- }
96
- // Whether the operator pinned a Claude permission mode in agent.json. When
97
- // set we forward it verbatim and let the SDK's mode govern the posture; when
98
- // unset we install the auto-allow `canUseTool` below so the default no-code
99
- // agent's Bash runs prompt-free (a never-answered prompt would hang the pod).
100
- const explicitMode = snapshot.claude?.permissions?.mode;
101
- const options = {
102
- model,
103
- mcpServers,
104
- allowedTools,
105
- // With GuueyFS layers bound, expose the file tools PLUS real `Bash`; without
106
- // them this is byte-identical to the source (purely MCP-driven). `Bash` is
107
- // safe here because the host already runs inside the Router's bubblewrap
108
- // jail — that bwrap, NOT the SDK's own `sandbox:{}` block, is the isolation.
109
- tools: fs ? [...FS_TOOLS, BASH_TOOL] : [],
110
- // Settings isolation. Empty array = "no filesystem settings loaded" — guards
111
- // against a future SDK change auto-pulling `~/.claude/settings.json` and
112
- // leaking the operator's logged-in Claude Code MCPs into the tool catalog.
113
- settingSources: [],
114
- strictMcpConfig: true,
115
- maxTurns,
116
- env,
117
- systemPrompt,
118
- // GuueyFS binding (opt-in): session dir as cwd, home+app as extra roots.
119
- ...(fs ? { cwd: fs.session, additionalDirectories: [fs.home, fs.app] } : {}),
120
- // Permission posture. Two mutually-exclusive paths:
121
- //
122
- // - Operator pinned `claude.permissions.mode` → forward it verbatim; the
123
- // operator owns the posture (e.g. `acceptEdits`).
124
- // - No explicit mode + fs bound → install an auto-allow `canUseTool`. In
125
- // the SDK permission flow (hooks → deny → allow → ask → mode/canUseTool),
126
- // `default` mode with no callback routes Bash subcommands through an
127
- // interactive permission prompt — which, in this headless ephemeral pod,
128
- // no one answers, so the agent would HANG. The auto-allow callback short-
129
- // circuits that: every tool the model picks (already constrained to
130
- // `tools`/`allowedTools` + `settingSources:[]` + `strictMcpConfig`) is
131
- // allowed without a prompt. This is safe precisely because the Router's
132
- // bubblewrap jail is the real isolation boundary — NOT the SDK's own
133
- // `sandbox:{}` block (which is intentionally absent to avoid a nested
134
- // bwrap inside the Router's bwrap). We do NOT use `bypassPermissions`
135
- // here: it requires `allowDangerouslySkipPermissions` and globally
136
- // disables hooks/deny-rule evaluation, whereas the callback keeps the
137
- // deny/hook stages intact while only collapsing the final ask stage.
138
- ...(explicitMode
139
- ? { permissionMode: explicitMode }
140
- : fs
141
- ? { canUseTool: autoAllowTool }
142
- : {}),
143
- ...(ctx.abortController ? { abortController: ctx.abortController } : {}),
144
- };
145
- return options;
146
- }
147
- /**
148
- * Auto-allow permission callback. Installed when fs is bound and the operator
149
- * did NOT pin `claude.permissions.mode`, so the default no-code agent's `Bash`
150
- * (and the file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
151
- * every request, passing the input through unchanged.
152
- *
153
- * Safe because the model's tool surface is already locked down BEFORE the
154
- * callback ever fires — `tools`/`allowedTools` cap which tools exist,
155
- * `settingSources:[]` blocks filesystem-loaded settings, `strictMcpConfig`
156
- * pins the MCP catalog — and the real OS isolation is the Router's bubblewrap
157
- * jail this whole process runs inside. The callback only collapses the SDK's
158
- * final interactive "ask" stage (which would otherwise hang a headless pod);
159
- * the earlier hook/deny-rule stages of the permission flow still run.
160
- */
161
- export const autoAllowTool = (_toolName, input) => Promise.resolve({ behavior: "allow", updatedInput: input });
162
- /**
163
- * Map the Router-resolved cred files to the framework-neutral SdkMcpServer map.
164
- * The Router (credential-broker) owns ALL resolution — default, federation, mint,
165
- * env-substitution; this worker just reads `<session>/.guuey/credentials/*.json`
166
- * (via ctx.listCredentials) and shapes each entry. Keyed by the server name.
167
- */
168
- export function resolveMcpServers(ctx) {
169
- const out = {};
170
- for (const { name, cred } of ctx.listCredentials()) {
171
- out[name] = {
172
- type: cred.transport,
173
- url: cred.url,
174
- // Declared MCP servers ARE this agent's tool surface. Without
175
- // alwaysLoad the CLI defers MCP tools behind its ToolSearch built-in —
176
- // absent here (tools: []) — leaving the model tool-less. Empirically
177
- // load-bearing; mirrors the scaffold template's worker (see
178
- // create-agentic-app templates-src claude worker.ts).
179
- alwaysLoad: true,
180
- ...(Object.keys(cred.headers).length > 0 ? { headers: cred.headers } : {}),
181
- };
182
- }
183
- return out;
184
- }
185
- /**
186
- * Build the SDK's `allowedTools` array. MCP tools are auto-namespaced by the SDK
187
- * as `mcp__<server>__<tool>`; we can't enumerate the namespaces ahead of time,
188
- * so the allowlist is necessarily wildcard-ish.
189
- *
190
- * - explicit `tools.allowlist` → pass those literal names through.
191
- * - else → allow every tool from every declared server via `mcp__<server>`.
192
- *
193
- * When GuueyFS layers are bound, the file tools AND `Bash` join the allowlist
194
- * (an allow rule in the SDK permission flow), so the model may use them
195
- * alongside the MCP allowlist. The auto-allow `canUseTool` (or the operator's
196
- * pinned mode) governs whether those still prompt — see `buildOptions`.
197
- */
198
- function buildAllowedTools(snapshot, declaredServerNames, fsBound) {
199
- const explicit = snapshot.tools?.allowlist;
200
- const base = explicit && explicit.length > 0
201
- ? explicit.slice()
202
- : declaredServerNames.map((s) => `mcp__${s}`);
203
- return fsBound ? [...base, ...FS_TOOLS, BASH_TOOL] : base;
204
- }
205
- /**
206
- * Render prior context sections (conversation history, thread memory, working
207
- * state) as a preamble and prepend to the system prompt. The SDK's `query()`
208
- * accepts only the current `input` as `prompt`, so feeding context here is how
209
- * an ephemeral worker gives the model memory across invokes.
210
- *
211
- * Empty sections are omitted; if all inputs are empty/undefined the original
212
- * system prompt is returned unchanged. Exported for unit testing + reuse by the
213
- * worker loop.
214
- */
215
- export function withContextPreamble(systemPrompt, history, priorMemory, priorState) {
216
- const sections = [];
217
- if (history && history.length > 0) {
218
- sections.push([
219
- "Prior conversation with this user, for context. Continue naturally;",
220
- "do not repeat it back verbatim.",
221
- "<conversation_history>",
222
- ...history.map((m) => `${roleLabel(m.role)}: ${m.text}`),
223
- "</conversation_history>",
224
- ].join("\n"));
225
- }
226
- if (priorMemory && priorMemory.length > 0) {
227
- sections.push([
228
- "Facts you previously recorded for this thread. Treat as known.",
229
- "<thread_memory>",
230
- ...priorMemory.map((m) => `${m.key ?? "(unkeyed)"}: ${JSON.stringify(m.value)}`),
231
- "</thread_memory>",
232
- ].join("\n"));
233
- }
234
- if (priorState !== undefined) {
235
- sections.push([
236
- "Your working state carried from the previous turn.",
237
- "<working_state>",
238
- JSON.stringify(priorState, null, 2),
239
- "</working_state>",
240
- ].join("\n"));
241
- }
242
- if (sections.length === 0)
243
- return systemPrompt;
244
- return `${sections.join("\n\n")}\n\n${systemPrompt}`;
245
- }
246
- function roleLabel(role) {
247
- return role === "agent" ? "Assistant" : "User";
248
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * The OpenAI (`@openai/agents`) arm of the universal host loop. Mirrors
3
- * `run.ts`'s `runInvoke` shape: one invoke → an `@openai/agents` `Agent` built
4
- * from the snapshot, run streamed, each raw `RunStreamEvent` emitted to fd-3 as
5
- * a `native('openai-agents-sdk', …)` WorkerEvent (the Router dispatches them to
6
- * the `@silverprotocol/openai-agents` normalizer). On clean completion it emits
7
- * `done`; on `MaxTurnsExceededError` it emits the `__host_error__` sentinel the
8
- * normalizer maps to `turn.error` (code `max_turns`), THEN `done(..,"max_turns")`;
9
- * on any other failure it emits `error`. Never throws — every failure path is a
10
- * terminal event so the Router always sees one. Emits `hello` FIRST (§8 item B)
11
- * — the SDK-version handshake.
12
- *
13
- * `run` (the `@openai/agents` runner) is injected so the loop is unit-testable
14
- * without a live model — the entrypoint (`index.ts`) passes the real SDK `run`.
15
- *
16
- * Snapshot → Agent mapping:
17
- * - `model` → `snapshot.model` (or the SDK's own default when absent —
18
- * NOT Claude's; this is the OpenAI path).
19
- * - `instructions` → `systemPrompt` + the §1.4 context preamble (reuses
20
- * `withContextPreamble`, identical to the Claude path).
21
- * - `mcpServers` → the framework-neutral `resolveMcpServers` output (same
22
- * federation/env-substitution as Claude), each `http`/`sse`
23
- * entry → an `MCPServerStreamableHttp` carrying its
24
- * `Authorization` (and any other) header via `requestInit`.
25
- * - `maxTurns` → `snapshot.runtime?.maxTurns` (passed to `run(...)`; the SDK
26
- * THROWS `MaxTurnsExceededError` from `stream.completed`).
27
- * - API key → `OPENAI_API_KEY` (the Router sets it at pod boot), applied
28
- * globally via `setDefaultOpenAIKey` by the entrypoint.
29
- */
30
- import { Agent, type RunStreamEvent } from "@openai/agents";
31
- import type { Emitter } from "@guuey/worker";
32
- import type { HostInvoke, HostRuntime } from "./run.js";
33
- import { type GuueyAgent } from "@guuey/config";
34
- /**
35
- * The streamed-result surface the loop CONSUMES — exactly the three members it
36
- * reads off the `@openai/agents` `StreamedRunResult`: async iteration over raw
37
- * events, `completed` (which THROWS `MaxTurnsExceededError`), and the resolved
38
- * `finalOutput`. A minimal structural interface (NOT the full `StreamedRunResult`
39
- * class) so the real runner result AND a test fake both satisfy it without a cast
40
- * — `StreamedRunResult` has private fields, so a plain object could never match
41
- * the class, but it DOES match this read-only projection.
42
- */
43
- export interface OpenaiRunResult extends AsyncIterable<RunStreamEvent> {
44
- readonly completed: Promise<void>;
45
- /** Resolved text output for the default text-output Agent (or `undefined`). */
46
- readonly finalOutput?: string | undefined;
47
- }
48
- /**
49
- * The `run` surface the loop needs — the real `@openai/agents` `run` (streamed
50
- * overload) satisfies it (its `StreamedRunResult` is assignable to
51
- * {@link OpenaiRunResult}). Injected so the loop is testable without a live model.
52
- */
53
- export type OpenaiRunFn = (agent: Agent, input: string, options: {
54
- stream: true;
55
- maxTurns?: number;
56
- }) => Promise<OpenaiRunResult>;
57
- /**
58
- * Run one invoke through `@openai/agents`. Emits `native` per `RunStreamEvent`,
59
- * `done` on completion (with the `max_turns` sentinel + reason on a turn-cap),
60
- * `error` on a build failure or any non-max-turns throw. Never throws.
61
- */
62
- export declare function runInvokeOpenai(snapshot: GuueyAgent & {
63
- framework?: string;
64
- }, invoke: HostInvoke, runtime: HostRuntime, emit: Emitter, run: OpenaiRunFn): Promise<void>;
65
- //# sourceMappingURL=run-openai.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"run-openai.d.ts","sourceRoot":"","sources":["../src/run-openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EACL,KAAK,EAIL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,eAAe,CAAC;AAOxD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAQ7E;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAgB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACpE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KACzC,OAAO,CAAC,eAAe,CAAC,CAAC;AAgB9B;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7C,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,IAAI,CAAC,CA4Gf"}
@@ -1,207 +0,0 @@
1
- /**
2
- * The OpenAI (`@openai/agents`) arm of the universal host loop. Mirrors
3
- * `run.ts`'s `runInvoke` shape: one invoke → an `@openai/agents` `Agent` built
4
- * from the snapshot, run streamed, each raw `RunStreamEvent` emitted to fd-3 as
5
- * a `native('openai-agents-sdk', …)` WorkerEvent (the Router dispatches them to
6
- * the `@silverprotocol/openai-agents` normalizer). On clean completion it emits
7
- * `done`; on `MaxTurnsExceededError` it emits the `__host_error__` sentinel the
8
- * normalizer maps to `turn.error` (code `max_turns`), THEN `done(..,"max_turns")`;
9
- * on any other failure it emits `error`. Never throws — every failure path is a
10
- * terminal event so the Router always sees one. Emits `hello` FIRST (§8 item B)
11
- * — the SDK-version handshake.
12
- *
13
- * `run` (the `@openai/agents` runner) is injected so the loop is unit-testable
14
- * without a live model — the entrypoint (`index.ts`) passes the real SDK `run`.
15
- *
16
- * Snapshot → Agent mapping:
17
- * - `model` → `snapshot.model` (or the SDK's own default when absent —
18
- * NOT Claude's; this is the OpenAI path).
19
- * - `instructions` → `systemPrompt` + the §1.4 context preamble (reuses
20
- * `withContextPreamble`, identical to the Claude path).
21
- * - `mcpServers` → the framework-neutral `resolveMcpServers` output (same
22
- * federation/env-substitution as Claude), each `http`/`sse`
23
- * entry → an `MCPServerStreamableHttp` carrying its
24
- * `Authorization` (and any other) header via `requestInit`.
25
- * - `maxTurns` → `snapshot.runtime?.maxTurns` (passed to `run(...)`; the SDK
26
- * THROWS `MaxTurnsExceededError` from `stream.completed`).
27
- * - API key → `OPENAI_API_KEY` (the Router sets it at pod boot), applied
28
- * globally via `setDefaultOpenAIKey` by the entrypoint.
29
- */
30
- import { Agent, MaxTurnsExceededError, MCPServerStreamableHttp, } from "@openai/agents";
31
- import { resolveMcpServers, withContextPreamble, } from "./options.js";
32
- import { GUUEY_DEFAULT_SYSTEM_PROMPT } from "@guuey/config";
33
- import { resolveSdkVersion } from "./sdk-version.js";
34
- /** The framework tag this arm runs — matches the `AgentFramework` enum value. */
35
- const OPENAI_FRAMEWORK = "openai-agents-sdk";
36
- /** The npm package whose installed version is this framework's `hello.sdkVersion`. */
37
- const OPENAI_SDK_PACKAGE = "@openai/agents";
38
- /**
39
- * Run one invoke through `@openai/agents`. Emits `native` per `RunStreamEvent`,
40
- * `done` on completion (with the `max_turns` sentinel + reason on a turn-cap),
41
- * `error` on a build failure or any non-max-turns throw. Never throws.
42
- */
43
- export async function runInvokeOpenai(snapshot, invoke, runtime, emit, run) {
44
- // The SDK-version handshake (§8 item B, additive-optional) — ALWAYS the
45
- // first event of this invoke's fd-3 stream, before any native/turn event.
46
- // `sdkVersion` is resolved at runtime, never hardcoded; `null` (SDK not
47
- // resolvable) is tolerated by the Router.
48
- emit.hello(OPENAI_FRAMEWORK, OPENAI_SDK_PACKAGE, resolveSdkVersion(OPENAI_SDK_PACKAGE));
49
- // The host runs inside the Router's bubblewrap jail with NO IRSA; a federated
50
- // MCP server reads its Router-written credential file. `resolveMcpServers`
51
- // needs the same per-invoke context the Claude path builds.
52
- const ctx = {
53
- input: invoke.input,
54
- identity: invoke.identity,
55
- fs: invoke.fs,
56
- history: invoke.history,
57
- listCredentials: runtime.listCredentials,
58
- ...(runtime.apiKey !== undefined ? { apiKey: runtime.apiKey } : {}),
59
- ...(invoke.priorMemory !== undefined ? { priorMemory: invoke.priorMemory } : {}),
60
- ...(invoke.priorState !== undefined ? { priorState: invoke.priorState } : {}),
61
- };
62
- // Build the Agent (+ connect its MCP servers). A build failure (e.g. an
63
- // unresolved {file} system prompt, or an unsupported MCP arm) is a terminal
64
- // `error` — never a throw out of this function.
65
- let instructions;
66
- let mcpServers;
67
- try {
68
- // Snapshots reach the worker fully resolved — the CLI inlines any `{file}`
69
- // system prompt before upload. A non-string reaching the worker means a
70
- // direct API hit with an un-resolved snapshot; reject loudly (same as Claude).
71
- if (snapshot.systemPrompt !== undefined && typeof snapshot.systemPrompt !== "string") {
72
- throw new Error(`@guuey/host: snapshot.systemPrompt must be a resolved string (got ${JSON.stringify(snapshot.systemPrompt)}). The CLI inlines {file} references before upload; workers never read the filesystem.`);
73
- }
74
- instructions = withContextPreamble(snapshot.systemPrompt ?? GUUEY_DEFAULT_SYSTEM_PROMPT, ctx.history, ctx.priorMemory, ctx.priorState);
75
- mcpServers = buildOpenaiMcpServers(ctx);
76
- }
77
- catch (err) {
78
- emit.error(err instanceof Error ? err.message : String(err));
79
- return;
80
- }
81
- try {
82
- for (const server of mcpServers) {
83
- await server.connect();
84
- }
85
- }
86
- catch (err) {
87
- await closeAll(mcpServers);
88
- emit.error(err instanceof Error ? err.message : String(err));
89
- return;
90
- }
91
- try {
92
- const maxTurns = snapshot.runtime?.maxTurns;
93
- // `MCPServerStreamableHttp` IS an `MCPServer`; the Agent's `mcpServers` field
94
- // is `MCPServer[]` — widen via the interface (no cast; structural subtype).
95
- const servers = mcpServers;
96
- const agent = new Agent({
97
- name: "guuey-agent",
98
- instructions,
99
- mcpServers: servers,
100
- ...(snapshot.model !== undefined ? { model: snapshot.model } : {}),
101
- });
102
- const stream = await run(agent, invoke.input, {
103
- stream: true,
104
- ...(maxTurns !== undefined ? { maxTurns } : {}),
105
- });
106
- // Drive the stream: each raw event crosses the wire as native.
107
- for await (const ev of stream) {
108
- emit.native(OPENAI_FRAMEWORK, toJson(ev));
109
- }
110
- // The runner THROWS `MaxTurnsExceededError` from `completed` AFTER the stream
111
- // ends. Catch it specifically → feed the normalizer the `__host_error__`
112
- // sentinel, then a terminal `done(.., "max_turns")`. Other throws → `error`.
113
- try {
114
- await stream.completed;
115
- }
116
- catch (err) {
117
- if (err instanceof MaxTurnsExceededError) {
118
- // The `__host_error__` sentinel — a fresh object literal so it is checked
119
- // directly against `JsonValue` (the `native` payload type). Shape is the
120
- // BINDING contract {@link HostErrorSentinel}.
121
- emit.native(OPENAI_FRAMEWORK, {
122
- type: "__host_error__",
123
- code: "max_turns",
124
- message: err.message,
125
- });
126
- emit.done(finalText(stream), "max_turns");
127
- return;
128
- }
129
- throw err;
130
- }
131
- emit.done(finalText(stream), "end_turn");
132
- }
133
- catch (err) {
134
- emit.error(err instanceof Error ? err.message : String(err));
135
- }
136
- finally {
137
- await closeAll(mcpServers);
138
- }
139
- }
140
- /**
141
- * The agent's final text output, or `""` when absent (a tool-only turn, or a
142
- * turn cut short by `max_turns`). For the default text-output Agent `finalOutput`
143
- * is `string | undefined`.
144
- */
145
- function finalText(stream) {
146
- const out = stream.finalOutput;
147
- return typeof out === "string" ? out : "";
148
- }
149
- /**
150
- * Translate the framework-neutral resolved MCP map → connected-capable
151
- * `MCPServerStreamableHttp` instances. Each `http`/`sse` entry's `headers`
152
- * (typically `{ authorization: 'Bearer <token>' }`, from the federation
153
- * credential file or `${env.NAME}` substitution) ride on the underlying MCP
154
- * transport's `RequestInit.headers` via `requestInit`.
155
- *
156
- * The cred dir only yields `http`/`sse` servers (the Router resolves all
157
- * transport to one of those two); the `stdio` arm below is unreachable
158
- * defensive code — kept so a future schema change can't silently drop a server.
159
- */
160
- function buildOpenaiMcpServers(ctx) {
161
- const resolved = resolveMcpServers(ctx);
162
- const servers = [];
163
- for (const [name, entry] of Object.entries(resolved)) {
164
- servers.push(toOpenaiMcpServer(name, entry));
165
- }
166
- return servers;
167
- }
168
- /**
169
- * One resolved `SdkMcpServer` → an `MCPServerStreamableHttp`. Both `http` and
170
- * `sse` arms map to the StreamableHTTP server (the SDK's HTTP MCP transport);
171
- * the `stdio` arm is unreachable (`resolveMcpServers` rejects colocated before
172
- * we get here) — handled with a loud throw so a future schema change can't
173
- * silently drop a server.
174
- */
175
- function toOpenaiMcpServer(name, entry) {
176
- if (entry.type === "stdio") {
177
- throw new Error(`mcpServers["${name}"]: stdio (colocated) MCP is not supported on the OpenAI host path.`);
178
- }
179
- const headers = entry.headers;
180
- return new MCPServerStreamableHttp({
181
- url: entry.url,
182
- name,
183
- ...(headers && Object.keys(headers).length > 0 ? { requestInit: { headers } } : {}),
184
- // `customDataExtractor` (agents 0.12+) is the ONLY channel that carries an
185
- // MCP tool result's `structuredContent` onto the wire (`item.customData`)
186
- // WITHOUT leaking it into model-visible text — without this, the ggui cache
187
- // marker never reaches the normalizer and render metering goes blind.
188
- // Mirrors the verified silverprotocol capture-agent wiring; the facet reads
189
- // `item.customData.structuredContent`.
190
- customDataExtractor: (context) => context.structuredContent !== undefined
191
- ? { structuredContent: context.structuredContent }
192
- : undefined,
193
- });
194
- }
195
- /** Best-effort close of every connected MCP server (release the transport). */
196
- async function closeAll(servers) {
197
- await Promise.allSettled(servers.map((s) => s.close()));
198
- }
199
- /**
200
- * Coerce one `RunStreamEvent` to the `JsonValue` the `native` event carries.
201
- * Stream events are plain JSON-serializable objects; the round-trip drops any
202
- * non-JSON surface and yields the exact shape the Router's normalizer parses off
203
- * the wire. Mirrors `run.ts`'s `toJson`.
204
- */
205
- function toJson(ev) {
206
- return JSON.parse(JSON.stringify(ev));
207
- }
package/dist/run.d.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * The testable core of the worker loop. `runInvoke` runs one invoke through the
3
- * Claude Agent SDK and emits each native `SDKMessage` to fd-3 as a `native`
4
- * WorkerEvent (the Router dispatches them to the matching normalizer). On the
5
- * SDK result message it emits `done`; on a throw or a framework-gate violation
6
- * it emits `error`. Emits `hello` FIRST (§8 item B) — the SDK-version handshake.
7
- *
8
- * `query` is injected so the loop is unit-testable without a live model. The
9
- * worker entrypoint (`index.ts`) passes the real `@anthropic-ai/claude-agent-sdk`
10
- * `query`.
11
- */
12
- import type { Options, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
13
- import type { Emitter, Fs, HistoryMessage, Identity, JsonValue } from "@guuey/worker";
14
- import { buildOptions, type CredentialFile, type PriorMemoryRecord } from "./options.js";
15
- /**
16
- * The invoke this host consumes. A superset of `@guuey/worker`'s `Invoke`:
17
- * `priorMemory`/`priorState` are the §1.4 push-by-value context the worker reads
18
- * for the preamble (the Worker Protocol `Invoke` is EXTENDED with these in
19
- * Task 3; until then the worker loop parses them off the raw control line).
20
- */
21
- export interface HostInvoke {
22
- input: string;
23
- identity: Identity;
24
- fs: Fs;
25
- history: HistoryMessage[];
26
- priorMemory?: PriorMemoryRecord[];
27
- priorState?: JsonValue;
28
- }
29
- /** Per-process config the worker resolves once at boot. */
30
- export interface HostRuntime {
31
- /**
32
- * Anthropic API key — local-dev fallback when `baseUrl`+`authToken` are
33
- * absent. One of (`baseUrl`+`authToken`) or `apiKey` must be provided;
34
- * `buildOptions` throws at invoke time if neither is present.
35
- */
36
- apiKey?: string;
37
- /**
38
- * Loopback proxy base URL (hosted/broker mode). Task 8 injects this as
39
- * `ANTHROPIC_BASE_URL` via `buildWorkerEnv`. When set together with
40
- * `authToken`, the Claude CLI subprocess routes through the managed-LLM
41
- * broker; the real API key is intentionally absent to prevent leaks.
42
- */
43
- baseUrl?: string;
44
- /**
45
- * Opaque session token for the loopback proxy (hosted/broker mode). Task 8
46
- * injects this as `ANTHROPIC_AUTH_TOKEN`. Required when `baseUrl` is set.
47
- */
48
- authToken?: string;
49
- /**
50
- * Returns every credential the Router broker wrote to
51
- * `<sessionDir>/.guuey/credentials/` this invoke. Injected so the run path
52
- * stays pure (no disk access inside `runInvoke`).
53
- */
54
- listCredentials: () => Array<{
55
- name: string;
56
- cred: CredentialFile;
57
- }>;
58
- }
59
- /** The `query` surface the loop needs — the real SDK `query` satisfies it. */
60
- export type QueryFn = (params: {
61
- prompt: string;
62
- options: Options;
63
- }) => AsyncIterable<SDKMessage>;
64
- /**
65
- * Run one invoke. Emits `native` per SDKMessage, `done` on the result, `error`
66
- * on a framework-gate violation or a thrown error. Never throws — every failure
67
- * path becomes an `error` event so the Router always sees a terminal event.
68
- */
69
- export declare function runInvoke(snapshot: {
70
- framework?: string;
71
- } & Parameters<typeof buildOptions>[0], invoke: HostInvoke, runtime: HostRuntime, emit: Emitter, query: QueryFn): Promise<void>;
72
- //# sourceMappingURL=run.d.ts.map
package/dist/run.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAc,MAAM,eAAe,CAAC;AAClG,OAAO,EACL,YAAY,EAEZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAQtB;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,EAAE,MAAM,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,8EAA8E;AAC9E,MAAM,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC;AA0BhC;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,EACrE,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,IAAI,CAAC,CAgDf"}