@nathapp/nax-agent 0.3.1 → 0.84.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 (62) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -1
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.js +3 -2
  5. package/dist/internal/atomic-write.d.ts +16 -0
  6. package/dist/internal/atomic-write.js +34 -0
  7. package/dist/internal/command-spec/index.js +1 -1
  8. package/dist/internal/git-env.d.ts +4 -3
  9. package/dist/internal/git-env.js +11 -4
  10. package/dist/internal.d.ts +5 -0
  11. package/dist/internal.js +5 -0
  12. package/dist/native/auth.d.ts +2 -0
  13. package/dist/native/auth.js +11 -1
  14. package/dist/native/index.d.ts +1 -1
  15. package/dist/native/index.js +1 -1
  16. package/dist/native/session/handle-invalid-tool-call.js +1 -0
  17. package/dist/native/session/instruction-file-name.d.ts +4 -0
  18. package/dist/native/session/instruction-file-name.js +21 -0
  19. package/dist/native/session/instruction-interaction.d.ts +7 -0
  20. package/dist/native/session/instruction-interaction.js +22 -0
  21. package/dist/native/session/instruction-render.d.ts +6 -0
  22. package/dist/native/session/instruction-render.js +22 -0
  23. package/dist/native/session/instruction-session.d.ts +4 -0
  24. package/dist/native/session/instruction-session.js +31 -0
  25. package/dist/native/session/instruction-transcript.d.ts +4 -0
  26. package/dist/native/session/instruction-transcript.js +14 -0
  27. package/dist/native/session/repository-instructions.d.ts +35 -0
  28. package/dist/native/session/repository-instructions.js +155 -0
  29. package/dist/native/session/session.d.ts +8 -0
  30. package/dist/native/session/session.js +29 -3
  31. package/dist/native/session/transcript-identity.d.ts +2 -0
  32. package/dist/native/session/transcript-identity.js +1 -1
  33. package/dist/native/session/transcript-store.js +4 -2
  34. package/dist/native/session/transcript-types.d.ts +3 -0
  35. package/dist/native/session/turn-complete-step.js +4 -2
  36. package/dist/native/session/turn-event-emitter.d.ts +4 -0
  37. package/dist/native/session/turn-event-emitter.js +6 -4
  38. package/dist/native/session/turn-loop-round-trip.js +20 -1
  39. package/dist/native/session/turn-loop.js +1 -0
  40. package/dist/native/session-adapter.js +21 -6
  41. package/dist/permissions/approvals-store.d.ts +3 -1
  42. package/dist/permissions/approvals-store.js +18 -5
  43. package/dist/sandbox/session-tmp.js +13 -7
  44. package/dist/session/agent-session-types.d.ts +4 -0
  45. package/dist/session/native-backend-options.d.ts +4 -0
  46. package/dist/session/native-backend-options.js +6 -0
  47. package/dist/session/native-backend.js +16 -4
  48. package/dist/session/session-ask-port.js +2 -1
  49. package/dist/session/session-interaction.js +29 -13
  50. package/dist/session/session-types.d.ts +10 -0
  51. package/dist/terminal-auth/index.d.ts +4 -0
  52. package/dist/terminal-auth/index.js +4 -0
  53. package/dist/terminal-auth/interaction.d.ts +15 -0
  54. package/dist/terminal-auth/interaction.js +55 -0
  55. package/dist/terminal-auth/open-url.d.ts +17 -0
  56. package/dist/terminal-auth/open-url.js +44 -0
  57. package/dist/terminal-auth/prompt.d.ts +51 -0
  58. package/dist/terminal-auth/prompt.js +196 -0
  59. package/dist/tools/instruction-access.d.ts +13 -0
  60. package/dist/tools/instruction-access.js +30 -0
  61. package/dist/tools/runtime.js +6 -1
  62. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.84.0] - 2026-10-09
8
+
9
+ - 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).
10
+
11
+ ### Added
12
+
13
+ - `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).
14
+ - `loginProviderIds()`: the providers `runLogin` can log in to (S5-4).
15
+ - `displayToolInput` and `toolResultPreview`: the live tool-display masking, for replaying stored transcripts (S5-1).
16
+ - `answerable?: false` on the `approval_requested` and `question` session events, set for profile auto-decisions and noted questions (S5-2).
17
+ - `carryHistoryAcrossModels` (`nativeBackend` option and `OpenSessionOpts`): keep a session's history across a model change; each assistant message records its origin model (S5-3).
18
+
7
19
  ## [0.3.1] - 2026-10-07
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -37,6 +37,12 @@ Three pieces of host knowledge are passed in as data or functions, because the p
37
37
  - **`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
