@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.
- package/CHANGELOG.md +40 -0
- package/dist/coding-tools/coding-tool-extras.d.ts +52 -0
- package/dist/coding-tools/coding-tool-extras.js +56 -0
- package/dist/coding-tools/coding-tool-support.d.ts +9 -0
- package/dist/coding-tools/coding-tool-support.js +12 -1
- package/dist/config/catalog-overrides.d.ts +9 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/internal/argv-exec.d.ts +6 -0
- package/dist/internal/argv-exec.js +158 -63
- package/dist/mcp/connect.js +26 -3
- package/dist/mcp/types.d.ts +7 -0
- package/dist/native/adapter-deps.d.ts +2 -0
- package/dist/native/adapter-deps.js +2 -0
- package/dist/native/models.js +2 -0
- package/dist/native/session/loop-events/types.d.ts +5 -0
- package/dist/native/session-adapter.js +19 -5
- package/dist/permissions/approval-memory.d.ts +18 -0
- package/dist/permissions/approval-memory.js +88 -0
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +1 -0
- package/dist/permissions/types.d.ts +6 -0
- package/dist/runtime/node-runtime.js +5 -0
- package/dist/sandbox/claude-settings.d.ts +2 -1
- package/dist/sandbox/claude-settings.js +7 -3
- package/dist/sandbox/defaults.js +1 -0
- package/dist/sandbox/launcher.js +7 -0
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/session/agent-session-command.d.ts +19 -0
- package/dist/session/agent-session-command.js +38 -0
- package/dist/session/agent-session-types.d.ts +26 -0
- package/dist/session/agent-session.js +86 -1
- package/dist/session/native-backend-options.d.ts +37 -2
- package/dist/session/native-backend-options.js +39 -3
- package/dist/session/native-backend.js +55 -5
- package/dist/session/native-posture-rules.d.ts +18 -0
- package/dist/session/native-posture-rules.js +33 -0
- package/dist/session/session-ask-link.d.ts +6 -0
- package/dist/session/session-ask-link.js +5 -0
- package/dist/session/session-ask-port.js +4 -0
- package/dist/session/session-backend.d.ts +23 -1
- package/dist/session/session-interaction.d.ts +2 -0
- package/dist/session/session-interaction.js +8 -1
- package/dist/session/session-tool-support.d.ts +49 -3
- package/dist/session/session-tool-support.js +66 -11
- package/dist/session-store/index.d.ts +3 -0
- package/dist/session-store/index.js +3 -0
- package/dist/session-store/session-lock-guard.d.ts +6 -0
- package/dist/session-store/session-lock-guard.js +85 -0
- package/dist/session-store/session-lock.d.ts +39 -0
- package/dist/session-store/session-lock.js +178 -0
- package/dist/session-store/session-meta.d.ts +27 -0
- package/dist/session-store/session-meta.js +20 -0
- package/dist/session-store/storage.d.ts +39 -0
- package/dist/session-store/storage.js +164 -0
- package/dist/tools/ask-request.d.ts +18 -0
- package/dist/tools/ask-request.js +30 -0
- package/dist/tools/bash-background.d.ts +74 -0
- package/dist/tools/bash-background.js +151 -0
- package/dist/tools/bash.d.ts +11 -0
- package/dist/tools/bash.js +77 -17
- package/dist/tools/git.js +10 -3
- package/dist/tools/policy-match.d.ts +2 -2
- package/dist/tools/policy-match.js +9 -6
- package/dist/tools/policy-paths-branch.js +4 -1
- package/dist/tools/provider-advertise.d.ts +7 -0
- package/dist/tools/provider-advertise.js +13 -1
- package/dist/tools/provider-types.d.ts +6 -0
- package/dist/tools/run-command-exec.js +12 -0
- package/dist/tools/run-command-surface.d.ts +28 -0
- package/dist/tools/run-command-surface.js +109 -0
- package/dist/tools/run-command.d.ts +17 -16
- package/dist/tools/run-command.js +48 -68
- package/dist/tools/runtime-calltool.d.ts +101 -0
- package/dist/tools/runtime-calltool.js +112 -3
- package/dist/tools/runtime.d.ts +12 -0
- package/dist/tools/runtime.js +36 -18
- package/dist/tools/types.d.ts +8 -0
- 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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
33
|
-
export
|
|
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[];
|