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

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 (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/coding-tools/coding-tool-extras.d.ts +42 -0
  3. package/dist/coding-tools/coding-tool-extras.js +31 -0
  4. package/dist/coding-tools/coding-tool-support.d.ts +6 -0
  5. package/dist/coding-tools/coding-tool-support.js +7 -1
  6. package/dist/config/catalog-overrides.d.ts +5 -0
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.js +2 -1
  9. package/dist/internal/argv-exec.d.ts +2 -0
  10. package/dist/internal/argv-exec.js +72 -7
  11. package/dist/mcp/connect.js +26 -3
  12. package/dist/mcp/types.d.ts +7 -0
  13. package/dist/native/models.js +1 -0
  14. package/dist/native/session/loop-events/types.d.ts +5 -0
  15. package/dist/native/session-adapter.js +16 -5
  16. package/dist/permissions/approval-memory.d.ts +18 -0
  17. package/dist/permissions/approval-memory.js +88 -0
  18. package/dist/permissions/index.d.ts +1 -0
  19. package/dist/permissions/index.js +1 -0
  20. package/dist/permissions/types.d.ts +6 -0
  21. package/dist/sandbox/claude-settings.d.ts +2 -1
  22. package/dist/sandbox/claude-settings.js +7 -3
  23. package/dist/sandbox/defaults.js +1 -0
  24. package/dist/sandbox/launcher.js +3 -0
  25. package/dist/sandbox/types.d.ts +2 -0
  26. package/dist/session/agent-session-command.d.ts +19 -0
  27. package/dist/session/agent-session-command.js +38 -0
  28. package/dist/session/agent-session-types.d.ts +9 -0
  29. package/dist/session/agent-session.js +49 -1
  30. package/dist/session/native-backend-options.d.ts +18 -2
  31. package/dist/session/native-backend-options.js +26 -3
  32. package/dist/session/native-backend.js +42 -4
  33. package/dist/session/session-ask-link.d.ts +6 -0
  34. package/dist/session/session-ask-link.js +5 -0
  35. package/dist/session/session-ask-port.js +4 -0
  36. package/dist/session/session-backend.d.ts +21 -1
  37. package/dist/session/session-tool-support.d.ts +39 -3
  38. package/dist/session/session-tool-support.js +54 -10
  39. package/dist/session-store/index.d.ts +3 -0
  40. package/dist/session-store/index.js +3 -0
  41. package/dist/session-store/session-lock-guard.d.ts +6 -0
  42. package/dist/session-store/session-lock-guard.js +85 -0
  43. package/dist/session-store/session-lock.d.ts +39 -0
  44. package/dist/session-store/session-lock.js +175 -0
  45. package/dist/session-store/session-meta.d.ts +27 -0
  46. package/dist/session-store/session-meta.js +20 -0
  47. package/dist/session-store/storage.d.ts +39 -0
  48. package/dist/session-store/storage.js +164 -0
  49. package/dist/tools/ask-request.d.ts +18 -0
  50. package/dist/tools/ask-request.js +30 -0
  51. package/dist/tools/bash.d.ts +9 -0
  52. package/dist/tools/bash.js +61 -16
  53. package/dist/tools/git.js +10 -3
  54. package/dist/tools/policy-match.d.ts +2 -2
  55. package/dist/tools/policy-match.js +9 -6
  56. package/dist/tools/policy-paths-branch.js +4 -1
  57. package/dist/tools/provider-advertise.d.ts +7 -0
  58. package/dist/tools/provider-advertise.js +13 -1
  59. package/dist/tools/provider-types.d.ts +6 -0
  60. package/dist/tools/run-command-exec.js +12 -0
  61. package/dist/tools/run-command-surface.d.ts +28 -0
  62. package/dist/tools/run-command-surface.js +109 -0
  63. package/dist/tools/run-command.d.ts +17 -16
  64. package/dist/tools/run-command.js +48 -68
  65. package/dist/tools/runtime-calltool.d.ts +100 -0
  66. package/dist/tools/runtime-calltool.js +112 -3
  67. package/dist/tools/runtime.d.ts +11 -0
  68. package/dist/tools/runtime.js +36 -18
  69. package/dist/tools/types.d.ts +8 -0
  70. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
5
5
  release may change the public API.
6
6
 
7
+ ## [0.91.7-canary.2] - 2026-10-26
8
+
9
+ - No changes in this package. Released in lockstep with the other nax packages.
10
+
11
+ ## [0.91.7-canary.1] - 2026-10-24
12
+
13
+ - No changes in this package. Released in lockstep with the other nax packages.
14
+
7
15
  ## [0.91.6] - 2026-10-24
8
16
 
9
17
  - No changes in this package. Released in lockstep with the other nax packages.
@@ -31,5 +31,47 @@ export interface DeclaredCommandToolsArgs {
31
31
  readonly runDeclaredCommand?: DeclaredCommandRunner;
32
32
  /** P4: how Bash and Exec-branch commands run; absent = direct spawn (tests). */
33
33
  readonly launcher?: CommandLauncher;
34
+ /**
35
+ * US-003/US-008: whether this session offers the per-call `unsandboxed`
36
+ * argument — on Bash (`createBashTool`) and on RunCommand's argv branch
37
+ * (`createRunCommandTool`'s `exec` option, US-008). Advertisement only:
38
+ * RunCommand's DECLARED-command branch has no argv to unwrap and refuses the
39
+ * flag by name.
40
+ */
41
+ readonly unsandboxedCommands?: boolean;
34
42
  }
43
+ /**
44
+ * US-003: `unsandboxedCommands` as a spreadable field — present only when the
45
+ * session opted in, so "off" and "absent" stay one shape all the way down to
46
+ * `createBashTool` and to RunCommand's `exec` option (US-008).
47
+ *
48
+ * A helper rather than an inline `cond ? { field } : {}` spread because
49
+ * `buildDeclaredCommandTools` and `buildCodingToolSupport` both sit at their
50
+ * cognitive-complexity baseline and a ternary inside either one would raise it.
51
+ */
52
+ export declare function unsandboxedToolOptions(args: {
53
+ readonly unsandboxedCommands?: boolean;
54
+ }): {
55
+ readonly unsandboxedCommands?: true;
56
+ };
57
+ /**
58
+ * US-005: the same option as the RUNTIME's field — whether a `Bash`/`Exec`
59
+ * call may run outside the sandbox at all, plus the Bash approval mode that
60
+ * decides how an enabled request is routed.
61
+ *
62
+ * Absent when the session did not opt in, so "off" and "absent" stay one shape
63
+ * all the way to `createCodingToolRuntime` (absent means disabled). A helper
64
+ * for the same reason as `unsandboxedToolOptions`: `buildCodingToolSupport`
65
+ * sits at its cognitive-complexity baseline and a ternary inside it would
66
+ * raise it.
67
+ */
68
+ export declare function unsandboxedRuntimeOption(args: {
69
+ readonly unsandboxedCommands?: boolean;
70
+ readonly bashApproval: BashApprovalMode;
71
+ }): {
72
+ readonly unsandboxed?: {
73
+ readonly enabled: boolean;
74
+ readonly bashApproval: BashApprovalMode;
75
+ };
76
+ };
35
77
  export declare function buildDeclaredCommandTools(args: DeclaredCommandToolsArgs): CodingTool[];
@@ -1,4 +1,30 @@
1
1
  import { createBashTool, createRunCommandTool, } from "#src/tools/index";
2
+ /**
3
+ * US-003: `unsandboxedCommands` as a spreadable field — present only when the
4
+ * session opted in, so "off" and "absent" stay one shape all the way down to
5
+ * `createBashTool` and to RunCommand's `exec` option (US-008).
6
+ *
7
+ * A helper rather than an inline `cond ? { field } : {}` spread because
8
+ * `buildDeclaredCommandTools` and `buildCodingToolSupport` both sit at their
9
+ * cognitive-complexity baseline and a ternary inside either one would raise it.
10
+ */
11
+ export function unsandboxedToolOptions(args) {
12
+ return args.unsandboxedCommands === true ? { unsandboxedCommands: true } : {};
13
+ }
14
+ /**
15
+ * US-005: the same option as the RUNTIME's field — whether a `Bash`/`Exec`
16
+ * call may run outside the sandbox at all, plus the Bash approval mode that
17
+ * decides how an enabled request is routed.
18
+ *
19
+ * Absent when the session did not opt in, so "off" and "absent" stay one shape
20
+ * all the way to `createCodingToolRuntime` (absent means disabled). A helper
21
+ * for the same reason as `unsandboxedToolOptions`: `buildCodingToolSupport`
22
+ * sits at its cognitive-complexity baseline and a ternary inside it would
23
+ * raise it.
24
+ */
25
+ export function unsandboxedRuntimeOption(args) {
26
+ return args.unsandboxedCommands === true ? { unsandboxed: { enabled: true, bashApproval: args.bashApproval } } : {};
27
+ }
2
28
  export function buildDeclaredCommandTools(args) {
3
29
  return [
4
30
  ...(args.declaredCommands.size > 0 || args.allowExec
@@ -31,6 +57,10 @@ export function buildDeclaredCommandTools(args) {
31
57
  patterns: args.execGrant?.patterns ?? [],
32
58
  ...(args.packageName !== undefined ? { packageName: args.packageName } : {}),
33
59
  ...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
60
+ // US-008: the same option Bash takes, forwarded to the argv
61
+ // branch's advertisement through the same helper — one
62
+ // shape for "off" and "absent" on both tools.
63
+ ...unsandboxedToolOptions(args),
34
64
  },
35
65
  }
36
66
  : {}),
@@ -49,6 +79,7 @@ export function buildDeclaredCommandTools(args) {
49
79
  bashApproval: args.bashApproval,
50
80
  ...(args.humanApproval === true ? { humanApproval: true } : {}),
51
81
  ...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
82
+ ...unsandboxedToolOptions(args),
52
83
  }),
53
84
  ]
