@nathapp/nax-agent 0.91.6 → 0.91.7-canary.10

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/coding-tools/coding-tool-extras.d.ts +52 -0
  3. package/dist/coding-tools/coding-tool-extras.js +56 -0
  4. package/dist/coding-tools/coding-tool-support.d.ts +9 -0
  5. package/dist/coding-tools/coding-tool-support.js +12 -1
  6. package/dist/config/catalog-overrides.d.ts +9 -0
  7. package/dist/index.d.ts +4 -2
  8. package/dist/index.js +2 -1
  9. package/dist/internal/argv-exec.d.ts +6 -0
  10. package/dist/internal/argv-exec.js +158 -63
  11. package/dist/mcp/connect.js +26 -3
  12. package/dist/mcp/types.d.ts +7 -0
  13. package/dist/native/adapter-deps.d.ts +2 -0
  14. package/dist/native/adapter-deps.js +2 -0
  15. package/dist/native/models.js +2 -0
  16. package/dist/native/session/loop-events/types.d.ts +5 -0
  17. package/dist/native/session-adapter.js +19 -5
  18. package/dist/permissions/approval-memory.d.ts +18 -0
  19. package/dist/permissions/approval-memory.js +88 -0
  20. package/dist/permissions/index.d.ts +1 -0
  21. package/dist/permissions/index.js +1 -0
  22. package/dist/permissions/types.d.ts +6 -0
  23. package/dist/runtime/node-runtime.js +5 -0
  24. package/dist/sandbox/claude-settings.d.ts +2 -1
  25. package/dist/sandbox/claude-settings.js +7 -3
  26. package/dist/sandbox/defaults.js +1 -0
  27. package/dist/sandbox/launcher.js +7 -0
  28. package/dist/sandbox/types.d.ts +6 -0
  29. package/dist/session/agent-session-command.d.ts +19 -0
  30. package/dist/session/agent-session-command.js +38 -0
  31. package/dist/session/agent-session-types.d.ts +26 -0
  32. package/dist/session/agent-session.js +86 -1
  33. package/dist/session/native-backend-options.d.ts +37 -2
  34. package/dist/session/native-backend-options.js +39 -3
  35. package/dist/session/native-backend.js +55 -5
  36. package/dist/session/native-posture-rules.d.ts +18 -0
  37. package/dist/session/native-posture-rules.js +33 -0
  38. package/dist/session/session-ask-link.d.ts +6 -0
  39. package/dist/session/session-ask-link.js +5 -0
  40. package/dist/session/session-ask-port.js +4 -0
  41. package/dist/session/session-backend.d.ts +23 -1
  42. package/dist/session/session-interaction.d.ts +2 -0
  43. package/dist/session/session-interaction.js +8 -1
  44. package/dist/session/session-tool-support.d.ts +49 -3
  45. package/dist/session/session-tool-support.js +66 -11
  46. package/dist/session-store/index.d.ts +3 -0
  47. package/dist/session-store/index.js +3 -0
  48. package/dist/session-store/session-lock-guard.d.ts +6 -0
  49. package/dist/session-store/session-lock-guard.js +85 -0
  50. package/dist/session-store/session-lock.d.ts +39 -0
  51. package/dist/session-store/session-lock.js +178 -0
  52. package/dist/session-store/session-meta.d.ts +27 -0
  53. package/dist/session-store/session-meta.js +20 -0
  54. package/dist/session-store/storage.d.ts +39 -0
  55. package/dist/session-store/storage.js +164 -0
  56. package/dist/tools/ask-request.d.ts +18 -0
  57. package/dist/tools/ask-request.js +30 -0
  58. package/dist/tools/bash-background.d.ts +74 -0
  59. package/dist/tools/bash-background.js +151 -0
  60. package/dist/tools/bash.d.ts +11 -0
  61. package/dist/tools/bash.js +77 -17
  62. package/dist/tools/git.js +10 -3
  63. package/dist/tools/policy-match.d.ts +2 -2
  64. package/dist/tools/policy-match.js +9 -6
  65. package/dist/tools/policy-paths-branch.js +4 -1
  66. package/dist/tools/provider-advertise.d.ts +7 -0
  67. package/dist/tools/provider-advertise.js +13 -1
  68. package/dist/tools/provider-types.d.ts +6 -0
  69. package/dist/tools/run-command-exec.js +12 -0
  70. package/dist/tools/run-command-surface.d.ts +28 -0
  71. package/dist/tools/run-command-surface.js +109 -0
  72. package/dist/tools/run-command.d.ts +17 -16
  73. package/dist/tools/run-command.js +48 -68
  74. package/dist/tools/runtime-calltool.d.ts +101 -0
  75. package/dist/tools/runtime-calltool.js +112 -3
  76. package/dist/tools/runtime.d.ts +12 -0
  77. package/dist/tools/runtime.js +36 -18
  78. package/dist/tools/types.d.ts +8 -0
  79. package/package.json +3 -3
