@nathapp/nax-agent 0.1.0 → 0.2.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 (113) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +47 -2
  3. package/dist/coding-tools/coding-tool-sandbox.d.ts +3 -0
  4. package/dist/coding-tools/coding-tool-sandbox.js +14 -3
  5. package/dist/coding-tools/coding-tool-support.d.ts +3 -0
  6. package/dist/coding-tools/coding-tool-support.js +2 -0
  7. package/dist/index.d.ts +10 -1
  8. package/dist/index.js +4 -1
  9. package/dist/internal/redact.d.ts +23 -0
  10. package/dist/internal/redact.js +60 -0
  11. package/dist/internal.d.ts +7 -0
  12. package/dist/internal.js +8 -0
  13. package/dist/native/adapter-deps.d.ts +3 -3
  14. package/dist/native/adapter-deps.js +3 -3
  15. package/dist/native/client.d.ts +7 -1
  16. package/dist/native/client.js +1 -1
  17. package/dist/native/credentials/change-guard.d.ts +8 -1
  18. package/dist/native/credentials/change-guard.js +1 -1
  19. package/dist/native/credentials/fingerprint.d.ts +4 -1
  20. package/dist/native/credentials/fingerprint.js +6 -3
  21. package/dist/native/credentials/session-source.d.ts +12 -0
  22. package/dist/native/credentials/session-source.js +33 -0
  23. package/dist/native/index.d.ts +3 -1
  24. package/dist/native/index.js +2 -1
  25. package/dist/native/session/loop-events/types.d.ts +1 -1
  26. package/dist/native/session/loop-handlers.d.ts +3 -0
  27. package/dist/native/session/loop-handlers.js +1 -1
  28. package/dist/native/session/memory-transcript-store.d.ts +12 -0
  29. package/dist/native/session/memory-transcript-store.js +36 -0
  30. package/dist/native/session/session.d.ts +138 -96
  31. package/dist/native/session/session.js +107 -127
  32. package/dist/native/session/transcript-identity.d.ts +39 -0
  33. package/dist/native/session/transcript-identity.js +81 -0
  34. package/dist/native/session/transcript-store.d.ts +22 -21
  35. package/dist/native/session/transcript-store.js +59 -68
  36. package/dist/native/session/transcript-types.d.ts +38 -0
  37. package/dist/native/session/transcript-types.js +7 -0
  38. package/dist/native/session/truncation-handler.d.ts +4 -3
  39. package/dist/native/session/truncation-handler.js +7 -10
  40. package/dist/native/session/turn-complete-step.d.ts +4 -1
  41. package/dist/native/session/turn-complete-step.js +11 -4
  42. package/dist/native/session/turn-event-emitter.d.ts +48 -0
  43. package/dist/native/session/turn-event-emitter.js +134 -0
  44. package/dist/native/session/turn-loop-round-trip.d.ts +5 -2
  45. package/dist/native/session/turn-loop-round-trip.js +8 -4
  46. package/dist/native/session/turn-loop.js +27 -13
  47. package/dist/native/session/turn-result.d.ts +1 -1
  48. package/dist/native/session/turn-result.js +1 -1
  49. package/dist/native/session/turn-tool-batch.d.ts +3 -0
  50. package/dist/native/session/turn-tool-batch.js +14 -6
  51. package/dist/native/session/turn-types.d.ts +17 -1
  52. package/dist/native/session-adapter.d.ts +22 -4
  53. package/dist/native/session-adapter.js +79 -35
  54. package/dist/native/stream-complete.d.ts +17 -0
  55. package/dist/native/stream-complete.js +40 -0
  56. package/dist/permissions/approvals-link.js +5 -4
  57. package/dist/sandbox/policy-builder.d.ts +6 -1
  58. package/dist/sandbox/policy-builder.js +18 -14
  59. package/dist/session/agent-session-deps.d.ts +12 -0
  60. package/dist/session/agent-session-deps.js +28 -0
  61. package/dist/session/agent-session-errors.d.ts +11 -0
  62. package/dist/session/agent-session-errors.js +12 -0
  63. package/dist/session/agent-session-options.d.ts +16 -0
  64. package/dist/session/agent-session-options.js +137 -0
  65. package/dist/session/agent-session-resume.d.ts +12 -0
  66. package/dist/session/agent-session-resume.js +47 -0
  67. package/dist/session/agent-session-turn.d.ts +45 -0
  68. package/dist/session/agent-session-turn.js +179 -0
  69. package/dist/session/agent-session-types.d.ts +182 -0
  70. package/dist/session/agent-session-types.js +1 -0
  71. package/dist/session/agent-session.d.ts +8 -0
  72. package/dist/session/agent-session.js +223 -0
  73. package/dist/session/pending-asks.d.ts +24 -0
  74. package/dist/session/pending-asks.js +77 -0
  75. package/dist/session/session-ask-link.d.ts +26 -0
  76. package/dist/session/session-ask-link.js +52 -0
  77. package/dist/session/session-event-channel.d.ts +17 -0
  78. package/dist/session/session-event-channel.js +132 -0
  79. package/dist/session/session-interaction.d.ts +20 -0
  80. package/dist/session/session-interaction.js +164 -0
  81. package/dist/session/session-tool-support.d.ts +48 -0
  82. package/dist/session/session-tool-support.js +101 -0
  83. package/dist/session/session-types.d.ts +31 -2
  84. package/dist/session/turn-event.d.ts +61 -0
  85. package/dist/session/turn-event.js +1 -0
  86. package/dist/tools/bash-cwd.d.ts +3 -3
  87. package/dist/tools/credential-read-deny.d.ts +22 -0
  88. package/dist/tools/credential-read-deny.js +56 -0
  89. package/dist/tools/glob.js +9 -2
  90. package/dist/tools/grep.js +55 -26
  91. package/dist/tools/owned-paths.d.ts +43 -0
  92. package/dist/tools/owned-paths.js +17 -0
  93. package/dist/tools/package-managers.d.ts +3 -0
  94. package/dist/tools/package-managers.js +7 -3
  95. package/dist/tools/policy-bash-raw.d.ts +3 -0
  96. package/dist/tools/policy-bash-raw.js +49 -62
  97. package/dist/tools/policy-command-branch.d.ts +3 -0
  98. package/dist/tools/policy-command-branch.js +2 -1
  99. package/dist/tools/policy-paths-branch.d.ts +14 -8
  100. package/dist/tools/policy-paths-branch.js +20 -19
  101. package/dist/tools/policy.d.ts +10 -7
  102. package/dist/tools/policy.js +9 -7
  103. package/dist/tools/protected-paths.d.ts +6 -6
  104. package/dist/tools/read.js +38 -20
  105. package/dist/tools/registry.d.ts +3 -0
  106. package/dist/tools/run-command-exec.js +1 -0
  107. package/dist/tools/runtime.d.ts +3 -0
  108. package/dist/tools/runtime.js +1 -0
  109. package/dist/tools/scratchpad.js +2 -1
  110. package/dist/tools/types.d.ts +2 -2
  111. package/package.json +2 -2
  112. package/dist/tools/nax-owned-writes.d.ts +0 -135
  113. package/dist/tools/nax-owned-writes.js +0 -259
