@guuey/host 0.7.0 → 0.7.1

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.
@@ -96,11 +96,27 @@ export interface BuildOptionsContext {
96
96
  */
97
97
  authToken?: string;
98
98
  /**
99
- * Per-session GuueyFS layer mounts (the invoke's `fs`). When present, the
100
- * invoke binds `cwd`=session, exposes home+app as `additionalDirectories`,
101
- * enables the file tools, and injects `GUUEY_*` env. Absent no FS binding.
99
+ * Per-session layer mounts (the invoke's `fs`). When present, the invoke
100
+ * binds `cwd`=session, exposes home+app as `additionalDirectories`, and
101
+ * injects the `HOME`/`GUUEY_*`/`CLAUDE_CONFIG_DIR` env. NOTE: the wire
102
+ * carries a NON-NULL `fs` on every turn (spec §1.4 — the spec-default
103
+ * `/app`/`/home`/`/session` mounts when GuueyFS is off), so presence here
104
+ * says nothing about whether durable storage is bound. It is therefore NOT
105
+ * the tool-catalog gate — {@link fsBound} is (guuey#234).
102
106
  */
103
107
  fs?: Fs;
108
+ /**
109
+ * Whether {@link fs} names REAL GuueyFS layers this turn — the pod has
110
+ * `GUUEY_FS_BASE` armed and the Router resolved per-session layer dirs under
111
+ * it — as opposed to the spec-default / federation-ephemeral mounts. THE gate
112
+ * for the built-in file tools + `Bash` (`tools`, and their `allowedTools`
113
+ * entries): absent/false → `tools: []`, purely MCP-driven. Threaded from the
114
+ * runtime on the invoke (`Invoke.fsBound`); mirrors {@link memoryAttached}
115
+ * (the memory-mcp T5 lesson: gate on the real signal, not on `fs` presence —
116
+ * which is why the pre-#234 `Boolean(ctx.fs)` gate put Bash in EVERY no-code
117
+ * agent's catalog on EVERY env).
118
+ */
119
+ fsBound?: boolean;
104
120
  /** Recent conversation window for the `<conversation_history>` preamble. */
105
121
  history?: HistoryMessage[];
106
122
  /** Thread-scoped memory for the `<thread_memory>` preamble (the §1.4 push). */
@@ -164,20 +180,34 @@ export interface BuildOptionsContext {
164
180
  */
165
181
  export declare function buildOptions(snapshot: GuueyAgent, ctx: BuildOptionsContext): Options;
166
182
  /**
167
- * Auto-allow permission callback. Installed when fs is bound and the operator
168
- * did NOT pin `claude.permissions.mode`, so the default no-code agent's `Bash`
169
- * (and the file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
183
+ * Auto-allow permission callback. Installed when the operator did NOT pin
184
+ * `claude.permissions.mode` and the snapshot has NO explicit `tools.allowlist`,
185
+ * so the default no-code agent's MCP tools (and, on a GuueyFS-armed pod, its
186
+ * `Bash` + file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
170
187
  * every request, passing the input through unchanged.
171
188
  *
172
189
  * Safe because the model's tool surface is already locked down BEFORE the
173
- * callback ever fires — `tools`/`allowedTools` cap which tools exist,
174
- * `settingSources:[]` blocks filesystem-loaded settings, `strictMcpConfig`
175
- * pins the MCP catalog — and the real OS isolation is the Router's bubblewrap
176
- * jail this whole process runs inside. The callback only collapses the SDK's
177
- * final interactive "ask" stage (which would otherwise hang a headless pod);
178
- * the earlier hook/deny-rule stages of the permission flow still run.
190
+ * callback ever fires — `tools`/`allowedTools`/`disallowedTools` cap which
191
+ * tools exist, `settingSources:[]` blocks filesystem-loaded settings,
192
+ * `strictMcpConfig` pins the MCP catalog — and the real OS isolation is the
193
+ * Router's bubblewrap jail this whole process runs inside. The callback only
194
+ * collapses the SDK's final interactive "ask" stage (which would otherwise
195
+ * hang a headless pod); the earlier hook/deny-rule stages still run.
179
196
  */
180
197
  export declare const autoAllowTool: CanUseTool;
198
+ /**
199
+ * Deny-unlisted permission callback (guuey#234). Installed instead of
200
+ * {@link autoAllowTool} when the snapshot carries an explicit `tools.allowlist`
201
+ * (and no pinned mode). Tools that ARE listed never reach any callback — they
202
+ * are SDK allow rules (`allowedTools`) and are auto-allowed upstream. So every
203
+ * tool that DOES arrive here is, by construction, one the builder did not
204
+ * list: it is denied with a message the model can act on. Never `null`, never
205
+ * an ask — the invariant "an explicit allowlist can never hang the pod".
206
+ *
207
+ * `allowedSet` is consulted anyway (belt-and-braces against an SDK that
208
+ * routes an allow-listed name here after all): a listed name is allowed.
209
+ */
210
+ export declare function denyUnlistedTool(allowedSet: ReadonlySet<string>): CanUseTool;
181
211
  /**
182
212
  * Map the Router-resolved cred files to the framework-neutral SdkMcpServer map.
183
213
  * The Router (credential-broker) owns ALL resolution — default, federation, mint,
@@ -185,5 +215,37 @@ export declare const autoAllowTool: CanUseTool;
185
215
  * (via ctx.listCredentials) and shapes each entry. Keyed by the server name.
186
216
  */
187
217
  export declare function resolveMcpServers(ctx: BuildOptionsContext): Record<string, SdkMcpServer>;
218
+ /**
219
+ * The resolved tool surface for one invoke — what `buildOptions` hands the SDK.
220
+ * Exported for the invariant tests ("an explicit allowlist can never hang").
221
+ */
222
+ export interface ResolvedToolGates {
223
+ /** The built-in catalog: `[...FS_TOOLS, Bash]` when fs is bound, else `[]`. */
224
+ tools: string[];
225
+ /** SDK allow rules — MCP names in the SDK's `mcp__<server>[__<tool>]` spelling plus the built-ins in `tools`. */
226
+ allowedTools: string[];
227
+ /** SDK `disallowedTools` — the translated `tools.denylist` (removed from the catalog). */
228
+ disallowedTools: string[];
229
+ /** Whether the snapshot carried a non-empty `tools.allowlist` (→ deny-unlisted posture). */
230
+ explicitAllowlist: boolean;
231
+ /** `allowedTools` as a set, for the deny-unlisted callback. */
232
+ allowedSet: ReadonlySet<string>;
233
+ }
234
+ /**
235
+ * Resolve the invoke's tool surface (guuey#234). Pure.
236
+ *
237
+ * - `tools` (the built-in catalog) is keyed ONLY on `fsBound` — the real
238
+ * "GuueyFS layers are bound this turn" signal from the runtime.
239
+ * - `allowedTools`: an explicit `tools.allowlist` is TRANSLATED from the config
240
+ * grammar (`<server>.<tool>` / `<server>.*` / bare) into SDK names — never
241
+ * passed verbatim; absent → every tool of every declared server
242
+ * (`mcp__<server>`). When fs is bound the built-ins join as allow rules so
243
+ * the platform's memory feature (file tools + `Bash` in the jail) is never
244
+ * silently switched off by a builder's MCP-only allowlist; a builder who
245
+ * wants them gone deny-lists them (`"Bash"`).
246
+ * - `disallowedTools`: the translated `tools.denylist` — the SDK removes those
247
+ * from the model's catalog outright.
248
+ */
249
+ export declare function resolveToolGates(snapshot: GuueyAgent, declaredServerNames: readonly string[], fsBound: boolean): ResolvedToolGates;
188
250
  export { withContextPreamble } from "../preamble.js";
189
251
  //# sourceMappingURL=claude-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-options.d.ts","sourceRoot":"","sources":["../../src/frameworks/claude-options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude-options.d.ts","sourceRoot":"","sources":["../../src/frameworks/claude-options.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { GUUEY_DEFAULT_SYSTEM_PROMPT, defaultModelFor, } from "@guuey/config";
1
+ import { GUUEY_DEFAULT_SYSTEM_PROMPT, defaultModelFor, parseToolGateEntry, } from "@guuey/config";
2
2
  /**
3
3
  * Default Claude model — only used when the snapshot omits `model`. Derived
4
4
  * from the `@guuey/config` registry (single source of truth per the
@@ -20,8 +20,9 @@ const DEFAULT_MAX_TURNS = 25;
20
20
  export const ENV_HOME_DIR = "GUUEY_HOME_DIR";
21
21
  export const ENV_APP_DIR = "GUUEY_APP_DIR";
22
22
  /**
23
- * File tools enabled when GuueyFS layers are bound. `Bash` is added separately
24
- * (see {@link BASH_TOOL}) so the two are independently testable.
23
+ * File tools enabled when GuueyFS layers are bound
24
+ * ({@link BuildOptionsContext.fsBound}). `Bash` is added separately (see
25
+ * {@link BASH_TOOL}) so the two are independently testable.
25
26
  */
26
27
  const FS_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
27
28
  /**
@@ -33,6 +34,12 @@ const FS_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
33
34
  * bubblewrap inside the Router's bwrap); the Router's bwrap IS the isolation.
34
35
  */
35
36
  const BASH_TOOL = "Bash";
37
+ /**
38
+ * The built-in tool catalog this host can expose — exactly the file tools plus
39
+ * `Bash`, all keyed on {@link BuildOptionsContext.fsBound}. A bare tool-gate
40
+ * entry (`"Bash"`, `"Write"`) matches one of these by name.
41
+ */
42
+ const BUILTIN_TOOLS = [...FS_TOOLS, BASH_TOOL];
36
43
  /**
37
44
  * Belt-and-braces (spec §4): guuey's memory mechanism is prompted file memory
38
45
  * (a platform-owned system-prompt section, below), never the SDK's own
@@ -105,7 +112,8 @@ export function buildOptions(snapshot, ctx) {
105
112
  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.`);
106
113
  }
107
114
  const mcpServers = resolveMcpServers(ctx);
108
- const allowedTools = buildAllowedTools(snapshot, Object.keys(mcpServers), Boolean(ctx.fs));
115
+ const fsBound = ctx.fsBound === true;
116
+ const gates = resolveToolGates(snapshot, Object.keys(mcpServers), fsBound);
109
117
  const systemPrompt = withContextPreamble(snapshot.systemPrompt ?? GUUEY_DEFAULT_SYSTEM_PROMPT, ctx.history, ctx.priorMemory, ctx.priorState) +
110
118
  buildMemorySection(ctx) +
111
119
  // cross-app-profile T7: the profile section is a SIBLING of the memory
@@ -175,13 +183,17 @@ export function buildOptions(snapshot, ctx) {
175
183
  }
176
184
  // Whether the operator pinned a Claude permission mode in agent.json. When
177
185
  // set we forward it verbatim and let the SDK's mode govern the posture; when
178
- // unset we install the auto-allow `canUseTool` below so the default no-code
179
- // agent's Bash runs prompt-free (a never-answered prompt would hang the pod).
186
+ // unset we ALWAYS install a `canUseTool` below (guuey#234) so no tool pick can
187
+ // ever reach the SDK's interactive ask stage a never-answered prompt would
188
+ // hang this headless pod.
180
189
  const explicitMode = snapshot.claude?.permissions?.mode;
181
190
  const options = {
182
191
  model,
183
192
  mcpServers,
184
- allowedTools,
193
+ allowedTools: gates.allowedTools,
194
+ // Deny-listed tools (`tools.denylist`, translated) are removed from the
195
+ // model's catalog outright — the SDK's own mechanism, not a callback.
196
+ ...(gates.disallowedTools.length > 0 ? { disallowedTools: gates.disallowedTools } : {}),
185
197
  // Token streaming (guuey#91 consumer half): the SDK interleaves
186
198
  // `stream_event` partials, which `@silverprotocol/claude-agent-sdk` ≥0.4
187
199
  // maps to token-granular text/reasoning/tool.args deltas and dedupes
@@ -191,11 +203,12 @@ export function buildOptions(snapshot, ctx) {
191
203
  // idempotent by tool_use id), so this flag's only observable effect is
192
204
  // incremental frames on the SSE wire.
193
205
  includePartialMessages: true,
194
- // With GuueyFS layers bound, expose the file tools PLUS real `Bash`; without
195
- // them this is byte-identical to the source (purely MCP-driven). `Bash` is
196
- // safe here because the host already runs inside the Router's bubblewrap
197
- // jail that bwrap, NOT the SDK's own `sandbox:{}` block, is the isolation.
198
- tools: fs ? [...FS_TOOLS, BASH_TOOL] : [],
206
+ // With GuueyFS layers REALLY bound (`fsBound`, guuey#234 never `fs`
207
+ // presence, which is unconditional on the wire), expose the file tools PLUS
208
+ // real `Bash`; otherwise purely MCP-driven. `Bash` is safe here because the
209
+ // host already runs inside the Router's bubblewrap jail that bwrap, NOT
210
+ // the SDK's own `sandbox:{}` block, is the isolation.
211
+ tools: gates.tools,
199
212
  // Settings isolation. Empty array = "no filesystem settings loaded" — guards
200
213
  // against a future SDK change auto-pulling `~/.claude/settings.json` and
201
214
  // leaking the operator's logged-in Claude Code MCPs into the tool catalog.
@@ -217,44 +230,68 @@ export function buildOptions(snapshot, ctx) {
217
230
  //
218
231
  // - Operator pinned `claude.permissions.mode` → forward it verbatim; the
219
232
  // operator owns the posture (e.g. `acceptEdits`).
220
- // - No explicit mode + fs bound → install an auto-allow `canUseTool`. In
221
- // the SDK permission flow (hooks deny allow ask mode/canUseTool),
222
- // `default` mode with no callback routes Bash subcommands through an
223
- // interactive permission prompt which, in this headless ephemeral pod,
224
- // no one answers, so the agent would HANG. The auto-allow callback short-
225
- // circuits that: every tool the model picks (already constrained to
226
- // `tools`/`allowedTools` + `settingSources:[]` + `strictMcpConfig`) is
227
- // allowed without a prompt. This is safe precisely because the Router's
228
- // bubblewrap jail is the real isolation boundary NOT the SDK's own
229
- // `sandbox:{}` block (which is intentionally absent to avoid a nested
230
- // bwrap inside the Router's bwrap). We do NOT use `bypassPermissions`
231
- // here: it requires `allowDangerouslySkipPermissions` and globally
232
- // disables hooks/deny-rule evaluation, whereas the callback keeps the
233
- // deny/hook stages intact while only collapsing the final ask stage.
233
+ // - No explicit mode ALWAYS install a `canUseTool` (guuey#234: it used
234
+ // to be installed only when fs was "bound", which was every turn — once
235
+ // the gate is real, an fs-off pod with an explicit allowlist would sit
236
+ // in `default` mode). In the SDK permission flow (hooks deny allow
237
+ // ask mode/canUseTool), `default` mode with no callback routes any
238
+ // tool not covered by an allow rule through an interactive permission
239
+ // prompt which, in this headless ephemeral pod, no one answers, so
240
+ // the agent would HANG. The callback short-circuits that stage: with no
241
+ // explicit allowlist every pick is allowed (the tool surface is already
242
+ // capped by `tools`/`allowedTools` + `settingSources:[]` +
243
+ // `strictMcpConfig`, and the Router's bubblewrap jail is the real
244
+ // isolation boundary NOT the SDK's own `sandbox:{}` block, which is
245
+ // intentionally absent to avoid a nested bwrap); with an explicit
246
+ // allowlist an UNLISTED pick is DENIED with a message the model can read
247
+ // — that is what makes the allowlist a real narrowing, and it can never
248
+ // hang. We do NOT use `bypassPermissions`: it requires
249
+ // `allowDangerouslySkipPermissions` and globally disables hooks/deny-rule
250
+ // evaluation, whereas the callback keeps those stages intact while only
251
+ // collapsing the final ask stage.
234
252
  ...(explicitMode
235
253
  ? { permissionMode: explicitMode }
236
- : fs
237
- ? { canUseTool: autoAllowTool }
238
- : {}),
254
+ : { canUseTool: gates.explicitAllowlist ? denyUnlistedTool(gates.allowedSet) : autoAllowTool }),
239
255
  ...(ctx.abortController ? { abortController: ctx.abortController } : {}),
240
256
  };
241
257
  return options;
242
258
  }
243
259
  /**
244
- * Auto-allow permission callback. Installed when fs is bound and the operator
245
- * did NOT pin `claude.permissions.mode`, so the default no-code agent's `Bash`
246
- * (and the file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
260
+ * Auto-allow permission callback. Installed when the operator did NOT pin
261
+ * `claude.permissions.mode` and the snapshot has NO explicit `tools.allowlist`,
262
+ * so the default no-code agent's MCP tools (and, on a GuueyFS-armed pod, its
263
+ * `Bash` + file tools) run prompt-free. Returns `{ behavior: 'allow' }` for
247
264
  * every request, passing the input through unchanged.
248
265
  *
249
266
  * Safe because the model's tool surface is already locked down BEFORE the
250
- * callback ever fires — `tools`/`allowedTools` cap which tools exist,
251
- * `settingSources:[]` blocks filesystem-loaded settings, `strictMcpConfig`
252
- * pins the MCP catalog — and the real OS isolation is the Router's bubblewrap
253
- * jail this whole process runs inside. The callback only collapses the SDK's
254
- * final interactive "ask" stage (which would otherwise hang a headless pod);
255
- * the earlier hook/deny-rule stages of the permission flow still run.
267
+ * callback ever fires — `tools`/`allowedTools`/`disallowedTools` cap which
268
+ * tools exist, `settingSources:[]` blocks filesystem-loaded settings,
269
+ * `strictMcpConfig` pins the MCP catalog — and the real OS isolation is the
270
+ * Router's bubblewrap jail this whole process runs inside. The callback only
271
+ * collapses the SDK's final interactive "ask" stage (which would otherwise
272
+ * hang a headless pod); the earlier hook/deny-rule stages still run.
256
273
  */
257
274
  export const autoAllowTool = (_toolName, input) => Promise.resolve({ behavior: "allow", updatedInput: input });
275
+ /**
276
+ * Deny-unlisted permission callback (guuey#234). Installed instead of
277
+ * {@link autoAllowTool} when the snapshot carries an explicit `tools.allowlist`
278
+ * (and no pinned mode). Tools that ARE listed never reach any callback — they
279
+ * are SDK allow rules (`allowedTools`) and are auto-allowed upstream. So every
280
+ * tool that DOES arrive here is, by construction, one the builder did not
281
+ * list: it is denied with a message the model can act on. Never `null`, never
282
+ * an ask — the invariant "an explicit allowlist can never hang the pod".
283
+ *
284
+ * `allowedSet` is consulted anyway (belt-and-braces against an SDK that
285
+ * routes an allow-listed name here after all): a listed name is allowed.
286
+ */
287
+ export function denyUnlistedTool(allowedSet) {
288
+ return (toolName, input) => Promise.resolve(allowedSet.has(toolName)
289
+ ? { behavior: "allow", updatedInput: input }
290
+ : {
291
+ behavior: "deny",
292
+ message: `Tool "${toolName}" is not in this agent's tools.allowlist. Allowed: ${[...allowedSet].join(", ") || "(none)"}.`,
293
+ });
294
+ }
258
295
  /**
259
296
  * Map the Router-resolved cred files to the framework-neutral SdkMcpServer map.
260
297
  * The Router (credential-broker) owns ALL resolution — default, federation, mint,
@@ -279,24 +316,68 @@ export function resolveMcpServers(ctx) {
279
316
  return out;
280
317
  }
281
318
  /**
282
- * Build the SDK's `allowedTools` array. MCP tools are auto-namespaced by the SDK
283
- * as `mcp__<server>__<tool>`; we can't enumerate the namespaces ahead of time,
284
- * so the allowlist is necessarily wildcard-ish.
285
- *
286
- * - explicit `tools.allowlist` pass those literal names through.
287
- * - else allow every tool from every declared server via `mcp__<server>`.
319
+ * Translate one parsed tool-gate entry (the `@guuey/config` grammar) into the
320
+ * SDK's tool-name spelling(s). MCP tools are auto-namespaced by the SDK as
321
+ * `mcp__<server>__<tool>`, and `mcp__<server>` is the SDK's own "every tool of
322
+ * that server" rule. A bare name fans out to EVERY declared server (we cannot
323
+ * enumerate a server's tools ahead of connecting) and, when it names one of the
324
+ * host's built-ins, to that built-in but only while fs is bound, since an
325
+ * unbound turn has no built-in catalog for it to match.
326
+ */
327
+ function toSdkToolNames(raw, declaredServerNames, fsBound) {
328
+ const parsed = parseToolGateEntry(raw);
329
+ // Deploy-time validation (`validateToolGates`) rejects malformed entries
330
+ // before a snapshot ever ships; a stale snapshot that slips one through is
331
+ // dropped here (NOT passed verbatim — an unknown allow rule is inert, and an
332
+ // unknown name reaching the ask stage is the hang this fix exists to close).
333
+ if ("error" in parsed)
334
+ return [];
335
+ switch (parsed.kind) {
336
+ case "server-tool":
337
+ return [`mcp__${parsed.server}__${parsed.tool}`];
338
+ case "server-all":
339
+ return [`mcp__${parsed.server}`];
340
+ case "bare":
341
+ return [
342
+ ...declaredServerNames.map((s) => `mcp__${s}__${parsed.tool}`),
343
+ ...(fsBound && BUILTIN_TOOLS.includes(parsed.tool) ? [parsed.tool] : []),
344
+ ];
345
+ }
346
+ }
347
+ /**
348
+ * Resolve the invoke's tool surface (guuey#234). Pure.
288
349
  *
289
- * When GuueyFS layers are bound, the file tools AND `Bash` join the allowlist
290
- * (an allow rule in the SDK permission flow), so the model may use them
291
- * alongside the MCP allowlist. The auto-allow `canUseTool` (or the operator's
292
- * pinned mode) governs whether those still promptsee `buildOptions`.
350
+ * - `tools` (the built-in catalog) is keyed ONLY on `fsBound` the real
351
+ * "GuueyFS layers are bound this turn" signal from the runtime.
352
+ * - `allowedTools`: an explicit `tools.allowlist` is TRANSLATED from the config
353
+ * grammar (`<server>.<tool>` / `<server>.*` / bare) into SDK names never
354
+ * passed verbatim; absent → every tool of every declared server
355
+ * (`mcp__<server>`). When fs is bound the built-ins join as allow rules so
356
+ * the platform's memory feature (file tools + `Bash` in the jail) is never
357
+ * silently switched off by a builder's MCP-only allowlist; a builder who
358
+ * wants them gone deny-lists them (`"Bash"`).
359
+ * - `disallowedTools`: the translated `tools.denylist` — the SDK removes those
360
+ * from the model's catalog outright.
293
361
  */
294
- function buildAllowedTools(snapshot, declaredServerNames, fsBound) {
295
- const explicit = snapshot.tools?.allowlist;
296
- const base = explicit && explicit.length > 0
297
- ? explicit.slice()
362
+ export function resolveToolGates(snapshot, declaredServerNames, fsBound) {
363
+ const explicit = snapshot.tools?.allowlist ?? [];
364
+ const explicitAllowlist = explicit.length > 0;
365
+ const builtins = fsBound ? [...BUILTIN_TOOLS] : [];
366
+ const mcpAllowed = explicitAllowlist
367
+ ? explicit.flatMap((e) => toSdkToolNames(e, declaredServerNames, fsBound))
298
368
  : declaredServerNames.map((s) => `mcp__${s}`);
299
- return fsBound ? [...base, ...FS_TOOLS, BASH_TOOL] : base;
369
+ const allowedTools = dedupe([...mcpAllowed, ...builtins]);
370
+ const disallowedTools = dedupe((snapshot.tools?.denylist ?? []).flatMap((e) => toSdkToolNames(e, declaredServerNames, fsBound)));
371
+ return {
372
+ tools: builtins,
373
+ allowedTools,
374
+ disallowedTools,
375
+ explicitAllowlist,
376
+ allowedSet: new Set(allowedTools),
377
+ };
378
+ }
379
+ function dedupe(names) {
380
+ return [...new Set(names)];
300
381
  }
301
382
  // withContextPreamble now lives in ../preamble.js (framework-neutral — the
302
383
  // ADK runner renders the same preamble); re-exported for existing importers.
@@ -23,6 +23,12 @@ export interface HostInvoke {
23
23
  input: string;
24
24
  identity: Identity;
25
25
  fs: Fs;
26
+ /**
27
+ * Whether `fs` names REAL GuueyFS layers this turn (guuey#234) — the gate
28
+ * for the built-in file tools + `Bash`. See `Invoke.fsBound` in
29
+ * `@guuey/worker`. Absent/false → purely MCP-driven.
30
+ */
31
+ fsBound?: boolean;
26
32
  history: HistoryMessage[];
27
33
  priorMemory?: PriorMemoryRecord[];
28
34
  priorState?: JsonValue;
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/frameworks/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,EAAE,EACF,cAAc,EACd,QAAQ,EACR,SAAS,EACT,cAAc,EAEf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EAEZ,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAQ7B;;;;;;GAMG;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;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC;gEAC4D;IAC5D,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;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,CAoDf"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/frameworks/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,EAAE,EACF,cAAc,EACd,QAAQ,EACR,SAAS,EACT,cAAc,EAEf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EAEZ,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAQ7B;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACtC;gEAC4D;IAC5D,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;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,CAqDf"}
@@ -42,6 +42,7 @@ export async function runInvoke(snapshot, invoke, runtime, emit, query) {
42
42
  input: invoke.input,
43
43
  identity: invoke.identity,
44
44
  fs: invoke.fs,
45
+ ...(invoke.fsBound !== undefined ? { fsBound: invoke.fsBound } : {}),
45
46
  history: invoke.history,
46
47
  listCredentials: runtime.listCredentials,
47
48
  ...(runtime.apiKey !== undefined ? { apiKey: runtime.apiKey } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/host",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "The universal config-driven Guuey worker. Reads the resolved agent.json snapshot, runs the Claude Agent SDK, and emits each native SDKMessage to fd-3 as a `native` WorkerEvent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,8 +22,8 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@guuey/worker": "0.7.0",
26
- "@guuey/config": "0.7.0"
25
+ "@guuey/worker": "0.7.1",
26
+ "@guuey/config": "0.7.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.0.0",