@nathapp/nax-agent 0.87.1 → 0.88.0-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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/coding-tools/coding-tool-support.d.ts +16 -0
  3. package/dist/coding-tools/coding-tool-support.js +12 -0
  4. package/dist/command-safety/rule-scorer.d.ts +1 -1
  5. package/dist/command-safety/rule-scorer.js +11 -113
  6. package/dist/config/schemas-sandbox.d.ts +1 -0
  7. package/dist/config/schemas-sandbox.js +9 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/internal/process-kill.d.ts +6 -2
  11. package/dist/internal/process-kill.js +6 -2
  12. package/dist/internal.d.ts +1 -0
  13. package/dist/internal.js +1 -0
  14. package/dist/mcp/connect.js +15 -1
  15. package/dist/mcp/types.d.ts +6 -0
  16. package/dist/native/auth.d.ts +2 -2
  17. package/dist/native/auth.js +4 -4
  18. package/dist/native/errors.js +7 -0
  19. package/dist/native/failed-round-trip-spend.d.ts +16 -0
  20. package/dist/native/failed-round-trip-spend.js +33 -0
  21. package/dist/native/session/partial-spend.d.ts +20 -0
  22. package/dist/native/session/partial-spend.js +14 -0
  23. package/dist/native/session/session.js +11 -4
  24. package/dist/native/session/turn-complete-step.js +23 -3
  25. package/dist/native/session-adapter.js +3 -2
  26. package/dist/permissions/bash-lex.js +36 -4
  27. package/dist/sandbox/claude-settings.d.ts +46 -0
  28. package/dist/sandbox/claude-settings.js +76 -0
  29. package/dist/sandbox/index.d.ts +2 -1
  30. package/dist/sandbox/index.js +1 -0
  31. package/dist/sandbox/launcher.js +4 -0
  32. package/dist/sandbox/messages.d.ts +1 -1
  33. package/dist/sandbox/messages.js +15 -3
  34. package/dist/sandbox/policy-builder.js +27 -2
  35. package/dist/sandbox/session-sandbox.d.ts +3 -0
  36. package/dist/sandbox/session-sandbox.js +9 -0
  37. package/dist/sandbox/srt-backend.d.ts +10 -1
  38. package/dist/sandbox/srt-backend.js +14 -4
  39. package/dist/sandbox/types.d.ts +23 -0
  40. package/dist/session/agent-session-options.js +1 -1
  41. package/dist/session/agent-session-types.d.ts +1 -1
  42. package/dist/session/native-backend-options.js +5 -3
  43. package/dist/session/session-backend.d.ts +9 -0
  44. package/dist/session/session-tool-support.d.ts +1 -1
  45. package/dist/session/session-tool-support.js +4 -2
  46. package/dist/session/session-types.d.ts +10 -0
  47. package/dist/tools/bash.d.ts +7 -1
  48. package/dist/tools/bash.js +18 -3
  49. package/dist/tools/denial-redirect.d.ts +22 -7
  50. package/dist/tools/denial-redirect.js +29 -13
  51. package/dist/tools/git-flags/locate.d.ts +47 -0
  52. package/dist/tools/git-flags/locate.js +361 -0
  53. package/dist/tools/index.d.ts +1 -0
  54. package/dist/tools/index.js +1 -0
  55. package/dist/tools/nax-dirs.d.ts +22 -0
  56. package/dist/tools/nax-dirs.js +89 -0
  57. package/dist/tools/nax-evasion.d.ts +19 -0
  58. package/dist/tools/nax-evasion.js +88 -0
  59. package/dist/tools/nax-revert.d.ts +34 -0
  60. package/dist/tools/nax-revert.js +233 -0
  61. package/dist/tools/nax-stash-restore.d.ts +7 -0
  62. package/dist/tools/nax-stash-restore.js +92 -0
  63. package/dist/tools/nax-state-guard.d.ts +18 -0
  64. package/dist/tools/nax-state-guard.js +478 -0
  65. package/dist/tools/policy-bash-raw.d.ts +3 -0
  66. package/dist/tools/policy-bash-raw.js +40 -12
  67. package/dist/tools/policy-bash.d.ts +3 -0
  68. package/dist/tools/policy-bash.js +38 -9
  69. package/dist/tools/policy-command-branch.d.ts +3 -0
  70. package/dist/tools/policy-command-branch.js +3 -1
  71. package/dist/tools/policy.js +13 -0
  72. package/dist/tools/provider-advertise.d.ts +13 -0
  73. package/dist/tools/provider-advertise.js +41 -1
  74. package/dist/tools/provider-instructions.d.ts +33 -0
  75. package/dist/tools/provider-instructions.js +63 -0
  76. package/dist/tools/provider-types.d.ts +12 -0
  77. package/dist/tools/run-command.d.ts +15 -0
  78. package/dist/tools/run-command.js +16 -1
  79. package/dist/tools/tool-audit.d.ts +1 -1
  80. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ 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.88.0-canary.2] - 2026-10-15
