@nathapp/nax-agent 0.3.1 → 0.85.0

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 (81) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +34 -1
  3. package/dist/config/catalog-overrides.d.ts +12 -0
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.js +3 -2
  6. package/dist/internal/atomic-write.d.ts +16 -0
  7. package/dist/internal/atomic-write.js +34 -0
  8. package/dist/internal/command-spec/index.js +1 -1
  9. package/dist/internal/git-env.d.ts +4 -3
  10. package/dist/internal/git-env.js +11 -4
  11. package/dist/internal.d.ts +6 -0
  12. package/dist/internal.js +6 -0
  13. package/dist/mcp/connect.d.ts +3 -0
  14. package/dist/mcp/connect.js +144 -0
  15. package/dist/mcp/errors.d.ts +9 -0
  16. package/dist/mcp/errors.js +16 -0
  17. package/dist/mcp/index.d.ts +9 -0
  18. package/dist/mcp/index.js +8 -0
  19. package/dist/mcp/result-text.d.ts +8 -0
  20. package/dist/mcp/result-text.js +38 -0
  21. package/dist/mcp/stdio-close.d.ts +1 -0
  22. package/dist/mcp/stdio-close.js +31 -0
  23. package/dist/mcp/transport.d.ts +14 -0
  24. package/dist/mcp/transport.js +41 -0
  25. package/dist/mcp/types.d.ts +48 -0
  26. package/dist/mcp/types.js +1 -0
  27. package/dist/native/auth.d.ts +2 -0
  28. package/dist/native/auth.js +11 -1
  29. package/dist/native/index.d.ts +2 -1
  30. package/dist/native/index.js +2 -1
  31. package/dist/native/provider-models.d.ts +13 -0
  32. package/dist/native/provider-models.js +22 -0
  33. package/dist/native/session/handle-invalid-tool-call.js +1 -0
  34. package/dist/native/session/instruction-file-name.d.ts +4 -0
  35. package/dist/native/session/instruction-file-name.js +21 -0
  36. package/dist/native/session/instruction-interaction.d.ts +7 -0
  37. package/dist/native/session/instruction-interaction.js +22 -0
  38. package/dist/native/session/instruction-render.d.ts +6 -0
  39. package/dist/native/session/instruction-render.js +22 -0
  40. package/dist/native/session/instruction-session.d.ts +4 -0
  41. package/dist/native/session/instruction-session.js +31 -0
  42. package/dist/native/session/instruction-transcript.d.ts +4 -0
  43. package/dist/native/session/instruction-transcript.js +14 -0
  44. package/dist/native/session/repository-instructions.d.ts +35 -0
  45. package/dist/native/session/repository-instructions.js +155 -0
  46. package/dist/native/session/session.d.ts +8 -0
  47. package/dist/native/session/session.js +29 -3
  48. package/dist/native/session/transcript-identity.d.ts +2 -0
  49. package/dist/native/session/transcript-identity.js +1 -1
  50. package/dist/native/session/transcript-store.js +4 -2
  51. package/dist/native/session/transcript-types.d.ts +3 -0
  52. package/dist/native/session/turn-complete-step.js +4 -2
  53. package/dist/native/session/turn-event-emitter.d.ts +4 -0
  54. package/dist/native/session/turn-event-emitter.js +6 -4
  55. package/dist/native/session/turn-loop-round-trip.js +20 -1
  56. package/dist/native/session/turn-loop.js +1 -0
  57. package/dist/native/session-adapter.js +21 -6
  58. package/dist/permissions/approvals-store.d.ts +3 -1
  59. package/dist/permissions/approvals-store.js +18 -5
  60. package/dist/sandbox/session-tmp.js +13 -7
  61. package/dist/session/agent-session-types.d.ts +4 -0
  62. package/dist/session/native-backend-options.d.ts +4 -0
  63. package/dist/session/native-backend-options.js +6 -0
  64. package/dist/session/native-backend.js +16 -4
  65. package/dist/session/session-ask-port.js +2 -1
  66. package/dist/session/session-interaction.js +29 -13
  67. package/dist/session/session-types.d.ts +10 -0
  68. package/dist/terminal-auth/index.d.ts +4 -0
  69. package/dist/terminal-auth/index.js +4 -0
  70. package/dist/terminal-auth/interaction.d.ts +15 -0
  71. package/dist/terminal-auth/interaction.js +55 -0
  72. package/dist/terminal-auth/open-url.d.ts +17 -0
  73. package/dist/terminal-auth/open-url.js +44 -0
  74. package/dist/terminal-auth/prompt.d.ts +51 -0
  75. package/dist/terminal-auth/prompt.js +196 -0
  76. package/dist/tools/denial-redirect.js +1 -1
  77. package/dist/tools/git.js +21 -5
  78. package/dist/tools/instruction-access.d.ts +13 -0
  79. package/dist/tools/instruction-access.js +30 -0
  80. package/dist/tools/runtime.js +6 -1
  81. package/package.json +7 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ 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.85.0] - 2026-10-09