38
  - **`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
39
 
40
+ ## Repository instructions
41
+
42
+ 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.
43
+
44
+ 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.
45
+
40
46
  ## Conversational sessions
41
47
 
42
48
  `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 +85,7 @@ Profiles set what a session may do:
79
85
  | `ask` | every Write, Edit, Delete, GitCommit and Bash is put to `answer()`; |
80
86
  | `full` | no prompts; `bashApproval` and the sandbox floor apply. |
81
87
 
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`.
88
+ `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
89
 
84
90
  `usage.costSource` is `computed` (native), `reported` or `unpriced`; never sum `unpriced` rows as cost.
85
91
 
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, 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, 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, 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";
@@ -57,10 +58,12 @@ export * from "#src/native/errors";
57
58
  export * from "#src/native/model-resolver";
58
59
  export * from "#src/native/models";
59
60
  export * from "#src/native/session/compaction";
61
+ export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
60
62
  export * from "#src/native/session/loop-events/external-handler";
61
63
  export * from "#src/native/session/loop-events/index";
62
64
  export * from "#src/native/session/loop-events/types";
63
65
  export * from "#src/native/session/memory-transcript-store";
66
+ export { _repositoryInstructionDeps } from "#src/native/session/repository-instructions";
64
67
  export * from "#src/native/session/session";
65
68
  export * as sessionModule from "#src/native/session/session";
66
69
  export * from "#src/native/session/tool-mapping";
@@ -80,11 +83,13 @@ export * from "#src/permissions/index";
80
83
  export * from "#src/sandbox/index";
81
84
  export * as policyInputsModule from "#src/sandbox/policy-inputs";
82
85
  export * from "#src/session/agent-session-deps";
86
+ export * from "#src/terminal-auth/index";
83
87
  export * from "#src/tools/credential-read-deny";
84
88
  export * from "#src/tools/git";
85
89
  export * from "#src/tools/git-commit";
86
90
  export * from "#src/tools/grep";
87
91
  export * from "#src/tools/index";
92
+ export { _instructionAccessDeps } from "#src/tools/instruction-access";
88
93
  export * from "#src/tools/owned-paths";
89
94
  export * from "#src/tools/package-managers";
90
95
  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";
@@ -57,10 +58,12 @@ export * from "#src/native/errors";
57
58
  export * from "#src/native/model-resolver";
58
59
  export * from "#src/native/models";
59
60
  export * from "#src/native/session/compaction";
61
+ export { DEFAULT_INSTRUCTION_FILE_NAME, isInstructionFileName } from "#src/native/session/instruction-file-name";
60
62
  export * from "#src/native/session/loop-events/external-handler";
61
63
  export * from "#src/native/session/loop-events/index";
62
64
  export * from "#src/native/session/loop-events/types";
63
65
  export * from "#src/native/session/memory-transcript-store";
66
+ export { _repositoryInstructionDeps } from "#src/native/session/repository-instructions";
64
67
  export * from "#src/native/session/session";
65
68
  export * as sessionModule from "#src/native/session/session";
66
69
  export * from "#src/native/session/tool-mapping";
@@ -80,11 +83,13 @@ export * from "#src/permissions/index";
80
83
  export * from "#src/sandbox/index";
81
84
  export * as policyInputsModule from "#src/sandbox/policy-inputs";
82
85
  export * from "#src/session/agent-session-deps";
86
+ export * from "#src/terminal-auth/index";
83
87
  export * from "#src/tools/credential-read-deny";
84
88
  export * from "#src/tools/git";
85
89
  export * from "#src/tools/git-commit";
86
90
  export * from "#src/tools/grep";
87
91
  export * from "#src/tools/index";
92
+ export { _instructionAccessDeps } from "#src/tools/instruction-access";
88
93
  // S3-2: the port (nax supplies the policy behind it from its own module).
89
94
  export * from "#src/tools/owned-paths";
90
95
  export * from "#src/tools/package-managers";
@@ -72,6 +72,8 @@ export declare function importPiCredentials(options?: {
72
72
  force?: boolean;
73
73
  }): Promise<ImportOutcome[]>;
74
74
  export declare function listStoredProviders(): Promise<StoredEntry[]>;