@@ -1,18 +1,50 @@
1
+ /**
2
+ * nativeBackend's options (S4 spec 5.2): the native half of the facade's
3
+ * createAgentSession options, validated here rather than by the facade. zod
4
+ * checks the shape; the caller's own object is kept as `raw` (zod returns
5
+ * copies, which would break identity for handlers the caller owns).
6
+ */
7
+ import { z } from "zod";
1
8
  import type { BashApprovalMode } from "#src/config/bash-approval";
9
+ import { SandboxConfigSchema } from "#src/config/schemas-sandbox";
2
10
  import type { NativeCatalogOverrides } from "#src/native/client";
3
11
  import type { CredentialSource } from "#src/native/credentials/session-source";
4
12
  import type { LoopHandlerSet } from "#src/native/session/loop-events/types";
13
+ import type { ToolProvider } from "#src/tools/provider-types";
5
14
  import type { AgentSessionHostPorts, AgentSessionProfile } from "./agent-session-types.ts";
6
15
  export interface NativeBackendOptions {
7
16
  readonly model: string;
17
+ /** Static or discovered tool providers attached to this session's profile and stage. */
18
+ readonly toolProviders?: readonly ToolProvider[];
8
19
  /** Repository instruction basename; defaults to AGENTS.md. */
9
20
  readonly instructionFileName?: string;
10
21
  readonly credentials?: CredentialSource;
11
22
  readonly catalogOverrides?: NativeCatalogOverrides;
12
23
  readonly loopHandlers?: LoopHandlerSet;
24
+ /** Copied into every turn's loop-handler context as `role` (for example "tui"). */
25
+ readonly loopHandlerRole?: string;
13
26
  readonly hostPorts?: AgentSessionHostPorts;
14
27
  readonly bashApproval?: BashApprovalMode;
28
+ /** Ask before a per-call sandbox escape, including under raw; requires allowUnsandboxedCommands. */
29
+ readonly askOnUnsandboxed?: boolean;
30
+ /** Gate Bash when the sandbox is disabled or unavailable at session open. */
31
+ readonly askWhenSandboxDown?: boolean;
32
+ /** Keep provider-designated mutating tool asks outside the ask profile too. */
33
+ readonly askDiscoveredTools?: boolean;
34
+ /** Rule overlays appended to the profile's own permission rules. */
35
+ readonly askRules?: readonly string[];
36
+ readonly denyRules?: readonly string[];
37
+ /** Partial sandbox input, validated here and default-filled at posture resolution. */
38
+ readonly sandbox?: z.input<typeof SandboxConfigSchema>;
15
39
  readonly allowUnsandboxed?: boolean;
40
+ /** Narrow the declared tools to these names (built-in names, `<provider>__<tool>`, or `<provider>__*`); scratchpad tools are always kept. */
41
+ readonly allowedTools?: readonly string[];
42
+ /**
43
+ * US-001: offer the Bash/Exec tools' `unsandboxed` argument, so a model can ask
44
+ * for one command to run outside the OS sandbox. Refused by the tool-less
45
+ * profiles, which have no shell to unwrap.
46
+ */
47
+ readonly allowUnsandboxedCommands?: boolean;
16
48
  /** Keep a resumed or continued conversation when the model differs (ACP server model switch, S5-3). */
17
49
  readonly carryHistoryAcrossModels?: boolean;
18
50
  /**
@@ -34,7 +66,10 @@ export interface ResolvedNativeOptions {
34
66
  readonly provider: string;
35
67
  }
36
68
  export declare function parseNativeBackendOptions(input: unknown): ResolvedNativeOptions;
37
- export declare function nativeProfileRules(profile: AgentSessionProfile, raw: NativeBackendOptions): {
69
+ /** The profile's resolved knobs: the validator's answer, consumed as data. */
70
+ export interface ResolvedProfileRules {
38
71
  readonly bashApproval: BashApprovalMode;
39
72
  readonly allowUnsandboxed: boolean;
40
- };
73
+ readonly allowUnsandboxedCommands: boolean;
74
+ }
75
+ export declare function nativeProfileRules(profile: AgentSessionProfile, raw: NativeBackendOptions): ResolvedProfileRules;
@@ -5,6 +5,7 @@
5
5
  * copies, which would break identity for handlers the caller owns).
6
6
  */
7
7
  import { z } from "zod";
8
+ import { SandboxConfigSchema } from "#src/config/schemas-sandbox";
8
9
  import { parseNativeModel } from "#src/native/models";
