@nathapp/nax-agent 0.3.0 → 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.
- package/CHANGELOG.md +27 -0
- package/README.md +7 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/internal/atomic-write.d.ts +16 -0
- package/dist/internal/atomic-write.js +34 -0
- package/dist/internal/command-spec/index.js +1 -1
- package/dist/internal/git-env.d.ts +4 -3
- package/dist/internal/git-env.js +11 -4
- package/dist/internal.d.ts +5 -0
- package/dist/internal.js +5 -0
- package/dist/native/auth.d.ts +2 -0
- package/dist/native/auth.js +11 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +1 -1
- package/dist/native/session/handle-invalid-tool-call.js +1 -0
- package/dist/native/session/instruction-file-name.d.ts +4 -0
- package/dist/native/session/instruction-file-name.js +21 -0
- package/dist/native/session/instruction-interaction.d.ts +7 -0
- package/dist/native/session/instruction-interaction.js +22 -0
- package/dist/native/session/instruction-render.d.ts +6 -0
- package/dist/native/session/instruction-render.js +22 -0
- package/dist/native/session/instruction-session.d.ts +4 -0
- package/dist/native/session/instruction-session.js +31 -0
- package/dist/native/session/instruction-transcript.d.ts +4 -0
- package/dist/native/session/instruction-transcript.js +14 -0
- package/dist/native/session/repository-instructions.d.ts +35 -0
- package/dist/native/session/repository-instructions.js +155 -0
- package/dist/native/session/session.d.ts +8 -0
- package/dist/native/session/session.js +29 -3
- package/dist/native/session/transcript-identity.d.ts +2 -0
- package/dist/native/session/transcript-identity.js +1 -1
- package/dist/native/session/transcript-store.js +4 -2
- package/dist/native/session/transcript-types.d.ts +3 -0
- package/dist/native/session/turn-complete-step.js +4 -2
- package/dist/native/session/turn-event-emitter.d.ts +4 -0
- package/dist/native/session/turn-event-emitter.js +6 -4
- package/dist/native/session/turn-loop-round-trip.js +20 -1
- package/dist/native/session/turn-loop.js +1 -0
- package/dist/native/session/turn-types.d.ts +4 -5
- package/dist/native/session/turn-types.js +8 -8
- package/dist/native/session-adapter.js +21 -6
- package/dist/permissions/approvals-store.d.ts +3 -1
- package/dist/permissions/approvals-store.js +18 -5
- package/dist/sandbox/session-tmp.js +13 -7
- package/dist/session/agent-session-errors.d.ts +1 -1
- package/dist/session/agent-session-turn.js +12 -5
- package/dist/session/agent-session-types.d.ts +4 -0
- package/dist/session/native-backend-options.d.ts +4 -0
- package/dist/session/native-backend-options.js +6 -0
- package/dist/session/native-backend.js +16 -4
- package/dist/session/session-ask-port.js +2 -1
- package/dist/session/session-interaction.js +29 -13
- package/dist/session/session-types.d.ts +20 -0
- package/dist/session/turn-event.d.ts +7 -0
- package/dist/session/turn-spend.d.ts +18 -0
- package/dist/session/turn-spend.js +9 -0
- package/dist/terminal-auth/index.d.ts +4 -0
- package/dist/terminal-auth/index.js +4 -0
- package/dist/terminal-auth/interaction.d.ts +15 -0
- package/dist/terminal-auth/interaction.js +55 -0
- package/dist/terminal-auth/open-url.d.ts +17 -0
- package/dist/terminal-auth/open-url.js +44 -0
- package/dist/terminal-auth/prompt.d.ts +51 -0
- package/dist/terminal-auth/prompt.js +196 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/instruction-access.d.ts +13 -0
- package/dist/tools/instruction-access.js +30 -0
- package/dist/tools/runtime.js +6 -1
- package/package.json +2 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal prompts for credential entry (moved from the nax CLI, S5-4 M-28).
|
|
3
|
+
*
|
|
4
|
+
* Nothing typed into a secret prompt is echoed, and the terminal is restored on
|
|
5
|
+
* every exit path: submit, cancel, stream end and stream error. Ctrl+D cancels;
|
|
6
|
+
* it never submits (it once fell through to "submit" in nax's confirm prompt).
|
|
7
|
+
*/
|
|
8
|
+
/** Ctrl+C. */
|
|
9
|
+
const ETX = "\u0003";
|
|
10
|
+
/** Ctrl+D. Conventionally cancel, never submit. */
|
|
11
|
+
const EOT = "\u0004";
|
|
12
|
+
const CR = "\r";
|
|
13
|
+
const LF = "\n";
|
|
14
|
+
const BACKSPACE = "\u007F";
|
|
15
|
+
const ARROW_UP = "\u001b[A";
|
|
16
|
+
const ARROW_DOWN = "\u001b[B";
|
|
17
|
+
const identity = (text) => text;
|
|
18
|
+
export const PLAIN_STYLE = { accent: identity, dim: identity, bold: identity };
|
|
19
|
+
export class PromptCancelledError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("Prompt cancelled");
|
|
22
|
+
this.name = "PromptCancelledError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Test seam. `stdin` is unset by default and resolved to `process.stdin` only
|
|
27
|
+
* when a prompt runs: this module is reachable from the public barrel, and
|
|
28
|
+
* touching `process.stdin` at import would open it for every consumer.
|
|
29
|
+
*/
|
|
30
|
+
export const _terminalPromptDeps = {
|
|
31
|
+
stdin: undefined,
|
|
32
|
+
write: (text) => process.stdout.write(text),
|
|
33
|
+
};
|
|
34
|
+
function stdinOf() {
|
|
35
|
+
return _terminalPromptDeps.stdin ?? process.stdin;
|
|
36
|
+
}
|
|
37
|
+
/** What one typed character does to a line prompt. */
|
|
38
|
+
function keyAction(char, bufferEmpty, hasEmptyHook) {
|
|
39
|
+
if (char === ETX || char === EOT)
|
|
40
|
+
return { kind: "cancel" };
|
|
41
|
+
if (char === CR || char === LF) {
|
|
42
|
+
return bufferEmpty && hasEmptyHook ? { kind: "empty-submit" } : { kind: "submit" };
|
|
43
|
+
}
|
|
44
|
+
if (char === BACKSPACE)
|
|
45
|
+
return { kind: "erase" };
|
|
46
|
+
return { kind: "append" };
|
|
47
|
+
}
|
|
48
|
+
/** Applies a non-terminal key to the buffer; echoes when the prompt is visible. */
|
|
49
|
+
function applyEdit(action, char, buffer, echo, onEmptySubmit) {
|
|
50
|
+
if (action.kind === "empty-submit") {
|
|
51
|
+
onEmptySubmit?.();
|
|
52
|
+
return buffer;
|
|
53
|
+
}
|
|
54
|
+
if (action.kind === "erase") {
|
|
55
|
+
if (echo)
|
|
56
|
+
_terminalPromptDeps.write("\b \b");
|
|
57
|
+
return buffer.slice(0, -1);
|
|
58
|
+
}
|
|
59
|
+
if (echo)
|
|
60
|
+
_terminalPromptDeps.write(char);
|
|
61
|
+
return buffer + char;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `onEmptySubmit` turns Enter-on-an-empty-buffer into an action rather than a
|
|
65
|
+
* submission. An empty answer is meaningless for the prompts that use it (a
|
|
66
|
+
* pasted auth code), and spending the keystroke here keeps the whole login on a
|
|
67
|
+
* single stdin reader.
|
|
68
|
+
*/
|
|
69
|
+
function read(message, echo, onEmptySubmit, style) {
|
|
70
|
+
const stdin = stdinOf();
|
|
71
|
+
if (stdin.isTTY !== true)
|
|
72
|
+
return Promise.reject(new PromptCancelledError());
|
|
73
|
+
_terminalPromptDeps.write(`${style.accent("?")} ${message} `);
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
let buffer = "";
|
|
76
|
+
let settled = false;
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
stdin.removeListener("data", onData);
|
|
82
|
+
stdin.removeListener("end", onEnd);
|
|
83
|
+
stdin.removeListener("error", onEnd);
|
|
84
|
+
stdin.setRawMode(false);
|
|
85
|
+
stdin.pause();
|
|
86
|
+
_terminalPromptDeps.write("\n");
|
|
87
|
+
};
|
|
88
|
+
const onEnd = () => {
|
|
89
|
+
cleanup();
|
|
90
|
+
reject(new PromptCancelledError());
|
|
91
|
+
};
|
|
92
|
+
const onData = (chunk) => {
|
|
93
|
+
for (const char of chunk) {
|
|
94
|
+
const action = keyAction(char, buffer.length === 0, onEmptySubmit !== undefined);
|
|
95
|
+
if (action.kind === "cancel") {
|
|
96
|
+
onEnd();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (action.kind === "submit") {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve(buffer);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
buffer = applyEdit(action, char, buffer, echo, onEmptySubmit);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
stdin.setRawMode(true);
|
|
108
|
+
stdin.resume();
|
|
109
|
+
stdin.setEncoding("utf8");
|
|
110
|
+
stdin.on("data", onData);
|
|
111
|
+
stdin.once("end", onEnd);
|
|
112
|
+
stdin.once("error", onEnd);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Reads a secret. Nothing is echoed, not even a masking character. */
|
|
116
|
+
export function promptForSecret(message, style = PLAIN_STYLE) {
|
|
117
|
+
return read(message, false, undefined, style);
|
|
118
|
+
}
|
|
119
|
+
/** Reads a visible line, for non-secret answers such as a pasted auth code. */
|
|
120
|
+
export function promptForLine(message, onEmptySubmit, style = PLAIN_STYLE) {
|
|
121
|
+
return read(message, true, onEmptySubmit, style);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reads a choice with the arrow keys and returns the chosen option's id. The
|
|
125
|
+
* option block is redrawn in place. Enter commits the highlighted row, so a
|
|
126
|
+
* value that is not an option can never be returned.
|
|
127
|
+
*/
|
|
128
|
+
export function promptForSelect(message, choices, style = PLAIN_STYLE) {
|
|
129
|
+
const stdin = stdinOf();
|
|
130
|
+
if (stdin.isTTY !== true)
|
|
131
|
+
return Promise.reject(new PromptCancelledError());
|
|
132
|
+
// Never silently pick for the user: an empty list is a caller bug.
|
|
133
|
+
if (choices.length === 0)
|
|
134
|
+
return Promise.reject(new PromptCancelledError());
|
|
135
|
+
_terminalPromptDeps.write(`${style.accent("?")} ${message}\n`);
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
let index = 0;
|
|
138
|
+
let settled = false;
|
|
139
|
+
let drawn = false;
|
|
140
|
+
const render = () => {
|
|
141
|
+
if (drawn)
|
|
142
|
+
_terminalPromptDeps.write(`\u001b[${choices.length}A`);
|
|
143
|
+
drawn = true;
|
|
144
|
+
for (const [i, choice] of choices.entries()) {
|
|
145
|
+
const active = i === index;
|
|
146
|
+
const marker = active ? style.accent(">") : " ";
|
|
147
|
+
const label = active ? style.accent(choice.label) : choice.label;
|
|
148
|
+
_terminalPromptDeps.write(`\r\u001b[2K${marker} ${label}\n`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
if (settled)
|
|
153
|
+
return;
|
|
154
|
+
settled = true;
|
|
155
|
+
stdin.removeListener("data", onData);
|
|
156
|
+
stdin.removeListener("end", onEnd);
|
|
157
|
+
stdin.removeListener("error", onEnd);
|
|
158
|
+
stdin.setRawMode(false);
|
|
159
|
+
stdin.pause();
|
|
160
|
+
};
|
|
161
|
+
const onEnd = () => {
|
|
162
|
+
cleanup();
|
|
163
|
+
reject(new PromptCancelledError());
|
|
164
|
+
};
|
|
165
|
+
const onData = (chunk) => {
|
|
166
|
+
// Whole-chunk matching: an arrow key is a three-byte escape sequence.
|
|
167
|
+
if (chunk.includes(ETX) || chunk.includes(EOT)) {
|
|
168
|
+
onEnd();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (chunk.includes(ARROW_UP)) {
|
|
172
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
173
|
+
render();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (chunk.includes(ARROW_DOWN)) {
|
|
177
|
+
index = (index + 1) % choices.length;
|
|
178
|
+
render();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (chunk.includes(CR) || chunk.includes(LF)) {
|
|
182
|
+
const chosen = choices[index];
|
|
183
|
+
cleanup();
|
|
184
|
+
// biome-ignore lint/style/noNonNullAssertion: index is held in range by the modulo above.
|
|
185
|
+
resolve(chosen.id);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
stdin.setRawMode(true);
|
|
189
|
+
stdin.resume();
|
|
190
|
+
stdin.setEncoding("utf8");
|
|
191
|
+
stdin.on("data", onData);
|
|
192
|
+
stdin.once("end", onEnd);
|
|
193
|
+
stdin.once("error", onEnd);
|
|
194
|
+
render();
|
|
195
|
+
});
|
|
196
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type { CodingToolOutcome, CodingToolRuntime, ToolCallContext } from "./ru
|
|
|
28
28
|
export { _codingToolDeps, _resetBuiltinsForTest, createCodingToolRuntime, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, registerBuiltinCodingTools, } from "./runtime.ts";
|
|
29
29
|
export { SCRATCHPAD_DIR, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool } from "./scratchpad.ts";
|
|
30
30
|
export { _spillDeps, applyModelTruncationPolicy, type ModelTruncationOptions, SPILL_DIR, type SpillRequest, spillRelativePath, writeSpill, } from "./spill.ts";
|
|
31
|
-
export type { RegisteredSink, ToolAuditSink, ToolCallRecord } from "./tool-audit.ts";
|
|
31
|
+
export type { RegisteredSink, ToolAuditHeader, ToolAuditSink, ToolCallRecord } from "./tool-audit.ts";
|
|
32
32
|
export { createNoOpToolAuditSink, createToolAuditSink, flushOpenToolAuditSinks, registerToolAuditSink, unregisterToolAuditSink, } from "./tool-audit.ts";
|
|
33
33
|
export { cutBufferToByteCap, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, READ_CEILING, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, } from "./truncate.ts";
|
|
34
34
|
export type { CodingToolName, PolicyVerdict, ToolGrant, ToolPolicy, ToolScope } from "./types.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Transport-neutral scope hook, invoked only after coding-tool authorization. */
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import type { ProtectedPathsPolicy } from "./protected-paths.ts";
|
|
4
|
+
import type { ToolPolicy } from "./types.ts";
|
|
5
|
+
type InstructionAccess = (paths: readonly string[], canRead: (path: string) => boolean, protectedPaths?: ProtectedPathsPolicy) => Promise<string | undefined>;
|
|
6
|
+
export declare const _instructionAccessDeps: {
|
|
7
|
+
stat: typeof stat;
|
|
8
|
+
};
|
|
9
|
+
export declare function withInstructionAccess<T>(observer: InstructionAccess, run: () => Promise<T>): Promise<T>;
|
|
10
|
+
export declare function discoverAuthorizedInstructions(name: string, paths: readonly string[], policy: ToolPolicy, protectedPaths?: ProtectedPathsPolicy): Promise<string | undefined>;
|
|
11
|
+
/** A mutation must be proposed again after the model has received new guidance. */
|
|
12
|
+
export declare function enforceInstructionAccess(name: string, paths: readonly string[], policy: ToolPolicy, protectedPaths?: ProtectedPathsPolicy): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Transport-neutral scope hook, invoked only after coding-tool authorization. */
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { NaxError } from "#src/infra/nax-error";
|
|
6
|
+
import { credentialReadRefusal } from "./credential-read-deny.js";
|
|
7
|
+
export const _instructionAccessDeps = { stat };
|
|
8
|
+
const access = new AsyncLocalStorage();
|
|
9
|
+
export function withInstructionAccess(observer, run) {
|
|
10
|
+
return access.run(observer, run);
|
|
11
|
+
}
|
|
12
|
+
export async function discoverAuthorizedInstructions(name, paths, policy, protectedPaths) {
|
|
13
|
+
if (!["Read", "Write", "Edit"].includes(name))
|
|
14
|
+
return;
|
|
15
|
+
const observer = access.getStore();
|
|
16
|
+
if (observer === undefined || paths.some((path) => credentialReadRefusal(protectedPaths, path) !== undefined))
|
|
17
|
+
return;
|
|
18
|
+
const targets = name === "Write" ? paths.map((path) => dirname(path)) : paths;
|
|
19
|
+
const present = await Promise.all(targets.map((path) => _instructionAccessDeps.stat(path).then((metadata) => (name === "Write" ? metadata.isDirectory() : metadata.isFile()), () => false)));
|
|
20
|
+
if (present.some((found) => !found))
|
|
21
|
+
return;
|
|
22
|
+
const guidance = await observer(paths, (path) => policy.check("Read", { pathFields: ["path"] }, { path }).allowed, protectedPaths);
|
|
23
|
+
return name === "Read" ? undefined : guidance || undefined;
|
|
24
|
+
}
|
|
25
|
+
/** A mutation must be proposed again after the model has received new guidance. */
|
|
26
|
+
export async function enforceInstructionAccess(name, paths, policy, protectedPaths) {
|
|
27
|
+
const guidance = await discoverAuthorizedInstructions(name, paths, policy, protectedPaths);
|
|
28
|
+
if (guidance !== undefined)
|
|
29
|
+
throw new NaxError(guidance, "REPOSITORY_INSTRUCTIONS_DISCOVERED", { stage: "coding-tool" });
|
|
30
|
+
}
|
package/dist/tools/runtime.js
CHANGED
|
@@ -17,6 +17,7 @@ import { gitTool } from "./git.js";
|
|
|
17
17
|
import { gitCommitTool } from "./git-commit.js";
|
|
18
18
|
import { globTool } from "./glob.js";
|
|
19
19
|
import { grepTool } from "./grep.js";
|
|
20
|
+
import { enforceInstructionAccess } from "./instruction-access.js";
|
|
20
21
|
import { readTool } from "./read.js";
|
|
21
22
|
import { getCodingTool, registerBuiltinTool } from "./registry.js";
|
|
22
23
|
import { requestCapabilityTool } from "./request-capability.js";
|
|
@@ -224,7 +225,10 @@ export function createCodingToolRuntime(opts) {
|
|
|
224
225
|
log(tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation);
|
|
225
226
|
tap?.settle(outcome, audit?.approval?.decidedBy, audit?.executed === undefined
|
|
226
227
|
? undefined
|
|
227
|
-
: {
|
|
228
|
+
: {
|
|
229
|
+
executed: audit.executed,
|
|
230
|
+
...(audit.cwd !== undefined ? { cwd: audit.cwd } : {}),
|
|
231
|
+
});
|
|
228
232
|
};
|
|
229
233
|
/**
|
|
230
234
|
* Executes a permitted call and records its outcome. Shared by the
|
|
@@ -249,6 +253,7 @@ export function createCodingToolRuntime(opts) {
|
|
|
249
253
|
// scope.
|
|
250
254
|
const callSignal = context?.signal ?? signal;
|
|
251
255
|
try {
|
|
256
|
+
await enforceInstructionAccess(target.name, resolvedPaths, opts.policy, opts.protectedPaths);
|
|
252
257
|
const result = await target.run(callInput, {
|
|
253
258
|
root: opts.policy.root,
|
|
254
259
|
resolvedPaths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.0",
|
|
4
4
|
"description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "William Khoo",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
45
|
-
"@nathapp/nax-ai": "0.
|
|
45
|
+
"@nathapp/nax-ai": "0.84.0",
|
|
46
46
|
"zod": "^4.3.6"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|