@nathapp/nax-agent 0.87.0 → 0.88.0-canary.1
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 +14 -0
- package/dist/coding-tools/coding-tool-support.d.ts +15 -1
- package/dist/coding-tools/coding-tool-support.js +19 -8
- package/dist/command-safety/rule-scorer.d.ts +1 -1
- package/dist/command-safety/rule-scorer.js +11 -113
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/native/errors.js +7 -0
- package/dist/native/session/session.d.ts +3 -0
- package/dist/native/session/session.js +7 -0
- package/dist/native/session/transcript-archive.d.ts +28 -0
- package/dist/native/session/transcript-archive.js +43 -0
- package/dist/session/session-types.d.ts +19 -0
- package/dist/tools/denial-redirect.d.ts +22 -7
- package/dist/tools/denial-redirect.js +29 -13
- package/dist/tools/git-flags/locate.d.ts +47 -0
- package/dist/tools/git-flags/locate.js +361 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/nax-dirs.d.ts +22 -0
- package/dist/tools/nax-dirs.js +89 -0
- package/dist/tools/nax-evasion.d.ts +19 -0
- package/dist/tools/nax-evasion.js +88 -0
- package/dist/tools/nax-revert.d.ts +34 -0
- package/dist/tools/nax-revert.js +233 -0
- package/dist/tools/nax-state-guard.d.ts +18 -0
- package/dist/tools/nax-state-guard.js +496 -0
- package/dist/tools/policy-bash-raw.d.ts +3 -0
- package/dist/tools/policy-bash-raw.js +40 -12
- package/dist/tools/policy-bash.d.ts +3 -0
- package/dist/tools/policy-bash.js +38 -9
- package/dist/tools/policy-command-branch.d.ts +3 -0
- package/dist/tools/policy-command-branch.js +3 -1
- package/dist/tools/policy.js +4 -0
- package/dist/tools/provider-advertise.d.ts +18 -0
- package/dist/tools/provider-advertise.js +16 -5
- package/dist/tools/tool-audit.d.ts +42 -0
- package/dist/tools/tool-audit.js +5 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
|
|
5
5
|
release may change the public API.
|
|
6
6
|
|
|
7
|
+
## [0.88.0-canary.1] - 2026-10-13
|
|
8
|
+
|
|
9
|
+
- Bash guard for nax run state (#2490): `git update-index --skip-worktree` / `--assume-unchanged` (and the `--no-` forms) on a `.nax` path, `git stash` / `push` / `pop` / `apply` in a repo that holds a `.nax/` directory (unless the pathspec excludes `.nax`), and `git checkout` / `restore` of a `.nax` path are now refused in every segment of a compound command, in gated and raw mode, also behind `git -C`/`-c`, env assignments, `env`/`command`/`nice`/`timeout`/`xargs` wrappers and `bash -c`/`eval`, plus a coarse text backstop for wrappers it cannot enumerate. `RULE_SET_VERSION` is now 4 (git global options, `-c alias.*` and more wrappers are resolved when locating the subcommand).
|
|
10
|
+
- Bash guard gaps closed (#2493): whole-tree reverts (`git reset --hard`, `git checkout .`, `git restore [--source=REV] .`, `:/`, `*`, the root, exclusion-only pathspecs) are refused unless a full `.nax` exclusion follows `--`; the `.nax` presence check also finds a `.nax/` up to three levels below the root (cached per compiled policy); `\git`, `GIT_WORK_TREE` / `GIT_DIR` / `GIT_CONFIG_*` assignments, `git config alias.X <verb>`, `!` aliases and aliases that begin with global options, unknown git global options (fail closed to the text backstop; `--attr-source` is now a known separate-value global), `python|node|perl|ruby -c|-e` code and verbs built by shell expansion are all read; `git checkout feature/.nax` (a branch name) is no longer a false positive. `RULE_SET_VERSION` is now 6 (`locateGit` reads `\git`, re-skips globals after an alias expansion, follows alias chains of any length and never expands an alias over a git builtin).
|
|
11
|
+
- Bash guard, final pass (#2493): a shell name or `-c` cluster anywhere in a segment triggers the quoted-string scan again (`if sh -c "..."`, `find -exec sh -c`, `su -c`, `setsid bash -c` were let through by round 2); `git clean` counts as a dry run only for an `n` before any `e` in a short cluster (`-fdenx` is `-e nx`, a real clean); `--literal-pathspecs` / `GIT_LITERAL_PATHSPECS` with an exclusion pathspec on checkout, restore, stash or clean is refused (it turns every exclusion into a literal path). The command guard stops here; detect-and-repair of nax run state is tracked in #2497.
|
|
12
|
+
- Bash guard, review round 2 (#2493): the covering exclusion must be the top-anchored `/**` form (real git: a glob without the `/**` matches only the `.nax` directory entry, not the files in it, and a relative glob is taken from the working directory); a `:(...)` pathspec with an `attr:` element, or whose magic is not exactly `exclude`, is no exclusion; `checkout-index`, `read-tree` and `clean` operands that name `.nax` are refused; long options are matched by any prefix git accepts (`--k`, `--f`, `--a`); the gated screen accepts the exact covering pathspec instead of rejecting its quoted `*`; the quoted-string scan covers only shells, interpreters and exec-style wrappers (`grep "git stash"`, `gh ... --body "..."`, `printf`, `sed` are no longer refused); `git clean -n` / `--dry-run` is allowed. Not covered, documented: git subcommands that execute a string (`rebase -x`, `submodule foreach`, `bisect run`, `difftool -x`, `fetch --upload-pack=`), executing environment variables (`GIT_EXTERNAL_DIFF`, `GIT_PAGER`, `GIT_EDITOR`, `GIT_SSH_COMMAND`), executing config keys beyond the listed ones (`remote.*.uploadpack`, `uploadpack.packObjectsHook`, `core.alternateRefsCommand`, `merge.*.driver`, `core.gitProxy`, `protocol.ext.allow` with `ext::` URLs), `deno eval` in raw mode, and commands built by string arithmetic in an interpreter.
|
|
13
|
+
- Bash guard, review round 1 (#2493): alias chains have no depth limit (a cycle is refused); backslash-built words (`g\it`, `st\ash`) are read; `restore --staged` is index-only only when every flag is known (`--wor`, `-SW` count as worktree); more interpreters and any quoted string next to a guarded verb are scanned; git config keys that execute commands (`core.fsmonitor`, `core.hooksPath`, `diff.external`, `include.path`, ...) are refused through `-c`, `--config-env` and `git config`; an alias over a git builtin is not expanded; the covering exclusion is now `':(top,exclude,glob)**/.nax/**'` (the literal `:!.nax` only counts while the root `.nax` is the only one); `checkout -f`, `switch -f`, `reset --keep|--merge`, `clean`, `checkout-index -a -f` and `read-tree -u --reset` are refused while a `.nax/` exists; an expansion head needs a `git` token in the segment. A `.nax/` below the root, for example a test fixture within three levels, makes `git stash` refused for the whole repository. The nested scan caches a found `.nax` but re-scans an empty result after one second.
|
|
14
|
+
- Native errors: a `quota` kind from `@nathapp/nax-ai` (usage-limit and token-plan 429s) now maps to
|
|
15
|
+
`fail-quota` instead of a transient rate limit.
|
|
16
|
+
|
|
17
|
+
## [0.87.1] - 2026-10-12
|
|
18
|
+
|
|
19
|
+
- No changes in this package. Released in lockstep with the other nax packages.
|
|
20
|
+
|
|
7
21
|
## [0.87.0] - 2026-10-11
|
|
8
22
|
|
|
9
23
|
- No changes in this package. Released in lockstep with the other nax packages.
|
|
@@ -14,12 +14,24 @@ import type { BashApprovalMode } from "#src/config/bash-approval";
|
|
|
14
14
|
import type { QualityCommandSpec } from "#src/internal/command-spec/index";
|
|
15
15
|
import type { AskResolver } from "#src/permissions/index";
|
|
16
16
|
import type { CommandLauncher } from "#src/sandbox/index";
|
|
17
|
-
import { type CodingTool, type CodingToolName, type CodingToolRuntime, type DeclaredCommandRunner, type ProtectedPathsPolicy, type ToolAuditSink, type ToolGrant, type ToolPatternNarrowing } from "#src/tools/index";
|
|
17
|
+
import { type CodingTool, type CodingToolName, type CodingToolRuntime, type DeclaredCommandRunner, type ProtectedPathsPolicy, type ToolAuditSink, type ToolGrant, type ToolPatternNarrowing, type ToolSurfaceDraft, type ToolSurfaceRecord } from "#src/tools/index";
|
|
18
18
|
import type { OwnedPathsPolicy } from "#src/tools/owned-paths";
|
|
19
19
|
export interface CodingToolSupport {
|
|
20
20
|
readonly runtime: CodingToolRuntime;
|
|
21
21
|
readonly tools: readonly CodingTool[];
|
|
22
22
|
readonly auditSink: ToolAuditSink;
|
|
23
|
+
/** The materialized declaration this support resolved from (nax#2444 D2) — set on every resolve. */
|
|
24
|
+
readonly declaredTools?: readonly CodingToolName[];
|
|
25
|
+
/** The session's MCP tool surface (nax#2410 D4), stamped into the audit file at flush. */
|
|
26
|
+
readonly toolSurface?: ToolSurfaceRecord;
|
|
27
|
+
/**
|
|
28
|
+
* The policy-filtered provider-tool delivery for an ACP session (nax#2444 D4),
|
|
29
|
+
* attached by the nax resolver; the ACP adapter adapts `tools` to EmbedderTools.
|
|
30
|
+
*/
|
|
31
|
+
readonly acpDelivery?: {
|
|
32
|
+
readonly tools: readonly CodingTool[];
|
|
33
|
+
readonly surface: ToolSurfaceDraft;
|
|
34
|
+
};
|
|
23
35
|
}
|
|
24
36
|
export declare function buildCodingToolSupport(args: {
|
|
25
37
|
root?: string;
|
|
@@ -76,6 +88,8 @@ export declare function buildCodingToolSupport(args: {
|
|
|
76
88
|
/** `quality.shell` — the shell the Bash tool spawns. Defaults to /bin/sh. */
|
|
77
89
|
shell?: string;
|
|
78
90
|
auditDir?: string;
|
|
91
|
+
/** Stamped into the audit file at flush (nax#2410 D4); absent ⇒ no surface block. */
|
|
92
|
+
toolSurface?: ToolSurfaceRecord;
|
|
79
93
|
sessionName?: string;
|
|
80
94
|
header?: import("../tools/tool-audit.ts").ToolAuditHeader;
|
|
81
95
|
callId?: string;
|
|
@@ -71,13 +71,7 @@ export function buildCodingToolSupport(args) {
|
|
|
71
71
|
// P4/S5: under `raw`, an UNAVAILABLE launcher refuses every Bash call.
|
|
72
72
|
// US-002: an AVAILABLE one marks the screen sandbox-wrapped, so a PRD it only reads is allowed.
|
|
73
73
|
const rawScreenOptions = rawScreenOptionsFor(args.launcher);
|
|
74
|
-
const sink = args
|
|
75
|
-
? createToolAuditSink({
|
|
76
|
-
dir: args.auditDir,
|
|
77
|
-
sessionName: args.sessionName ?? "unattached",
|
|
78
|
-
...(args.header ? { header: args.header } : {}),
|
|
79
|
-
})
|
|
80
|
-
: createNoOpToolAuditSink();
|
|
74
|
+
const sink = supportSink(args);
|
|
81
75
|
const runtime = createCodingToolRuntime({
|
|
82
76
|
policy: compileToolPolicy(effectiveGrants, args.root, {
|
|
83
77
|
bashApproval,
|
|
@@ -144,7 +138,24 @@ export function buildCodingToolSupport(args) {
|
|
|
144
138
|
}
|
|
145
139
|
if (tools.length === 0)
|
|
146
140
|
return undefined;
|
|
147
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
runtime,
|
|
143
|
+
tools,
|
|
144
|
+
auditSink: sink,
|
|
145
|
+
declaredTools: args.declared,
|
|
146
|
+
...(args.toolSurface !== undefined ? { toolSurface: args.toolSurface } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The audit sink a support resolution records through; no dir ⇒ a non-recording sink. */
|
|
150
|
+
function supportSink(args) {
|
|
151
|
+
if (args.auditDir === undefined)
|
|
152
|
+
return createNoOpToolAuditSink();
|
|
153
|
+
return createToolAuditSink({
|
|
154
|
+
dir: args.auditDir,
|
|
155
|
+
sessionName: args.sessionName ?? "unattached",
|
|
156
|
+
...(args.header !== undefined ? { header: args.header } : {}),
|
|
157
|
+
...(args.toolSurface !== undefined ? { toolSurface: args.toolSurface } : {}),
|
|
158
|
+
});
|
|
148
159
|
}
|
|
149
160
|
/**
|
|
150
161
|
* Ledger session name.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RuleResult } from "./types.ts";
|
|
2
|
-
export declare const RULE_SET_VERSION =
|
|
2
|
+
export declare const RULE_SET_VERSION = 6;
|
|
3
3
|
/** Context the scorer may use; every field is optional and absent means v1 behaviour. */
|
|
4
4
|
export interface RuleContext {
|
|
5
5
|
/** The project root the command runs against (the Bash call's cwd). */
|
|
@@ -27,14 +27,24 @@
|
|
|
27
27
|
* hit. The existing eleven regex families stay; the new patterns live in a
|
|
28
28
|
* per-segment pass.
|
|
29
29
|
*
|
|
30
|
+
* v5: `locateGit` (shared with the nax-state guard, #2493) reads a
|
|
31
|
+
* backslash-prefixed `\git`, runs the global-option skip again over an
|
|
32
|
+
* expanded alias, and follows alias chains, so a discard behind any of those
|
|
33
|
+
* is now located by the v3 per-segment pass.
|
|
34
|
+
*
|
|
35
|
+
* v6: `locateGit` never expands an alias that shadows a git builtin (git ignores it) and follows alias
|
|
36
|
+
* chains of any length (a cycle is reported, not looped on), so `git -c alias.checkout=status checkout -f`
|
|
37
|
+
* is the discard it really is.
|
|
38
|
+
*
|
|
30
39
|
* Frozen before the red-team corpus was written. Any pattern or behaviour
|
|
31
40
|
* change bumps RULE_SET_VERSION.
|
|
32
41
|
*/
|
|
33
42
|
import { posix } from "node:path";
|
|
34
43
|
import { errorMessage } from "#src/infra/errors";
|
|
35
44
|
import { lexBashCommand } from "#src/permissions/index";
|
|
45
|
+
import { locateGit } from "#src/tools/git-flags/locate";
|
|
36
46
|
import { QUESTION_IDS } from "./types.js";
|
|
37
|
-
export const RULE_SET_VERSION =
|
|
47
|
+
export const RULE_SET_VERSION = 6;
|
|
38
48
|
const RULES = {
|
|
39
49
|
deletes_data: [
|
|
40
50
|
/\brm\s+(?:\S+\s+)*?(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)\b/,
|
|
@@ -140,118 +150,6 @@ const FORCE_FLAGS = new Set(["-f", "--force", "--discard-changes"]);
|
|
|
140
150
|
const STAGED_FLAGS = new Set(["--staged", "-S"]);
|
|
141
151
|
/** True when `word` is a worktree-restore flag — its own pattern handles these. */
|
|
142
152
|
const WORKTREE_FLAGS = new Set(["--worktree", "-W"]);
|
|
143
|
-
/** `git`'s global options that take no value — pass-through, then look for
|
|
144
|
-
* the subcommand. */
|
|
145
|
-
const GIT_BOOLEAN_GLOBALS = new Set([
|
|
146
|
-
"--no-pager",
|
|
147
|
-
"--no-replace-objects",
|
|
148
|
-
"--bare",
|
|
149
|
-
"--literal-pathspecs",
|
|
150
|
-
"--glob-pathspecs",
|
|
151
|
-
"--noglob-pathspecs",
|
|
152
|
-
"--icase-pathspecs",
|
|
153
|
-
]);
|
|
154
|
-
/** `git`'s global options that take a separate value (the next token). */
|
|
155
|
-
const GIT_VALUE_GLOBALS = new Set(["-c", "-C"]);
|
|
156
|
-
/** `git`'s global long options that embed their value (`--key=value`). */
|
|
157
|
-
const GIT_KEY_VALUE_GLOBALS = new Set([
|
|
158
|
-
"--git-dir=",
|
|
159
|
-
"--exec-path=",
|
|
160
|
-
"--work-tree=",
|
|
161
|
-
"--namespace=",
|
|
162
|
-
"--super-prefix=",
|
|
163
|
-
]);
|
|
164
|
-
/** `sudo` options whose values occupy the next shell word. */
|
|
165
|
-
const SUDO_VALUE_OPTIONS = new Set([
|
|
166
|
-
"-u",
|
|
167
|
-
"-g",
|
|
168
|
-
"-h",
|
|
169
|
-
"-D",
|
|
170
|
-
"-C",
|
|
171
|
-
"-p",
|
|
172
|
-
"-r",
|
|
173
|
-
"-t",
|
|
174
|
-
"-U",
|
|
175
|
-
"--user",
|
|
176
|
-
"--group",
|
|
177
|
-
"--host",
|
|
178
|
-
"--chdir",
|
|
179
|
-
"--close-from",
|
|
180
|
-
"--prompt",
|
|
181
|
-
"--role",
|
|
182
|
-
"--type",
|
|
183
|
-
"--other-user",
|
|
184
|
-
]);
|
|
185
|
-
/** Skip a sudo/doas prefix, including options with a separate value. */
|
|
186
|
-
function afterPrivilegeWrapper(words) {
|
|
187
|
-
if (words[0] !== "sudo" && words[0] !== "doas")
|
|
188
|
-
return 0;
|
|
189
|
-
let i = 1;
|
|
190
|
-
while (words[i]?.startsWith("-")) {
|
|
191
|
-
const option = words[i++];
|
|
192
|
-
if (option === "--")
|
|
193
|
-
break;
|
|
194
|
-
if (option !== undefined && SUDO_VALUE_OPTIONS.has(option))
|
|
195
|
-
i += 1;
|
|
196
|
-
}
|
|
197
|
-
return i;
|
|
198
|
-
}
|
|
199
|
-
/** Skip an env prefix and its assignments and options. */
|
|
200
|
-
function afterEnvWrapper(words, start) {
|
|
201
|
-
if (words[start] !== "env")
|
|
202
|
-
return start;
|
|
203
|
-
let i = start + 1;
|
|
204
|
-
while (i < words.length) {
|
|
205
|
-
const word = words[i];
|
|
206
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word) || word === "-i" || word === "--ignore-environment") {
|
|
207
|
-
i += 1;
|
|
208
|
-
}
|
|
209
|
-
else if (word === "-u" || word === "--unset") {
|
|
210
|
-
i += 2;
|
|
211
|
-
}
|
|
212
|
-
else
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
return i;
|
|
216
|
-
}
|
|
217
|
-
/** Find `git` in the executable position after supported shell wrappers. */
|
|
218
|
-
function gitExecutableIndex(words) {
|
|
219
|
-
let i = afterEnvWrapper(words, afterPrivilegeWrapper(words));
|
|
220
|
-
if (words[i] === "command" || words[i] === "exec")
|
|
221
|
-
i += 1;
|
|
222
|
-
return words[i] === "git" ? i : undefined;
|
|
223
|
-
}
|
|
224
|
-
/** Locate executable `git` in `words` and advance past its global options. Returns the
|
|
225
|
-
subcommand and the index of the first argument (or `undefined` when `git`
|
|
226
|
-
isn't there or has no subcommand). Handles `sudo git`, `env VAR=x git`,
|
|
227
|
-
`git --no-pager checkout`, etc. */
|
|
228
|
-
function locateGit(words) {
|
|
229
|
-
const gitIdx = gitExecutableIndex(words);
|
|
230
|
-
if (gitIdx === undefined)
|
|
231
|
-
return undefined;
|
|
232
|
-
let i = gitIdx + 1;
|
|
233
|
-
while (i < words.length) {
|
|
234
|
-
const w = words[i];
|
|
235
|
-
if (GIT_BOOLEAN_GLOBALS.has(w)) {
|
|
236
|
-
i += 1;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (GIT_VALUE_GLOBALS.has(w)) {
|
|
240
|
-
// The value is a separate token; skip it.
|
|
241
|
-
i += 2;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
if ([...GIT_KEY_VALUE_GLOBALS].some((prefix) => w.startsWith(prefix))) {
|
|
245
|
-
i += 1;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
const subcommand = words[i];
|
|
251
|
-
if (subcommand === undefined)
|
|
252
|
-
return undefined;
|
|
253
|
-
return { subcommand, args: words.slice(i + 1) };
|
|
254
|
-
}
|
|
255
153
|
/** True when `token` is a short-flag bundle that includes `f` (so `-f`,
|
|
256
154
|
* `-fq`, `-fb`, `-fqH`, ...). Stops at `--` so a long flag like
|
|
257
155
|
* `--force-something` is not misread as a bundle. */
|
package/dist/index.d.ts
CHANGED
|
@@ -42,5 +42,5 @@ export { attachTurnSpend, type FailedTurnSpend, readTurnSpend } from "#src/sessi
|
|
|
42
42
|
export type { JSONSchema, ToolDescriptor, } from "#src/session/tool-descriptor";
|
|
43
43
|
export { createTurnDeadline, type TurnDeadline, } from "#src/session/turn-deadline";
|
|
44
44
|
export type { TurnEvent, TurnEventSink } from "#src/session/turn-event";
|
|
45
|
-
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
45
|
+
export { adaptProviderTool, advertisedSchemaBytes, applyModelTruncationPolicy, BASH_TIMEOUT_MS, BASH_TOOL_NAME, buildCommitArgvs, buildGitArgv, buildGrepArgv, type CodingTool, type CodingToolName, type CodingToolOutcome, type CodingToolRuntime, classifyExec, compileToolPolicy, createBashTool, createCodingToolRuntime, createNoOpToolAuditSink, createRunCommandTool, createToolAuditSink, cutBufferToByteCap, DEFAULT_BASH_SHELL, DEFAULT_LOG_FORMAT, DEFAULT_LOG_MAX_COUNT, DEFAULT_TOOL_MAX_BYTES, DEFAULT_TOOL_MAX_FILE_BYTES, type DeclaredCommandRequest, type DeclaredCommandResult, type DeclaredCommandRunner, deleteTool, EXEC_TOOL_NAME, type ExecTarget, editTool, expandMcpRuleGrants, expandProviderGrants, flushOpenToolAuditSinks, GIT_ESCAPE_FLAGS, GIT_READ_VERBS, getCodingTool, gitCommitTool, gitExcludePathspecsOf, gitIgnorePatternsOf, gitTool, globTool, grepTool, isKnownManager, listCodingTools, MAX_PROVIDER_DESCRIPTION_BYTES, MAX_PROVIDER_SCHEMA_BYTES, MCP_RULE_TOOL, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, type ModelTruncationOptions, mcpRuleAdmits, type NormalizeInput, type NormalizeResult, namespacedToolName, narrowGrants, normalizeExec, normalizeManagerBinary, type PolicyVerdict, PROVIDER_ID_RE, type ProtectedPathsPolicy, type ProviderGrantEntry, type ProviderKind, type ProviderTool, partitionMcpRules, partitionNaxOwnedPaths, providerAttachesTo, READ_CEILING, RESERVED_TOOL_NAMES, type ReadFileSliceOptions, type ReadFileSliceResult, type RegisteredSink, type ResolvedProviderTools, readFileSlice, readTool, registerBuiltinCodingTools, registerBuiltinTool, registerCodingTool, registerToolAuditSink, requestCapabilityTool, resolveProviderTools, resolveWithin, SCRATCHPAD_DIR, SPILL_DIR, type SpillRequest, sanitizeProviderTools, scratchpadListTool, scratchpadReadTool, scratchpadWriteTool, spillRelativePath, substituteCommand, substituteCommandSpec, type ToolAuditHeader, type ToolAuditSink, type ToolCallContext, type ToolCallRecord, type ToolGrant, type ToolPatternNarrowing, type ToolPolicy, type ToolPolicyOptions, type ToolProvider, type ToolResult, type ToolRunContext, type ToolScope, type ToolSurfaceDraft, type ToolSurfaceRecord, type TruncateForModelOptions, type TruncationDirection, type TruncationResult, truncateForModel, truncationDirectionFor, unregisterToolAuditSink, validateProviderId, writeSpill, writeTool, } from "#src/tools/index";
|
|
46
46
|
export { EMPTY_OWNED_PATHS_POLICY, type OwnedBashCandidate, type OwnedPathsPolicy, } from "#src/tools/owned-paths";
|
package/dist/internal.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export { _repositoryInstructionDeps } from "#src/native/session/repository-instr
|
|
|
69
69
|
export * from "#src/native/session/session";
|
|
70
70
|
export * as sessionModule from "#src/native/session/session";
|
|
71
71
|
export * from "#src/native/session/tool-mapping";
|
|
72
|
+
export { _transcriptArchiveDeps } from "#src/native/session/transcript-archive";
|
|
72
73
|
export * from "#src/native/session/transcript-identity";
|
|
73
74
|
export * from "#src/native/session/transcript-store";
|
|
74
75
|
export * as transcriptStoreModule from "#src/native/session/transcript-store";
|
package/dist/internal.js
CHANGED
|
@@ -69,6 +69,7 @@ export { _repositoryInstructionDeps } from "#src/native/session/repository-instr
|
|
|
69
69
|
export * from "#src/native/session/session";
|
|
70
70
|
export * as sessionModule from "#src/native/session/session";
|
|
71
71
|
export * from "#src/native/session/tool-mapping";
|
|
72
|
+
export { _transcriptArchiveDeps } from "#src/native/session/transcript-archive";
|
|
72
73
|
export * from "#src/native/session/transcript-identity";
|
|
73
74
|
export * from "#src/native/session/transcript-store";
|
|
74
75
|
export * as transcriptStoreModule from "#src/native/session/transcript-store";
|
package/dist/native/errors.js
CHANGED
|
@@ -17,6 +17,13 @@ const FAILURES = Object.freeze({
|
|
|
17
17
|
outcome: "fail-rate-limit",
|
|
18
18
|
retriable: true,
|
|
19
19
|
},
|
|
20
|
+
// The account or plan is exhausted: waiting does not help, so not retriable.
|
|
21
|
+
quota: {
|
|
22
|
+
message: "nax-ai account or plan quota exhausted",
|
|
23
|
+
category: "availability",
|
|
24
|
+
outcome: "fail-quota",
|
|
25
|
+
retriable: false,
|
|
26
|
+
},
|
|
20
27
|
auth: {
|
|
21
28
|
message: "nax-ai authentication failed",
|
|
22
29
|
category: "availability",
|
|
@@ -11,6 +11,7 @@ import type { AgentStreamEvent } from "#src/session/agent-stream-events";
|
|
|
11
11
|
import type { OpenSessionOpts, SessionHandle } from "#src/session/session-types";
|
|
12
12
|
import type { ResolvedCompaction } from "./compaction.ts";
|
|
13
13
|
import type { RepositoryInstructions } from "./repository-instructions.ts";
|
|
14
|
+
import { type TranscriptArchive } from "./transcript-archive.ts";
|
|
14
15
|
import type { TranscriptStore } from "./transcript-types.ts";
|
|
15
16
|
import type { TurnRetryConfig } from "./turn-retry.ts";
|
|
16
17
|
/** The runtime hooks handed to openSession. */
|
|
@@ -22,6 +23,8 @@ export interface NativeSessionStreamHooks {
|
|
|
22
23
|
export interface SessionTranscript {
|
|
23
24
|
readonly store: TranscriptStore;
|
|
24
25
|
readonly retainOnClose: boolean;
|
|
26
|
+
/** When set, close writes a redacted copy here before deleting or retaining the live document (nax#2453). */
|
|
27
|
+
readonly transcriptArchive?: TranscriptArchive;
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* The per-owner state behind the native session lifecycle.
|
|
@@ -13,6 +13,7 @@ import { nativeSessionId } from "../session-affinity.js";
|
|
|
13
13
|
import { instructionFileNameFor } from "./instruction-file-name.js";
|
|
14
14
|
import { openRepositoryInstructions } from "./instruction-session.js";
|
|
15
15
|
import { instructionTranscriptStore } from "./instruction-transcript.js";
|
|
16
|
+
import { archiveTranscript } from "./transcript-archive.js";
|
|
16
17
|
import { createFileTranscriptStore } from "./transcript-store.js";
|
|
17
18
|
export function createNativeSessionState() {
|
|
18
19
|
return {
|
|
@@ -106,6 +107,7 @@ export async function openNativeSession(state, name, opts) {
|
|
|
106
107
|
state.transcripts.set(name, {
|
|
107
108
|
store,
|
|
108
109
|
retainOnClose: opts.retainOnClose === true,
|
|
110
|
+
transcriptArchive: opts.transcriptArchive,
|
|
109
111
|
});
|
|
110
112
|
state.timeouts.set(name, opts.timeoutSeconds);
|
|
111
113
|
state.scratchpadRoots.set(name, opts.workdir);
|
|
@@ -123,6 +125,7 @@ export async function openNativeSession(state, name, opts) {
|
|
|
123
125
|
state.transcripts.set(name, {
|
|
124
126
|
store: instructionTranscriptStore(store, instructions),
|
|
125
127
|
retainOnClose: opts.retainOnClose === true,
|
|
128
|
+
transcriptArchive: opts.transcriptArchive,
|
|
126
129
|
});
|
|
127
130
|
// `resume` is SessionManager's "this name already has a descriptor in this
|
|
128
131
|
// process" signal, and it had no consumer on this transport (nax#1877) — a
|
|
@@ -199,6 +202,10 @@ export async function closeNativeSession(state, handle, failed) {
|
|
|
199
202
|
const treatAsFailed = failed ?? state.failed.has(handle.id);
|
|
200
203
|
try {
|
|
201
204
|
if (transcript !== undefined && !transcript.retainOnClose) {
|
|
205
|
+
// Archive first (nax#2453): best-effort, never throws, so the branch below is unchanged.
|
|
206
|
+
if (transcript.transcriptArchive !== undefined) {
|
|
207
|
+
await archiveTranscript(transcript.store, handle.id, transcript.transcriptArchive);
|
|
208
|
+
}
|
|
202
209
|
// Retain for a human, out of reach of the next session of this name.
|
|
203
210
|
if (treatAsFailed)
|
|
204
211
|
await transcript.store.retainFailed(handle.id);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit archive for native session transcripts (nax#2453).
|
|
3
|
+
*
|
|
4
|
+
* `closeNativeSession` deletes a clean session's transcript and renames a failed
|
|
5
|
+
* one, so a passing session, which is where most tokens go, left no turn-level
|
|
6
|
+
* record. When the caller opts in, this writes a redacted copy into an archive
|
|
7
|
+
* directory first. The archive file name contains `.transcript.` and ends in
|
|
8
|
+
* `.json`, so `pruneRetainedTranscripts` already counts it; it never sits in the
|
|
9
|
+
* live `sessions/` directory, so it can never be loaded as a resumption source
|
|
10
|
+
* (nax#1877).
|
|
11
|
+
*/
|
|
12
|
+
import type { TranscriptStore } from "./transcript-types.ts";
|
|
13
|
+
/** Injectable clock for the archive stamp, so tests order archives without sleeping. */
|
|
14
|
+
export declare const _transcriptArchiveDeps: {
|
|
15
|
+
now: () => Date;
|
|
16
|
+
};
|
|
17
|
+
/** Where, and how many, archived transcripts are kept (the `OpenSessionOpts.transcriptArchive` shape). */
|
|
18
|
+
export interface TranscriptArchive {
|
|
19
|
+
readonly dir: string;
|
|
20
|
+
readonly maxRetained: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Write a redacted copy of the session's transcript into `archive.dir`, then
|
|
24
|
+
* prune the directory to `archive.maxRetained`. Nothing to archive (no saved
|
|
25
|
+
* document) is a quiet return. Every error is logged and swallowed: the archive
|
|
26
|
+
* is supplementary and must never change how a session closes.
|
|
27
|
+
*/
|
|
28
|
+
export declare function archiveTranscript(store: TranscriptStore, sessionId: string, archive: TranscriptArchive): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit archive for native session transcripts (nax#2453).
|
|
3
|
+
*
|
|
4
|
+
* `closeNativeSession` deletes a clean session's transcript and renames a failed
|
|
5
|
+
* one, so a passing session, which is where most tokens go, left no turn-level
|
|
6
|
+
* record. When the caller opts in, this writes a redacted copy into an archive
|
|
7
|
+
* directory first. The archive file name contains `.transcript.` and ends in
|
|
8
|
+
* `.json`, so `pruneRetainedTranscripts` already counts it; it never sits in the
|
|
9
|
+
* live `sessions/` directory, so it can never be loaded as a resumption source
|
|
10
|
+
* (nax#1877).
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getLogger } from "#src/infra/index";
|
|
15
|
+
import { writeFileAtomic } from "#src/internal/atomic-write";
|
|
16
|
+
import { redactSecrets } from "#src/internal/redact";
|
|
17
|
+
import { pruneRetainedTranscripts } from "./transcript-store.js";
|
|
18
|
+
/** Injectable clock for the archive stamp, so tests order archives without sleeping. */
|
|
19
|
+
export const _transcriptArchiveDeps = { now: () => new Date() };
|
|
20
|
+
/**
|
|
21
|
+
* Write a redacted copy of the session's transcript into `archive.dir`, then
|
|
22
|
+
* prune the directory to `archive.maxRetained`. Nothing to archive (no saved
|
|
23
|
+
* document) is a quiet return. Every error is logged and swallowed: the archive
|
|
24
|
+
* is supplementary and must never change how a session closes.
|
|
25
|
+
*/
|
|
26
|
+
export async function archiveTranscript(store, sessionId, archive) {
|
|
27
|
+
try {
|
|
28
|
+
const doc = await store.load(sessionId);
|
|
29
|
+
if (doc === null)
|
|
30
|
+
return;
|
|
31
|
+
const stamp = _transcriptArchiveDeps.now().toISOString().replace(/[:.]/g, "-");
|
|
32
|
+
await mkdir(archive.dir, { recursive: true });
|
|
33
|
+
await writeFileAtomic(join(archive.dir, `${sessionId}.transcript.${stamp}.json`), JSON.stringify(redactSecrets(doc), null, 2));
|
|
34
|
+
await pruneRetainedTranscripts(archive.dir, archive.maxRetained);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
getLogger().warn("native-session", "Transcript archive failed; close continues unchanged", {
|
|
38
|
+
sessionId,
|
|
39
|
+
dir: archive.dir,
|
|
40
|
+
error: err instanceof Error ? err.message : String(err),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -146,6 +146,17 @@ export interface OpenSessionOpts extends TrackedSpawnDeadlineOptions {
|
|
|
146
146
|
readonly dir: string;
|
|
147
147
|
readonly header: import("#src/tools/tool-audit").ToolAuditHeader;
|
|
148
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* ACP delivery (nax#2444, D3): provider tools already policy-filtered at the
|
|
151
|
+
* hop (deny/ask-ruled tools withheld), adapted by the ACP backend only when
|
|
152
|
+
* its registry entry declares a pre-approval channel. Native ignores it —
|
|
153
|
+
* provider tools ride `codingTools` on the turn.
|
|
154
|
+
*/
|
|
155
|
+
providerTools?: readonly import("#src/tools/registry").CodingTool[];
|
|
156
|
+
/** ACP: the op's materialized declaration, which narrows the session profile (nax#2444, D2). */
|
|
157
|
+
declaredTools?: readonly string[];
|
|
158
|
+
/** ACP: the transport-less surface draft; the adapter stamps transport + profile (nax#2410, D4). */
|
|
159
|
+
toolSurface?: import("#src/tools/tool-audit").ToolSurfaceDraft;
|
|
149
160
|
/**
|
|
150
161
|
* Native: identity that owns this session's transcript — the op invocation's
|
|
151
162
|
* `scopeId ?? callId`. Stable across the retries and hops of one invocation,
|
|
@@ -169,6 +180,14 @@ export interface OpenSessionOpts extends TrackedSpawnDeadlineOptions {
|
|
|
169
180
|
* that close failed.
|
|
170
181
|
*/
|
|
171
182
|
retainOnClose?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Native: when set, closeNativeSession writes a redacted copy of the transcript
|
|
185
|
+
* here before deleting or retaining it. ACP ignores it.
|
|
186
|
+
*/
|
|
187
|
+
transcriptArchive?: {
|
|
188
|
+
readonly dir: string;
|
|
189
|
+
readonly maxRetained: number;
|
|
190
|
+
};
|
|
172
191
|
/**
|
|
173
192
|
* Native: the session's system prompt, sent as the request's top-level
|
|
174
193
|
* `system` on every round trip (not on the compaction summary). The S3
|
|
@@ -19,16 +19,31 @@
|
|
|
19
19
|
* `grep -n x y` did not. 20 of 34 denials in the two runs after #1971 carried
|
|
20
20
|
* no redirect (nax#1999). A single table cannot drift against itself.
|
|
21
21
|
*/
|
|
22
|
-
export declare
|
|
22
|
+
export declare const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written by the harness during this run. It is not part of your diff and must not be reverted.";
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* The sentence explaining a revert of nax's own run state, or undefined.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT an `Intent`: `render()` wraps every hit as "this session
|
|
27
|
+
* already has `X`", which is false here -- no tool reverts `.nax/`, and the
|
|
28
|
+
* module's header forbids naming one it does not hold. So this predicate is
|
|
29
|
+
* checked BEFORE `intentFor` in both entry points and returns its own sentence.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* Verb scope is `checkout` and `restore` only; `git stash` has no operands to
|
|
32
|
+
* gate on and is handled by `nax-state-guard.ts` instead. The gate is a `.nax`
|
|
33
|
+
* path SEGMENT (split on `/`), never a substring, so `src/nax-helpers.ts` and
|
|
34
|
+
* `docs/.naxignore` stay unanswered. `--` is a separator, not an operand, and a
|
|
35
|
+
* `timeout N` wrapper is stripped the same way `intentFor` strips it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function naxOwnedRunStateExplanation(tokens: readonly string[]): string | undefined;
|
|
38
|
+
/** A `timeout N ...` prefix wraps the real command; look past it. */
|
|
39
|
+
export declare function withoutTimeoutPrefix(tokens: readonly string[]): readonly string[];
|
|
40
|
+
export declare function redirectForArgv(argv: readonly string[], available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The crude segment split shared by the redirect and the raw screen's fallback
|
|
43
|
+
* for a command the lexer refused. Deliberately not the lexer: it must still
|
|
44
|
+
* produce segments when the lexer returns nothing to read.
|
|
31
45
|
*/
|
|
46
|
+
export declare function crudeCommandSegments(command: string): string[][];
|
|
32
47
|
export declare function redirectForCommand(command: string, available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
|
|
33
48
|
/**
|
|
34
49
|
* Name the tool that serves the intent behind a denied VERB call.
|
|
@@ -32,7 +32,7 @@ const GIT_COMMIT_VERBS = new Set(["add", "commit"]);
|
|
|
32
32
|
* intent, so the refusal teaches instead of naming one (nax#2007).
|
|
33
33
|
*/
|
|
34
34
|
const GIT_REVERT_VERBS = new Set(["checkout", "restore"]);
|
|
35
|
-
const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written by the harness during this run. It is not part of your diff and must not be reverted.";
|
|
35
|
+
export const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written by the harness during this run. It is not part of your diff and must not be reverted.";
|
|
36
36
|
/**
|
|
37
37
|
* The sentence explaining a revert of nax's own run state, or undefined.
|
|
38
38
|
*
|
|
@@ -41,13 +41,13 @@ const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written
|
|
|
41
41
|
* module's header forbids naming one it does not hold. So this predicate is
|
|
42
42
|
* checked BEFORE `intentFor` in both entry points and returns its own sentence.
|
|
43
43
|
*
|
|
44
|
-
* Verb scope is `checkout` and `restore` only; `git stash`
|
|
45
|
-
*
|
|
44
|
+
* Verb scope is `checkout` and `restore` only; `git stash` has no operands to
|
|
45
|
+
* gate on and is handled by `nax-state-guard.ts` instead. The gate is a `.nax`
|
|
46
46
|
* path SEGMENT (split on `/`), never a substring, so `src/nax-helpers.ts` and
|
|
47
47
|
* `docs/.naxignore` stay unanswered. `--` is a separator, not an operand, and a
|
|
48
48
|
* `timeout N` wrapper is stripped the same way `intentFor` strips it.
|
|
49
49
|
*/
|
|
50
|
-
function naxOwnedRunStateExplanation(tokens) {
|
|
50
|
+
export function naxOwnedRunStateExplanation(tokens) {
|
|
51
51
|
const av = withoutTimeoutPrefix(tokens);
|
|
52
52
|
const hasGitPrefix = av[0] === "git";
|
|
53
53
|
const verb = hasGitPrefix ? av[1] : av[0];
|
|
@@ -142,7 +142,7 @@ function isSingleFileDelete(tokens, from) {
|
|
|
142
142
|
return tokens.length === from + 1 && !tokens[from]?.startsWith("-");
|
|
143
143
|
}
|
|
144
144
|
/** A `timeout N ...` prefix wraps the real command; look past it. */
|
|
145
|
-
function withoutTimeoutPrefix(tokens) {
|
|
145
|
+
export function withoutTimeoutPrefix(tokens) {
|
|
146
146
|
return tokens[0] === "timeout" ? tokens.slice(2) : tokens;
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
@@ -211,23 +211,39 @@ export function redirectForArgv(argv, available, declaredCommands) {
|
|
|
211
211
|
/**
|
|
212
212
|
* Name the tool that serves the intent behind a denied Bash COMMAND.
|
|
213
213
|
*
|
|
214
|
-
*
|
|
215
|
-
* later segment's intent is not what to teach when the call
|
|
214
|
+
* The intent redirect reads the first segment only: that is what the model
|
|
215
|
+
* reached for, and a later segment's intent is not what to teach when the call
|
|
216
|
+
* is refused. The
|
|
216
217
|
* split is deliberately its own crude one rather than the lexer's
|
|
217
218
|
* (`lexBashCommand`): a command REFUSED for an unanalysable construct must
|
|
218
219
|
* still get a redirect, and the lexer returns nothing to read in that case.
|
|
219
220
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const tokens = first
|
|
221
|
+
function tokenize(segment) {
|
|
222
|
+
return segment
|
|
223
223
|
.trim()
|
|
224
224
|
.split(/\s+/)
|
|
225
225
|
.filter((token) => token.length > 0);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The crude segment split shared by the redirect and the raw screen's fallback
|
|
229
|
+
* for a command the lexer refused. Deliberately not the lexer: it must still
|
|
230
|
+
* produce segments when the lexer returns nothing to read.
|
|
231
|
+
*/
|
|
232
|
+
export function crudeCommandSegments(command) {
|
|
233
|
+
return command.split(/&&|\|\||;|\||\n/).map(tokenize);
|
|
234
|
+
}
|
|
235
|
+
export function redirectForCommand(command, available, declaredCommands) {
|
|
236
|
+
const segments = crudeCommandSegments(command);
|
|
237
|
+
// A revert of nax's run state is explained wherever it sits in a compound
|
|
238
|
+
// command (#2490); only the intent redirect below is first-segment-only.
|
|
239
|
+
for (const segment of segments) {
|
|
240
|
+
const ownedRunState = naxOwnedRunStateExplanation(segment);
|
|
241
|
+
if (ownedRunState !== undefined)
|
|
242
|
+
return ownedRunState;
|
|
243
|
+
}
|
|
244
|
+
const tokens = segments[0] ?? [];
|
|
226
245
|
if (tokens.length === 0)
|
|
227
246
|
return undefined;
|
|
228
|
-
const ownedRunState = naxOwnedRunStateExplanation(tokens);
|
|
229
|
-
if (ownedRunState !== undefined)
|
|
230
|
-
return ownedRunState;
|
|
231
247
|
const hit = intentFor(tokens);
|
|
232
248
|
if (hit === undefined)
|
|
233
249
|
return taskRunnerFallback(tokens, available, declaredCommands);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locating `git` and its subcommand inside a command line.
|
|
3
|
+
*
|
|
4
|
+
* A leaf module with NO imports, like `./index.ts`, so both `command-safety`
|
|
5
|
+
* and `tools` can use it without a cycle. Shared by the command-safety scorer
|
|
6
|
+
* and the Bash guard for nax's run state (#2490), so a wrapper or global
|
|
7
|
+
* option that hides `git` from one cannot hide it from the other.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Index of the word that actually names the command, after leading `VAR=x`
|
|
11
|
+
* assignments and the wrappers that pass through to it (`sudo`, `env`,
|
|
12
|
+
* `command`, `exec`, `nice`, `timeout`, `xargs`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function commandHeadIndex(words: readonly string[]): number;
|
|
15
|
+
/** Find `git` in the executable position after supported wrappers. */
|
|
16
|
+
export declare function gitExecutableIndex(words: readonly string[]): number | undefined;
|
|
17
|
+
export interface LocatedGit {
|
|
18
|
+
readonly subcommand: string;
|
|
19
|
+
readonly args: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Values of `-C`, `--work-tree` and `--git-dir` (both `--key value` and
|
|
22
|
+
* `--key=value`) and of leading `GIT_WORK_TREE=` / `GIT_DIR=` assignments, in order.
|
|
23
|
+
*/
|
|
24
|
+
readonly dirs: readonly string[];
|
|
25
|
+
/** True when an alias is defined through `--config-env=alias.*`: its value lives in the environment and cannot be read. */
|
|
26
|
+
readonly unresolvedAlias: boolean;
|
|
27
|
+
/** True when a global option this reader does not know sat before the subcommand: the subcommand found may be that option's value. */
|
|
28
|
+
readonly uncertain: boolean;
|
|
29
|
+
/** The shell text a `!` alias runs (its value without the `!`, then the call's remaining arguments), or undefined. */
|
|
30
|
+
readonly shellAlias: string | undefined;
|
|
31
|
+
/** Lower-cased keys set by `-c key=value` and `--config-env=key=VAR`, in order. */
|
|
32
|
+
readonly configKeys: readonly string[];
|
|
33
|
+
/** True when a leading assignment sets a `GIT_CONFIG*` variable (config, including aliases, injected through the environment). */
|
|
34
|
+
readonly configEnv: boolean;
|
|
35
|
+
/** True under `--literal-pathspecs` or a non-empty, non-`0` `GIT_LITERAL_PATHSPECS`: every `:(...)` / `:!` exclusion is a literal path. */
|
|
36
|
+
readonly literalPathspecs: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Locate executable `git` in `words` and advance past its global options.
|
|
40
|
+
* Returns the subcommand (alias-expanded), its arguments, and the directories
|
|
41
|
+
* the global options point git at, or `undefined` when `git` isn't there or has
|
|
42
|
+
* no subcommand. Handles `sudo git`, `env VAR=x git`, a backslash-prefixed
|
|
43
|
+
* `git`, `git --no-pager checkout`, and an UNKNOWN `-x` / `--xxx` global
|
|
44
|
+
* (skipped as one word and flagged `uncertain`, so a new git option cannot
|
|
45
|
+
* hide the subcommand silently).
|
|
46
|
+
*/
|
|
47
|
+
export declare function locateGit(words: readonly string[]): LocatedGit | undefined;
|