@guuey/host 0.1.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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/agent-entry.d.ts +38 -0
- package/dist/agent-entry.d.ts.map +1 -0
- package/dist/agent-entry.js +87 -0
- package/dist/boot-context.d.ts +50 -0
- package/dist/boot-context.d.ts.map +1 -0
- package/dist/boot-context.js +22 -0
- package/dist/creds.d.ts +26 -0
- package/dist/creds.d.ts.map +1 -0
- package/dist/creds.js +45 -0
- package/dist/frameworks/claude-options.d.ts +149 -0
- package/dist/frameworks/claude-options.d.ts.map +1 -0
- package/dist/frameworks/claude-options.js +208 -0
- package/dist/frameworks/claude-runner.d.ts +3 -0
- package/dist/frameworks/claude-runner.d.ts.map +1 -0
- package/dist/frameworks/claude-runner.js +33 -0
- package/dist/frameworks/claude.d.ts +72 -0
- package/dist/frameworks/claude.d.ts.map +1 -0
- package/dist/frameworks/claude.js +80 -0
- package/dist/frameworks/google-adk.d.ts +105 -0
- package/dist/frameworks/google-adk.d.ts.map +1 -0
- package/dist/frameworks/google-adk.js +269 -0
- package/dist/frameworks/openai-runner.d.ts +3 -0
- package/dist/frameworks/openai-runner.d.ts.map +1 -0
- package/dist/frameworks/openai-runner.js +37 -0
- package/dist/frameworks/openai.d.ts +65 -0
- package/dist/frameworks/openai.d.ts.map +1 -0
- package/dist/frameworks/openai.js +207 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +134 -0
- package/dist/options.d.ts +173 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +248 -0
- package/dist/preamble.d.ts +16 -0
- package/dist/preamble.d.ts.map +1 -0
- package/dist/preamble.js +34 -0
- package/dist/run-openai.d.ts +65 -0
- package/dist/run-openai.d.ts.map +1 -0
- package/dist/run-openai.js +207 -0
- package/dist/run.d.ts +72 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +80 -0
- package/dist/sdk-version.d.ts +5 -0
- package/dist/sdk-version.d.ts.map +1 -0
- package/dist/sdk-version.js +56 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `@guuey/host` — the universal config-driven Guuey worker.
|
|
4
|
+
*
|
|
5
|
+
* Reads the resolved agent.json snapshot (`GUUEY_AGENT_SNAPSHOT`), lazily
|
|
6
|
+
* loads the runner for `snapshot.framework`, and drives one turn per
|
|
7
|
+
* `invoke`, emitting each framework-native event to fd-3 as a `native`
|
|
8
|
+
* WorkerEvent. The Router dispatches those to the matching
|
|
9
|
+
* `@silverprotocol/<framework>` normalizer. On the runner's result it emits
|
|
10
|
+
* `done`; on a throw it emits `error`; on `shutdown` (or stdin EOF) it exits.
|
|
11
|
+
*
|
|
12
|
+
* **Thin-wrapper contract:** the agent runtimes
|
|
13
|
+
* (`@anthropic-ai/claude-agent-sdk`, `@openai/agents`, `@google/adk`) are
|
|
14
|
+
* OPTIONAL PEER dependencies — the host is orchestration; the runtime is the
|
|
15
|
+
* installer's declaration (the platform pins them in
|
|
16
|
+
* `@guuey-private/host-shared`, the `/shared` composition package). Runners
|
|
17
|
+
* are loaded via dynamic `import()` so a pod only ever loads the one SDK its
|
|
18
|
+
* framework needs; a missing peer fails with an actionable install hint, not
|
|
19
|
+
* a bare module-resolution stack.
|
|
20
|
+
*
|
|
21
|
+
* Runs inside bubblewrap with NO IRSA — it never mints federation tokens. A
|
|
22
|
+
* federated MCP server's credentials are read from the well-known path the
|
|
23
|
+
* Router-side credential broker wrote: `<sessionDir>/.guuey/credentials/<srv>.json`.
|
|
24
|
+
*
|
|
25
|
+
* Protocol wiring (per `@guuey/worker`): Router→Worker control on fd 0 (stdin),
|
|
26
|
+
* Worker→Router events on fd 3. We use the raw emitter (NOT the text-only
|
|
27
|
+
* `serve(handler)`) because the host emits `native`.
|
|
28
|
+
*/
|
|
29
|
+
import { createWriteStream } from "node:fs";
|
|
30
|
+
import { createEmitter, isInvoke, isShutdown, parseControl, } from "@guuey/worker";
|
|
31
|
+
/**
|
|
32
|
+
* Per-framework runner registry: module path + the peer package whose absence
|
|
33
|
+
* is the overwhelmingly likely cause of an import failure (the install hint).
|
|
34
|
+
*/
|
|
35
|
+
const RUNNERS = {
|
|
36
|
+
"claude-agent-sdk": { module: "./frameworks/claude-runner.js", peer: "@anthropic-ai/claude-agent-sdk", agentEntry: false },
|
|
37
|
+
"openai-agents-sdk": { module: "./frameworks/openai-runner.js", peer: "@openai/agents", agentEntry: false },
|
|
38
|
+
"google-adk": { module: "./frameworks/google-adk.js", peer: "@google/adk", agentEntry: true },
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Graceful mode (`GUUEY_AGENT_ENTRY`) is per-runner: a framework whose runner
|
|
42
|
+
* ignores the entry env must FAIL LOUDLY, not silently run the no-code
|
|
43
|
+
* snapshot instead of the dev's module (review finding — "non-goal" means
|
|
44
|
+
* rejected, not ignored). Boot-time check, same posture as a missing peer.
|
|
45
|
+
*/
|
|
46
|
+
export function assertGracefulSupport(framework, agentEntryEnv) {
|
|
47
|
+
if (agentEntryEnv === undefined || agentEntryEnv === "")
|
|
48
|
+
return;
|
|
49
|
+
const entry = RUNNERS[framework];
|
|
50
|
+
if (entry !== undefined && !entry.agentEntry) {
|
|
51
|
+
throw new Error(`@guuey/host: guuey.json#agent.entry (graceful mode) is not supported for framework "${framework}" yet — ` +
|
|
52
|
+
`only ${Object.entries(RUNNERS)
|
|
53
|
+
.filter(([, r]) => r.agentEntry)
|
|
54
|
+
.map(([f]) => f)
|
|
55
|
+
.join(", ")} run a dev-exported agent module. ` +
|
|
56
|
+
`Use a full worker (serveNative) for this framework, or remove agent.entry.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Parse the boot snapshot — the resolved `agent` section (a {@link GuueyAgent}). */
|
|
60
|
+
function readSnapshot() {
|
|
61
|
+
const raw = process.env.GUUEY_AGENT_SNAPSHOT ?? "{}";
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
64
|
+
throw new Error("@guuey/host: GUUEY_AGENT_SNAPSHOT must be a JSON object (the agent section).");
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load the runner for `framework`, translating a module-resolution failure
|
|
70
|
+
* into the actionable missing-peer message (the runtimes are optional peers —
|
|
71
|
+
* the host deliberately does not bundle them).
|
|
72
|
+
*/
|
|
73
|
+
export async function loadRunner(framework) {
|
|
74
|
+
const entry = RUNNERS[framework];
|
|
75
|
+
if (!entry) {
|
|
76
|
+
throw new Error(`@guuey/host: unknown framework "${framework}" — supported: ${Object.keys(RUNNERS).join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const mod = (await import(entry.module));
|
|
80
|
+
return mod.createRunner();
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const code = err.code;
|
|
84
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
85
|
+
throw new Error(`@guuey/host: cannot load the "${framework}" runner — its runtime is an optional peer. ` +
|
|
86
|
+
`Install ${entry.peer} next to @guuey/host to run this framework. (${String(err)})`);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Async-iterate NDJSON lines off stdin. */
|
|
92
|
+
async function* lines(input) {
|
|
93
|
+
let buf = "";
|
|
94
|
+
for await (const chunk of input) {
|
|
95
|
+
buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
96
|
+
let nl = buf.indexOf("\n");
|
|
97
|
+
while (nl !== -1) {
|
|
98
|
+
const line = buf.slice(0, nl).trim();
|
|
99
|
+
buf = buf.slice(nl + 1);
|
|
100
|
+
if (line.length > 0)
|
|
101
|
+
yield line;
|
|
102
|
+
nl = buf.indexOf("\n");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const tail = buf.trim();
|
|
106
|
+
if (tail.length > 0)
|
|
107
|
+
yield tail;
|
|
108
|
+
}
|
|
109
|
+
/** The worker loop: per `invoke` run the framework runner; on `shutdown`/EOF exit. */
|
|
110
|
+
async function main() {
|
|
111
|
+
const snapshot = readSnapshot();
|
|
112
|
+
const framework = snapshot.framework ?? "claude-agent-sdk";
|
|
113
|
+
// fd 3 is the write end of the pipe the Router created at spawn.
|
|
114
|
+
const out = createWriteStream("", { fd: 3 });
|
|
115
|
+
const emit = createEmitter(out);
|
|
116
|
+
// Load ONCE at boot — the pod runs one framework for its whole life, and a
|
|
117
|
+
// missing peer must fail the first turn loudly, not lazily mid-session.
|
|
118
|
+
assertGracefulSupport(framework, process.env.GUUEY_AGENT_ENTRY);
|
|
119
|
+
const runner = await loadRunner(framework);
|
|
120
|
+
for await (const line of lines(process.stdin)) {
|
|
121
|
+
const msg = parseControl(line);
|
|
122
|
+
if (isShutdown(msg))
|
|
123
|
+
break;
|
|
124
|
+
if (!isInvoke(msg))
|
|
125
|
+
continue;
|
|
126
|
+
const { type: _type, ...turn } = msg;
|
|
127
|
+
// Turns are sequential — await this invoke before reading the next line.
|
|
128
|
+
await runner.runTurn(snapshot, turn, emit);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
process.stderr.write(`@guuey/host fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot → Claude Agent SDK `Options` construction. Lifted from
|
|
3
|
+
* `backend/services/nocode-runtime/src/agent-runner.ts` (the pure-logic half),
|
|
4
|
+
* with the B2-mcp amendment: `@guuey/host` is a THIN CRED-DIR READER. All MCP
|
|
5
|
+
* resolution (default, federation, mint, env-substitution) now lives once on the
|
|
6
|
+
* Router-side credential broker. The worker just reads
|
|
7
|
+
* `<sessionDir>/.guuey/credentials/*.json` (via ctx.listCredentials) and shapes
|
|
8
|
+
* each entry into the framework-neutral `SdkMcpServer` map.
|
|
9
|
+
*
|
|
10
|
+
* Two responsibilities:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Snapshot → SDK options mapping.** Translates the agent.json shape
|
|
13
|
+
* (model, allowedTools, maxTurns, GuueyFS binding) and the cred-dir contents
|
|
14
|
+
* into the Claude Agent SDK's `mcpServers` + `allowedTools` + `maxTurns`.
|
|
15
|
+
* 2. **Cred-dir mapping.** `resolveMcpServers(ctx)` globs the cred dir via
|
|
16
|
+
* `ctx.listCredentials()` → one `SdkMcpServer` per file; ALL the old
|
|
17
|
+
* federation/default/isGguiUrl/env-sub logic is DELETED (Router-side now).
|
|
18
|
+
*
|
|
19
|
+
* OSS-legality: this package imports ONLY `@anthropic-ai/claude-agent-sdk`,
|
|
20
|
+
* `@guuey/worker`, `@guuey/config`, and Node built-ins.
|
|
21
|
+
*/
|
|
22
|
+
import type { CanUseTool, Options, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
23
|
+
import type { Fs, HistoryMessage, JsonValue } from "@guuey/worker";
|
|
24
|
+
import { type GuueyAgent } from "@guuey/config";
|
|
25
|
+
export type { SDKMessage };
|
|
26
|
+
/**
|
|
27
|
+
* Env-var names the Router injects so agent code reaches the home/app layers
|
|
28
|
+
* portably. Host-owned copies of `@guuey/fs`'s `ENV_HOME_DIR`/`ENV_APP_DIR`
|
|
29
|
+
* (trivial string literals — not imported, to keep this package OSS-legal).
|
|
30
|
+
*/
|
|
31
|
+
export declare const ENV_HOME_DIR = "GUUEY_HOME_DIR";
|
|
32
|
+
export declare const ENV_APP_DIR = "GUUEY_APP_DIR";
|
|
33
|
+
/**
|
|
34
|
+
* The credential file the Router-side broker writes per invoke at
|
|
35
|
+
* `<sessionDir>/.guuey/credentials/<server>.json`. Shape from spec §7.1 (B2-mcp).
|
|
36
|
+
* `transport` is required so the worker knows which SDK arm to build without
|
|
37
|
+
* consulting the snapshot — the broker owns ALL resolution including transport.
|
|
38
|
+
*/
|
|
39
|
+
export interface CredentialFile {
|
|
40
|
+
/** The resolved MCP URL (may be scoped `<host>/apps/<id>` for federated ggui). */
|
|
41
|
+
url: string;
|
|
42
|
+
/** Transport the broker selected for this server. */
|
|
43
|
+
transport: "http" | "sse";
|
|
44
|
+
/** Headers to forward — typically `{ authorization: 'Bearer <token>' }`. */
|
|
45
|
+
headers: Record<string, string>;
|
|
46
|
+
/** ISO expiry; informational for the worker (the Router refreshes per invoke). */
|
|
47
|
+
expiresAt?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* SDK's `mcpServers` value shape — recreated structurally rather than imported
|
|
51
|
+
* because the SDK ships it as part of `Options['mcpServers']` (a record-of-union)
|
|
52
|
+
* and pulling out a single arm is awkward in TS.
|
|
53
|
+
*/
|
|
54
|
+
export type SdkMcpServer = {
|
|
55
|
+
type: "http";
|
|
56
|
+
url: string;
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
alwaysLoad?: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
type: "sse";
|
|
61
|
+
url: string;
|
|
62
|
+
headers?: Record<string, string>;
|
|
63
|
+
alwaysLoad?: boolean;
|
|
64
|
+
} | {
|
|
65
|
+
type: "stdio";
|
|
66
|
+
command: string;
|
|
67
|
+
args?: string[];
|
|
68
|
+
alwaysLoad?: boolean;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* One prior memory record fed into the `<thread_memory>` preamble. Host-owned,
|
|
72
|
+
* minimal projection of `@silverprotocol/core`'s `AgMemoryRecord` (the preamble
|
|
73
|
+
* reads only `key`/`value`). Not imported — OSS-legality.
|
|
74
|
+
*/
|
|
75
|
+
export interface PriorMemoryRecord {
|
|
76
|
+
key?: string;
|
|
77
|
+
value: JsonValue;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Per-invoke context `buildOptions` needs beyond the static snapshot. Sourced by
|
|
81
|
+
* the worker loop from the `invoke` control message + boot env.
|
|
82
|
+
*/
|
|
83
|
+
export interface BuildOptionsContext {
|
|
84
|
+
/** The user message — passed to `query({ prompt })` by the caller. */
|
|
85
|
+
input: string;
|
|
86
|
+
/** Router-vouched end-user identity. */
|
|
87
|
+
identity: {
|
|
88
|
+
userId: string;
|
|
89
|
+
authMode: "anonymous" | "authenticated";
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Anthropic API key — used for local-dev / off-sandbox fallback when
|
|
93
|
+
* `baseUrl` + `authToken` are absent. One of (`baseUrl`+`authToken`) or
|
|
94
|
+
* `apiKey` must be provided; `buildOptions` throws if neither is present.
|
|
95
|
+
*/
|
|
96
|
+
apiKey?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Loopback proxy base URL for the managed-LLM broker (`ANTHROPIC_BASE_URL`).
|
|
99
|
+
* When present together with `authToken`, the Claude CLI subprocess is routed
|
|
100
|
+
* through the broker; the real API key is intentionally omitted from
|
|
101
|
+
* `options.env` so it cannot leak to agent code.
|
|
102
|
+
*/
|
|
103
|
+
baseUrl?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Opaque session token for the loopback proxy (`ANTHROPIC_AUTH_TOKEN`).
|
|
106
|
+
* Required when `baseUrl` is set; ignored when only `apiKey` is present.
|
|
107
|
+
*/
|
|
108
|
+
authToken?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Per-session GuueyFS layer mounts (the invoke's `fs`). When present, the
|
|
111
|
+
* invoke binds `cwd`=session, exposes home+app as `additionalDirectories`,
|
|
112
|
+
* enables the file tools, and injects `GUUEY_*` env. Absent → no FS binding.
|
|
113
|
+
*/
|
|
114
|
+
fs?: Fs;
|
|
115
|
+
/** Recent conversation window for the `<conversation_history>` preamble. */
|
|
116
|
+
history?: HistoryMessage[];
|
|
117
|
+
/** Thread-scoped memory for the `<thread_memory>` preamble (the §1.4 push). */
|
|
118
|
+
priorMemory?: PriorMemoryRecord[];
|
|
119
|
+
/** Prior working-state blob for the `<working_state>` preamble. */
|
|
120
|
+
priorState?: JsonValue;
|
|
121
|
+
/**
|
|
122
|
+
* Returns every credential the Router broker wrote to
|
|
123
|
+
* `<sessionDir>/.guuey/credentials/` this invoke — one `{name, cred}` per
|
|
124
|
+
* usable MCP server. `name` is the filename stem (server name); `cred` is the
|
|
125
|
+
* parsed `CredentialFile`. Injected so option-building stays pure (no disk).
|
|
126
|
+
*/
|
|
127
|
+
listCredentials: () => Array<{
|
|
128
|
+
name: string;
|
|
129
|
+
cred: CredentialFile;
|
|
130
|
+
}>;
|
|
131
|
+
/** Cancels the in-flight `query` when the client disconnects. */
|
|
132
|
+
abortController?: AbortController;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the Claude Agent SDK `Options` for one invoke. Pure: all disk/env access
|
|
136
|
+
* is injected via {@link BuildOptionsContext}. Throws on an unresolved `{file}`
|
|
137
|
+
* system prompt or a missing API key (the same loud failures the source had).
|
|
138
|
+
*/
|
|
139
|
+
export declare function buildOptions(snapshot: GuueyAgent, ctx: BuildOptionsContext): Options;
|
|
140
|
+
/**
|
|
141
|
+
* Auto-allow permission callback. Installed when fs is bound and the operator
|
|
142
|
+
* did NOT pin `claude.permissions.mode`, so the default no-code agent's `Bash`
|
|
143
|
+
* (and the file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
|
|
144
|
+
* every request, passing the input through unchanged.
|
|
145
|
+
*
|
|
146
|
+
* Safe because the model's tool surface is already locked down BEFORE the
|
|
147
|
+
* callback ever fires — `tools`/`allowedTools` cap which tools exist,
|
|
148
|
+
* `settingSources:[]` blocks filesystem-loaded settings, `strictMcpConfig`
|
|
149
|
+
* pins the MCP catalog — and the real OS isolation is the Router's bubblewrap
|
|
150
|
+
* jail this whole process runs inside. The callback only collapses the SDK's
|
|
151
|
+
* final interactive "ask" stage (which would otherwise hang a headless pod);
|
|
152
|
+
* the earlier hook/deny-rule stages of the permission flow still run.
|
|
153
|
+
*/
|
|
154
|
+
export declare const autoAllowTool: CanUseTool;
|
|
155
|
+
/**
|
|
156
|
+
* Map the Router-resolved cred files to the framework-neutral SdkMcpServer map.
|
|
157
|
+
* The Router (credential-broker) owns ALL resolution — default, federation, mint,
|
|
158
|
+
* env-substitution; this worker just reads `<session>/.guuey/credentials/*.json`
|
|
159
|
+
* (via ctx.listCredentials) and shapes each entry. Keyed by the server name.
|
|
160
|
+
*/
|
|
161
|
+
export declare function resolveMcpServers(ctx: BuildOptionsContext): Record<string, SdkMcpServer>;
|
|
162
|
+
/**
|
|
163
|
+
* Render prior context sections (conversation history, thread memory, working
|
|
164
|
+
* state) as a preamble and prepend to the system prompt. The SDK's `query()`
|
|
165
|
+
* accepts only the current `input` as `prompt`, so feeding context here is how
|
|
166
|
+
* an ephemeral worker gives the model memory across invokes.
|
|
167
|
+
*
|
|
168
|
+
* Empty sections are omitted; if all inputs are empty/undefined the original
|
|
169
|
+
* system prompt is returned unchanged. Exported for unit testing + reuse by the
|
|
170
|
+
* worker loop.
|
|
171
|
+
*/
|
|
172
|
+
export declare function withContextPreamble(systemPrompt: string, history: HistoryMessage[] | undefined, priorMemory: PriorMemoryRecord[] | undefined, priorState: JsonValue | undefined): string;
|
|
173
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-neutral context preamble, shared by every runner.
|
|
3
|
+
*
|
|
4
|
+
* Render prior context sections (conversation history, thread memory, working
|
|
5
|
+
* state) as a preamble and prepend to the system prompt. Ephemeral workers
|
|
6
|
+
* accept only the current `input` as the turn prompt, so feeding context here
|
|
7
|
+
* is how they give the model memory across invokes. The rendering is
|
|
8
|
+
* byte-identical across runners (the Python ADK host carried a verbatim port
|
|
9
|
+
* of this function; the JS ADK runner now shares the original).
|
|
10
|
+
*
|
|
11
|
+
* Empty sections are omitted; if all inputs are empty/undefined the original
|
|
12
|
+
* system prompt is returned unchanged.
|
|
13
|
+
*/
|
|
14
|
+
import type { HistoryMessage, JsonValue, PriorMemoryRecord } from "@guuey/worker";
|
|
15
|
+
export declare function withContextPreamble(systemPrompt: string, history: HistoryMessage[] | undefined, priorMemory: PriorMemoryRecord[] | undefined, priorState: JsonValue | undefined): string;
|
|
16
|
+
//# sourceMappingURL=preamble.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElF,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,cAAc,EAAE,GAAG,SAAS,EACrC,WAAW,EAAE,iBAAiB,EAAE,GAAG,SAAS,EAC5C,UAAU,EAAE,SAAS,GAAG,SAAS,GAChC,MAAM,CAuCR"}
|
package/dist/preamble.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function withContextPreamble(systemPrompt, history, priorMemory, priorState) {
|
|
2
|
+
const sections = [];
|
|
3
|
+
if (history && history.length > 0) {
|
|
4
|
+
sections.push([
|
|
5
|
+
"Prior conversation with this user, for context. Continue naturally;",
|
|
6
|
+
"do not repeat it back verbatim.",
|
|
7
|
+
"<conversation_history>",
|
|
8
|
+
...history.map((m) => `${roleLabel(m.role)}: ${m.text}`),
|
|
9
|
+
"</conversation_history>",
|
|
10
|
+
].join("\n"));
|
|
11
|
+
}
|
|
12
|
+
if (priorMemory && priorMemory.length > 0) {
|
|
13
|
+
sections.push([
|
|
14
|
+
"Facts you previously recorded for this thread. Treat as known.",
|
|
15
|
+
"<thread_memory>",
|
|
16
|
+
...priorMemory.map((m) => `${m.key ?? "(unkeyed)"}: ${JSON.stringify(m.value)}`),
|
|
17
|
+
"</thread_memory>",
|
|
18
|
+
].join("\n"));
|
|
19
|
+
}
|
|
20
|
+
if (priorState !== undefined) {
|
|
21
|
+
sections.push([
|
|
22
|
+
"Your working state carried from the previous turn.",
|
|
23
|
+
"<working_state>",
|
|
24
|
+
JSON.stringify(priorState, null, 2),
|
|
25
|
+
"</working_state>",
|
|
26
|
+
].join("\n"));
|
|
27
|
+
}
|
|
28
|
+
if (sections.length === 0)
|
|
29
|
+
return systemPrompt;
|
|
30
|
+
return `${sections.join("\n\n")}\n\n${systemPrompt}`;
|
|
31
|
+
}
|
|
32
|
+
function roleLabel(role) {
|
|
33
|
+
return role === "agent" ? "Assistant" : "User";
|
|
34
|
+
}
|