8
+
9
+ ### Added
10
+
11
+ - `listProviderModels(providerId, catalogOverrides?)` and `ProviderModel`: the tool-capable models the catalog offers for one provider, as plain data (#2414).
12
+
13
+ ## [0.84.0] - 2026-10-09
14
+
15
+ - Versioning: released in lockstep with `@nathapp/nax`, `@nathapp/nax-ai` and the other agent package at one shared version, starting at 0.84.0 (previously 0.3.x).
16
+
17
+ ### Added
18
+
19
+ - `createTerminalAuthInteraction({ log, style?, openUrl? })`, `PromptCancelledError`, `TerminalStyle` and `PLAIN_STYLE`: the terminal login UI (hidden secret entry, arrow-key picker, browser handoff for OAuth), moved from the nax CLI so `nax-agent login` shares it (S5-4).
20
+ - `loginProviderIds()`: the providers `runLogin` can log in to (S5-4).
21
+ - `displayToolInput` and `toolResultPreview`: the live tool-display masking, for replaying stored transcripts (S5-1).
22
+ - `answerable?: false` on the `approval_requested` and `question` session events, set for profile auto-decisions and noted questions (S5-2).
23
+ - `carryHistoryAcrossModels` (`nativeBackend` option and `OpenSessionOpts`): keep a session's history across a model change; each assistant message records its origin model (S5-3).
24
+
7
25
  ## [0.3.1] - 2026-10-07
8
26
 
9
27
  ### Added
package/README.md CHANGED
@@ -16,6 +16,7 @@ Requires **Node.js >= 22.19.0**. The package is ESM only and ships no Bun code:
16
16
 
17
17
  - **`@nathapp/nax-agent`** is the supported entry. Every export is named, and none starts with `_`. Its exact names are pinned in [`api/nax-agent.api.txt`](api/nax-agent.api.txt), which CI compares with the built declarations.
18
18
  - **`@nathapp/nax-agent/internal`** is **nax-only and outside semver.** nax bundles this package and reaches below the public entry for shared helpers, `NaxError`, deep modules and the `_*Deps` test seams. Names, shapes and behaviour there can change in any release, patch included, and a change there is not a breaking change. Do not import it from another project.
19
+ - **`@nathapp/nax-agent/mcp`** — the shared MCP connection layer; see below.
19
20
 
20
21
  ## Process-wide slots
21
22
 
@@ -37,6 +38,12 @@ Three pieces of host knowledge are passed in as data or functions, because the p
37
38
  - **`ProtectedPathsPolicy`** names the paths you own and want kept away from the agent: git pathspecs the Git tool excludes from its default view, gitignore patterns `GitCommit` refuses to stage, the project state directory, the credential directory and the trust-store file the sandbox protects. If you do not supply it, the Git tool excludes nothing from its default view, and `GitCommit` fails closed: it refuses every path and stages nothing until the policy supplies a non-empty `gitIgnorePatterns` (an empty list refuses the same way). Supply it whenever the agent works in a directory that holds files you own. Building a sandboxed session requires it.
38
39
  - **`commandInterceptor`** may rewrite a command before it runs (for example to prefix a wrapper binary). Rewrites are validated: an argv rewrite may only prefix the original argv with the provider's own binary, and a shell rewrite goes through the same narrowing. If you do not supply one, commands run unchanged. An interceptor that throws, or returns a rewrite that fails validation, is treated as a decline, and the original command runs.
39
40
 
41
+ ## Repository instructions
42
+
43
+ Native sessions load repository-local instructions from the session workdir. In each directory, `AGENTS.override.md` takes precedence over `AGENTS.md`, with `CLAUDE.md` as a fallback. The native adapter's `OpenSessionOpts.instructionDirectories` adds package scopes relative to that workdir; a monorepo session can supply `["apps/api", "packages/client"]` to load each root-to-package chain. `OpenSessionOpts.instructionFileName` selects another Markdown basename, such as `TEAM.md`. A custom name loads only that filename in each directory, with no override or legacy fallback. Names must be nonhidden Markdown basenames without path separators, colon or control characters. More specific directory instructions override ancestor instructions within their scope. Other packages are discovered when authorized file tools enter them.
44
+
45
+ Local Markdown `@path.md` imports are deduplicated and bounded. Discovery stays inside the active checkout and respects protected paths and denied paths. Each session retains its own instructions outside compactable message history; transcripts record source paths, scopes and hashes. Resume reloads the applicable files from disk using the filename supplied again in the session options. ACP sessions use their CLI's instruction discovery.
46
+
40
47
  ## Conversational sessions
41
48
 
42
49
  `createAgentSession` gives an embedder (for example a long-running Node service) a multi-turn chat with a person in the loop. Events stream, tools come from the embedder, history lives in a store the embedder supplies, and a turn can be cancelled or answered with an approval.
@@ -79,7 +86,7 @@ Profiles set what a session may do:
79
86
  | `ask` | every Write, Edit, Delete, GitCommit and Bash is put to `answer()`; |
80
87
  | `full` | no prompts; `bashApproval` and the sandbox floor apply. |
81
88
 
82
- `createAgentSession` takes a `SessionBackend`. `nativeBackend(opts)` takes `model`, `credentials`, `catalogOverrides`, `loopHandlers`, `hostPorts`, `bashApproval` and `allowUnsandboxed`. The ACP backend ships in `@nathapp/nax-agent-acp`.
89
+ `createAgentSession` takes a `SessionBackend`. `nativeBackend(opts)` takes `instructionFileName` (default `AGENTS.md`), `model`, `credentials`, `catalogOverrides`, `loopHandlers`, `hostPorts`, `bashApproval` and `allowUnsandboxed`. The ACP backend ships in `@nathapp/nax-agent-acp`.
83
90
 
84
91
  `usage.costSource` is `computed` (native), `reported` or `unpriced`; never sum `unpriced` rows as cost.
85
92
 
@@ -91,6 +98,32 @@ Profiles set what a session may do:
91
98
  - **History and restarts.** The store holds one document per session, saved at the end of every turn. `close()` keeps it. After a restart, `resumeAgentSession(sessionId, options)` reopens it with the same options you created it with; pass `instructions` and `tools` again, since they are not stored. If the process died mid-turn, `session.lastTurn` is `{ turnId, status: "interrupted" }`, and that turn's message is not in history. A resume with a different model throws `AGENT_SESSION_MODEL_MISMATCH`; a different reasoning effort (`[high]`) is the same model. Resuming with a different backend kind throws `AGENT_SESSION_BACKEND_MISMATCH`; a stored document with no `backend` field is native. Do not open one session id twice at once: the store has no lock.
92
99
  - **Errors.** Every error is an `AgentSessionError` with a code `AGENT_SESSION_*`: `INVALID_OPTIONS`, `EXISTS`, `BUSY`, `CLOSED`, `INVALID_ANSWER`, `NOT_FOUND`, `SCHEMA_UNSUPPORTED`, `MODEL_MISMATCH`, `SANDBOX_UNAVAILABLE`, `TOOL_NAME_RESERVED`, `BACKEND_UNAVAILABLE`, `AUTH_REQUIRED`, `CAPABILITY_UNSUPPORTED` or `BACKEND_MISMATCH`. A stored document that cannot be read is a `NaxError` with `TRANSCRIPT_CORRUPT`.
93
100
 
101
+ ## MCP connections (`@nathapp/nax-agent/mcp`)
102
+
103
+ A small MCP client for embedders that bridge MCP servers into their own tools.
104
+ The root entry never loads it.
105
+
106
+ ```ts
107
+ import { connectMcp } from "@nathapp/nax-agent/mcp";
108
+
109
+ const connection = await connectMcp(
110
+ { kind: "stdio", command: "my-mcp-server", args: [], env: { API_KEY: "..." }, cwd: "/repo" },
111
+ { signal, timeoutMs: 30_000, clientInfo: { name: "my-app", version: "1.0.0" } },
112
+ );
113
+ connection.tools; // listed once, every page
114
+ await connection.call("search", { q: "x" }, { signal, timeoutMs: 600_000, maxBytes: 1_048_576 });
115
+ await connection.close(); // waits for the process; SIGKILL after 3 s
116
+ ```
117
+
118
+ - Transports: `stdio` and streamable `http` (with `headers`). SSE is not supported.
119
+ - `env` is laid over the default environment, so `PATH` is kept. Server stderr is
120
+ never inherited; its last 512 bytes are attached to a connect failure.
121
+ - A server-side tool error is a result with `isError: true`; protocol errors,
122
+ timeouts, aborts and closed connections throw `McpCallError`.
123
+ - `onClose` reports a stdio server that exits on its own; HTTP has no such signal.
124
+ - The facade (`createAgentSession`) has no `mcpServers` option: wrap MCP tools as
125
+ `EmbedderTool`s yourself.
126
+
94
127
  ## Status and roadmap
95
128
 
96
129
  `0.x` may reshape `.` with a minor bump. `@nathapp/nax-agent-acp` provides an ACP backend for the same API (0.3.0). See [`CHANGELOG.md`](CHANGELOG.md).
@@ -52,6 +52,18 @@ export interface OpenRouterRouting {
52
52
  ignore?: readonly string[];
53
53
  quantizations?: readonly string[];
54
54
  sort?: "price" | "throughput" | "latency";
55
+ preferred_min_throughput?: number | {
56
+ p50?: number;
57
+ p75?: number;
58
+ p90?: number;
59
+ p99?: number;
60
+ };
61
+ preferred_max_latency?: number | {
62
+ p50?: number;
63
+ p75?: number;
64
+ p90?: number;
65
+ p99?: number;
66
+ };
55
67
  }
56
68
  /**
57
69
  * One complete catalog entry for a model the bundled pi-ai snapshot does not
package/dist/index.d.ts CHANGED
@@ -18,11 +18,12 @@ export { isProcessAlive } from "#src/internal/process-alive";
18
18
  export { killProcessGroup } from "#src/internal/process-kill";
19
19
  export { capStrings, redactSecrets } from "#src/internal/redact";
20
20
  export { NaxError } from "#src/infra/nax-error";
21
+ export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, type TerminalAuthOptions, type TerminalStyle, } from "#src/terminal-auth/index";
21
22
  export type { CredentialSource } from "#src/native/credentials/session-source";
22
- export { AuthCancelledError, type AuthEvent, type AuthInteraction, type AuthLink, type AuthMethod, type AuthOption, type AuthPrompt, type AuthResult, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listStoredProviders, MAX_RETAINED_TRANSCRIPTS, type MemoryTranscriptStore, NATIVE_AGENT, type NativeCatalogOverrides, type NativeCompleteContext, type NativeCompleteOptions, type NativeCompleteResult, NativeSessionAdapter, NativeSessionUnsupportedError, type NativeTierConfig, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, providersWithoutCredentials, pruneRetainedTranscripts, type ResolveResult, type ResolveStatus, removeStoredProvider, resolveNativeId, runLogin, type StoredEntry, servedAuth, type TranscriptAcpRecord, type TranscriptDoc, type TranscriptStore, type TurnMarker, } from "#src/native/index";
23
+ export { AuthCancelledError, type AuthEvent, type AuthInteraction, type AuthLink, type AuthMethod, type AuthOption, type AuthPrompt, type AuthResult, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listProviderModels, listStoredProviders, loginProviderIds, MAX_RETAINED_TRANSCRIPTS, type MemoryTranscriptStore, NATIVE_AGENT, type NativeCatalogOverrides, type NativeCompleteContext, type NativeCompleteOptions, type NativeCompleteResult, NativeSessionAdapter, NativeSessionUnsupportedError, type NativeTierConfig, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, type ProviderModel, providersWithoutCredentials, pruneRetainedTranscripts, type ResolveResult, type ResolveStatus, removeStoredProvider, resolveNativeId, runLogin, type StoredEntry, servedAuth, type TranscriptAcpRecord, type TranscriptDoc, type TranscriptStore, type TurnMarker, } from "#src/native/index";
23
24
  export type { NativeSessionAdapterOptions } from "#src/native/session-adapter";
24
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";
25
- export { TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES } from "#src/native/session/turn-event-emitter";
26
+ export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
26
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
28
  export { type AgentGlobOptions, type AgentRuntime, type AgentSpawnOptions, type AgentSpawnResult, type AgentSpawnStdin, getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
28
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";
package/dist/index.js CHANGED
@@ -16,8 +16,9 @@ export { isProcessAlive } from "#src/internal/process-alive";
16
16
  export { killProcessGroup } from "#src/internal/process-kill";
17
17
  export { capStrings, redactSecrets } from "#src/internal/redact";
18
18
  export { NaxError } from "#src/infra/nax-error";
19
- export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, createFileTranscriptStore, createMemoryTranscriptStore, credentialFilePath, DEFAULT_PI_AUTH_PATH, importPiCredentials, listStoredProviders, MAX_RETAINED_TRANSCRIPTS, NATIVE_AGENT, NativeSessionAdapter, NativeSessionUnsupportedError, nativeComplete, nativeTierProviders, naxCredentialStore, newSessionKey, providersWithoutCredentials, pruneRetainedTranscripts, removeStoredProvider, resolveNativeId, runLogin, servedAuth, } from "#src/native/index";
20
- export { TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES } from "#src/native/session/turn-event-emitter";
19
+ export { createTerminalAuthInteraction, PLAIN_STYLE, PromptCancelledError, } from "#src/terminal-auth/index";
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
+ export { displayToolInput, TOOL_CALL_INPUT_BYTES, TOOL_RESULT_PREVIEW_BYTES, toolResultPreview, } from "#src/native/session/turn-event-emitter";
21
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
23
  export { getAgentRuntime, nodeRuntime, setAgentRuntime, } from "#src/runtime/index";
23
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";
@@ -0,0 +1,16 @@
1
+ import { rename, rm } from "node:fs/promises";
2
+ /** The one `writeFile` shape this module uses; narrower than the overloaded node signature so stubs type-check. */
3
+ type WriteText = (path: string, content: string, options: {
4
+ encoding: "utf8";
5
+ mode?: number;
6
+ }) => Promise<void>;
7
+ /** Injectable for tests that simulate a write dying part-way. */
8
+ export declare const _atomicWriteDeps: {
9
+ writeFile: WriteText;
10
+ rename: typeof rename;
11
+ rm: typeof rm;
12
+ };
13
+ export declare function writeFileAtomic(path: string, content: string, options?: {
14
+ mode?: number;
15
+ }): Promise<void>;
16
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Whole-file writes that a crash cannot tear.
3
+ *
4
+ * `writeFile` truncates the target before the new bytes land, so a process
5
+ * killed mid-write (SIGKILL, OOM, ENOSPC) leaves a partial file and the old
6
+ * content is gone. Staging the bytes in a sibling and `rename`-ing it over the
7
+ * target publishes the new content in one step: a reader, or the next run,
8
+ * sees the old file or the new one. Same-directory staging keeps the rename on
9
+ * one filesystem, where POSIX makes it atomic. Not fsync'd: this guards against
10
+ * a killed PROCESS, not a power loss.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import { rename, rm, writeFile } from "node:fs/promises";
14
+ /** Injectable for tests that simulate a write dying part-way. */
15
+ export const _atomicWriteDeps = {
16
+ writeFile: (path, content, options) => writeFile(path, content, options),
17
+ rename,
18
+ rm,
19
+ };
20
+ export async function writeFileAtomic(path, content, options = {}) {
21
+ // Ends in `.tmp`, not `.json`, so directory sweeps that match `*.json` never see it.
22
+ const staged = `${path}.${randomUUID()}.tmp`;
23
+ try {
24
+ await _atomicWriteDeps.writeFile(staged, content, {
25
+ encoding: "utf8",
26
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
27
+ });
28
+ await _atomicWriteDeps.rename(staged, path);
29
+ }
30
+ catch (error) {
31
+ await _atomicWriteDeps.rm(staged, { force: true });
32
+ throw error;
33
+ }
34
+ }
@@ -22,7 +22,7 @@ export function commandSpecIncludes(spec, fragment) {
22
22
  }
23
23
  /** Apply a literal replacement to every command entry without changing its shape. */
24
24
  export function replaceInCommandSpec(spec, searchValue, replacement) {
25
- const replace = (command) => command.replaceAll(searchValue, replacement);
25
+ const replace = (command) => command.replaceAll(searchValue, () => replacement);
26
26
  return typeof spec === "string" ? replace(spec) : spec.map(replace);
27
27
  }
28
28
  /**
@@ -50,9 +50,10 @@ export declare function hardenedGitEnv(base: Readonly<Record<string, string | un
50
50
  export declare function gitSpawnEnv(overlay?: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
51
51
  export declare const IGNORE_DIRTY_SUBMODULES_FLAG = "--ignore-submodules=dirty";
52
52
  /**
53
- * `argv` (argv[0] is the git program) with `--ignore-submodules=dirty` placed
54
- * right after a `status` / `diff` subcommand. Unlike the config key, the flag
55
- * overrides a `submodule.<name>.ignore` from `.gitmodules`. A later explicit
53
+ * `argv` (argv[0] is the git program, or an interceptor token followed by it)
54
+ * with `--ignore-submodules=dirty` placed right after a `status` / `diff`
55
+ * subcommand. Unlike the config key, the flag overrides a
56
+ * `submodule.<name>.ignore` from `.gitmodules`. A later explicit
56
57
  * `--ignore-submodules` in `argv` still wins (git takes the last), which the
57
58
  * agent-facing git tool cannot supply: it refuses every `-`-leading element.
58
59
  */
@@ -73,16 +73,23 @@ const SUBMODULE_DIRTY_CHECK_VERBS = new Set(["status", "diff"]);
73
73
  /** Global options that consume the next argument (`-C <dir>`, `-c <k=v>`). */
74
74
  const GLOBAL_OPTIONS_WITH_VALUE = new Set(["-C", "-c"]);
75
75
  export const IGNORE_DIRTY_SUBMODULES_FLAG = "--ignore-submodules=dirty";
76
+ /** `git` or a path ending in `/git`: the program, as opposed to a wrapper in front of it. */
77
+ function isGitProgram(token) {
78
+ return token === "git" || (token?.endsWith("/git") ?? false);
79
+ }
76
80
  /**
77
- * `argv` (argv[0] is the git program) with `--ignore-submodules=dirty` placed
78
- * right after a `status` / `diff` subcommand. Unlike the config key, the flag
79
- * overrides a `submodule.<name>.ignore` from `.gitmodules`. A later explicit
81
+ * `argv` (argv[0] is the git program, or an interceptor token followed by it)
82
+ * with `--ignore-submodules=dirty` placed right after a `status` / `diff`
83
+ * subcommand. Unlike the config key, the flag overrides a
84
+ * `submodule.<name>.ignore` from `.gitmodules`. A later explicit
80
85
  * `--ignore-submodules` in `argv` still wins (git takes the last), which the
81
86
  * agent-facing git tool cannot supply: it refuses every `-`-leading element.
82
87
  */
83
88
  export function hardenedGitArgv(argv) {
84
89
  const out = [...argv];
85
- let i = 1;
90
+ // A command interceptor may add exactly ONE leading token (validateRewrite), so a
91
+ // rewritten argv is `<provider> git <verb> ...`: the walk starts after the git program.
92
+ let i = !isGitProgram(out[0]) && isGitProgram(out[1]) ? 2 : 1;
86
93
  while (i < out.length && out[i].startsWith("-")) {
87
94
  i += GLOBAL_OPTIONS_WITH_VALUE.has(out[i]) ? 2 : 1;
88
95
  }
@@ -30,6 +30,7 @@ export * as infraModule from "#src/infra/index";
30
30
  export * from "#src/infra/spin-breaker/index";
31
31
  export * from "#src/internal/agent-output-env";
32
32
  export * from "#src/internal/argv-exec";
33
+ export * from "#src/internal/atomic-write";
33
34
  export * from "#src/internal/command-spec/index";
34
35
  export * from "#src/internal/file-lock";
35
36
  export * from "#src/internal/git-add";
@@ -56,11 +57,14 @@ export * from "#src/native/credentials/index";
56
57
  export * from "#src/native/errors";
57
58
  export * from "#src/native/model-resolver";
58
59
  export * from "#src/native/models";
60
+ export * from "#src/native/provider-models";
59
61
  export * from "#src/native/session/compaction";
62
+ export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
60
63
  export * from "#src/native/session/loop-events/external-handler";
61
64
  export * from "#src/native/session/loop-events/index";
62
65
  export * from "#src/native/session/loop-events/types";
63
66
  export * from "#src/native/session/memory-transcript-store";
67
+ export { _repositoryInstructionDeps } from "#src/native/session/repository-instructions";
64
68
  export * from "#src/native/session/session";
65
69
  export * as sessionModule from "#src/native/session/session";
66
70
  export * from "#src/native/session/tool-mapping";
@@ -80,11 +84,13 @@ export * from "#src/permissions/index";
80
84
  export * from "#src/sandbox/index";
81
85
  export * as policyInputsModule from "#src/sandbox/policy-inputs";
82
86
  export * from "#src/session/agent-session-deps";
87
+ export * from "#src/terminal-auth/index";
83
88
  export * from "#src/tools/credential-read-deny";
84
89
  export * from "#src/tools/git";
85
90
  export * from "#src/tools/git-commit";
86
91
  export * from "#src/tools/grep";
87
92
  export * from "#src/tools/index";
93
+ export { _instructionAccessDeps } from "#src/tools/instruction-access";
88
94
  export * from "#src/tools/owned-paths";
89
95
  export * from "#src/tools/package-managers";
90
96
  export * from "#src/tools/policy";
package/dist/internal.js CHANGED
@@ -30,6 +30,7 @@ export * as infraModule from "#src/infra/index";
30
30
  export * from "#src/infra/spin-breaker/index";
31
31
  export * from "#src/internal/agent-output-env";
32
32
  export * from "#src/internal/argv-exec";
33
+ export * from "#src/internal/atomic-write";
33
34
  export * from "#src/internal/command-spec/index";
34
35
  export * from "#src/internal/file-lock";
35
36
  export * from "#src/internal/git-add";
@@ -56,11 +57,14 @@ export * from "#src/native/credentials/index";
56
57
  export * from "#src/native/errors";
57
58
  export * from "#src/native/model-resolver";
58
59
  export * from "#src/native/models";
60
+ export * from "#src/native/provider-models";
59
61
  export * from "#src/native/session/compaction";
62
+ export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
60
63
  export * from "#src/native/session/loop-events/external-handler";
61
64
  export * from "#src/native/session/loop-events/index";
62
65
  export * from "#src/native/session/loop-events/types";
63
66
  export * from "#src/native/session/memory-transcript-store";
67
+ export { _repositoryInstructionDeps } from "#src/native/session/repository-instructions";
64
68
  export * from "#src/native/session/session";
65
69
  export * as sessionModule from "#src/native/session/session";
66
70
  export * from "#src/native/session/tool-mapping";
@@ -80,11 +84,13 @@ export * from "#src/permissions/index";
80
84
  export * from "#src/sandbox/index";
81
85
  export * as policyInputsModule from "#src/sandbox/policy-inputs";
82
86
  export * from "#src/session/agent-session-deps";
87
+ export * from "#src/terminal-auth/index";
83
88
  export * from "#src/tools/credential-read-deny";
84
89
  export * from "#src/tools/git";
85
90
  export * from "#src/tools/git-commit";
86
91
  export * from "#src/tools/grep";
87
92
  export * from "#src/tools/index";
93
+ export { _instructionAccessDeps } from "#src/tools/instruction-access";
88
94
  // S3-2: the port (nax supplies the policy behind it from its own module).
89
95
  export * from "#src/tools/owned-paths";
90
96
  export * from "#src/tools/package-managers";
@@ -0,0 +1,3 @@
1
+ import type { ConnectMcpOptions, McpConnection, McpTransportConfig } from "#src/mcp/types";
2
+ export declare const DEFAULT_CLOSE_GRACE_MS = 3000;
3
+ export declare function connectMcp(config: McpTransportConfig, opts: ConnectMcpOptions): Promise<McpConnection>;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * connectMcp (S5-5 spec §3.1): one MCP server over stdio or streamable HTTP.
3
+ * Connect (initialize + every tools/list page) is bounded by timeoutMs and the
4
+ * caller's signal; a call honours its signal and timeout (reset on progress);
5
+ * close is idempotent, bounded by closeGraceMs, and for stdio resolves only
6
+ * once the process is gone. A failed connect is closed the same way.
7
+ */
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
10
+ import { McpCallError, McpConnectError } from "#src/mcp/errors";
11
+ import { resultText } from "#src/mcp/result-text";
12
+ import { waitForStdioExit } from "#src/mcp/stdio-close";
13
+ import { _mcpTransportDeps } from "#src/mcp/transport";
14
+ export const DEFAULT_CLOSE_GRACE_MS = 3000;
15
+ const EXIT_REASON = "the server process exited";
16
+ const isRaw = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
18
+ /** Waits for `promise` at most `ms`; the timer is cleared, so nothing keeps the process alive. */
19
+ async function within(promise, ms) {
20
+ let timer;
21
+ const limit = new Promise((resolve) => {
22
+ timer = setTimeout(resolve, ms);
23
+ });
24
+ await Promise.race([promise.catch(() => undefined), limit]);
25
+ clearTimeout(timer);
26
+ }
27
+ function trackLife(client, kind) {
28
+ const life = { exited: false, closing: false, exitReason: undefined, listeners: [] };
29
+ client.onclose = () => {
30
+ life.exited = true;
31
+ if (life.closing || kind !== "stdio" || life.exitReason !== undefined)
32
+ return;
33
+ life.exitReason = EXIT_REASON;
34
+ for (const listener of life.listeners)
35
+ listener(EXIT_REASON);
36
+ };
37
+ return life;
38
+ }
39
+ async function closeTransport(client, handle, graceMs, life) {
40
+ life.closing = true;
41
+ const pid = handle.pid();
42
+ if (handle.terminate !== undefined)
43
+ await within(handle.terminate(), graceMs);
44
+ const closed = client.close();
45
+ // The stdio path below waits on the process, not on this promise: attach a
46
+ // handler now so a transport close that later rejects is never unhandled.
47
+ void closed.catch(() => undefined);
48
+ if (pid === null)
49
+ return within(closed, graceMs);
50
+ await waitForStdioExit(pid, () => life.exited, graceMs);
51
+ }
52
+ function toolInfo(raw) {
53
+ if (!isRaw(raw) || typeof raw.name !== "string" || raw.name.length === 0)
54
+ return undefined;
55
+ const inputSchema = isRaw(raw.inputSchema) ? { ...raw.inputSchema } : {};
56
+ return { name: raw.name, description: typeof raw.description === "string" ? raw.description : "", inputSchema };
57
+ }
58
+ async function listAll(client, signal, timeout) {
59
+ const tools = [];
60
+ let cursor;
61
+ do {
62
+ const page = await client.listTools(cursor !== undefined ? { cursor } : undefined, { signal, timeout });
63
+ for (const raw of page.tools ?? []) {
64
+ const info = toolInfo(raw);
65
+ if (info !== undefined)
66
+ tools.push(info);
67
+ }
68
+ cursor = typeof page.nextCursor === "string" && page.nextCursor !== "" ? page.nextCursor : undefined;
69
+ } while (cursor !== undefined);
70
+ return tools;
71
+ }
72
+ function connectFailure(error, deadline, opts, handle) {
73
+ const timedOut = deadline.aborted || (error instanceof McpError && error.code === ErrorCode.RequestTimeout);
74
+ const reason = timedOut ? `timed out after ${opts.timeoutMs} ms` : messageOf(error);
75
+ const tail = handle.stderrTail().trim();
76
+ return new McpConnectError(`MCP connect failed: ${reason}`, tail === "" ? undefined : tail);
77
+ }
78
+ function createHandle(config) {
79
+ try {
80
+ return _mcpTransportDeps.create(config);
81
+ }
82
+ catch (error) {
83
+ throw new McpConnectError(`MCP connect failed: ${messageOf(error)}`);
84
+ }
85
+ }
86
+ export async function connectMcp(config, opts) {
87
+ const handle = createHandle(config);
88
+ const client = new Client({ name: opts.clientInfo.name, version: opts.clientInfo.version });
89
+ const graceMs = opts.closeGraceMs ?? DEFAULT_CLOSE_GRACE_MS;
90
+ const life = trackLife(client, config.kind);
91
+ const deadline = AbortSignal.timeout(opts.timeoutMs);
92
+ const signal = AbortSignal.any([opts.signal, deadline]);
93
+ try {
94
+ if (signal.aborted)
95
+ throw new McpConnectError("MCP connect failed: aborted");
96
+ await client.connect(handle.transport, { signal, timeout: opts.timeoutMs });
97
+ const tools = await listAll(client, signal, opts.timeoutMs);
98
+ return liveConnection(config, client, handle, tools, { graceMs, life });
99
+ }
100
+ catch (error) {
101
+ await closeTransport(client, handle, graceMs, life);
102
+ throw error instanceof McpConnectError ? error : connectFailure(error, deadline, opts, handle);
103
+ }
104
+ }
105
+ async function callTool(client, name, input, opts) {
106
+ try {
107
+ const raw = await client.callTool({ name, arguments: isRaw(input) ? { ...input } : {} }, undefined, {
108
+ signal: opts.signal,
109
+ timeout: opts.timeoutMs,
110
+ resetTimeoutOnProgress: true,
111
+ // The SDK sends a progressToken (so servers can report progress and reset
112
+ // the timeout) only when onprogress is set (shared/protocol.js:643-650).
113
+ onprogress: () => undefined,
114
+ });
115
+ return resultText(raw, opts.maxBytes);
116
+ }
117
+ catch (error) {
118
+ throw new McpCallError(`MCP call ${name} failed: ${messageOf(error)}`);
119
+ }
120
+ }
121
+ function liveConnection(config, client, handle, tools, state) {
122
+ let closing;
123
+ const { life } = state;
124
+ return {
125
+ kind: config.kind,
126
+ tools,
127
+ call(name, input, opts) {
128
+ if (closing !== undefined || life.exited)
129
+ return Promise.reject(new McpCallError(`MCP call ${name} failed: the connection is closed`));
130
+ return callTool(client, name, input, opts);
131
+ },
132
+ onClose(listener) {
133
+ // An exit before anyone listened (spec §4.5, plan review I3) is still reported, once.
134
+ if (life.exitReason !== undefined)
135
+ listener(life.exitReason);
136
+ else
137
+ life.listeners.push(listener);
138
+ },
139
+ close() {
140
+ closing ??= closeTransport(client, handle, state.graceMs, life);
141
+ return closing;
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,9 @@
1
+ /** Errors of the shared MCP layer (S5-5 spec §3.1, plan M-42). */
2
+ import { NaxError } from "#src/infra/nax-error";
3
+ export declare class McpConnectError extends NaxError {
4
+ readonly stderrTail?: string | undefined;
5
+ constructor(message: string, stderrTail?: string | undefined, context?: Record<string, unknown>);
6
+ }
7
+ export declare class McpCallError extends NaxError {
8
+ constructor(message: string, context?: Record<string, unknown>);
9
+ }
@@ -0,0 +1,16 @@
1
+ /** Errors of the shared MCP layer (S5-5 spec §3.1, plan M-42). */
2
+ import { NaxError } from "#src/infra/nax-error";
3
+ export class McpConnectError extends NaxError {
4
+ stderrTail;
5
+ constructor(message, stderrTail, context = {}) {
6
+ super(message, "MCP_CONNECT_FAILED", context);
7
+ this.stderrTail = stderrTail;
8
+ this.name = "McpConnectError";
9
+ }
10
+ }
11
+ export class McpCallError extends NaxError {
12
+ constructor(message, context = {}) {
13
+ super(message, "MCP_CALL_FAILED", context);
14
+ this.name = "McpCallError";
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@nathapp/nax-agent/mcp`: the shared MCP connection layer (S5-5 spec §3.1).
3
+ * Never imported from the root entry, so `@nathapp/nax-agent` alone never loads
4
+ * the MCP SDK.
5
+ */
6
+ export { connectMcp, DEFAULT_CLOSE_GRACE_MS } from "#src/mcp/connect";
7
+ export { McpCallError, McpConnectError } from "#src/mcp/errors";
8
+ export { resultText } from "#src/mcp/result-text";
9
+ export type { ConnectMcpOptions, McpCallOptions, McpCallResult, McpConnection, McpToolInfo, McpTransportConfig, } from "#src/mcp/types";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@nathapp/nax-agent/mcp`: the shared MCP connection layer (S5-5 spec §3.1).
3
+ * Never imported from the root entry, so `@nathapp/nax-agent` alone never loads
4
+ * the MCP SDK.
5
+ */
6
+ export { connectMcp, DEFAULT_CLOSE_GRACE_MS } from "#src/mcp/connect";
7
+ export { McpCallError, McpConnectError } from "#src/mcp/errors";
8
+ export { resultText } from "#src/mcp/result-text";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A `tools/call` result as text (S5-5 spec §5.4). Text items are kept; embedded
3
+ * text resources keep their uri; binary content becomes a one-line placeholder;
4
+ * structuredContent is printed only when no text item exists. Capped at
5
+ * maxBytes on a code point boundary.
6
+ */
7
+ import type { McpCallResult } from "#src/mcp/types";
8
+ export declare function resultText(raw: unknown, maxBytes: number): McpCallResult;
@@ -0,0 +1,38 @@
1
+ import { cutToByteCap } from "#src/tools/truncate";
2
+ const isItem = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ const str = (value) => (typeof value === "string" ? value : undefined);
4
+ function resourceText(resource) {
5
+ if (!isItem(resource))
6
+ return "[resource omitted]";
7
+ const text = str(resource.text);
8
+ if (text !== undefined)
9
+ return `${str(resource.uri) ?? ""}\n${text}`;
10
+ return `[resource omitted: ${str(resource.mimeType) ?? "binary"}]`;
11
+ }
12
+ function itemText(item) {
13
+ switch (item.type) {
14
+ case "text":
15
+ return str(item.text) ?? "";
16
+ case "resource":
17
+ return resourceText(item.resource);
18
+ case "image":
19
+ case "audio":
20
+ return `[${item.type} omitted: ${str(item.mimeType) ?? "unknown"}]`;
21
+ case "resource_link":
22
+ return `[resource link: ${str(item.uri) ?? ""}]`;
23
+ default:
24
+ return `[${String(item.type ?? "unknown")} content omitted]`;
25
+ }
26
+ }
27
+ export function resultText(raw, maxBytes) {
28
+ const body = isItem(raw) ? raw : {};
29
+ const items = Array.isArray(body.content) ? body.content.filter(isItem) : [];
30
+ const hasText = items.some((item) => item.type === "text");
31
+ const parts = items.map(itemText);
32
+ if (!hasText && body.structuredContent !== undefined)
33
+ parts.push(JSON.stringify(body.structuredContent));
34
+ const full = parts.join("\n\n");
35
+ const bytes = Buffer.byteLength(full, "utf8");
36
+ const text = bytes <= maxBytes ? full : `${cutToByteCap(full, maxBytes)}\n[truncated: ${bytes} bytes in total]`;
37
+ return { text, isError: body.isError === true, bytesBeforeCap: bytes };
38
+ }
@@ -0,0 +1 @@
1
+ export declare function waitForStdioExit(pid: number, exited: () => boolean, graceMs: number): Promise<void>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * After close(), wait for an MCP stdio child to exit (S5-5 spec §3.1, I1). The
3
+ * SDK ends stdin, then SIGTERM after 2 s, then SIGKILL after 2 s more, on
4
+ * unref()'d timers: a server process exiting in between could orphan the child.
5
+ * These timers are ref'd, and after graceMs the child is SIGKILLed here.
6
+ */
7
+ import { isProcessAlive } from "#src/internal/process-alive";
8
+ const POLL_MS = 25;
9
+ const AFTER_KILL_MS = 1000;
10
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ async function waitUntil(gone, ms) {
12
+ const until = Date.now() + ms;
13
+ while (Date.now() < until) {
14
+ if (gone())
15
+ return true;
16
+ await sleep(POLL_MS);
17
+ }
18
+ return gone();
19
+ }
20
+ export async function waitForStdioExit(pid, exited, graceMs) {
21
+ const gone = () => exited() || !isProcessAlive(pid);
22
+ if (await waitUntil(gone, graceMs))
23
+ return;
24
+ try {
25
+ process.kill(pid, "SIGKILL");
26
+ }
27
+ catch {
28
+ return; // ESRCH: it exited between the probe and the kill
29
+ }
30
+ await waitUntil(gone, AFTER_KILL_MS);
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import type { McpTransportConfig } from "#src/mcp/types";
3
+ export declare const STDERR_TAIL_BYTES = 512;
4
+ export interface TransportHandle {
5
+ readonly transport: Transport;
6
+ readonly pid: () => number | null;
7
+ readonly stderrTail: () => string;
8
+ /** http: DELETE the MCP session, best effort. */
9
+ readonly terminate?: () => Promise<void>;
10
+ }
11
+ /** Injectable seam: unit tests return an in-memory transport. */
12
+ export declare const _mcpTransportDeps: {
13
+ create: (config: McpTransportConfig) => TransportHandle;
14
+ };