@nathapp/nax-agent 0.85.1 → 0.86.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 +4 -0
- package/dist/internal/redact.d.ts +4 -0
- package/dist/internal/redact.js +12 -3
- package/dist/permissions/secret-spans.js +3 -2
- package/dist/terminal-auth/interaction.js +5 -1
- package/dist/terminal-auth/prompt.d.ts +7 -2
- package/dist/terminal-auth/prompt.js +103 -56
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ 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.86.0] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
10
|
+
|
|
7
11
|
## [0.85.1] - 2026-10-10
|
|
8
12
|
|
|
9
13
|
### Added
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Matches must pass `acceptMatch`, when present, before masking: the URL scanner also
|
|
3
|
+
* consumes scheme-only runs to avoid rescanning them on failure.
|
|
2
4
|
* Patterns are reset via `re.lastIndex = 0` before every call because they carry
|
|
3
5
|
* the `/g` flag (required for `String.replace`). Resetting prevents the stale
|
|
4
6
|
* `lastIndex` bug that skips matches on subsequent calls.
|
|
@@ -7,6 +9,8 @@
|
|
|
7
9
|
export interface SecretValuePattern {
|
|
8
10
|
readonly kind: string;
|
|
9
11
|
readonly re: RegExp;
|
|
12
|
+
/** Reject scanner-only matches that advance past a non-secret run. */
|
|
13
|
+
readonly acceptMatch?: (match: string) => boolean;
|
|
10
14
|
}
|
|
11
15
|
export declare const SECRET_VALUE_PATTERNS: readonly SecretValuePattern[];
|
|
12
16
|
/**
|
package/dist/internal/redact.js
CHANGED
|
@@ -91,7 +91,16 @@ export const SECRET_VALUE_PATTERNS = [
|
|
|
91
91
|
// postgres://admin:s3cret@db.internal:5432/prod → "postgres://admin:s3cret@"
|
|
92
92
|
// redis://:hunter2@cache.internal:6379/0 → "redis://:hunter2@" (empty user)
|
|
93
93
|
// mongodb://root:mongoPwd@mongo.internal:27017 → "mongodb://root:mongoPwd@"
|
|
94
|
-
|
|
94
|
+
// Issue #2428: requiring the credential suffix made a failed scheme run
|
|
95
|
+
// rescan at every word boundary (quadratic on "a-b-c-..."). Consume the
|
|
96
|
+
// whole run even without credentials; acceptMatch leaves those
|
|
97
|
+
// non-secret matches visible. This preserves the original word boundary
|
|
98
|
+
// and supports arbitrary scheme lengths without rescanning the run.
|
|
99
|
+
{
|
|
100
|
+
kind: "url-credentials",
|
|
101
|
+
re: /\b[a-z][a-z0-9+.-]*(?::\/\/(?:[^/\s@]*:[^/\s@]+)@)?/gi,
|
|
102
|
+
acceptMatch: (match) => match.endsWith("@"),
|
|
103
|
+
},
|
|
95
104
|
];
|
|
96
105
|
const REDACTED = "[REDACTED]";
|
|
97
106
|
/**
|
|
@@ -102,11 +111,11 @@ const REDACTED = "[REDACTED]";
|
|
|
102
111
|
const STRUCTURED_KINDS = new Set(["assignment", "api-key-header"]);
|
|
103
112
|
function redactString(value) {
|
|
104
113
|
let out = value;
|
|
105
|
-
for (const { kind, re } of SECRET_VALUE_PATTERNS) {
|
|
114
|
+
for (const { kind, re, acceptMatch } of SECRET_VALUE_PATTERNS) {
|
|
106
115
|
if (STRUCTURED_KINDS.has(kind))
|
|
107
116
|
continue;
|
|
108
117
|
re.lastIndex = 0;
|
|
109
|
-
out = out.replace(re, REDACTED);
|
|
118
|
+
out = out.replace(re, (match) => (acceptMatch && !acceptMatch(match) ? match : REDACTED));
|
|
110
119
|
}
|
|
111
120
|
return redactStructured(out);
|
|
112
121
|
}
|
|
@@ -28,13 +28,14 @@ function referencesVariable(kind, match) {
|
|
|
28
28
|
.startsWith("$"));
|
|
29
29
|
}
|
|
30
30
|
function rawSpans(text) {
|
|
31
|
-
return SECRET_VALUE_PATTERNS.flatMap(({ kind, re }) => {
|
|
31
|
+
return SECRET_VALUE_PATTERNS.flatMap(({ kind, re, acceptMatch }) => {
|
|
32
32
|
// A private copy: the shared /g regex carries lastIndex between callers.
|
|
33
33
|
const scan = new RegExp(re.source, re.flags);
|
|
34
34
|
const found = [];
|
|
35
35
|
for (let m = scan.exec(text); m !== null; m = scan.exec(text)) {
|
|
36
|
-
if (!referencesVariable(kind, m[0]))
|
|
36
|
+
if ((!acceptMatch || acceptMatch(m[0])) && !referencesVariable(kind, m[0])) {
|
|
37
37
|
found.push({ start: m.index, end: m.index + m[0].length, kind });
|
|
38
|
+
}
|
|
38
39
|
}
|
|
39
40
|
return found;
|
|
40
41
|
});
|
|
@@ -25,7 +25,11 @@ export function createTerminalAuthInteraction(options) {
|
|
|
25
25
|
if (prompt.type === "text")
|
|
26
26
|
return promptForLine(prompt.message, undefined, style);
|
|
27
27
|
if (prompt.type === "select") {
|
|
28
|
-
return promptForSelect(prompt.message, prompt.options.map((option) => ({
|
|
28
|
+
return promptForSelect(prompt.message, prompt.options.map((option) => ({
|
|
29
|
+
id: option.id,
|
|
30
|
+
label: option.label,
|
|
31
|
+
...(option.description !== undefined ? { description: option.description } : {}),
|
|
32
|
+
})), style);
|
|
29
33
|
}
|
|
30
34
|
return promptForSecret(prompt.message, style);
|
|
31
35
|
},
|
|
@@ -42,10 +42,15 @@ export declare function promptForLine(message: string, onEmptySubmit?: () => voi
|
|
|
42
42
|
export interface SelectChoice {
|
|
43
43
|
readonly id: string;
|
|
44
44
|
readonly label: string;
|
|
45
|
+
readonly description?: string;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
48
|
* Reads a choice with the arrow keys and returns the chosen option's id. The
|
|
48
|
-
* option block is redrawn in place
|
|
49
|
-
*
|
|
49
|
+
* option block is redrawn in place, so a value that is not an option can never
|
|
50
|
+
* be returned. Typed characters narrow the block rather than answering it: a
|
|
51
|
+
* catalog-sized list is scrolled by filtering, not printed whole, and the redraw
|
|
52
|
+
* moves up only by the rows it drew — a block taller than the terminal cannot be
|
|
53
|
+
* repainted, because the cursor-up cannot cross the scrollback. Enter with no
|
|
54
|
+
* match commits nothing.
|
|
50
55
|
*/
|
|
51
56
|
export declare function promptForSelect(message: string, choices: readonly SelectChoice[], style?: TerminalStyle): Promise<string>;
|
|
@@ -120,10 +120,95 @@ export function promptForSecret(message, style = PLAIN_STYLE) {
|
|
|
120
120
|
export function promptForLine(message, onEmptySubmit, style = PLAIN_STYLE) {
|
|
121
121
|
return read(message, true, onEmptySubmit, style);
|
|
122
122
|
}
|
|
123
|
+
/** Rows drawn at once. A longer list is narrowed by typing, never printed whole. */
|
|
124
|
+
const MAX_VISIBLE_ROWS = 12;
|
|
125
|
+
/** Case-insensitive label match; an empty filter matches everything. */
|
|
126
|
+
function matchedRows(choices, filter) {
|
|
127
|
+
const needle = filter.toLowerCase();
|
|
128
|
+
return needle === "" ? [...choices] : choices.filter((c) => c.label.toLowerCase().includes(needle));
|
|
129
|
+
}
|
|
130
|
+
/** The first row index the window must show so that `active` is visible. */
|
|
131
|
+
function windowStart(active, total) {
|
|
132
|
+
return Math.min(active, Math.max(0, total - MAX_VISIBLE_ROWS));
|
|
133
|
+
}
|
|
134
|
+
function drawSelectLine(run, text) {
|
|
135
|
+
_terminalPromptDeps.write(`\r\u001b[2K${text}\n`);
|
|
136
|
+
run.drawn += 1;
|
|
137
|
+
}
|
|
138
|
+
function drawSelectRow(run, choice, isActive) {
|
|
139
|
+
const marker = isActive ? run.style.accent(">") : " ";
|
|
140
|
+
const label = isActive ? run.style.accent(choice.label) : choice.label;
|
|
141
|
+
const note = isActive && choice.description !== undefined ? ` ${run.style.dim(choice.description)}` : "";
|
|
142
|
+
drawSelectLine(run, `${marker} ${label}${note}`);
|
|
143
|
+
}
|
|
144
|
+
function renderSelect(run) {
|
|
145
|
+
const rows = matchedRows(run.choices, run.filter);
|
|
146
|
+
if (run.active >= rows.length)
|
|
147
|
+
run.active = 0;
|
|
148
|
+
const start = windowStart(run.active, rows.length);
|
|
149
|
+
if (run.drawn > 0)
|
|
150
|
+
_terminalPromptDeps.write(`\u001b[${run.drawn}A`);
|
|
151
|
+
run.drawn = 0;
|
|
152
|
+
drawSelectLine(run, run.style.dim(run.filter));
|
|
153
|
+
for (const [i, choice] of rows.slice(start, start + MAX_VISIBLE_ROWS).entries()) {
|
|
154
|
+
drawSelectRow(run, choice, start + i === run.active);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function moveSelect(run, delta) {
|
|
158
|
+
const rows = matchedRows(run.choices, run.filter);
|
|
159
|
+
if (rows.length > 0)
|
|
160
|
+
run.active = (run.active + delta + rows.length) % rows.length;
|
|
161
|
+
renderSelect(run);
|
|
162
|
+
}
|
|
163
|
+
function cleanupSelect(run) {
|
|
164
|
+
if (run.settled)
|
|
165
|
+
return;
|
|
166
|
+
run.settled = true;
|
|
167
|
+
run.stdin.removeListener("data", run.onData);
|
|
168
|
+
run.stdin.removeListener("end", run.onEnd);
|
|
169
|
+
run.stdin.removeListener("error", run.onEnd);
|
|
170
|
+
run.stdin.setRawMode(false);
|
|
171
|
+
run.stdin.pause();
|
|
172
|
+
}
|
|
173
|
+
function endSelect(run) {
|
|
174
|
+
cleanupSelect(run);
|
|
175
|
+
run.reject();
|
|
176
|
+
}
|
|
177
|
+
function handleSelectKey(run, chunk) {
|
|
178
|
+
// Whole-chunk matching: an arrow key is a three-byte escape sequence.
|
|
179
|
+
if (chunk.includes(ETX) || chunk.includes(EOT)) {
|
|
180
|
+
endSelect(run);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (chunk.includes(ARROW_UP)) {
|
|
184
|
+
moveSelect(run, -1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (chunk.includes(ARROW_DOWN)) {
|
|
188
|
+
moveSelect(run, 1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (chunk.includes(CR) || chunk.includes(LF)) {
|
|
192
|
+
const rows = matchedRows(run.choices, run.filter);
|
|
193
|
+
if (rows.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
cleanupSelect(run);
|
|
196
|
+
// biome-ignore lint/style/noNonNullAssertion: active is held below rows.length by renderSelect and the guard above.
|
|
197
|
+
run.resolve(rows[run.active].id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
run.filter = chunk.includes(BACKSPACE) ? run.filter.slice(0, -1) : run.filter + chunk;
|
|
201
|
+
run.active = 0;
|
|
202
|
+
renderSelect(run);
|
|
203
|
+
}
|
|
123
204
|
/**
|
|
124
205
|
* Reads a choice with the arrow keys and returns the chosen option's id. The
|
|
125
|
-
* option block is redrawn in place
|
|
126
|
-
*
|
|
206
|
+
* option block is redrawn in place, so a value that is not an option can never
|
|
207
|
+
* be returned. Typed characters narrow the block rather than answering it: a
|
|
208
|
+
* catalog-sized list is scrolled by filtering, not printed whole, and the redraw
|
|
209
|
+
* moves up only by the rows it drew — a block taller than the terminal cannot be
|
|
210
|
+
* repainted, because the cursor-up cannot cross the scrollback. Enter with no
|
|
211
|
+
* match commits nothing.
|
|
127
212
|
*/
|
|
128
213
|
export function promptForSelect(message, choices, style = PLAIN_STYLE) {
|
|
129
214
|
const stdin = stdinOf();
|
|
@@ -134,63 +219,25 @@ export function promptForSelect(message, choices, style = PLAIN_STYLE) {
|
|
|
134
219
|
return Promise.reject(new PromptCancelledError());
|
|
135
220
|
_terminalPromptDeps.write(`${style.accent("?")} ${message}\n`);
|
|
136
221
|
return new Promise((resolve, reject) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
}
|
|
222
|
+
const run = {
|
|
223
|
+
choices,
|
|
224
|
+
style,
|
|
225
|
+
stdin,
|
|
226
|
+
resolve,
|
|
227
|
+
reject: () => reject(new PromptCancelledError()),
|
|
228
|
+
onData: (chunk) => handleSelectKey(run, chunk),
|
|
229
|
+
onEnd: () => endSelect(run),
|
|
230
|
+
filter: "",
|
|
231
|
+
active: 0,
|
|
232
|
+
drawn: 0,
|
|
233
|
+
settled: false,
|
|
187
234
|
};
|
|
188
235
|
stdin.setRawMode(true);
|
|
189
236
|
stdin.resume();
|
|
190
237
|
stdin.setEncoding("utf8");
|
|
191
|
-
stdin.on("data", onData);
|
|
192
|
-
stdin.once("end", onEnd);
|
|
193
|
-
stdin.once("error", onEnd);
|
|
194
|
-
|
|
238
|
+
stdin.on("data", run.onData);
|
|
239
|
+
stdin.once("end", run.onEnd);
|
|
240
|
+
stdin.once("error", run.onEnd);
|
|
241
|
+
renderSelect(run);
|
|
195
242
|
});
|
|
196
243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
50
|
-
"@nathapp/nax-ai": "0.
|
|
50
|
+
"@nathapp/nax-ai": "0.86.0",
|
|
51
51
|
"zod": "^4.3.6"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|