75
+ /** The providers `runLogin` can log in to: nax-ai's default catalog (S5-4 M-33). */
76
+ export declare function loginProviderIds(): Promise<string[]>;
75
77
  /**
76
78
  * Removal, not revocation. pi has no revocation anywhere — its own types
77
79
  * define logout as deletion — so the provider-side token stays live until it
@@ -167,11 +167,17 @@ export async function importPiCredentials(options) {
167
167
  cause: error,
168
168
  });
169
169
  }
170
+ // JSON.parse accepts `null`, arrays and scalars; none of them is a provider map.
171
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
172
+ throw new NaxError(`The file at ${path} is not a provider credential map.`, "AUTH_IMPORT_SOURCE_UNREADABLE", {
173
+ path,
174
+ });
175
+ }
170
176
  const store = naxCredentialStore();
171
177
  const outcomes = [];
172
178
  for (const providerId of Object.keys(parsed).sort()) {
173
179
  const entry = parsed[providerId];
174
- const credential = entry === undefined ? undefined : fromPiEntry(entry);
180
+ const credential = typeof entry === "object" && entry !== null ? fromPiEntry(entry) : undefined;
175
181
  if (credential === undefined) {
176
182
  outcomes.push({ providerId, status: "unsupported" });
177
183
  continue;
@@ -191,6 +197,10 @@ export async function importPiCredentials(options) {
191
197
  export async function listStoredProviders() {
192
198
  return readStoredEntries();
193
199
  }
200
+ /** The providers `runLogin` can log in to: nax-ai's default catalog (S5-4 M-33). */
201
+ export async function loginProviderIds() {
202
+ return _authDeps.providerIds();
203
+ }
194
204
  /**
195
205
  * Removal, not revocation. pi has no revocation anywhere — its own types
196
206
  * define logout as deletion — so the provider-side token stays live until it
@@ -11,7 +11,7 @@
11
11
  * cycle fails it.
12
12
  */
13
13
  export { _adapterDeps } from "./adapter-deps.ts";
14
- export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listStoredProviders, providersWithoutCredentials, removeStoredProvider, runLogin, } from "./auth.ts";
14
+ export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, DEFAULT_PI_AUTH_PATH, type ImportOutcome, importPiCredentials, listStoredProviders, loginProviderIds, providersWithoutCredentials, removeStoredProvider, runLogin, } from "./auth.ts";
15
15
  export type { AuthEvent, AuthInteraction, AuthLink, AuthMethod, AuthOption, AuthPrompt, AuthResult, } from "./auth-types.ts";
16
16
  export type { NativeCatalogOverrides } from "./client.ts";
17
17
  export { type NativeCompleteContext, type NativeCompleteOptions, type NativeCompleteResult, nativeComplete, } from "./complete.ts";
@@ -11,7 +11,7 @@
11
11
  * cycle fails it.
12
12
  */
13
13
  export { _adapterDeps } from "./adapter-deps.js";
14
- export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, DEFAULT_PI_AUTH_PATH, importPiCredentials, listStoredProviders, providersWithoutCredentials, removeStoredProvider, runLogin, } from "./auth.js";
14
+ export { AuthCancelledError, ambientShadows, anyAmbientCredential, authImportOutcomeLabel, DEFAULT_PI_AUTH_PATH, importPiCredentials, listStoredProviders, loginProviderIds, providersWithoutCredentials, removeStoredProvider, runLogin, } from "./auth.js";
15
15
  export { nativeComplete, } from "./complete.js";
16
16
  export { credentialFilePath, naxCredentialStore, servedAuth } from "./credentials/index.js";
17
17
  export { NativeSessionUnsupportedError } from "./errors.js";
@@ -113,6 +113,7 @@ export function rewriteToolCallInput(messages, callId, input) {
113
113
  content: lastAssistant.content,
114
114
  toolCalls,
115
115
  ...(lastAssistant.thinking !== undefined ? { thinking: lastAssistant.thinking } : {}),
116
+ ...(lastAssistant.origin !== undefined ? { origin: lastAssistant.origin } : {}),
116
117
  },
117
118
  ...messages.slice(lastAssistantIndex + 1),
118
119
  ];