54
85
  : []),
@@ -142,6 +142,12 @@ export declare function buildCodingToolSupport(args: {
142
142
  commandShadow?: CommandShadow;
143
143
  /** P4: resolved by resolveCodingToolSupport (async); data only here. */
144
144
  launcher?: CommandLauncher;
145
+ /**
146
+ * US-003: whether this session offers Bash's per-call `unsandboxed` argument
147
+ * (the session profile's `allowUnsandboxedCommands`). Advertisement only —
148
+ * the runtime's own reading of the field is a later story.
149
+ */
150
+ unsandboxedCommands?: boolean;
145
151
  /** US-001: forwarded into ToolRunContext so Bash/Exec can SIGKILL on cancel. */
146
152
  abortSignal?: AbortSignal;
147
153
  /**
@@ -11,7 +11,7 @@
11
11
  import { getSafeLogger, NaxError } from "#src/infra/index";
12
12
  import { advertisedSchemaBytes, BASH_TOOL_NAME, compileToolPolicy, createCodingToolRuntime, createNoOpToolAuditSink, createToolAuditSink, EXEC_TOOL_NAME, } from "#src/tools/index";
13
13
  import { resolveBashSupport } from "./coding-tool-bash.js";
14
- import { buildDeclaredCommandTools } from "./coding-tool-extras.js";
14
+ import { buildDeclaredCommandTools, unsandboxedRuntimeOption, unsandboxedToolOptions } from "./coding-tool-extras.js";
15
15
  import { isTempConfined, rawScreenOptionsFor } from "./coding-tool-sandbox.js";
16
16
  export function buildCodingToolSupport(args) {
17
17
  if (args.declared.length === 0)
@@ -119,9 +119,15 @@ export function buildCodingToolSupport(args) {
119
119
  ...(args.stripEnvVars !== undefined ? { stripEnvVars: args.stripEnvVars } : {}),
120
120
  ...(args.shell !== undefined ? { shell: args.shell } : {}),
121
121
  ...(args.launcher !== undefined ? { launcher: args.launcher } : {}),
122
+ ...unsandboxedToolOptions(args),
122
123
  }),
123
124
  ],
124
125
  ...(args.providerIdByTool !== undefined ? { providerIdByTool: args.providerIdByTool } : {}),
126
+ // US-005: the escape option reaches the RUNTIME too, not just the Bash
127
+ // schema — with the locally resolved mode, so a caller that left
128
+ // `bashApproval` unset cannot enable an escape under a mode it did not
129
+ // choose.
130
+ ...unsandboxedRuntimeOption({ unsandboxedCommands: args.unsandboxedCommands, bashApproval }),
125
131
  });
126
132
  const tools = runtime.advertised(advertised);
127
133
  // The fixed per-hop cost of advertising provider tools: their schemas enter
@@ -91,6 +91,11 @@ export interface CatalogModelOverride {
91
91
  * `protocol: "openai-completions"` (nax-ai `assertOverrideModelRouting`).
92
92
  */
93
93
  openRouterRouting?: OpenRouterRouting;
94
+ /**
95
+ * Thinking wire format for an `openai-completions` model (nax#2696).
96
+ * Mirrored from nax-ai's ThinkingFormat; omit to inherit the provider's format.
97
+ */
98
+ thinkingFormat?: "openai" | "qwen" | "qwen-chat-template" | "deepseek" | "zai";
94
99
  }
95
100
  /** Provider-scoped catalog overrides — maps 1:1 onto nax-ai's `ProviderOverride[]`. */
96
101
  export interface ProviderCatalogOverride {
package/dist/index.d.ts CHANGED
@@ -24,13 +24,14 @@ export { AuthCancelledError, type AuthEvent, type AuthInteraction, type AuthLink
24
24
  export type { NativeSessionAdapterOptions } from "#src/native/session-adapter";
25
25
  export type { AfterResponsePatch, AfterResponsePayload, AfterToolPatch, AfterToolPayload, BeforeCompactionPatch, BeforeCompactionPayload, BeforeRequestPatch, BeforeRequestPayload, BeforeToolOutcome, BeforeToolPayload, BeforeTurnEndPatch, BeforeTurnEndPayload, BeforeTurnPatch, BeforeTurnPayload, CompleteCallOptions, ExternalHandlerOf, LoopEvent, LoopEventMap, LoopHandlerContext, LoopHandlerEntry, LoopHandlerSet, PatchOf, PayloadOf, TransformContextPatch, TransformContextPayload, } from "#src/native/session/loop-events/types";
26
26
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
27
- export { type ApprovalAuditRow, type ApprovalEntry, type ApprovalsFile, type ApprovalsFileRead, type ApprovalsFileState, type ApprovalsTaint, ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, type AskControl, type AskDecidedBy, type AskDecision, type AskLink, type AskLinkOutcome, type AskRequest, type AskResolver, type AskVerdict, appendApproval, appendApprovalAudit, approvalId, approvalsPath, type BashLexResult, type BashRedirect, type BashSegment, type BashSegmentSeparator, type BashToken, type ClearTaintOutcome, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, type MaskResult, maskForPrompt, type PrepareApprovalsStoreOptions, parseRuleList, parseToolExpression, prepareApprovalsStore, type RemovalDecision, type RemovalResult, type ResolvedPermissions, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, type SecretSpan, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
27
+ export { type ApprovalAuditRow, type ApprovalEntry, type ApprovalMemoryDecision, type ApprovalMemoryRequest, approvalMemoryApplies, approvalMemoryKey, type ApprovalsFile, type ApprovalsFileRead, type ApprovalsFileState, type ApprovalsTaint, ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, type AskControl, type AskDecidedBy, type AskDecision, type AskLink, type AskLinkOutcome, type AskRequest, type AskResolver, type AskVerdict, appendApproval, appendApprovalAudit, approvalId, approvalsPath, type BashLexResult, type BashRedirect, type BashSegment, type BashSegmentSeparator, type BashToken, type ClearTaintOutcome, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, type MaskResult, maskForPrompt, type PrepareApprovalsStoreOptions, parseRuleList, parseToolExpression, prepareApprovalsStore, type RemovalDecision, type RemovalResult, type ResolvedPermissions, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, type SecretSpan, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
28
28
  export { type AgentGlobOptions, type AgentRuntime, type AgentSpawnOptions, type AgentSpawnResult, type AgentSpawnStdin, getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
29
29
  export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, SANDBOXED_ENV, defaultTempRoots, denialHintLine, type GitLayout, type LaunchRequest, type LaunchResult, type LaunchSpec, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, type ProbeResult, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, type SandboxBackend, type SandboxBackendName, type SandboxNetworkPolicy, type SandboxPolicy, type SandboxPolicyInput, type SandboxRecord, type SandboxState, type SandboxWrapRequest, type SessionSandbox, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
30
+ export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, SESSION_ID_PATTERN, SESSION_META_MODES, type SessionMeta, type SessionMetaMode, type SessionListEntry, type SessionListPage, type SessionLockHolder, type SessionLockHolderState, type SessionStorage, type SessionStorageDeps, } from "#src/session-store/index";
30
31
  export type { AdapterFailure } from "#src/session/adapter-failure";
31
32
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
32
33
  export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
33
- export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
34
+ export type { AgentSession, AgentSessionHostPorts, AgentSessionProfile, AnswerReply, AnswerStatus, ApprovalDecidedBy, CostSource, CommandRunResult, CreateAgentSessionOptions, EmbedderTool, EmbedderToolContext, EmbedderToolResult, SessionEvent, SessionEventBase, SessionEventBody, TurnEndStatus, } from "#src/session/agent-session-types";
34
35
  export type { AgentAwaitingHumanEvent, AgentCallEndedEvent, AgentCallStartedEvent, AgentMessageUpdateEvent, AgentProcessUpdateEvent, AgentStreamEvent, AgentStreamEventBase, AgentThinkingUpdateEvent, AgentToolCallUpdateEvent, AgentUsageUpdateEvent, } from "#src/session/agent-stream-events";
35
36
  export { type AdapterInteraction, type AdapterInteractionResponse, type InteractionHandler, NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
36
37
  export { nativeBackend } from "#src/session/native-backend";
package/dist/index.js CHANGED
@@ -19,9 +19,10 @@ export { NaxError } from "#src/infra/nax-error";
19
19
  export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, } from "#src/terminal-auth/index";
20
20
  export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, importPiCredentials, listProviderModels, listStoredProviders, loginProviderIds, MAX_RETAINED_TRANSCRIPTS, NATIVE_AGENT, NativeSessionAdapter, NativeSessionUnsupportedError, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, providersWithoutCredentials, pruneRetainedTranscripts, removeStoredProvider, resolveNativeId, runLogin, servedAuth, } from "#src/native/index";
21
21
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
22
- export { ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, appendApproval, appendApprovalAudit, approvalId, approvalsPath, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, maskForPrompt, parseRuleList, parseToolExpression, prepareApprovalsStore, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
22
+ export { approvalMemoryApplies, approvalMemoryKey, ASK_CANCELLED_REASON, ASK_DENIED_REASON, ASK_NO_CHANNEL_REASON, ASK_PROFILE_REASON, ASK_TIMEOUT_REASON, ASK_UNAVAILABLE_REASON, ASK_UNSHOWABLE_REASON, appendApproval, appendApprovalAudit, approvalId, approvalsPath, chainAskLinks, clearApprovalsTaint, createApprovalsLink, findApproval, findSecretSpans, headlessAskResolver, isForgeCapable, lexBashCommand, maskForPrompt, parseRuleList, parseToolExpression, prepareApprovalsStore, readApprovals, readApprovalsFile, readApprovalsFileDetailed, redactForRow, redactRowStrings, removeApprovals, taintApprovals, writeApprovalsFile, } from "#src/permissions/index";
23
23
  export { getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
24
24
  export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, SANDBOXED_ENV, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sessionSandboxFrom, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
25
+ export { createSessionStorage, defaultSessionsDir, processAlive, LIST_PAGE_SIZE, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, SESSION_ID_PATTERN, SESSION_META_MODES, } from "#src/session-store/index";
25
26
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
26
27
  export { AgentSessionError } from "#src/session/agent-session-errors";
27
28
  export { NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
@@ -45,6 +45,8 @@ export declare const _argvExecDeps: {
45
45
  * stays at the DRAIN_GRACE_MS constant.
46
46
  */
47
47
  drainGraceMs: number;
48
+ /** Per-stream capture cap (STREAM_CAPTURE_BYTES); a test shrinks it to exercise the cut. */
49
+ streamCaptureBytes: number;
48
50
  /**
49
51
  * Injectable timer pair — lets the BUG-13 "hung install" test drive the
50
52
  * timeout off a virtual clock instead of waiting the full 1s schema minimum.
@@ -23,6 +23,15 @@ import { killProcessGroup } from "./process-kill.js";
23
23
  * Injectable for tests; not exposed as user configuration.
24
24
  */
25
25
  export const DRAIN_GRACE_MS = 500;
26
+ /**
27
+ * Most bytes of ONE stream runArgv keeps in memory: its first half and its last half, with the
28
+ * middle replaced by a marker naming how much was dropped. Callers cap what they show far below
29
+ * this (Bash at READ_CEILING, 2 MB), so it bounds the work rather than the answer: before it, a
30
+ * `yes` or a looping test runner was buffered whole for up to the timeout and could exhaust the
31
+ * orchestrator's memory. The stream is still read to EOF, so the child never blocks or takes a
32
+ * SIGPIPE on a full pipe; the tail survives because that is where a failure's error usually is.
33
+ */
34
+ const STREAM_CAPTURE_BYTES = 8_000_000;
26
35
  /** Injectable seam, mirroring _worktreeDependencyDeps. */
27
36
  export const _argvExecDeps = {
28
37
  spawn: runtimeSpawn,
@@ -33,6 +42,8 @@ export const _argvExecDeps = {
33
42
  * stays at the DRAIN_GRACE_MS constant.
34
43
  */
35
44
  drainGraceMs: DRAIN_GRACE_MS,
45
+ /** Per-stream capture cap (STREAM_CAPTURE_BYTES); a test shrinks it to exercise the cut. */
46
+ streamCaptureBytes: STREAM_CAPTURE_BYTES,
36
47
  /**
37
48
  * Injectable timer pair — lets the BUG-13 "hung install" test drive the
38
49
  * timeout off a virtual clock instead of waiting the full 1s schema minimum.
@@ -75,11 +86,10 @@ function buildEnv(options) {
75
86
  */
76
87
  async function drainToEof(stream, signal) {
77
88
  const reader = stream.getReader();
78
- const decoder = new TextDecoder();
79
- let text = "";
89
+ const capture = new BoundedCapture(_argvExecDeps.streamCaptureBytes);
80
90
  const onAbort = () => {
81
91
  // Closing the stream forces the in-flight `read()` below to resolve with
82
- // `{done: true}`; any bytes already captured stay in `text`.
92
+ // `{done: true}`; any bytes already captured stay in `capture`.
83
93
  reader.cancel().catch(() => {
84
94
  // Cancelling after the pipe already closed is a no-op race; ignore.
85
95
  });
@@ -91,15 +101,14 @@ async function drainToEof(stream, signal) {
91
101
  if (done === true)
92
102
  break;
93
103
  if (value !== undefined)
94
- text += decoder.decode(value, { stream: true });
104
+ capture.push(value);
95
105
  }
96
106
  }
97
107
  catch {
98
108
  // Stream cancelled by SIGKILL on the process group; the partial text
99
- // is still in `text`, so resolve with closed=false and let the caller
109
+ // is still in `capture`, so resolve with what it holds and let the caller
100
110
  // decide.
101
111
  }
102
- text += decoder.decode();
103
112
  if (signal !== undefined)
104
113
  signal.removeEventListener("abort", onAbort);
105
114
  try {
@@ -108,7 +117,63 @@ async function drainToEof(stream, signal) {
108
117
  catch {
109
118
  // Already released; nothing to do.
110
119
  }
111
- return { text, closed: true };
120
+ return { text: capture.text(), closed: true };
121
+ }
122
+ /**
123
+ * Keeps the first and the last `maxBytes / 2` bytes of a stream and counts what falls between
124
+ * (STREAM_CAPTURE_BYTES). The head is decoded as it arrives; the tail is kept as bytes, trimmed from
125
+ * the front, and decoded once at the end from a codepoint boundary.
126
+ */
127
+ class BoundedCapture {
128
+ half;
129
+ decoder = new TextDecoder();
130
+ head = "";
131
+ headBytes = 0;
132
+ tail = [];
133
+ tailBytes = 0;
134
+ omitted = 0;
135
+ constructor(maxBytes) {
136
+ this.half = Math.max(1, Math.floor(maxBytes / 2));
137
+ }
138
+ push(chunk) {
139
+ let rest = chunk;
140
+ if (this.headBytes < this.half) {
141
+ const take = Math.min(this.half - this.headBytes, rest.length);
142
+ this.head += this.decoder.decode(rest.subarray(0, take), { stream: true });
143
+ this.headBytes += take;
144
+ rest = rest.subarray(take);
145
+ }
146
+ if (rest.length === 0)
147
+ return;
148
+ this.tail.push(rest);
149
+ this.tailBytes += rest.length;
150
+ while (this.tailBytes > this.half) {
151
+ const first = this.tail[0];
152
+ const excess = this.tailBytes - this.half;
153
+ if (first.length <= excess) {
154
+ this.tail.shift();
155
+ this.tailBytes -= first.length;
156
+ this.omitted += first.length;
157
+ }
158
+ else {
159
+ this.tail[0] = first.subarray(excess);
160
+ this.tailBytes -= excess;
161
+ this.omitted += excess;
162
+ }
163
+ }
164
+ }
165
+ text() {
166
+ const tail = Buffer.concat(this.tail);
167
+ if (this.omitted === 0)
168
+ return this.head + this.decoder.decode(tail);
169
+ // The cut can land inside a codepoint: skip its continuation bytes so the tail opens cleanly.
170
+ let start = 0;
171
+ while (start < tail.length && (tail[start] & 0xc0) === 0x80)
172
+ start += 1;
173
+ const head = this.head + this.decoder.decode();
174
+ const marker = `\n[... ${this.omitted + start} bytes of output omitted ...]\n`;
175
+ return head + marker + new TextDecoder().decode(tail.subarray(start));
176
+ }
112
177
  }
113
178
  export async function runArgv(options) {
114
179
  const signal = options.signal;
@@ -6,7 +6,8 @@
6
6
  * once the process is gone. A failed connect is closed the same way.
7
7
  */
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
- import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
9
+ import { ErrorCode, ListToolsResultSchema, McpError, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
10
+ import { z } from "zod";
10
11
  import { McpCallError, McpConnectError } from "#src/mcp/errors";
11
12
  import { resultText } from "#src/mcp/result-text";
12
13
  import { waitForStdioExit } from "#src/mcp/stdio-close";
@@ -53,13 +54,35 @@ function toolInfo(raw) {
53
54
  if (!isRaw(raw) || typeof raw.name !== "string" || raw.name.length === 0)
54
55
  return undefined;
55
56
  const inputSchema = isRaw(raw.inputSchema) ? { ...raw.inputSchema } : {};
56
- return { name: raw.name, description: typeof raw.description === "string" ? raw.description : "", inputSchema };
57
+ const readOnly = isRaw(raw.annotations) && raw.annotations.readOnlyHint === true;
58
+ return {
59
+ name: raw.name,
60
+ description: typeof raw.description === "string" ? raw.description : "",
61
+ inputSchema,
62
+ ...(readOnly ? { readOnly: true } : {}),
63
+ };
64
+ }
65
+ /**
66
+ * The SDK's own tools/list schema, except that a tool's `annotations` are not
67
+ * type-checked: the SDK would reject the WHOLE page over one server's
68
+ * `readOnlyHint: "yes"`, which would cost the connection every tool. `toolInfo`
69
+ * reads the hint strictly (`=== true`), so a malformed one just means "not
70
+ * read-only" and every other field is still validated.
71
+ */
72
+ const LenientListToolsSchema = ListToolsResultSchema.extend({
73
+ tools: z.array(ToolSchema.extend({ annotations: z.record(z.string(), z.unknown()).optional() })),
74
+ });
75
+ async function listPage(client, cursor, signal, timeout) {
76
+ const page = await client.request({ method: "tools/list", ...(cursor !== undefined ? { params: { cursor } } : {}) }, LenientListToolsSchema, { signal, timeout });
77
+ // What Client.listTools does after its own request: the SDK reads these caches in callTool.
78
+ client["cacheToolMetadata"](page.tools);
79
+ return page;
57
80
  }
58
81
  async function listAll(client, signal, timeout) {
59
82
  const tools = [];
60
83
  let cursor;
61
84
  do {
62
- const page = await client.listTools(cursor !== undefined ? { cursor } : undefined, { signal, timeout });
85
+ const page = await listPage(client, cursor, signal, timeout);
63
86
  for (const raw of page.tools ?? []) {
64
87
  const info = toolInfo(raw);
65
88
  if (info !== undefined)
@@ -15,6 +15,13 @@ export interface McpToolInfo {
15
15
  readonly name: string;
16
16
  readonly description: string;
17
17
  readonly inputSchema: JSONSchema;
18
+ /**
19
+ * Present (as `true`) only when the server annotated the tool
20
+ * `readOnlyHint: true`. Absent in every other case, so an unannotated tool
21
+ * keeps its `{ name, description, inputSchema }` shape. Trusted as the server
22
+ * states it; the lock pins input schemas, not this hint.
23
+ */
24
+ readonly readOnly?: true;
18
25
  }
19
26
  export interface McpCallResult {
20
27
  readonly text: string;
@@ -117,6 +117,7 @@ export function toProviderOverrides(overrides) {
117
117
  // Omitted when the override declares none: nax-ai then falls back to the
118
118
  // template sibling's ceiling, which is the only honest value available.
119
119
  ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}),
120
+ ...(model.thinkingFormat !== undefined ? { thinkingFormat: model.thinkingFormat } : {}),
120
121
  supportsTools: model.supportsTools,
121
122
  thinkingLevels: model.thinkingLevels,
122
123
  // nax#2191: forwarded verbatim. nax-ai's `assertOverrideModelRouting`
@@ -128,6 +128,11 @@ export interface BeforeRequestPatch {
128
128
  * parameter, not a new concept.
129
129
  */
130
130
  export interface CompleteCallOptions {
131
+ /**
132
+ * `false` asks for no thinking on this request; on a model that cannot
133
+ * disable thinking it gets the least thinking the model supports instead.
134
+ * `true` keeps the session's resolved level.
135
+ */
131
136
  readonly thinking?: boolean;
132
137
  readonly temperature?: number;
133
138
  }
@@ -271,6 +271,12 @@ export class NativeSessionAdapter {
271
271
  messages: [...span, { role: "user", content: summaryPrompt(previousSummary) }],
272
272
  sessionId,
273
273
  signal,
274
+ // A summary restates the transcript; it does not need reasoning.
275
+ // "off", not an absent level: absent means the backend default,
276
+ // which on a model that cannot disable thinking is adaptive at
277
+ // effort high (#2686). The client clamps "off" to the least
278
+ // thinking such a model allows.
279
+ thinking: "off",
274
280
  });
275
281
  const summaryUsage = res.usage;
276
282
  const { costUsd, resolvedRates } = priceCall(summaryUsage, rates);
@@ -299,11 +305,16 @@ export class NativeSessionAdapter {
299
305
  const signal = AbortSignal.any(opts.signal !== undefined
300
306
  ? [opts.signal, controller.signal, turnController.signal, deadlineController.signal]
301
307
  : [controller.signal, turnController.signal, deadlineController.signal]);
302
- // The loop-event bag speaks in booleans: `false` explicitly drops
303
- // the session's inherited thinking level for this request, while
304
- // `true` leaves that resolved level in place. nax-ai accepts a
305
- // thinking LEVEL rather than a boolean, so normalize at this boundary.
306
- const requestThinking = requestOptions?.thinking === false ? undefined : thinking;
308
+ // The loop-event bag speaks in booleans: `false` turns thinking off
309
+ // for this request, while `true` leaves the session's resolved level
310
+ // in place. nax-ai accepts a thinking LEVEL rather than a boolean, so
311
+ // normalize at this boundary. `false` must become "off", not an
312
+ // absent level: absent means the backend default, which on a model
313
+ // that cannot disable thinking is adaptive at effort high (#2686).
314
+ // nax-ai clamps "off" to the lowest level such a model supports, so
315
+ // there `false` still enables thinking, and a `temperature` set
316
+ // alongside it is dropped by the backend as for any thinking request.
317
+ const requestThinking = requestOptions?.thinking === false ? "off" : thinking;
307
318
  try {
308
319
  instructionInteraction.acknowledge();
309
320
  const res = await billedOnFailure(rates, () => streamComplete(client, resolved, {
@@ -0,0 +1,18 @@
1
+ /** Shared session approval-memory policy, ported from the ACP server (nax#2553). */
2
+ export interface ApprovalMemoryRequest {
3
+ readonly tool: string;
4
+ readonly command?: string;
5
+ }
6
+ export type ApprovalMemoryDecision = "allow" | "deny";
7
+ /**
8
+ * Tool key, or execute tool plus command/subcommand. Unsafe, masked, wrapper and
9
+ * flag-second-word requests have no key. A key alone never authorizes a command:
10
+ * callers must also check approvalMemoryApplies against the remembered decision.
11
+ */
12
+ export declare function approvalMemoryKey(request: ApprovalMemoryRequest): string | undefined;
13
+ /**
14
+ * Whether a decision found under this request's key may answer without asking.
15
+ * Allows cover only simple commands with plain, non-flag words after the key;
16
+ * denies cover their matching key regardless. Does not validate key eligibility.
17
+ */
18
+ export declare function approvalMemoryApplies(request: ApprovalMemoryRequest, decision: ApprovalMemoryDecision): boolean;
@@ -0,0 +1,88 @@
1
+ /** Keep aligned with the ACP display's execute tools, without depending on that transport. */
2
+ const APPROVAL_EXECUTE_TOOLS = new Set(["Bash", "RunCommand", "Git", "GitCommit"]);
3
+ /** Chaining, pipes, substitution, subshells, redirection and line breaks. */
4
+ const SHELL_METACHARACTERS = /[;&|`$()<>\n\r]/;
5
+ /** First words that run another command or arbitrary code. */
6
+ const WRAPPERS = new Set([
7
+ "sh",
8
+ "bash",
9
+ "zsh",
10
+ "dash",
11
+ "ksh",
12
+ "fish",
13
+ "env",
14
+ "xargs",
15
+ "sudo",
16
+ "doas",
17
+ "su",
18
+ "command",
19
+ "builtin",
20
+ "eval",
21
+ "exec",
22
+ "source",
23
+ ".",
24
+ "nohup",
25
+ "time",
26
+ "timeout",
27
+ "nice",
28
+ "ionice",
29
+ "stdbuf",
30
+ "watch",
31
+ "find",
32
+ "node",
33
+ "bun",
34
+ "deno",
35
+ "npx",
36
+ "bunx",
37
+ "pnpx",
38
+ "python",
39
+ "python3",
40
+ "perl",
41
+ "ruby",
42
+ "php",
43
+ "uv",
44
+ "uvx",
45
+ ]);
46
+ /** A plain command name or path: no quotes, escapes, globs or expansions the shell would rewrite. */
47
+ const PLAIN_WORD = /^[A-Za-z0-9._/-]+$/;
48
+ /** The masking prefix S3 puts in place of a secret in a shown command. */
49
+ const MASK_MARKER = "[REDACTED";
50
+ /**
51
+ * Tool key, or execute tool plus command/subcommand. Unsafe, masked, wrapper and
52
+ * flag-second-word requests have no key. A key alone never authorizes a command:
53
+ * callers must also check approvalMemoryApplies against the remembered decision.
54
+ */
55
+ export function approvalMemoryKey(request) {
56
+ if (!APPROVAL_EXECUTE_TOOLS.has(request.tool))
57
+ return request.tool;
58
+ const command = request.command ?? "";
59
+ if (command.includes(MASK_MARKER))
60
+ return undefined;
61
+ const [first = "", second] = command.trim().split(/\s+/);
62
+ if (!PLAIN_WORD.test(first))
63
+ return undefined;
64
+ const name = first.slice(first.lastIndexOf("/") + 1).toLowerCase();
65
+ if (name === "" || WRAPPERS.has(name))
66
+ return undefined;
67
+ if (second === undefined)
68
+ return `${request.tool}:${first}`;
69
+ // A flag can change what the command does (`rm -f`, `git -C repo`): remembering the bare command would
70
+ // approve every later invocation of it, so a flag second word is never remembered (nax#2553).
71
+ if (second.startsWith("-"))
72
+ return undefined;
73
+ return PLAIN_WORD.test(second) ? `${request.tool}:${first} ${second}` : undefined;
74
+ }
75
+ /**
76
+ * Whether a decision found under this request's key may answer without asking.
77
+ * Allows cover only simple commands with plain, non-flag words after the key;
78
+ * denies cover their matching key regardless. Does not validate key eligibility.
79
+ */
80
+ export function approvalMemoryApplies(request, decision) {
81
+ if (decision === "deny" || !APPROVAL_EXECUTE_TOOLS.has(request.tool))
82
+ return true;
83
+ const command = request.command ?? "";
84
+ if (SHELL_METACHARACTERS.test(command))
85
+ return false;
86
+ const rest = command.trim().split(/\s+/).slice(2);
87
+ return rest.every((word) => PLAIN_WORD.test(word) && !word.startsWith("-"));
88
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./approval-audit.ts";
2
+ export { type ApprovalMemoryDecision, type ApprovalMemoryRequest, approvalMemoryApplies, approvalMemoryKey, } from "./approval-memory.ts";
2
3
  export * from "./approvals-link.ts";
3
4
  export * from "./approvals-store.ts";
4
5
  export * from "./approvals-taint.ts";
@@ -1,4 +1,5 @@
1
1
  export * from "./approval-audit.js";
2
+ export { approvalMemoryApplies, approvalMemoryKey, } from "./approval-memory.js";
2
3
  export * from "./approvals-link.js";
3
4
  export * from "./approvals-store.js";
4
5
  export * from "./approvals-taint.js";
@@ -36,6 +36,12 @@ export interface AskRequest {
36
36
  * hide shell syntax (review #9). The human link denies without prompting.
37
37
  */
38
38
  readonly unshowable?: true;
39
+ /**
40
+ * US-004: the call wants to run OUTSIDE the OS sandbox (the sandbox is never
41
+ * turned off for the whole session). A host that keys on it can offer a
42
+ * distinct decision; one that ignores it shows the ordinary approval prompt.
43
+ */
44
+ readonly unsandboxed?: true;
39
45
  /** The permitted root -- where the shell actually starts (src/tools/bash.ts:165). */
40
46
  readonly root?: string;
41
47
  /** The verdict's original reason, NOT rewritten as `matched ask rule "..."`. */
@@ -41,6 +41,7 @@ export declare const CLAUDE_SETTINGS_FILES: readonly string[];
41
41
  * policy compile time, so a symlink created mid-session is still seen.
42
42
  *
43
43
  * Segment-exact on the full path — a sibling such as `settings.json.bak`, or any
44
- * other file under `.claude/`, is an ordinary path a tool must still reach.
44
+ * other file under `.claude/`, is an ordinary path a tool must still reach. The
45
+ * compare ignores case: `.Claude/settings.json` is the same file on macOS and Windows.
45
46
  */
46
47
  export declare function claudeSettingsWriteRefusal(tool: string, rel: string, resolvedRoot: string): string | undefined;
@@ -53,15 +53,19 @@ const CLAUDE_SETTINGS_WRITE_TOOLS = new Set(["Write", "Edit", "Delete"]);
53
53
  * policy compile time, so a symlink created mid-session is still seen.
54
54
  *
55
55
  * Segment-exact on the full path — a sibling such as `settings.json.bak`, or any
56
- * other file under `.claude/`, is an ordinary path a tool must still reach.
56
+ * other file under `.claude/`, is an ordinary path a tool must still reach. The
57
+ * compare ignores case: `.Claude/settings.json` is the same file on macOS and Windows.
57
58
  */
58
59
  export function claudeSettingsWriteRefusal(tool, rel, resolvedRoot) {
59
60
  if (!CLAUDE_SETTINGS_WRITE_TOOLS.has(tool))
60
61
  return undefined;
61
- const entry = CLAUDE_SETTINGS_FILES.find((e) => e === rel || resolvedSettingsRel(resolvedRoot, e) === rel);
62
+ // Case-folded for the same reason as the `.git/` ban: on a case-insensitive filesystem
63
+ // `.Claude/settings.json` is the settings file, under a spelling a literal compare misses.
64
+ const folded = rel.toLowerCase();
65
+ const entry = CLAUDE_SETTINGS_FILES.find((e) => e.toLowerCase() === folded || resolvedSettingsRel(resolvedRoot, e).toLowerCase() === folded);
62
66
  if (entry === undefined)
63
67
  return undefined;
64
- const subject = entry === rel
68
+ const subject = entry.toLowerCase() === folded
65
69
  ? `"${rel}" is a Claude Code settings file`
66
70
  : `"${rel}" is the target of the Claude Code settings file "${entry}"`;
67
71
  return (`${subject}: Claude loads allow rules, hooks and MCP servers from it for every ` +
@@ -44,4 +44,5 @@ export const BUILTIN_CREDENTIAL_READ_DENIES = [
44
44
  ".pypirc",
45
45
  ".git-credentials",
46
46
  ".config/gh",
47
+ ".config/glab-cli",
47
48
  ];