@@ -1,259 +0,0 @@
1
- /**
2
- * Which paths nax refuses to let an agent touch, and to which tools.
3
- *
4
- * A separate file rather than an addition to src/tools/policy.ts, for the same
5
- * reason src/tools/deny-paths.ts is one: that file carries the containment
6
- * seam and sits at the project's file-size ratchet, while this is a narrower
7
- * concern -- it has nothing to do with resolving or containing a path, only
8
- * with refusing one containment would otherwise allow.
9
- *
10
- * Segment-exact, never a prefix or substring match: `.naxignore`,
11
- * `docs/nax/config.json` and `.nax/mono/api/notes.md` are ordinary paths a
12
- * tool must still reach.
13
- */
14
- import { isAbsolute, relative, resolve, sep } from "node:path";
15
- import { realOrRaw } from "#src/internal/realpath";
16
- /**
17
- * The one top-level `.nax/` entry agents may write freely: the scratchpad.
18
- * Must equal the last segment of SCRATCHPAD_DIR (pinned by a test; importing
19
- * it here would close a scratchpad -> policy -> nax-owned-writes cycle).
20
- */
21
- export const NAX_SCRATCHPAD_ENTRY = "scratchpad";
22
- /**
23
- * Top-level `.nax/` entries an allowWrite opt-in can never open (nax#2260):
24
- * config is the trust anchor for ungated commands, `mono` holds package
25
- * configs, and `features` holds every PRD plus the running feature's state.
26
- */
27
- const NAX_NEVER_OPT_IN = new Set(["config.json", "mono", "features"]);
28
- /**
29
- * Entries nax loads as human-authored input. The sandbox denies these even
30
- * when absent, so an agent cannot create one: a new `.nax/rules/x.md` would be
31
- * injected into every later session's prompt, and `hooks.json` / `plugins/`
32
- * run unsandboxed on the next `nax run`. `templates/` and `prompts/` are where
33
- * `prompts.overrides` points. nax creates none of them mid-run (CLI only).
34
- */
35
- export const NAX_ALWAYS_DENIED_ENTRIES = [
36
- "config.json",
37
- "mono",
38
- "rules",
39
- "context.md",
40
- "hooks.json",
41
- "plugins",
42
- "templates",
43
- "prompts",
44
- ];
45
- /**
46
- * A test file directly inside a feature dir: the acceptance and suggested
47
- * tests under their per-language names (`.nax-acceptance.test.ts`,
48
- * `_nax_acceptance_test.py`, `.nax-acceptance.rs`, ...) or a custom
49
- * `acceptance.testPath`, which is itself a test file name.
50
- */
51
- const FEATURE_TEST_FILE = /^\.nax-(acceptance|suggested)|[._-](test|spec)\.[a-z0-9]+$/i;
52
- const NO_OPT_INS = new Set();
53
- /**
54
- * Top-level `.nax/` entries a human opened for agent writes by listing them in
55
- * `execution.sandbox.filesystem.allowWrite` (nax#2260). Only an exact entry
56
- * counts -- `.nax/rules`, never `.nax/rules/a.md` -- and never the scratchpad
57
- * (always open) or a NAX_NEVER_OPT_IN entry. Relative entries resolve against
58
- * `root`, as the sandbox's own write roots do.
59
- */
60
- export function naxWriteOptIns(root, allowWrite) {
61
- const naxDir = resolve(root, ".nax");
62
- const names = allowWrite
63
- .map((p) => relative(naxDir, isAbsolute(p) ? resolve(p) : resolve(root, p)))
64
- .filter((rel) => rel !== "" && !rel.startsWith("..") && !rel.includes(sep) && !isAbsolute(rel))
65
- .filter((name) => name !== NAX_SCRATCHPAD_ENTRY && !NAX_NEVER_OPT_IN.has(name));
66
- return new Set(names);
67
- }
68
- /** `.nax/config.json`, or `.nax/mono/<package...>/config.json`, as root-relative segments. */
69
- function isNaxConfigSegments(segments) {
70
- if (segments[0] !== ".nax" || segments[segments.length - 1] !== "config.json")
71
- return false;
72
- return segments.length === 2 || (segments.length >= 4 && segments[1] === "mono");
73
- }
74
- /**
75
- * Is `resolved` one of nax's own CONFIG files, relative to `root`?
76
- *
77
- * `.nax/config.json`, and `.nax/mono/<package>/config.json` in a monorepo.
78
- *
79
- * Why these at all: `quality.commands` and `acceptance.command` are run by key
80
- * through a shell and never pass the permission gate -- they are trusted
81
- * because a HUMAN wrote them. That trust rests entirely on a model being
82
- * unable to write them. An agent holding `Write` under the default
83
- * `unrestricted` profile could otherwise add a quality command and receive an
84
- * ungated shell on the next run, routing around every `Bash(...)` rule, the
85
- * lexer's construct refusals and containment itself.
86
- *
87
- * Refused to EVERY tool, reads included -- this is the pre-existing behaviour
88
- * and it is deliberate.
89
- */
90
- export function isNaxConfigFile(root, resolved) {
91
- const rel = relative(realOrRaw(root), resolved);
92
- if (rel === "" || rel.startsWith(".."))
93
- return false;
94
- // `.nax/config.json` (2), or `.nax/mono/<package>/config.json` at ANY package
95
- // depth (>= 4). The real override path is nested -- `loadConfigForWorkdir`
96
- // reads `.nax/mono/<packageDir>/config.json` where packageDir is the
97
- // repo-relative package path (`src/config/loader.ts:382`), so a normal
98
- // `packages/*` layout is 5 segments, not 4. A length-exact rule left every
99
- // such override writable whenever the story's root was the repo root.
100
- return isNaxConfigSegments(rel.split(sep));
101
- }
102
- /**
103
- * The path-bearing tools that MUTATE. Read, Grep, Glob and Git are read-only
104
- * and are deliberately absent: an agent legitimately reads its own PRD, and
105
- * refusing that would break ordinary work to close one hole.
106
- *
107
- * Bash and Exec are absent because they carry no path fields -- a shell
108
- * redirect into `.nax/` is gated by the human-authored `Bash(...)` rules and
109
- * the lexer, which is a different seam from this one.
110
- */
111
- export const NAX_OWNED_WRITE_TOOLS = new Set(["Write", "Edit", "Delete", "GitCommit"]);
112
- /**
113
- * nax's run-control files at the root: the command channel an agent could
114
- * otherwise use to PAUSE, ABORT or SKIP stories without writing any code.
115
- *
116
- * `.queue.txt.processing` is the atomic-rename target the queue handler reads
117
- * from (src/execution/queue-handler.ts), so guarding only `.queue.txt` would
118
- * leave the same hole one rename downstream.
119
- */
120
- export const QUEUE_CONTROL_FILES = new Set([".queue.txt", ".queue.txt.processing"]);
121
- /**
122
- * Is this path one nax owns the writes to, independent of WHICH tool is asking?
123
- *
124
- * `naxOwnedWriteRefusal` answers the same question for the four file-writing
125
- * tools and returns prose. This predicate is the tool-agnostic half, extracted
126
- * so the `raw` bash screen (ADR-030) can consult exactly the same path set
127
- * without being in `NAX_OWNED_WRITE_TOOLS`. One definition, two callers.
128
- *
129
- * @param rel - Path relative to the permitted root, `/`-joined.
130
- */
131
- export function isNaxOwnedWritePath(rel) {
132
- return naxOwnedKind(rel) !== undefined;
133
- }
134
- /**
135
- * Which kind of nax-owned write path `rel` is, or undefined when it is not one.
136
- *
137
- * `/`-joined and root-relative, like `isNaxOwnedWritePath`. Never returns
138
- * `"config"` -- nax config files are recognised by the lexical
139
- * `isNaxConfigFile` pass, which callers (`protectedHit` in policy-bash-raw.ts)
140
- * run FIRST. Keeping the two apart is what lets the raw screen name the kind it
141
- * matched without duplicating the config rule here.
142
- */
143
- export function naxOwnedKind(rel) {
144
- const segments = rel.split("/");
145
- // SEC-5: the run-control file lives at the root, so match it exactly there
146
- // and not at any other depth -- a nested `sub/.queue.txt` is an ordinary
147
- // file, not the queue nax reads.
148
- if (segments.length === 1 && QUEUE_CONTROL_FILES.has(segments[0] ?? ""))
149
- return "queue";
150
- if (segments[0] === ".nax" && segments[1] === "features" && segments[segments.length - 1] === "prd.json") {
151
- return "prd";
152
- }
153
- return undefined;
154
- }
155
- /**
156
- * Refusal text for a Bash command that names (verb "names") or redirects into
157
- * (verb "redirects into") a nax-owned file.
158
- *
159
- * Kind-specific on purpose. The previous screen returned one modification-only
160
- * sentence for every kind, which read as "you cannot WRITE this" -- so an agent
161
- * that only wanted to READ a PRD retried a command it will never be allowed to
162
- * run. Each branch now says what the file IS and what the agent can do instead.
163
- *
164
- * The text never spells `.nax/features/` literally: `src/tools/` is covered by
165
- * the `check:feature-dir-ssot` gate, so the PRD branch names the token the agent
166
- * used (`hit`) rather than the tree layout.
167
- *
168
- * US-002: `opts.sandboxWrapped` (an available launcher) swaps only the PRD
169
- * sentence -- reading through Bash is then allowed, writing still is not.
170
- * `config` and `queue` are unchanged in both modes.
171
- */
172
- export function naxOwnedBashRefusal(tool, kind, hit, verb, opts) {
173
- switch (kind) {
174
- case "prd":
175
- return (`${tool} command ${verb} "${hit}", which holds this story's acceptance criteria. ` +
176
- "nax updates it itself during the run, so it shows as modified. " +
177
- (opts?.sandboxWrapped === true
178
- ? "Reading it through Bash is allowed; writing it is not -- nax updates it itself. "
179
- : "Bash commands naming it are refused, reads included -- leave it as is. ") +
180
- "To view it, use the `Read` tool.");
181
- case "queue":
182
- return (`${tool} command ${verb} "${hit}", which is nax's run-control queue. ` +
183
- "Bash commands naming it are refused, reads included -- change the run through the queue command.");
184
- case "config":
185
- return (`${tool} command ${verb} "${hit}", which is nax configuration. ` +
186
- "Bash commands naming it are refused, reads included -- nax configuration is not changed from inside a run.");
187
- }
188
- }
189
- /**
190
- * Why `tool` may not touch `rel`, or `undefined` when it may.
191
- *
192
- * `rel` MUST be the canonical, posix-separated, root-relative spelling the
193
- * policy itself derived -- never the caller's own. Matching the caller's
194
- * spelling means this guard and the policy that approved the call are reading
195
- * different strings, and every alternate spelling ("./x", "a/../x") walks past
196
- * the guard. Same rule, and the same reason, as `matchesDenyPaths`.
197
- *
198
- * The PRD defines the acceptance criteria the story is judged against. An
199
- * agent that can rewrite it can pass any review without writing any code,
200
- * which defeats the review layer without touching a config file.
201
- *
202
- * The queue file is the other half of the same concern: it is the run-control
203
- * channel, and a write there can pause, abort or skip stories outright.
204
- */
205
- export function naxOwnedWriteRefusal(tool, rel, exemptRel, optIns = NO_OPT_INS) {
206
- if (!NAX_OWNED_WRITE_TOOLS.has(tool))
207
- return undefined;
208
- if (!isNaxOwnedWritePath(rel)) {
209
- return exemptRel !== undefined && rel === exemptRel ? undefined : naxStateRefusal(rel, optIns);
210
- }
211
- const segments = rel.split("/");
212
- // SEC-5: the run-control file lives at the root, so match it exactly
213
- // there and not at any other depth -- a nested `sub/.queue.txt` is an
214
- // ordinary file, not the queue nax reads.
215
- if (segments.length === 1) {
216
- return `"${rel}" is nax's own run state: it carries the PAUSE/ABORT/SKIP commands that control this run, so no tool may modify it. Change the run through the queue command, not by writing its file.`;
217
- }
218
- // nax#2115: the plan session is the ONE writer of a PRD -- its op declares
219
- // `fileOutput: (input) => input.outputPath` and every plan prompt instructs
220
- // the agent to write the PRD there rather than reply with it. #2095 added
221
- // this guard without an exemption, which severed that contract and left
222
- // `nax plan` unable to produce a PRD in ANY mode.
223
- //
224
- // The exemption is PATH-EXACT, not role-shaped: the plan session may write
225
- // the single path its own op declared, and nothing else. A sibling feature's
226
- // PRD stays refused even to the plan session, so a planner cannot reach
227
- // across features to rewrite criteria it is not authoring. `exemptRel` must
228
- // be the caller's canonical, posix-separated, root-relative spelling --
229
- // derived by the policy itself, never taken from the agent's own arguments.
230
- if (exemptRel !== undefined && rel === exemptRel)
231
- return undefined;
232
- return `"${rel}" is nax's own run state: it holds the acceptance criteria this story is judged against, so no tool may modify it. Change the code, not the criteria.`;
233
- }
234
- /**
235
- * nax#2260: everything else under `.nax/` is nax's own state. Writable: the
236
- * scratchpad, test files directly inside a feature dir (FEATURE_TEST_FILE --
237
- * the tool-audit ledgers show these are the only file-tool writes agents need
238
- * there; the feature's context.md, spec and state files stay refused), and
239
- * opted-in entries. Config files are left to the earlier, more specific
240
- * `isNaxConfigFile` refusal, which covers reads too.
241
- *
242
- * Only a leading `.nax/` segment is matched, so a monorepo package's own
243
- * `.nax/` is excluded on purpose -- see `naxDenies` in
244
- * src/sandbox/policy-builder.ts for why that is mostly safe to delete and
245
- * where the one gap (package `rules/`) remains.
246
- */
247
- function naxStateRefusal(rel, optIns) {
248
- const segments = rel.split("/");
249
- if (segments[0] !== ".nax" || isNaxConfigSegments(segments))
250
- return undefined;
251
- const entry = segments[1];
252
- if (segments.length > 2 && (entry === NAX_SCRATCHPAD_ENTRY || optIns.has(entry ?? "")))
253
- return undefined;
254
- if (segments.length === 4 && entry === "features" && FEATURE_TEST_FILE.test(segments[3] ?? ""))
255
- return undefined;
256
- return (`"${rel}" is nax's own state, which agents do not modify. Under .nax/, write only to your scratchpad ` +
257
- "(.nax/scratchpad/) or to a feature's acceptance test file. " +
258
- "A human can open a path for a story by listing it in execution.sandbox.filesystem.allowWrite in the project config.");
259
- }