@@ -0,0 +1,4 @@
1
+ export declare const DEFAULT_INSTRUCTION_FILE_NAME = "AGENTS.md";
2
+ /** A plain, nonhidden Markdown basename, never a path or URI. */
3
+ export declare function isInstructionFileName(value: unknown): value is string;
4
+ export declare function instructionFileNameFor(value: unknown): string;
@@ -0,0 +1,21 @@
1
+ import { NaxError } from "#src/infra/nax-error";
2
+ export const DEFAULT_INSTRUCTION_FILE_NAME = "AGENTS.md";
3
+ /** A plain, nonhidden Markdown basename, never a path or URI. */
4
+ export function isInstructionFileName(value) {
5
+ return (typeof value === "string" &&
6
+ !value.startsWith(".") &&
7
+ value.slice(0, -3).trim().length > 0 &&
8
+ /\.md$/i.test(value) &&
9
+ !/[\\/:]/.test(value) &&
10
+ ![...value].some((char) => {
11
+ const code = char.charCodeAt(0);
12
+ return code <= 31 || (code >= 127 && code <= 159);
13
+ }));
14
+ }
15
+ export function instructionFileNameFor(value) {
16
+ if (value === undefined)
17
+ return DEFAULT_INSTRUCTION_FILE_NAME;
18
+ if (isInstructionFileName(value))
19
+ return value;
20
+ throw new NaxError("instructionFileName must be a nonhidden Markdown basename without path separators, colon or control characters", "INVALID_INSTRUCTION_FILE_NAME", { stage: "native-session" });
21
+ }
@@ -0,0 +1,7 @@
1
+ import type { InteractionHandler } from "#src/session/interaction-handler";
2
+ import type { RepositoryInstructions } from "./repository-instructions.ts";
3
+ /** Scope discovery runs in the authorized coding runtime without coupling host handlers to native. */
4
+ export declare function createInstructionInteraction(instructions: RepositoryInstructions | undefined, handler: InteractionHandler): {
5
+ handler: InteractionHandler;
6
+ acknowledge: () => void;
7
+ };
@@ -0,0 +1,22 @@
1
+ import { dirname } from "node:path";
2
+ import { withInstructionAccess } from "#src/tools/instruction-access";
3
+ /** Scope discovery runs in the authorized coding runtime without coupling host handlers to native. */
4
+ export function createInstructionInteraction(instructions, handler) {
5
+ let acknowledged = instructions?.render() ?? "";
6
+ return {
7
+ acknowledge: () => {
8
+ acknowledged = instructions?.render() ?? "";
9
+ },
10
+ handler: {
11
+ onInteraction: (request) => withInstructionAccess(async (paths, canRead, protectedPaths) => {
12
+ if (instructions === undefined)
13
+ return undefined;
14
+ for (const path of paths)
15
+ await instructions.discover(dirname(path), canRead, protectedPaths);
16
+ if (instructions.render() === acknowledged)
17
+ return undefined;
18
+ return "Repository instructions were discovered before this mutation. No files were changed. Review the directory-scoped instructions in your updated system context and retry the tool call.";
19
+ }, () => handler.onInteraction(request)),
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,6 @@
1
+ import type { InstructionSource } from "./repository-instructions.ts";
2
+ export declare const MIN_INSTRUCTION_NOTICE_BYTES: number;
3
+ /** Keep whole guidance sources: partial instructions could reverse a requirement. */
4
+ export declare function renderRepositoryInstructions(sources: readonly (InstructionSource & {
5
+ readonly text: string;
6
+ })[], limited: boolean, maxBytes: number): string;
@@ -0,0 +1,22 @@
1
+ const BUDGET_NOTICE = "Repository instruction budget reached; some sources were omitted. Read applicable instruction files before further changes.";
2
+ export const MIN_INSTRUCTION_NOTICE_BYTES = Buffer.byteLength(BUDGET_NOTICE, "utf8") + 32;
3
+ /** Keep whole guidance sources: partial instructions could reverse a requirement. */
4
+ export function renderRepositoryInstructions(sources, limited, maxBytes) {
5
+ const parts = [];
6
+ let used = 0;
7
+ let omitted = 0;
8
+ const contentBudget = Math.max(0, maxBytes - MIN_INSTRUCTION_NOTICE_BYTES);
9
+ for (const source of sources) {
10
+ const body = `Repository instructions: ${source.path} (scope: ${source.scope}; sha256: ${source.hash})\nApply these instructions only within their directory scope. On conflicts, instructions from a deeper directory override ancestor instructions.\n${source.text}`;
11
+ const bytes = Buffer.byteLength(body, "utf8") + 2;
12
+ if (used + bytes > contentBudget) {
13
+ omitted++;
14
+ continue;
15
+ }
16
+ parts.push(body);
17
+ used += bytes;
18
+ }
19
+ if (limited || omitted > 0)
20
+ parts.push(`${BUDGET_NOTICE} (${omitted} loaded sources omitted.)`);
21
+ return parts.join("\n\n");
22
+ }
@@ -0,0 +1,4 @@
1
+ import type { OpenSessionOpts } from "#src/session/session-types";
2
+ import { RepositoryInstructions } from "./repository-instructions.ts";
3
+ import type { TranscriptStore } from "./transcript-types.ts";
4
+ export declare function openRepositoryInstructions(name: string, opts: OpenSessionOpts, store: TranscriptStore): Promise<RepositoryInstructions>;
@@ -0,0 +1,31 @@
1
+ import { RepositoryInstructions } from "./repository-instructions.js";
2
+ import { transcriptModelIdentity } from "./transcript-identity.js";
3
+ export async function openRepositoryInstructions(name, opts, store) {
4
+ const instructions = new RepositoryInstructions(opts.workdir, opts.instructionProtectedPaths, opts.instructionDenyPaths, opts.instructionFileName);
5
+ for (const directory of [".", ...(opts.instructionDirectories ?? [])])
6
+ await instructions.discover(directory);
7
+ if (opts.resume !== true)
8
+ return instructions;
9
+ // Open historically performs no history I/O that can fail the session. The
10
+ // turn loop remains the authoritative load/error boundary; unavailable
11
+ // provenance simply leaves the explicitly supplied scopes in place.
12
+ const doc = await Promise.resolve()
13
+ .then(() => store.load(name))
14
+ .catch(() => null);
15
+ if (doc === null || (doc.schemaVersion !== undefined && doc.schemaVersion !== 1) || !Array.isArray(doc.messages))
16
+ return instructions;
17
+ const ownerMatches = opts.transcriptOwner === undefined || doc?.owner === opts.transcriptOwner;
18
+ const modelMatches = opts.carryHistoryAcrossModels === true ||
19
+ doc?.model === undefined ||
20
+ doc.model === transcriptModelIdentity(opts.modelDef?.model);
21
+ if (ownerMatches && modelMatches) {
22
+ const directories = doc?.instructionDirectories;
23
+ if (Array.isArray(directories)) {
24
+ for (const directory of directories.slice(0, 64)) {
25
+ if (typeof directory === "string")
26
+ await instructions.discover(directory);
27
+ }
28
+ }
29
+ }
30
+ return instructions;
31
+ }
@@ -0,0 +1,4 @@
1
+ import type { RepositoryInstructions } from "./repository-instructions.ts";
2
+ import type { TranscriptStore } from "./transcript-types.ts";
3
+ /** Preserve instruction provenance alongside history, including compacted snapshots. */
4
+ export declare function instructionTranscriptStore(store: TranscriptStore, instructions: RepositoryInstructions): TranscriptStore;
@@ -0,0 +1,14 @@
1
+ /** Preserve instruction provenance alongside history, including compacted snapshots. */
2
+ export function instructionTranscriptStore(store, instructions) {
3
+ return {
4
+ load: (id) => store.load(id),
5
+ save: (id, doc) => store.save(id, {
6
+ ...doc,
7
+ instructionSources: instructions.sources,
8
+ instructionDirectories: instructions.directories,
9
+ }),
10
+ delete: (id) => store.delete(id),
11
+ retainFailed: (id) => store.retainFailed(id),
12
+ markTurn: (id, marker) => store.markTurn(id, marker),
13
+ };
14
+ }
@@ -0,0 +1,35 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { readPrefix } from "#src/internal/bounded-io";
3
+ import type { ProtectedPathsPolicy } from "#src/tools/protected-paths";
4
+ export declare const _repositoryInstructionDeps: {
5
+ realpath: typeof realpath;
6
+ readPrefix: typeof readPrefix;
7
+ };
8
+ export interface InstructionSource {
9
+ readonly path: string;
10
+ readonly scope: string;
11
+ readonly hash: string;
12
+ }
13
+ export declare class RepositoryInstructions {
14
+ private readonly workdir;
15
+ private readonly protectedPaths?;
16
+ private readonly denyPaths?;
17
+ private readonly loaded;
18
+ private readonly visited;
19
+ private readonly scopes;
20
+ private bytes;
21
+ private root;
22
+ private limited;
23
+ private pending;
24
+ private renderMaxBytes;
25
+ private readonly fileNames;
26
+ constructor(workdir: string, protectedPaths?: ProtectedPathsPolicy | undefined, denyPaths?: readonly string[] | undefined, fileName?: string);
27
+ get directories(): readonly string[];
28
+ get sources(): readonly InstructionSource[];
29
+ /** Bytes conservatively upper-bound token count; host instructions consume the same system reserve. */
30
+ setContextBudget(contextWindow: number | undefined, hostPrompt: string | undefined): void;
31
+ render(): string;
32
+ discover(directory: string, canRead?: (path: string) => boolean, protectedPaths?: ProtectedPathsPolicy | undefined): Promise<void>;
33
+ private discoverScope;
34
+ private load;
35
+ }