9
10
  import { compactionSettingsSchema } from "#src/native/session/compaction-settings";
10
11
  import { isInstructionFileName } from "#src/native/session/instruction-file-name";
@@ -18,6 +19,14 @@ function hasMethods(names) {
18
19
  const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
19
20
  const OptionsSchema = z.strictObject({
20
21
  model: z.string().min(1),
22
+ toolProviders: z
23
+ .custom((value) => Array.isArray(value) &&
24
+ value.every((provider) => isRecord(provider) &&
25
+ typeof provider.id === "string" &&
26
+ (provider.kind === "static" || provider.kind === "discovered") &&
27
+ isStringArray(provider.stages) &&
28
+ typeof provider.tools === "function"), "must be an array of tool providers with id, kind, stages and tools()")
29
+ .optional(),
21
30
  instructionFileName: z
22
31
  .string()
23
32
  .refine(isInstructionFileName, "must be a nonhidden Markdown basename without path separators, colon or control characters")
@@ -27,16 +36,31 @@ const OptionsSchema = z.strictObject({
27
36
  .optional(),
28
37
  catalogOverrides: z.array(z.custom(isRecord, "must be a catalog override object")).optional(),
29
38
  loopHandlers: z.array(z.custom(isRecord, "must be a loop handler entry")).optional(),
39
+ loopHandlerRole: z.string().min(1).optional(),
30
40
  hostPorts: z
31
41
  .strictObject({
32
42
  protectedPaths: z
33
43
  .custom((value) => isRecord(value) && isStringArray(value.gitExcludePathspecs) && isStringArray(value.gitIgnorePatterns), "must be a protected-paths policy with gitExcludePathspecs and gitIgnorePatterns string arrays")
34
44
  .optional(),
45
+ commandShadow: z
46
+ .custom(hasMethods(["observe", "settle", "drain"]), "must implement observe, settle and drain")
47
+ .optional(),
48
+ backgroundCommands: z
49
+ .custom(hasMethods(["adopt", "foreground"]), "must implement adopt and foreground")
50
+ .optional(),
35
51
  commandInterceptor: z.custom(hasMethods(["intercept"]), "must implement intercept").optional(),
36
52
  })
37
53
  .optional(),
38
54
  bashApproval: z.enum(["raw", "gated", "escalate"]).optional(),
55
+ askOnUnsandboxed: z.boolean().optional(),
56
+ askWhenSandboxDown: z.boolean().optional(),
57
+ askDiscoveredTools: z.boolean().optional(),
58
+ askRules: z.array(z.string().min(1)).optional(),
59
+ denyRules: z.array(z.string().min(1)).optional(),
60
+ sandbox: SandboxConfigSchema.optional(),
39
61
  allowUnsandboxed: z.boolean().optional(),
62
+ allowedTools: z.array(z.string().min(1)).optional(),
63
+ allowUnsandboxedCommands: z.boolean().optional(),
40
64
  carryHistoryAcrossModels: z.boolean().optional(),
41
65
  compaction: compactionSettingsSchema.optional(),
42
66
  });
@@ -78,14 +102,26 @@ export function nativeProfileRules(profile, raw) {
78
102
  path: "backend.allowUnsandboxed",
79
103
  });
80
104
  }
105
+ if (!tools && raw.allowUnsandboxedCommands !== undefined) {
106
+ throw invalid('allowUnsandboxedCommands applies to profiles "ask", "full" and "review" only', {
107
+ path: "backend.allowUnsandboxedCommands",
108
+ });
109
+ }
81
110
  const bashApproval = raw.bashApproval ?? "gated";
82
111
  if (profile === "ask" && bashApproval !== "gated") {
83
112
  throw invalid('profile "ask" requires bashApproval "gated" (other modes run Bash without asking)', {
84
113
  path: "backend.bashApproval",
85
114
  });
86
115
  }
87
- if (raw.allowUnsandboxed === true && bashApproval !== "gated") {
88
- throw invalid('allowUnsandboxed requires bashApproval "gated"', { path: "backend.allowUnsandboxed" });
116
+ // Raw never asks, so an unsandboxed fallback under it is the person's own standing
117
+ // decision; escalate has no one to fall back to. Gated and raw are the two that may.
118
+ const fallbackAllowed = bashApproval === "gated" || bashApproval === "raw";
119
+ if (raw.allowUnsandboxed === true && !fallbackAllowed) {
120
+ throw invalid('allowUnsandboxed requires bashApproval "gated" or "raw"', { path: "backend.allowUnsandboxed" });
89
121
  }
90
- return { bashApproval, allowUnsandboxed: raw.allowUnsandboxed === true };
122
+ return {
123
+ bashApproval,
124
+ allowUnsandboxed: raw.allowUnsandboxed === true,
125
+ allowUnsandboxedCommands: raw.allowUnsandboxedCommands === true,
126
+ };
91
127
  }
@@ -4,16 +4,21 @@
4
4
  * with built-in and embedder tools, the NativeSessionAdapter, loop handlers
5
5
  * and the resume model check.
6
6
  */
7
+ import { backgroundToolOptions } from "#src/coding-tools/coding-tool-extras";
8
+ import { getLogger } from "#src/infra/agent-logger";
9
+ import { errorMessage } from "#src/infra/errors";
7
10
  import { DEFAULT_SPIN_BREAKER_SETTINGS } from "#src/infra/spin-breaker/index";
8
11
  import { NATIVE_AGENT } from "#src/native/models";
9
12
  import { resolveCompaction } from "#src/native/session/compaction-settings";
10
13
  import { transcriptModelIdentity } from "#src/native/session/transcript-identity";
11
14
  import { NativeSessionAdapter } from "#src/native/session-adapter";
15
+ import { resolveProviderTools } from "#src/tools/provider-advertise";
12
16
  import { AgentSessionError } from "./agent-session-errors.js";
13
17
  import { nativeProfileRules, parseNativeBackendOptions, } from "./native-backend-options.js";
18
+ import { effectiveBashApproval, nativePostureRules } from "./native-posture-rules.js";
14
19
  import { createSessionAskLink, createSessionAskResolver } from "./session-ask-link.js";
15
20
  import { createSessionInteractionHandler, embedderToolDescriptor } from "./session-interaction.js";
16
- import { buildSessionToolSupport, defaultProtectedPaths, resolveSessionLauncher } from "./session-tool-support.js";
21
+ import { buildSessionToolSupport, defaultProtectedPaths, providerAdmits, resolveSessionLauncher, SESSION_STAGE, } from "./session-tool-support.js";
17
22
  export const NATIVE_BACKEND_KIND = "native";
18
23
  /** nax's agent.native.transportRetry default (S3 spec 5.3). */
19
24
  const SESSION_TRANSPORT_RETRY = {
@@ -38,32 +43,58 @@ async function openNative(resolved, ctx) {
38
43
  const raw = resolved.raw;
39
44
  if (ctx.resume !== undefined && raw.carryHistoryAcrossModels !== true)
40
45
  checkResumeModel(ctx.resume.doc, ctx.sessionId, raw.model);
41
- const { bashApproval, allowUnsandboxed } = nativeProfileRules(ctx.profile, raw);
46
+ const { bashApproval: profileMode, allowUnsandboxed, allowUnsandboxedCommands, } = nativeProfileRules(ctx.profile, raw);
47
+ const posture = nativePostureRules(ctx.profile, raw);
42
48
  const protectedPaths = {
43
49
  ...defaultProtectedPaths(raw.credentials !== undefined),
44
50
  ...raw.hostPorts?.protectedPaths,
45
51
  };
46
- const launcher = await resolveSessionLauncher({
52
+ const { launcher, sandbox } = await resolveSessionLauncher({
47
53
  profile: ctx.profile,
48
54
  root: ctx.workdir,
49
55
  protectedPaths,
50
- bashApproval,
56
+ bashApproval: profileMode,
51
57
  allowUnsandboxed,
58
+ config: posture.sandbox,
52
59
  });
60
+ const bashApproval = effectiveBashApproval(profileMode, posture.askWhenSandboxDown, sandbox.kind);
61
+ const providerTools = ctx.profile === "none" || raw.toolProviders === undefined
62
+ ? undefined
63
+ : await resolveProviderTools(raw.toolProviders, SESSION_STAGE, ctx.workdir, raw.allowedTools !== undefined ? { admits: providerAdmits(raw.allowedTools) } : undefined);
64
+ for (const failure of providerTools?.failures ?? []) {
65
+ getLogger().warn("session", "Tool provider skipped", { sessionId: ctx.sessionId, ...failure });
66
+ }
67
+ for (const tool of ctx.tools) {
68
+ if (providerTools?.providerIdByTool.has(tool.name)) {
69
+ throw new AgentSessionError(`Embedder tool "${tool.name}" conflicts with a provider tool`, "AGENT_SESSION_TOOL_NAME_RESERVED", { tool: tool.name });
70
+ }
71
+ }
53
72
  let callId;
54
73
  const { support, grants } = buildSessionToolSupport({
55
74
  profile: ctx.profile,
56
75
  root: ctx.workdir,
57
76
  sessionName: ctx.sessionId,
77
+ ...(providerTools !== undefined ? { providerTools } : {}),
78
+ ...(raw.allowedTools !== undefined ? { allowedTools: raw.allowedTools } : {}),
58
79
  protectedPaths,
59
80
  bashApproval,
60
81
  launcher,
61
82
  askResolver: createSessionAskResolver(createSessionAskLink({ port: ctx.asks, currentCallId: () => callId })),
62
83
  interceptor: raw.hostPorts?.commandInterceptor,
84
+ askRules: posture.askRules,
85
+ denyRules: posture.denyRules,
86
+ askOnUnsandboxed: posture.askOnUnsandboxed,
87
+ askDiscoveredTools: posture.askDiscoveredTools,
88
+ commandShadow: raw.hostPorts?.commandShadow,
89
+ ...backgroundToolOptions(raw.hostPorts),
90
+ // US-003: the profile's validated opt-in, so an interactive "full" session
91
+ // can offer Bash's per-call escape without turning the sandbox off.
92
+ ...(allowUnsandboxedCommands ? { unsandboxedCommands: true } : {}),
63
93
  });
64
94
  const interactionHandler = createSessionInteractionHandler({
65
95
  sessionId: ctx.sessionId,
66
96
  runtime: support.runtime,
97
+ unavailableTools: new Set((support.declaredTools ?? []).filter((name) => !support.tools.some((tool) => tool.name === name))),
67
98
  embedderTools: new Map(ctx.tools.map((tool) => [tool.name, tool])),
68
99
  asks: ctx.asks,
69
100
  turnSignal: ctx.turnSignal,
@@ -77,6 +108,7 @@ async function openNative(resolved, ctx) {
77
108
  workdir: ctx.workdir,
78
109
  instructionProtectedPaths: protectedPaths,
79
110
  instructionFileName: raw.instructionFileName,
111
+ ...(support.providerInstructions !== undefined ? { providerInstructions: support.providerInstructions } : {}),
80
112
  resolvedPermissions: { mode: "default", toolGrants: grants, bashApproval },
81
113
  modelDef: { provider: resolved.provider, model: raw.model },
82
114
  timeoutSeconds: ctx.turnTimeoutSeconds,
@@ -95,11 +127,29 @@ async function openNative(resolved, ctx) {
95
127
  workdir: ctx.workdir,
96
128
  model: raw.model,
97
129
  provider: resolved.provider,
130
+ ...(raw.loopHandlerRole !== undefined ? { role: raw.loopHandlerRole } : {}),
98
131
  };
99
132
  return {
100
133
  adapter,
101
134
  handle,
102
- info: { kind: NATIVE_BACKEND_KIND, capabilities: {} },
135
+ runCommand: async ({ command, callId, signal }) => {
136
+ try {
137
+ const response = await interactionHandler.onInteraction({
138
+ kind: "coding-tool",
139
+ name: "Bash",
140
+ input: { command },
141
+ toolCallId: callId,
142
+ signal,
143
+ });
144
+ if (response === null)
145
+ return { content: "Bash is not available in this session", isError: true };
146
+ return { content: response.answer, isError: response.denied !== undefined };
147
+ }
148
+ catch (error) {
149
+ return { content: errorMessage(error), isError: true };
150
+ }
151
+ },
152
+ info: { kind: NATIVE_BACKEND_KIND, capabilities: {}, sandbox },
103
153
  turnOpts: () => ({
104
154
  interactionHandler,
105
155
  codingTools,
@@ -0,0 +1,18 @@
1
+ /** Optional native-session overlays, separate from the existing profile defaults. */
2
+ import type { BashApprovalMode } from "#src/config/bash-approval";
3
+ import { type SandboxConfig } from "#src/config/schemas-sandbox";
4
+ import type { SandboxState } from "#src/sandbox/index";
5
+ import type { ToolGrant } from "#src/tools/types";
6
+ import type { AgentSessionProfile } from "./agent-session-types.ts";
7
+ import type { NativeBackendOptions } from "./native-backend-options.ts";
8
+ export interface ResolvedPostureRules {
9
+ readonly askRules: readonly ToolGrant[];
10
+ readonly denyRules: readonly ToolGrant[];
11
+ readonly sandbox: SandboxConfig;
12
+ readonly askOnUnsandboxed: boolean;
13
+ readonly askWhenSandboxDown: boolean;
14
+ readonly askDiscoveredTools: boolean;
15
+ }
16
+ export declare function nativePostureRules(profile: AgentSessionProfile, raw: NativeBackendOptions): ResolvedPostureRules;
17
+ /** Snapshot the sandbox posture at open; a running session never silently changes modes. */
18
+ export declare function effectiveBashApproval(profileMode: BashApprovalMode, askWhenSandboxDown: boolean, sandboxKind: SandboxState["kind"]): BashApprovalMode;
@@ -0,0 +1,33 @@
1
+ import { DEFAULT_SANDBOX_CONFIG, SandboxConfigSchema } from "#src/config/schemas-sandbox";
2
+ import { parseRuleList } from "#src/permissions/grammar";
3
+ import { AgentSessionError } from "./agent-session-errors.js";
4
+ export function nativePostureRules(profile, raw) {
5
+ const toolOnlyOptions = [
6
+ "askRules",
7
+ "denyRules",
8
+ "askOnUnsandboxed",
9
+ "askWhenSandboxDown",
10
+ "askDiscoveredTools",
11
+ ];
12
+ for (const name of toolOnlyOptions) {
13
+ if ((profile === "none" || profile === "read") && raw[name] !== undefined) {
14
+ const path = `backend.${name}`;
15
+ throw new AgentSessionError(`Invalid agent session options: ${path} applies to profiles "ask", "full" and "review" only`, "AGENT_SESSION_INVALID_OPTIONS", { path });
16
+ }
17
+ }
18
+ if (raw.askOnUnsandboxed === true && raw.allowUnsandboxedCommands !== true) {
19
+ throw new AgentSessionError("Invalid agent session options: backend.askOnUnsandboxed requires backend.allowUnsandboxedCommands: true", "AGENT_SESSION_INVALID_OPTIONS", { path: "backend.askOnUnsandboxed" });
20
+ }
21
+ return {
22
+ askOnUnsandboxed: raw.askOnUnsandboxed === true,
23
+ askWhenSandboxDown: raw.askWhenSandboxDown === true,
24
+ askDiscoveredTools: raw.askDiscoveredTools === true,
25
+ askRules: parseRuleList(raw.askRules ?? []),
26
+ denyRules: parseRuleList(raw.denyRules ?? []),
27
+ sandbox: raw.sandbox === undefined ? DEFAULT_SANDBOX_CONFIG : SandboxConfigSchema.parse(raw.sandbox),
28
+ };
29
+ }
30
+ /** Snapshot the sandbox posture at open; a running session never silently changes modes. */
31
+ export function effectiveBashApproval(profileMode, askWhenSandboxDown, sandboxKind) {
32
+ return askWhenSandboxDown && sandboxKind !== "available" ? "gated" : profileMode;
33
+ }
@@ -19,6 +19,12 @@ export interface ApprovalAsk {
19
19
  readonly command?: string;
20
20
  readonly reason: string;
21
21
  readonly callId?: string;
22
+ /**
23
+ * US-004: the call wants to run OUTSIDE the OS sandbox. Forwarded verbatim
24
+ * from `AskRequest` onto the `approval_requested` event; a host that does not
25
+ * recognise it shows the same prompt it always did.
26
+ */
27
+ readonly unsandboxed?: true;
22
28
  }
23
29
  export declare function askPerson(deps: SessionAskDeps, ask: ApprovalAsk, signal: AbortSignal | undefined): Promise<{
24
30
  readonly decision: "allow" | "deny";
@@ -15,6 +15,7 @@ export async function askPerson(deps, ask, signal) {
15
15
  summary: ask.summary,
16
16
  ...(ask.command !== undefined ? { command: ask.command } : {}),
17
17
  reason: ask.reason,
18
+ ...(ask.unsandboxed !== undefined ? { unsandboxed: ask.unsandboxed } : {}),
18
19
  expiresAt,
19
20
  });
20
21
  const settlement = await settled;
@@ -42,6 +43,10 @@ export function createSessionAskLink(deps) {
42
43
  ...(masked !== undefined ? { command: masked.masked } : {}),
43
44
  reason: req.reason ?? `matched ${req.rule}`,
44
45
  ...(callId !== undefined ? { callId } : {}),
46
+ // US-004: carried through untouched, so a host that offers a distinct
47
+ // unsandboxed decision sees it on the port; a host that ignores it
48
+ // keeps the one approval prompt it has.
49
+ ...(req.unsandboxed !== undefined ? { unsandboxed: req.unsandboxed } : {}),
45
50
  ...(control?.signal !== undefined ? { signal: control.signal } : {}),
46
51
  });
47
52
  },
@@ -40,6 +40,10 @@ export function createSessionAskPort(deps) {
40
40
  tool: req.tool,
41
41
  summary: req.summary,
42
42
  reason: req.reason,
43
+ // US-004: an auto-decision carries whatever the request carried. No
44
+ // caller sets this today (an unsandboxed ask is never auto-decided),
45
+ // so it rides along rather than being silently dropped.
46
+ ...(req.unsandboxed !== undefined ? { unsandboxed: req.unsandboxed } : {}),
43
47
  expiresAt: now(),
44
48
  answerable: false,
45
49
  });
@@ -6,13 +6,21 @@
6
6
  * satisfy this contract; nothing here is backend-specific.
7
7
  */
8
8
  import type { TranscriptDoc, TranscriptStore } from "#src/native/session/transcript-types";
9
- import type { AgentSessionProfile, ApprovalDecidedBy, EmbedderTool } from "./agent-session-types.ts";
9
+ import type { SandboxState } from "#src/sandbox/types";
10
+ import type { AgentSessionProfile, ApprovalDecidedBy, CommandRunResult, EmbedderTool, SessionConfigureInput } from "./agent-session-types.ts";
10
11
  import type { AgentSessionAdapter, SendTurnOpts, SessionHandle } from "./session-types.ts";
11
12
  export interface BackendInfo {
12
13
  /** "native" or "acp:<agent>"; recorded in the transcript document. */
13
14
  readonly kind: string;
14
15
  /** Read-only, JSON-safe capability summary. `{}` for native. */
15
16
  readonly capabilities: Readonly<Record<string, unknown>>;
17
+ /**
18
+ * US-001: the sandbox this backend resolved when it opened, so the embedder can
19
+ * say what the session's commands actually run under. Reported once at open — a
20
+ * sandbox that stops working mid-session is not re-probed. Absent for a backend
21
+ * that runs no shell of its own (the ACP backend leaves it out).
22
+ */
23
+ readonly sandbox?: SandboxState;
16
24
  }
17
25
  export interface ApprovalRequest {
18
26
  readonly callId?: string;
@@ -20,6 +28,12 @@ export interface ApprovalRequest {
20
28
  readonly summary: string;
21
29
  readonly command?: string;
22
30
  readonly reason: string;
31
+ /**
32
+ * US-004: the ask is for a run OUTSIDE the OS sandbox. Copied from the
33
+ * `AskRequest` by `createSessionAskLink` and forwarded onto the
34
+ * `approval_requested` event by `askPerson`; absent on every ordinary ask.
35
+ */
36
+ readonly unsandboxed?: true;
23
37
  /** Extra abort source, combined with the turn signal (for example a tool-level abort). */
24
38
  readonly signal?: AbortSignal;
25
39
  }
@@ -75,6 +89,14 @@ export interface BackendOpenContext {
75
89
  readonly providerInstructions?: string;
76
90
  }
77
91
  export interface OpenedBackend {
92
+ /** Absent on a backend that cannot change model or effort on a live session. The facade validates `input` first. */
93
+ configure?(input: SessionConfigureInput): Promise<void>;
94
+ /** Absent on a backend that cannot run a host-side command (ACP). */
95
+ runCommand?(request: {
96
+ command: string;
97
+ callId: string;
98
+ signal: AbortSignal;
99
+ }): Promise<CommandRunResult>;
78
100
  readonly adapter: AgentSessionAdapter;
79
101
  readonly handle: SessionHandle;
80
102
  readonly info: BackendInfo;
@@ -9,6 +9,8 @@ export interface SessionInteractionDeps {
9
9
  readonly sessionId: string;
10
10
  readonly runtime: CodingToolRuntime;
11
11
  readonly embedderTools: ReadonlyMap<string, EmbedderTool>;
12
+ /** Declared coding tools withheld by the runtime's advertisement policy. Never execute these calls. */
13
+ readonly unavailableTools?: ReadonlySet<string>;
12
14
  readonly asks: SessionAskPort;
13
15
  /** The running turn's signal (a never-aborting one between turns). */
14
16
  readonly turnSignal: () => AbortSignal;
@@ -168,8 +168,15 @@ export function createSessionInteractionHandler(deps) {
168
168
  async onInteraction(request) {
169
169
  if (request.kind === "question")
170
170
  return answerQuestion(deps, request.text);
171
- if (request.kind === "context-tool")
171
+ if (request.kind === "context-tool") {
172
+ // A model may still call a denied tool it remembers from earlier turns.
173
+ // Keep it hidden, but distinguish a policy refusal from an unknown name.
174
+ if (deps.unavailableTools?.has(request.name)) {
175
+ const reason = `Tool "${request.name}" is not available under the session policy`;
176
+ return { answer: `Denied: ${reason}`, denied: { reason, breach: false } };
177
+ }
172
178
  throw toolError(`Unknown tool "${request.name}"`, request.name);
179
+ }
173
180
  const tool = deps.embedderTools.get(request.name);
174
181
  return tool !== undefined ? runEmbedderTool(deps, tool, request) : runCodingTool(deps, request);
175
182
  },
@@ -1,9 +1,13 @@
1
1
  import { type CodingToolSupport } from "#src/coding-tools/coding-tool-support";
2
2
  import type { CommandInterceptor } from "#src/command-interceptor/index";
3
+ import type { CommandShadow } from "#src/command-safety/types";
3
4
  import type { BashApprovalMode } from "#src/config/bash-approval";
5
+ import { type SandboxConfig } from "#src/config/schemas-sandbox";
4
6
  import type { AskResolver } from "#src/permissions/index";
5
- import type { CommandLauncher } from "#src/sandbox/index";
7
+ import type { CommandLauncher, SandboxState } from "#src/sandbox/index";
8
+ import type { BackgroundCommandPort } from "#src/tools/bash-background";
6
9
  import type { ProtectedPathsPolicy } from "#src/tools/protected-paths";
10
+ import type { ResolvedProviderTools } from "#src/tools/provider-advertise";
7
11
  import type { CodingToolName, ToolGrant } from "#src/tools/types";
8
12
  import type { AgentSessionProfile } from "./agent-session-types.ts";
9
13
  /** The pipeline stage the facade's tool calls and asks carry. */
@@ -26,11 +30,29 @@ export interface SessionLauncherArgs {
26
30
  readonly profile: AgentSessionProfile;
27
31
  readonly root: string;
28
32
  readonly protectedPaths: ProtectedPathsPolicy;
33
+ /**
34
+ * The resolved mode. Since US-001 the floor itself no longer keys on it —
35
+ * `nativeProfileRules` has already validated which mode may pair with
36
+ * `allowUnsandboxed` — but it stays part of the call so the caller declares
37
+ * the session's posture in one place.
38
+ */
29
39
  readonly bashApproval: BashApprovalMode;
30
40
  readonly allowUnsandboxed: boolean;
41
+ readonly config?: SandboxConfig;
31
42
  }
32
- /** The sandbox floor (spec 6.3): "ask", "full" and "review" need a usable sandbox unless gated + allowUnsandboxed. */
33
- export declare function resolveSessionLauncher(args: SessionLauncherArgs): Promise<CommandLauncher | undefined>;
43
+ /** What the floor decided: the launcher to run under (if any), and the state to report. */
44
+ export interface ResolvedSessionLauncher {
45
+ readonly launcher: CommandLauncher | undefined;
46
+ readonly sandbox: SandboxState;
47
+ }
48
+ /**
49
+ * The sandbox floor (spec 6.3): "ask", "full" and "review" need a usable sandbox,
50
+ * unless the session explicitly allows running without one — under `gated` (every
51
+ * call still goes to the person) or under `raw` (the person's own standing choice).
52
+ * The resolved state rides back beside the launcher, so the session can report
53
+ * what its commands actually run under.
54
+ */
55
+ export declare function resolveSessionLauncher(args: SessionLauncherArgs): Promise<ResolvedSessionLauncher>;
34
56
  export interface SessionToolSupportArgs {
35
57
  readonly profile: AgentSessionProfile;
36
58
  readonly root: string;
@@ -40,7 +62,31 @@ export interface SessionToolSupportArgs {
40
62
  readonly launcher: CommandLauncher | undefined;
41
63
  readonly askResolver: AskResolver;
42
64
  readonly interceptor: CommandInterceptor | undefined;
65
+ readonly askRules?: readonly ToolGrant[];
66
+ readonly denyRules?: readonly ToolGrant[];
67
+ readonly askOnUnsandboxed?: boolean;
68
+ readonly askDiscoveredTools?: boolean;
69
+ readonly commandShadow?: CommandShadow;
70
+ /**
71
+ * US-003: offer Bash's per-call `unsandboxed` argument for this session
72
+ * (from `nativeProfileRules(...).allowUnsandboxedCommands`). Optional and
73
+ * defaulting to off, so every existing caller keeps today's tool surface.
74
+ */
75
+ readonly unsandboxedCommands?: boolean;
76
+ readonly backgroundCommands?: BackgroundCommandPort;
77
+ readonly providerTools?: ResolvedProviderTools;
78
+ /**
79
+ * Narrow the profile's built-in tools to these names; the universal scratchpad
80
+ * tools are always kept. ABSENT keeps every profile tool. Never adds a tool.
81
+ * Provider tools are narrowed earlier, by `providerAdmits` at resolution.
82
+ */
83
+ readonly allowedTools?: readonly string[];
43
84
  }
85
+ /**
86
+ * The `admits` predicate for `resolveProviderTools`: a tool `<id>__<local>` is
87
+ * admitted when `allowedTools` names it exactly or names `<id>__*`.
88
+ */
89
+ export declare function providerAdmits(allowedTools: readonly string[]): (providerId: string, localName: string) => boolean;
44
90
  export interface SessionToolSupport {
45
91
  readonly support: CodingToolSupport;
46
92
  readonly grants: readonly ToolGrant[];