8
+
9
+ - No changes in this package. Released in lockstep with the other nax packages.
10
+
11
+ ## [0.88.0-canary.1] - 2026-10-13
12
+
13
+ - Bash guard for nax run state (#2490): `git update-index --skip-worktree` / `--assume-unchanged` (and the `--no-` forms) on a `.nax` path, `git stash` / `push` / `pop` / `apply` in a repo that holds a `.nax/` directory (unless the pathspec excludes `.nax`), and `git checkout` / `restore` of a `.nax` path are now refused in every segment of a compound command, in gated and raw mode, also behind `git -C`/`-c`, env assignments, `env`/`command`/`nice`/`timeout`/`xargs` wrappers and `bash -c`/`eval`, plus a coarse text backstop for wrappers it cannot enumerate. `RULE_SET_VERSION` is now 4 (git global options, `-c alias.*` and more wrappers are resolved when locating the subcommand).
14
+ - Bash guard gaps closed (#2493): whole-tree reverts (`git reset --hard`, `git checkout .`, `git restore [--source=REV] .`, `:/`, `*`, the root, exclusion-only pathspecs) are refused unless a full `.nax` exclusion follows `--`; the `.nax` presence check also finds a `.nax/` up to three levels below the root (cached per compiled policy); `\git`, `GIT_WORK_TREE` / `GIT_DIR` / `GIT_CONFIG_*` assignments, `git config alias.X <verb>`, `!` aliases and aliases that begin with global options, unknown git global options (fail closed to the text backstop; `--attr-source` is now a known separate-value global), `python|node|perl|ruby -c|-e` code and verbs built by shell expansion are all read; `git checkout feature/.nax` (a branch name) is no longer a false positive. `RULE_SET_VERSION` is now 6 (`locateGit` reads `\git`, re-skips globals after an alias expansion, follows alias chains of any length and never expands an alias over a git builtin).
15
+ - Bash guard, final pass (#2493): a shell name or `-c` cluster anywhere in a segment triggers the quoted-string scan again (`if sh -c "..."`, `find -exec sh -c`, `su -c`, `setsid bash -c` were let through by round 2); `git clean` counts as a dry run only for an `n` before any `e` in a short cluster (`-fdenx` is `-e nx`, a real clean); `--literal-pathspecs` / `GIT_LITERAL_PATHSPECS` with an exclusion pathspec on checkout, restore, stash or clean is refused (it turns every exclusion into a literal path). The command guard stops here; detect-and-repair of nax run state is tracked in #2497.
16
+ - Bash guard, review round 2 (#2493): the covering exclusion must be the top-anchored `/**` form (real git: a glob without the `/**` matches only the `.nax` directory entry, not the files in it, and a relative glob is taken from the working directory); a `:(...)` pathspec with an `attr:` element, or whose magic is not exactly `exclude`, is no exclusion; `checkout-index`, `read-tree` and `clean` operands that name `.nax` are refused; long options are matched by any prefix git accepts (`--k`, `--f`, `--a`); the gated screen accepts the exact covering pathspec instead of rejecting its quoted `*`; the quoted-string scan covers only shells, interpreters and exec-style wrappers (`grep "git stash"`, `gh ... --body "..."`, `printf`, `sed` are no longer refused); `git clean -n` / `--dry-run` is allowed. Not covered, documented: git subcommands that execute a string (`rebase -x`, `submodule foreach`, `bisect run`, `difftool -x`, `fetch --upload-pack=`), executing environment variables (`GIT_EXTERNAL_DIFF`, `GIT_PAGER`, `GIT_EDITOR`, `GIT_SSH_COMMAND`), executing config keys beyond the listed ones (`remote.*.uploadpack`, `uploadpack.packObjectsHook`, `core.alternateRefsCommand`, `merge.*.driver`, `core.gitProxy`, `protocol.ext.allow` with `ext::` URLs), `deno eval` in raw mode, and commands built by string arithmetic in an interpreter.
17
+ - Bash guard, review round 1 (#2493): alias chains have no depth limit (a cycle is refused); backslash-built words (`g\it`, `st\ash`) are read; `restore --staged` is index-only only when every flag is known (`--wor`, `-SW` count as worktree); more interpreters and any quoted string next to a guarded verb are scanned; git config keys that execute commands (`core.fsmonitor`, `core.hooksPath`, `diff.external`, `include.path`, ...) are refused through `-c`, `--config-env` and `git config`; an alias over a git builtin is not expanded; the covering exclusion is now `':(top,exclude,glob)**/.nax/**'` (the literal `:!.nax` only counts while the root `.nax` is the only one); `checkout -f`, `switch -f`, `reset --keep|--merge`, `clean`, `checkout-index -a -f` and `read-tree -u --reset` are refused while a `.nax/` exists; an expansion head needs a `git` token in the segment. A `.nax/` below the root, for example a test fixture within three levels, makes `git stash` refused for the whole repository. The nested scan caches a found `.nax` but re-scans an empty result after one second.
18
+ - Native errors: a `quota` kind from `@nathapp/nax-ai` (usage-limit and token-plan 429s) now maps to
19
+ `fail-quota` instead of a transient rate limit.
20
+
7
21
  ## [0.87.1] - 2026-10-12
8
22
 
9
23
  - No changes in this package. Released in lockstep with the other nax packages.
@@ -32,6 +32,16 @@ export interface CodingToolSupport {
32
32
  readonly tools: readonly CodingTool[];
33
33
  readonly surface: ToolSurfaceDraft;
34
34
  };
35
+ /** nax#2500: the dispatch launcher's sandbox as data, attached by the nax resolver for ACP opens. */
36
+ readonly acpSandbox?: import("#src/sandbox/types").SessionSandbox;
37
+ /**
38
+ * The rendered `## MCP servers` section (nax#2517), built by the nax resolver
39
+ * with `renderProviderInstructions` from this hop's resolved provider
40
+ * instructions. The session open folds it into the session's `system` field
41
+ * (native) or hands it to the ACP tool host. ABSENT when no attached provider
42
+ * contributed instructions, so "no section" and "an empty one" stay distinct.
43
+ */
44
+ readonly providerInstructions?: string;
35
45
  }
36
46
  export declare function buildCodingToolSupport(args: {
37
47
  root?: string;
@@ -134,6 +144,12 @@ export declare function buildCodingToolSupport(args: {
134
144
  launcher?: CommandLauncher;
135
145
  /** US-001: forwarded into ToolRunContext so Bash/Exec can SIGKILL on cancel. */
136
146
  abortSignal?: AbortSignal;
147
+ /**
148
+ * The rendered `## MCP servers` section for this hop (nax#2517), produced by
149
+ * the resolver from `ResolvedProviderTools.instructions`; returned on the
150
+ * support unchanged.
151
+ */
152
+ providerInstructions?: string;
137
153
  }): CodingToolSupport | undefined;
138
154
  /**
139
155
  * Ledger session name.
@@ -144,8 +144,20 @@ export function buildCodingToolSupport(args) {
144
144
  auditSink: sink,
145
145
  declaredTools: args.declared,
146
146
  ...(args.toolSurface !== undefined ? { toolSurface: args.toolSurface } : {}),
147
+ ...providerInstructionsField(args),
147
148
  };
148
149
  }
150
+ /**
151
+ * The support's optional `providerInstructions` (#2517) as a spreadable field —
152
+ * ABSENT when unset, so `"providerInstructions" in support` answers "is there
153
+ * an MCP section?". A helper rather than an inline conditional because
154
+ * `buildCodingToolSupport` sits at its complexity baseline.
155
+ */
156
+ function providerInstructionsField(args) {
157
+ if (args.providerInstructions === undefined)
158
+ return {};
159
+ return { providerInstructions: args.providerInstructions };
160
+ }
149
161
  /** The audit sink a support resolution records through; no dir ⇒ a non-recording sink. */
150
162
  function supportSink(args) {
151
163
  if (args.auditDir === undefined)
@@ -1,5 +1,5 @@
1
1
  import { type RuleResult } from "./types.ts";
2
- export declare const RULE_SET_VERSION = 3;
2
+ export declare const RULE_SET_VERSION = 6;
3
3
  /** Context the scorer may use; every field is optional and absent means v1 behaviour. */
4
4
  export interface RuleContext {
5
5
  /** The project root the command runs against (the Bash call's cwd). */
@@ -27,14 +27,24 @@
27
27
  * hit. The existing eleven regex families stay; the new patterns live in a
28
28
  * per-segment pass.
29
29
  *
30
+ * v5: `locateGit` (shared with the nax-state guard, #2493) reads a
31
+ * backslash-prefixed `\git`, runs the global-option skip again over an
32
+ * expanded alias, and follows alias chains, so a discard behind any of those
33
+ * is now located by the v3 per-segment pass.
34
+ *
35
+ * v6: `locateGit` never expands an alias that shadows a git builtin (git ignores it) and follows alias
36
+ * chains of any length (a cycle is reported, not looped on), so `git -c alias.checkout=status checkout -f`
37
+ * is the discard it really is.
38
+ *
30
39
  * Frozen before the red-team corpus was written. Any pattern or behaviour
31
40
  * change bumps RULE_SET_VERSION.
32
41
  */
33
42
  import { posix } from "node:path";
34
43
  import { errorMessage } from "#src/infra/errors";
35
44
  import { lexBashCommand } from "#src/permissions/index";
45
+ import { locateGit } from "#src/tools/git-flags/locate";
36
46
  import { QUESTION_IDS } from "./types.js";
37
- export const RULE_SET_VERSION = 3;
47
+ export const RULE_SET_VERSION = 6;
38
48
  const RULES = {
39
49
  deletes_data: [
40
50
  /\brm\s+(?:\S+\s+)*?(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)\b/,
@@ -140,118 +150,6 @@ const FORCE_FLAGS = new Set(["-f", "--force", "--discard-changes"]);
140
150
  const STAGED_FLAGS = new Set(["--staged", "-S"]);
141
151
  /** True when `word` is a worktree-restore flag — its own pattern handles these. */
142
152
  const WORKTREE_FLAGS = new Set(["--worktree", "-W"]);
143
- /** `git`'s global options that take no value — pass-through, then look for
144
- * the subcommand. */
145
- const GIT_BOOLEAN_GLOBALS = new Set([
146
- "--no-pager",
147
- "--no-replace-objects",
148
- "--bare",
149
- "--literal-pathspecs",
150
- "--glob-pathspecs",
151
- "--noglob-pathspecs",
152
- "--icase-pathspecs",
153
- ]);
154
- /** `git`'s global options that take a separate value (the next token). */
155
- const GIT_VALUE_GLOBALS = new Set(["-c", "-C"]);
156
- /** `git`'s global long options that embed their value (`--key=value`). */
157
- const GIT_KEY_VALUE_GLOBALS = new Set([
158
- "--git-dir=",
159
- "--exec-path=",
160
- "--work-tree=",
161
- "--namespace=",
162
- "--super-prefix=",
163
- ]);
164
- /** `sudo` options whose values occupy the next shell word. */
165
- const SUDO_VALUE_OPTIONS = new Set([
166
- "-u",
167
- "-g",
168
- "-h",
169
- "-D",
170
- "-C",
171
- "-p",
172
- "-r",
173
- "-t",
174
- "-U",
175
- "--user",
176
- "--group",
177
- "--host",
178
- "--chdir",
179
- "--close-from",
180
- "--prompt",
181
- "--role",
182
- "--type",
183
- "--other-user",
184
- ]);
185
- /** Skip a sudo/doas prefix, including options with a separate value. */
186
- function afterPrivilegeWrapper(words) {
187
- if (words[0] !== "sudo" && words[0] !== "doas")
188
- return 0;
189
- let i = 1;
190
- while (words[i]?.startsWith("-")) {
191
- const option = words[i++];
192
- if (option === "--")
193
- break;
194
- if (option !== undefined && SUDO_VALUE_OPTIONS.has(option))
195
- i += 1;
196
- }
197
- return i;
198
- }
199
- /** Skip an env prefix and its assignments and options. */
200
- function afterEnvWrapper(words, start) {
201
- if (words[start] !== "env")
202
- return start;
203
- let i = start + 1;
204
- while (i < words.length) {
205
- const word = words[i];
206
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word) || word === "-i" || word === "--ignore-environment") {
207
- i += 1;
208
- }
209
- else if (word === "-u" || word === "--unset") {
210
- i += 2;
211
- }
212
- else
213
- break;
214
- }
215
- return i;
216
- }
217
- /** Find `git` in the executable position after supported shell wrappers. */
218
- function gitExecutableIndex(words) {
219
- let i = afterEnvWrapper(words, afterPrivilegeWrapper(words));
220
- if (words[i] === "command" || words[i] === "exec")
221
- i += 1;
222
- return words[i] === "git" ? i : undefined;
223
- }
224
- /** Locate executable `git` in `words` and advance past its global options. Returns the
225
- subcommand and the index of the first argument (or `undefined` when `git`
226
- isn't there or has no subcommand). Handles `sudo git`, `env VAR=x git`,
227
- `git --no-pager checkout`, etc. */
228
- function locateGit(words) {
229
- const gitIdx = gitExecutableIndex(words);
230
- if (gitIdx === undefined)
231
- return undefined;
232
- let i = gitIdx + 1;
233
- while (i < words.length) {
234
- const w = words[i];
235
- if (GIT_BOOLEAN_GLOBALS.has(w)) {
236
- i += 1;
237
- continue;
238
- }
239
- if (GIT_VALUE_GLOBALS.has(w)) {
240
- // The value is a separate token; skip it.
241
- i += 2;
242
- continue;
243
- }
244
- if ([...GIT_KEY_VALUE_GLOBALS].some((prefix) => w.startsWith(prefix))) {
245
- i += 1;
246
- continue;
247
- }
248
- break;
249
- }
250
- const subcommand = words[i];
251
- if (subcommand === undefined)
252
- return undefined;
253
- return { subcommand, args: words.slice(i + 1) };
254
- }
255
153
  /** True when `token` is a short-flag bundle that includes `f` (so `-f`,
256
154
  * `-fq`, `-fb`, `-fqH`, ...). Stops at `--` so a long flag like
257
155
  * `--force-something` is not misread as a bundle. */
@@ -24,6 +24,7 @@ export declare const SandboxConfigSchema: z.ZodObject<{
24
24
  }, z.core.$strip>>;
25
25
  network: z.ZodPrefault<z.ZodObject<{
26
26
  allowedDomains: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ allowLocalBinding: z.ZodOptional<z.ZodBoolean>;
27
28
  }, z.core.$strip>>;
28
29
  }, z.core.$strip>;
29
30
  export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
@@ -38,6 +38,15 @@ export const SandboxConfigSchema = z.object({
38
38
  .object({
39
39
  /** absent = unrestricted (spec S2); [] = no network; a list = allow-list. */
40
40
  allowedDomains: z.array(z.string()).optional(),
41
+ /**
42
+ * US-002 (#2538): let a sandboxed command reach loopback ports.
43
+ *
44
+ * Off when absent, and it only changes anything alongside an
45
+ * `allowedDomains` list: with an open network srt's macOS profile already
46
+ * allows all traffic. srt reads it once per process, from the config given
47
+ * to `SandboxManager.initialize`.
48
+ */
49
+ allowLocalBinding: z.boolean().optional(),
41
50
  })
42
51
  .prefault({}),
43
52
  });
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export type { AfterResponsePatch, AfterResponsePayload, AfterToolPatch, AfterToo
26
26
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
27
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";
28
28
  export { type AgentGlobOptions, type AgentRuntime, type AgentSpawnOptions, type AgentSpawnResult, type AgentSpawnStdin, getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
29
- export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, 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, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
29
+ export { buildSandboxPolicy, type CommandLauncher, type CommandLauncherOptions, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, 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
30
  export type { AdapterFailure } from "#src/session/adapter-failure";
31
31
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
32
32
  export { AgentSessionError, type AgentSessionErrorCode } from "#src/session/agent-session-errors";
@@ -42,5 +42,5 @@ export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/sessi
42
42
  export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
43
43
  export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
44
44
  export type { TurnEvent, TurnEventSink } from "#src/session/turn-event";
45
- export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
45
+ export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
46
46
  export { EMPTY_OWNED_PATHS_POLICY, type OwnedBashCandidate, type OwnedPathsPolicy, } from "#src/tools/owned-paths";
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOut
21
21
  export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
22
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";
23
23
  export { getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
24
- export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, defaultTempRoots, denialHintLine, LIKELY_SANDBOX_DENIAL, listCredentialFiles, listGitGuardFiles, listNaxEntries, probeSandbox, probeSandboxOnce, quoteArgvForShell, rawBashRefusalReason, resetSandboxBackend, resolveGitLayout, runTempRoots, runTmpRoot, sandboxBackendFor, sandboxSentence, sessionTmpDir, sessionTmpDirUnder, strayCommonDirTripwire, unsandboxedSentence, warnSandboxUnavailableOnce, } from "#src/sandbox/index";
24
+ export { buildSandboxPolicy, createCommandLauncher, createSrtBackend, DISABLED_SANDBOX_STATE, 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
25
  export { createAgentSession, resumeAgentSession } from "#src/session/agent-session";
26
26
  export { AgentSessionError } from "#src/session/agent-session-errors";
27
27
  export { NO_OP_INTERACTION_HANDLER, } from "#src/session/interaction-handler";
@@ -29,5 +29,5 @@ export { nativeBackend } from "#src/session/native-backend";
29
29
  export { SessionTurnError, } from "#src/session/session-types";
30
30
  export { attachTurnSpend, readTurnSpend } from "#src/session/turn-spend";
31
31
  export { createTurnDeadline, } from "#src/session/turn-deadline";
32
- export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
32
+ export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, deleteTool, EXEC_TOOL_NAME, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_MAX_CHARS_TOTAL, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, mcpRuleAdmits, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, PROVIDER_ID_RE, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, renderProviderInstructions, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
33
33
  export { EMPTY_OWNED_PATHS_POLICY, } from "#src/tools/owned-paths";
@@ -12,7 +12,9 @@
12
12
  * 2. Falls back to killing the single process if group kill fails
13
13
  *
14
14
  * @param pid - Process ID (positive, not negative)
15
- * @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
15
+ * @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
16
+ * @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
17
+ * exited: the bare pid may have been reused by an unrelated process by then.
16
18
  * @returns true if kill was sent successfully, false if process already exited
17
19
  *
18
20
  * @example
@@ -24,4 +26,6 @@
24
26
  * }
25
27
  * ```
26
28
  */
27
- export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number): boolean;
29
+ export declare function killProcessGroup(pid: number, signal: NodeJS.Signals | number, opts?: {
30
+ readonly singleProcessFallback?: boolean;
31
+ }): boolean;
@@ -12,7 +12,9 @@
12
12
  * 2. Falls back to killing the single process if group kill fails
13
13
  *
14
14
  * @param pid - Process ID (positive, not negative)
15
- * @param signal - Signal to send (SIGTERM, SIGKILL, etc.)
15
+ * @param signal - Signal to send (SIGTERM, SIGKILL, etc.); `0` probes whether the group still has a member
16
+ * @param opts - `singleProcessFallback: false` skips step 2. Pass it once the group leader is known to have
17
+ * exited: the bare pid may have been reused by an unrelated process by then.
16
18
  * @returns true if kill was sent successfully, false if process already exited
17
19
  *
18
20
  * @example
@@ -24,7 +26,7 @@
24
26
  * }
25
27
  * ```
26
28
  */
27
- export function killProcessGroup(pid, signal) {
29
+ export function killProcessGroup(pid, signal, opts = {}) {
28
30
  // A pid that is not a real child reaches other processes: 0 is our own group,
29
31
  // 1 (as -1) is every process we may signal, and a negative pid is init or a
30
32
  // foreign group. A spawn that failed has no pid, so refuse rather than signal.
@@ -39,6 +41,8 @@ export function killProcessGroup(pid, signal) {
39
41
  const err = error;
40
42
  // ESRCH = no such process — process group doesn't exist, try single process
41
43
  if (err.code === "ESRCH") {
44
+ if (opts.singleProcessFallback === false)
45
+ return false;
42
46
  try {
43
47
  process.kill(pid, signal);
44
48
  return true;
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
93
93
  export * from "#src/tools/grep";
94
94
  export * from "#src/tools/index";
95
95
  export { _instructionAccessDeps } from "#src/tools/instruction-access";
96
+ export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
96
97
  export * from "#src/tools/owned-paths";
97
98
  export * from "#src/tools/package-managers";
98
99
  export * from "#src/tools/policy";
package/dist/internal.js CHANGED
@@ -93,6 +93,7 @@ export * from "#src/tools/git-commit";
93
93
  export * from "#src/tools/grep";
94
94
  export * from "#src/tools/index";
95
95
  export { _instructionAccessDeps } from "#src/tools/instruction-access";
96
+ export { _naxStateGuardDeps } from "#src/tools/nax-stash-restore";
96
97
  // S3-2: the port (nax supplies the policy behind it from its own module).
97
98
  export * from "#src/tools/owned-paths";
98
99
  export * from "#src/tools/package-managers";
@@ -83,6 +83,15 @@ function createHandle(config) {
83
83
  throw new McpConnectError(`MCP connect failed: ${messageOf(error)}`);
84
84
  }
85
85
  }
86
+ /**
87
+ * The server's `initialize` instructions (#2517), or `undefined` for anything
88
+ * that is not a non-empty string: an empty one would render an empty prompt
89
+ * section, and a `getInstructions()` that returns nothing is the same silence.
90
+ */
91
+ function serverInstructions(client) {
92
+ const text = client.getInstructions();
93
+ return typeof text === "string" && text !== "" ? text : undefined;
94
+ }
86
95
  export async function connectMcp(config, opts) {
87
96
  const handle = createHandle(config);
88
97
  const client = new Client({ name: opts.clientInfo.name, version: opts.clientInfo.version });
@@ -95,7 +104,11 @@ export async function connectMcp(config, opts) {
95
104
  throw new McpConnectError("MCP connect failed: aborted");
96
105
  await client.connect(handle.transport, { signal, timeout: opts.timeoutMs });
97
106
  const tools = await listAll(client, signal, opts.timeoutMs);
98
- return liveConnection(config, client, handle, tools, { graceMs, life });
107
+ return liveConnection(config, client, handle, tools, {
108
+ graceMs,
109
+ life,
110
+ instructions: serverInstructions(client),
111
+ });
99
112
  }
100
113
  catch (error) {
101
114
  await closeTransport(client, handle, graceMs, life);
@@ -127,6 +140,7 @@ function liveConnection(config, client, handle, tools, state) {
127
140
  return handle.pid();
128
141
  },
129
142
  tools,
143
+ ...(state.instructions !== undefined ? { instructions: state.instructions } : {}),
130
144
  call(name, input, opts) {
131
145
  if (closing !== undefined || life.exited)
132
146
  return Promise.reject(new McpCallError(`MCP call ${name} failed: the connection is closed`));
@@ -31,6 +31,12 @@ export interface McpConnection {
31
31
  /** The stdio child process pid; null for HTTP. Read before close() — the SDK may clear it after. */
32
32
  readonly pid: number | null;
33
33
  readonly tools: readonly McpToolInfo[];
34
+ /**
35
+ * The server's own `initialize` instructions — how to use it, in its words
36
+ * (#2517). Absent when it sent none (or an empty string): an MCP server that
37
+ * says nothing must not produce an empty prompt section.
38
+ */
39
+ readonly instructions?: string;
34
40
  /** Server-side tool errors are `isError` results; transport failures throw McpCallError. */
35
41
  call(name: string, input: unknown, opts: McpCallOptions): Promise<McpCallResult>;
36
42
  /** stdio only: called once when the process exits without close() having been called. */
@@ -29,8 +29,8 @@ export declare const _authDeps: {
29
29
  ambientAuthAvailable: typeof ambientAuthAvailable;
30
30
  providerIds: () => Promise<string[]>;
31
31
  /**
32
- * Injectable timer pair — lets tests drive `anyAmbientCredential`'s
33
- * expiry branch off a virtual clock instead of waiting the full 2s
32
+ * Injectable timer pair — lets tests drive ambient credential probes'
33
+ * expiry branches off a virtual clock instead of waiting the full 2s
34
34
  * production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
35
35
  *
36
36
  * @internal
@@ -41,8 +41,8 @@ export const _authDeps = {
41
41
  ambientAuthAvailable,
42
42
  providerIds: async () => (await defaultProviders()).map((provider) => provider.id),
43
43
  /**
44
- * Injectable timer pair — lets tests drive `anyAmbientCredential`'s
45
- * expiry branch off a virtual clock instead of waiting the full 2s
44
+ * Injectable timer pair — lets tests drive ambient credential probes'
45
+ * expiry branches off a virtual clock instead of waiting the full 2s
46
46
  * production timeout. Mirrors `_heartbeatDeps` / `_idleWatchdogDeps`.
47
47
  *
48
48
  * @internal
@@ -301,14 +301,14 @@ export async function providersWithoutCredentials(providerIds) {
301
301
  })).then((missing) => missing.filter((id) => id !== undefined));
302
302
  let timer;
303
303
  const expiry = new Promise((resolve) => {
304
- timer = setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
304
+ timer = _authDeps.setTimeout(() => resolve([]), AMBIENT_PROBE_TIMEOUT_MS);
305
305
  });
306
306
  try {
307
307
  return await Promise.race([sweep, expiry]);
308
308
  }
309
309
  finally {
310
310
  if (timer !== undefined)
311
- clearTimeout(timer);
311
+ _authDeps.clearTimeout(timer);
312
312
  }
313
313
  }
314
314
  /**
@@ -17,6 +17,13 @@ const FAILURES = Object.freeze({
17
17
  outcome: "fail-rate-limit",
18
18
  retriable: true,
19
19
  },
20
+ // The account or plan is exhausted: waiting does not help, so not retriable.
21
+ quota: {
22
+ message: "nax-ai account or plan quota exhausted",
23
+ category: "availability",
24
+ outcome: "fail-quota",
25
+ retriable: false,
26
+ },
20
27
  auth: {
21
28
  message: "nax-ai authentication failed",
22
29
  category: "availability",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prices the tokens a failed or aborted round trip was billed for and attaches
3
+ * them to the thrown error (`session/partial-spend.ts`), where the turn loop's
4
+ * `completeWithRecovery` folds them into the turn's accumulated usage.
5
+ *
6
+ * nax-ai's `collectStream` hands the partial usage over on the error
7
+ * (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
8
+ * here, once, with the same `priceCall` the success path uses.
9
+ */
10
+ import type { Pricing } from "#src/cost/standard-types";
11
+ /**
12
+ * Runs one round trip; if it throws, prices whatever it was billed first and
13
+ * attaches that to the error, which is rethrown untouched. A wrapper (rather
14
+ * than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
15
+ */
16
+ export declare function billedOnFailure<T>(rates: Pricing, roundTrip: () => Promise<T>): Promise<T>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Prices the tokens a failed or aborted round trip was billed for and attaches
3
+ * them to the thrown error (`session/partial-spend.ts`), where the turn loop's
4
+ * `completeWithRecovery` folds them into the turn's accumulated usage.
5
+ *
6
+ * nax-ai's `collectStream` hands the partial usage over on the error
7
+ * (`partialUsageOf`); only the adapter holds the rate card, so pricing happens
8
+ * here, once, with the same `priceCall` the success path uses.
9
+ */
10
+ import { partialUsageOf } from "@nathapp/nax-ai";
11
+ import { priceCall } from "#src/cost/core/index";
12
+ import { attachPartialSpend } from "./session/partial-spend.js";
13
+ function recordFailedRoundTripSpend(err, rates) {
14
+ const usage = partialUsageOf(err);
15
+ if (usage === undefined)
16
+ return;
17
+ const { costUsd, resolvedRates } = priceCall(usage, rates);
18
+ attachPartialSpend(err, { usage, costUsd, rates: resolvedRates });
19
+ }
20
+ /**
21
+ * Runs one round trip; if it throws, prices whatever it was billed first and
22
+ * attaches that to the error, which is rethrown untouched. A wrapper (rather
23
+ * than a catch at the call site) keeps `sendTurn` under the complexity ratchet.
24
+ */
25
+ export async function billedOnFailure(rates, roundTrip) {
26
+ try {
27
+ return await roundTrip();
28
+ }
29
+ catch (err) {
30
+ recordFailedRoundTripSpend(err, rates);
31
+ throw err;
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Spend billed by a round trip that then failed.
3
+ *
4
+ * A failed or aborted request still bills the tokens it streamed (nax-ai's
5
+ * `partialUsageOf`). The adapter's `complete` closure is the only place that
6
+ * can price them, but it can only throw, so the priced spend rides on the
7
+ * thrown error's identity (a WeakMap, like `turn-spend.ts`) and the turn loop's
8
+ * `completeWithRecovery` folds it into the turn accumulator. `take` removes the
9
+ * entry, so an error reaching the fold twice cannot count twice.
10
+ */
11
+ import type { PricingRates, TokenUsage } from "#src/cost/standard-types";
12
+ export interface PartialSpend {
13
+ readonly usage: TokenUsage;
14
+ readonly costUsd: number;
15
+ readonly rates?: PricingRates;
16
+ }
17
+ /** Attaches the priced partial spend of a failed round trip to the error it threw. */
18
+ export declare function attachPartialSpend(err: unknown, spend: PartialSpend): void;
19
+ /** Removes and returns the partial spend attached to `err`, if any. */
20
+ export declare function takePartialSpend(err: unknown): PartialSpend | undefined;
@@ -0,0 +1,14 @@
1
+ const partialByError = new WeakMap();
2
+ /** Attaches the priced partial spend of a failed round trip to the error it threw. */
3
+ export function attachPartialSpend(err, spend) {
4
+ if (typeof err === "object" && err !== null)
5
+ partialByError.set(err, spend);
6
+ }
7
+ /** Removes and returns the partial spend attached to `err`, if any. */
8
+ export function takePartialSpend(err) {
9
+ if (typeof err !== "object" || err === null)
10
+ return undefined;
11
+ const spend = partialByError.get(err);
12
+ partialByError.delete(err);
13
+ return spend;
14
+ }
@@ -83,12 +83,19 @@ function openTranscriptStore(name, opts) {
83
83
  /**
84
84
  * Records, or clears, the session's system prompt at open. A helper so that
85
85
  * openNativeSession gains no branch.
86
+ *
87
+ * `providerInstructions` (the rendered `## MCP servers` section, #2517) is
88
+ * folded in HERE rather than kept in a map of its own: the model sees one
89
+ * `system` string, and every consumer of `state.systemPrompts`
90
+ * (`systemFieldFor`, the adapter's context-budget setup) keeps working
91
+ * unchanged.
86
92
  */
87
- function recordSystemPrompt(state, name, systemPrompt) {
88
- if (systemPrompt === undefined)
93
+ function recordSystemPrompt(state, name, prompts) {
94
+ const parts = [prompts.systemPrompt, prompts.providerInstructions].filter((part) => part !== undefined && part !== "");
95
+ if (parts.length === 0)
89
96
  state.systemPrompts.delete(name);
90
97
  else
91
- state.systemPrompts.set(name, systemPrompt);
98
+ state.systemPrompts.set(name, parts.join("\n\n"));
92
99
  }
93
100
  /** The request field for the session's system prompt: `{ system }`, or `{}` when it has none. */
94
101
  export function systemFieldFor(state, sessionName) {
@@ -119,7 +126,7 @@ export async function openNativeSession(state, name, opts) {
119
126
  state.carryHistoryAcrossModels.add(name);
120
127
  else
121
128
  state.carryHistoryAcrossModels.delete(name);
122
- recordSystemPrompt(state, name, opts.systemPrompt);
129
+ recordSystemPrompt(state, name, opts);
123
130
  const instructions = await openRepositoryInstructions(name, opts, store);
124
131
  state.repositoryInstructions.set(name, instructions);
125
132
  state.transcripts.set(name, {