nexrall-code 0.5.48 → 0.5.49

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 (2) hide show
  1. package/dist/index.js +389 -11
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -10819,6 +10819,144 @@ var require_editCompleteness = __commonJS({
10819
10819
  }
10820
10820
  });
10821
10821
 
10822
+ // ../core/dist/agent/securityLint.js
10823
+ var require_securityLint = __commonJS({
10824
+ "../core/dist/agent/securityLint.js"(exports) {
10825
+ "use strict";
10826
+ Object.defineProperty(exports, "__esModule", { value: true });
10827
+ exports.checkSecurity = checkSecurity;
10828
+ exports.securityNoteText = securityNoteText;
10829
+ var SAMPLE_MAX = 160;
10830
+ function redact(line) {
10831
+ const masked = line.replace(/(['"`]?[\w.-]*(?:secret|password|passwd|token|api[_-]?key|apikey|auth|credential|private[_-]?key)[\w.-]*['"`]?\s*[:=]\s*)(['"`])([^'"`]{4,})\2/gi, (_m, head, q) => `${head}${q}[REDACTED]${q}`).replace(/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, "[REDACTED]");
10832
+ return masked.length > SAMPLE_MAX ? masked.slice(0, SAMPLE_MAX) + "\u2026" : masked;
10833
+ }
10834
+ function isLikelyCommentLine(line) {
10835
+ return /^\s*(\/\/|\*|#|--|<!--)/.test(line);
10836
+ }
10837
+ var RULES = [
10838
+ // ── Hardcoded credentials ───────────────────────────────────────────────────
10839
+ // Provider-prefixed tokens are near-zero false positive: the prefixes are
10840
+ // registered formats, not something that occurs naturally in source. Checked
10841
+ // inside comments too — a key commented out is still a committed key.
10842
+ {
10843
+ kind: "hardcoded-secret",
10844
+ re: /\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})\b/,
10845
+ message: "Looks like a real API key/token committed to source. Move it to an environment variable and rotate the exposed key.",
10846
+ includeComments: true
10847
+ },
10848
+ // NOTE: the `private-key` check is NOT here — it needs to span multiple lines
10849
+ // (BEGIN header on one, base64 body on the next), which this per-line loop
10850
+ // cannot express. It runs separately in checkSecurity below.
10851
+ {
10852
+ kind: "hardcoded-password",
10853
+ // An ASSIGNMENT of a credential-ish name to a non-trivial literal.
10854
+ //
10855
+ // Tightened after measuring against the real backend, where the looser version
10856
+ // fired on `missingSecret:'FIREBASE_TOKEN'` — code that NAMES a secret in an
10857
+ // error message, the opposite of leaking one. So a value that is itself just a
10858
+ // SCREAMING_SNAKE identifier (an env-var name) is excluded, along with the
10859
+ // usual placeholder vocabulary. The value must also look like actual secret
10860
+ // material: mixed case or digits, not a lone lowercase word.
10861
+ re: /(?:password|passwd|secret|api[_-]?key|apikey|access[_-]?token)['"`]?\s*[:=]\s*['"`](?![A-Z0-9_]+['"`])(?!.*(?:\$\{|process\.env|os\.environ|example|changeme|placeholder|redacted|xxx|test|dummy|fake|sample|your[_-]?|<|\*{3}))(?=[^'"`]*[0-9A-Z])[^'"`\s]{10,}['"`]/,
10862
+ message: "Hardcoded credential literal. Read it from the environment/secret store instead, and rotate the exposed value.",
10863
+ includeComments: true
10864
+ },
10865
+ // ── Injection ───────────────────────────────────────────────────────────────
10866
+ {
10867
+ kind: "dynamic-eval",
10868
+ // The negative lookbehind for `.` is what makes this usable: `redisClient.eval`
10869
+ // (a Redis Lua script), `page.eval` (Playwright), `vm.eval` and friends are
10870
+ // METHOD calls on an object and have nothing to do with JavaScript's global
10871
+ // eval. Without it, the real backend's Redis idempotency script was flagged.
10872
+ // Only a bare `eval(` / `new Function(` with a non-literal argument counts.
10873
+ re: /(?<![.\w$])(?:eval|new\s+Function)\s*\(\s*(?!['"`][^'"`]*['"`]\s*\))[^)]*[a-zA-Z_$][\w$]*/,
10874
+ message: "eval / new Function on a non-literal value executes arbitrary code if that value is ever user-controlled. Use an explicit parser or a lookup table."
10875
+ },
10876
+ {
10877
+ kind: "sql-injection",
10878
+ // Only fires when the interpolated expression is plausibly REQUEST-DERIVED.
10879
+ //
10880
+ // The obvious pattern — any `${...}` inside a SQL string — was measured
10881
+ // against the real backend and flagged 15 of 183 files, essentially all of
10882
+ // them safe and idiomatic: `${sets.join(', ')}` for a dynamic UPDATE, `${field}`
10883
+ // for a server-chosen column, `${CONSUMPTION}` for a module constant. At that
10884
+ // hit rate the warning is pure noise, and noise is worse than silence because
10885
+ // it teaches everyone to skip the channel.
10886
+ //
10887
+ // So the interpolation must name something that plausibly came from the
10888
+ // outside: req/request/params/query/body/input/user/args, or a bare
10889
+ // `'...' + ident`. This trades recall for precision on purpose — thorough SQL
10890
+ // review is the security-auditor agent's job, not an inline regex's.
10891
+ re: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\b[^;'"`]{0,160}(?:\$\{\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|['"`]\s*\+\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|%\s*\(\s*(?:request|params?|query|body|input|user)\b)/i,
10892
+ message: "SQL built by interpolating a request-derived value. Use a parameterised query ($1 / ? placeholders) \u2014 this is the classic injection sink."
10893
+ },
10894
+ {
10895
+ kind: "command-injection",
10896
+ // Shell execution with an interpolated or concatenated argument.
10897
+ re: /\b(?:exec|execSync|spawnSync?|system|popen|os\.system|subprocess\.(?:call|run|Popen))\s*\(\s*(?:[`'"][^`'"]*(?:\$\{|['"]\s*\+)|[a-zA-Z_$][\w$]*\s*\+)/,
10898
+ message: "Shell command built from a variable. Pass arguments as an array (no shell), or validate against an allowlist \u2014 a value containing ; or $() becomes command execution."
10899
+ },
10900
+ // ── Transport / verification ────────────────────────────────────────────────
10901
+ {
10902
+ kind: "tls-verification-disabled",
10903
+ re: /(?:rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/,
10904
+ message: "TLS certificate verification is disabled, which removes protection against man-in-the-middle attacks. Trust a specific CA instead if the cert is self-signed."
10905
+ }
10906
+ ];
10907
+ function checkSecurity(content, max = 5) {
10908
+ if (!content)
10909
+ return [];
10910
+ const findings = [];
10911
+ const lines = content.split(/\r?\n/);
10912
+ const pemIdx = lines.findIndex((l) => /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH|PGP)?\s*PRIVATE KEY-----/.test(l));
10913
+ if (pemIdx !== -1) {
10914
+ const following = lines.slice(pemIdx, pemIdx + 4).join("\n");
10915
+ if (/[A-Za-z0-9+/]{40,}/.test(following.replace(/-----[^-]+-----/g, ""))) {
10916
+ findings.push({
10917
+ kind: "private-key",
10918
+ message: "A private key with real key material is being written into source. Store it outside the repo (secret manager / env var) and rotate it.",
10919
+ line: pemIdx + 1,
10920
+ sample: "-----BEGIN PRIVATE KEY----- [REDACTED]"
10921
+ });
10922
+ }
10923
+ }
10924
+ const seenKinds = /* @__PURE__ */ new Set();
10925
+ for (let i2 = 0; i2 < lines.length && findings.length < max; i2++) {
10926
+ const line = lines[i2];
10927
+ if (!line || line.length > 2e3)
10928
+ continue;
10929
+ const commentish = isLikelyCommentLine(line);
10930
+ for (const rule of RULES) {
10931
+ if (seenKinds.has(rule.kind))
10932
+ continue;
10933
+ if (commentish && !rule.includeComments)
10934
+ continue;
10935
+ if (!rule.re.test(line))
10936
+ continue;
10937
+ seenKinds.add(rule.kind);
10938
+ findings.push({ kind: rule.kind, message: rule.message, line: i2 + 1, sample: redact(line.trim()) });
10939
+ break;
10940
+ }
10941
+ }
10942
+ return findings;
10943
+ }
10944
+ function securityNoteText(findings) {
10945
+ if (process.env.NEXRALL_SECURITY_LINT === "off")
10946
+ return "";
10947
+ if (!findings.length)
10948
+ return "";
10949
+ const lines = findings.map((f3) => ` \u2022 line ${f3.line} [${f3.kind}]: ${f3.message}
10950
+ ${f3.sample}`);
10951
+ return `
10952
+
10953
+ \u26A0 SECURITY REVIEW (${findings.length} finding${findings.length > 1 ? "s" : ""}) \u2014 the write succeeded; check these before moving on:
10954
+ ` + lines.join("\n") + `
10955
+ If a finding is a false positive (test fixture, placeholder, intentionally dynamic), say so and continue \u2014 do NOT rewrite correct code to silence it.`;
10956
+ }
10957
+ }
10958
+ });
10959
+
10822
10960
  // ../core/dist/agent/memory.js
10823
10961
  var require_memory = __commonJS({
10824
10962
  "../core/dist/agent/memory.js"(exports) {
@@ -12482,6 +12620,7 @@ var require_executor = __commonJS({
12482
12620
  var sandbox_1 = require_sandbox();
12483
12621
  var auth_1 = require_auth();
12484
12622
  var editCompleteness_1 = require_editCompleteness();
12623
+ var securityLint_1 = require_securityLint();
12485
12624
  var memory_1 = require_memory();
12486
12625
  var skills_1 = require_skills();
12487
12626
  var testIntegrity_1 = require_testIntegrity();
@@ -12737,15 +12876,16 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
12737
12876
  atomicWrite(resolved, content, existingMode);
12738
12877
  const bytes = Buffer.byteLength(content, "utf-8");
12739
12878
  const lines = content.split("\n").length;
12879
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
12740
12880
  if (isNew) {
12741
- return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
12881
+ return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
12742
12882
  }
12743
12883
  const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
12744
12884
  let tiMarker = "";
12745
12885
  const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
12746
12886
  if (ti.suspicious)
12747
12887
  tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
12748
- return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
12888
+ return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
12749
12889
  } catch (err) {
12750
12890
  return { error: err.message };
12751
12891
  }
@@ -13531,9 +13671,10 @@ ${globalCapMatches(output)}` : "";
13531
13671
  const linesBefore = origNorm.split("\n").length;
13532
13672
  const linesAfter = updated.split("\n").length;
13533
13673
  const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
13674
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
13534
13675
  return { output: `Edited ${resolved} (${linesBefore} \u2192 ${linesAfter} lines)
13535
13676
 
13536
- ${diff3}${xfile}` };
13677
+ ${diff3}${xfile}${sec}` };
13537
13678
  } catch (err) {
13538
13679
  return { error: err.message };
13539
13680
  }
@@ -13801,6 +13942,7 @@ ${body}${truncNote}
13801
13942
  let content = normalizeLF(rawFile);
13802
13943
  const originalNorm = content;
13803
13944
  const diffs = [];
13945
+ const insertedText = [];
13804
13946
  for (let i2 = 0; i2 < edits.length; i2++) {
13805
13947
  const edit = edits[i2];
13806
13948
  const oldStr = normalizeLF(typeof edit.old_string === "string" ? edit.old_string : "");
@@ -13815,16 +13957,18 @@ ${body}${truncNote}
13815
13957
  return { error: `Edit #${i2 + 1}: old_string appears ${count} times \u2014 it must be unique. Add more surrounding context.` };
13816
13958
  }
13817
13959
  diffs.push(buildDiff(filePath, oldStr, newStr, content));
13960
+ insertedText.push(newStr);
13818
13961
  content = literalReplace(content, oldStr, newStr);
13819
13962
  }
13820
13963
  const finalContent = wasCRLF ? content.replace(/\n/g, "\r\n") : content;
13821
13964
  atomicWritePreservingMode(resolved, finalContent, pre.mode);
13822
13965
  const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
13966
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join("\n")));
13823
13967
  return {
13824
13968
  output: `Applied ${edits.length} edit(s) to ${resolved}:
13825
13969
  ` + diffs.map((d, i2) => `
13826
13970
  --- edit #${i2 + 1} ---
13827
- ${d}`).join("\n") + xfile
13971
+ ${d}`).join("\n") + xfile + sec
13828
13972
  };
13829
13973
  } catch (err) {
13830
13974
  return { error: err.message };
@@ -14405,11 +14549,40 @@ var require_agentTypes = __commonJS({
14405
14549
  var path6 = __importStar(__require("path"));
14406
14550
  var os6 = __importStar(__require("os"));
14407
14551
  var index_1 = require_plugins();
14552
+ var READ_ONLY_TOOLS = [
14553
+ // Universal
14554
+ "read_file",
14555
+ "search_files",
14556
+ "glob",
14557
+ "list_directory",
14558
+ "bash",
14559
+ "bash_output",
14560
+ "notebook_read",
14561
+ "todo_write",
14562
+ "todo_read",
14563
+ // VS Code language server (ignored on the CLI)
14564
+ "get_symbols",
14565
+ "get_workspace_symbols",
14566
+ "find_references",
14567
+ "go_to_definition",
14568
+ "get_hover",
14569
+ "get_diagnostics"
14570
+ ];
14571
+ var RESEARCH_TOOLS = [...READ_ONLY_TOOLS, "web_search", "fetch_url"];
14572
+ var WRITE_TOOLS = [
14573
+ ...READ_ONLY_TOOLS,
14574
+ "write_file",
14575
+ "edit_file",
14576
+ "multi_edit",
14577
+ "create_directory",
14578
+ "move_file",
14579
+ "copy_file"
14580
+ ];
14408
14581
  var BUILTIN_AGENTS = [
14409
14582
  {
14410
14583
  name: "reviewer",
14411
14584
  description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
14412
- tools: ["read_file", "search_files", "glob", "list_directory", "bash", "get_symbols", "get_workspace_symbols", "find_references", "go_to_definition", "get_diagnostics"],
14585
+ tools: READ_ONLY_TOOLS,
14413
14586
  source: "builtin",
14414
14587
  prompt: [
14415
14588
  "You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
@@ -14425,6 +14598,162 @@ var require_agentTypes = __commonJS({
14425
14598
  "Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
14426
14599
  "then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
14427
14600
  ].join("\n")
14601
+ },
14602
+ // Promoted from the security-audit plugin to a builtin.
14603
+ //
14604
+ // Leaving it plugin-only was indefensible next to `reviewer` being builtin:
14605
+ // reviewer's own prompt already tells it to look for security issues, so
14606
+ // security IS treated as default work — yet the specialist agent for it was
14607
+ // invisible unless the user happened to know the plugin existed. For an agent
14608
+ // that WRITES code, "you only get a security review if you knew to install
14609
+ // something" is the wrong default.
14610
+ {
14611
+ name: "security-auditor",
14612
+ description: "Read-only security auditor \u2014 hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.",
14613
+ tools: READ_ONLY_TOOLS,
14614
+ model: "pro",
14615
+ source: "builtin",
14616
+ prompt: [
14617
+ "You are a security auditor. You find real, exploitable flaws \u2014 not style issues.",
14618
+ "",
14619
+ "Method:",
14620
+ "1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network",
14621
+ " input, deserialization), then trace user-controlled data inward to where it is used.",
14622
+ "2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.",
14623
+ "3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;",
14624
+ " Medium = needs unusual preconditions; Low = hardening.",
14625
+ "",
14626
+ "Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken",
14627
+ "authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,",
14628
+ "path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,",
14629
+ "and crypto misuse (hand-rolled comparison, predictable randomness).",
14630
+ "",
14631
+ "Hard rules:",
14632
+ "- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.",
14633
+ "- NEVER print a discovered secret's value. Report its location and advise rotation.",
14634
+ "- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.",
14635
+ '- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.'
14636
+ ].join("\n")
14637
+ },
14638
+ // The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
14639
+ // search that would otherwise flood the parent's context. Defaults to the
14640
+ // cheapest model on purpose — "find every caller of X" has no need of a
14641
+ // frontier model, and this is the agent most likely to be spawned in bulk.
14642
+ {
14643
+ name: "explorer",
14644
+ description: "Fast read-only codebase explorer \u2014 locates files, symbols, and call sites and reports concise findings. Use to keep bulk searching out of the main context. Cannot modify files.",
14645
+ tools: READ_ONLY_TOOLS,
14646
+ model: "turbo",
14647
+ source: "builtin",
14648
+ prompt: [
14649
+ "You map code. You NEVER modify anything.",
14650
+ "",
14651
+ "Method:",
14652
+ "1. Prefer structural search over text search where available (get_workspace_symbols, find_references,",
14653
+ " go_to_definition); fall back to search_files/glob otherwise.",
14654
+ "2. Read only the sections you need \u2014 use read_file with offset/limit on large files instead of",
14655
+ " pulling in thousands of lines.",
14656
+ "3. Follow the real call graph rather than guessing from names.",
14657
+ "",
14658
+ "Your ONLY output is a compact report: the file:line locations that matter, how they relate, and the",
14659
+ "direct answer to the question you were given. This exists to keep bulk search OUT of the parent's",
14660
+ "context, so do not paste large file contents back \u2014 cite locations and summarise. Say plainly when",
14661
+ 'something does not exist; a confident wrong answer is far worse than "not found".'
14662
+ ].join("\n")
14663
+ },
14664
+ // Matches Claude Code's built-in `Plan`: research a change and return a
14665
+ // strategy, deliberately WITHOUT write access so "make a plan" can never
14666
+ // quietly become "start editing".
14667
+ {
14668
+ name: "planner",
14669
+ description: "Read-only planning agent \u2014 researches a change and returns a concrete step-by-step implementation plan with risks and affected files. Cannot modify files.",
14670
+ tools: RESEARCH_TOOLS,
14671
+ model: "pro",
14672
+ source: "builtin",
14673
+ prompt: [
14674
+ "You produce implementation plans. You NEVER modify files \u2014 planning and doing are separate steps,",
14675
+ 'and this agent exists so "plan it" cannot silently turn into "change it".',
14676
+ "",
14677
+ "Method:",
14678
+ "1. Read the actual code before proposing anything. No plan may rest on an assumed API shape.",
14679
+ "2. Find every affected call site (find_references / search_files) and list them.",
14680
+ "3. Order the steps so the tree stays working after each one \u2014 types, then implementation, then",
14681
+ " tests, then exports/registration.",
14682
+ "",
14683
+ "Output:",
14684
+ "- Goal, in one sentence.",
14685
+ "- Numbered steps, each with the exact files touched and what changes in them.",
14686
+ "- Risks + the specific thing that could break, and how it would be detected.",
14687
+ "- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).",
14688
+ "- Anything genuinely ambiguous, stated as an open question rather than a silent assumption."
14689
+ ].join("\n")
14690
+ },
14691
+ // Promoted from the test-gen plugin. Needs write access — it produces test
14692
+ // files — but is deliberately forbidden from touching source, because "make the
14693
+ // tests pass" is the single most common way an agent destroys signal.
14694
+ {
14695
+ name: "test-writer",
14696
+ description: "Writes tests that follow the project's existing conventions. May create/edit TEST files only \u2014 never production source.",
14697
+ tools: WRITE_TOOLS,
14698
+ // Enforced, not merely requested: the permission gate refuses a write whose
14699
+ // path is not a test file. Without this the allowlist would grant edit_file
14700
+ // for every path and the rule below would be a suggestion the model is free
14701
+ // to rationalise its way past.
14702
+ testFilesOnly: true,
14703
+ source: "builtin",
14704
+ prompt: [
14705
+ "You write tests. You may create and edit TEST files only.",
14706
+ "",
14707
+ "Hard rules \u2014 these are the ways test-writing agents destroy value, so they are non-negotiable:",
14708
+ "- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.",
14709
+ "- NEVER weaken, delete or skip an existing assertion or test.",
14710
+ "- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.",
14711
+ "",
14712
+ "Method:",
14713
+ "1. Read the existing tests FIRST and copy their conventions exactly \u2014 runner, file naming, layout,",
14714
+ " assertion style, fixture/helper patterns. Never introduce a new framework.",
14715
+ "2. Test observable behaviour and the contract, not private internals.",
14716
+ "3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,",
14717
+ " boundaries, error paths, concurrency where it applies.",
14718
+ "4. No sleeps or wall-clock dependence \u2014 those produce the flaky tests that get deleted later.",
14719
+ "5. RUN the tests you wrote and report the real output. Never claim a test passes without running it."
14720
+ ].join("\n")
14721
+ },
14722
+ // The DevOps gap — answered with a READ-ONLY advisor, not an operator.
14723
+ //
14724
+ // A "DevOps agent" with write/apply access is a genuinely different risk class
14725
+ // from the others here: its mistakes are `kubectl delete`, a bad `terraform
14726
+ // apply`, a broken deploy pipeline — often not revertible and affecting
14727
+ // production rather than a working tree. So this one diagnoses and proposes a
14728
+ // diff; a human applies it. That asymmetry is the whole design.
14729
+ {
14730
+ name: "devops-advisor",
14731
+ description: "Read-only CI/CD, container, and infrastructure advisor \u2014 diagnoses pipelines, Dockerfiles, and k8s manifests and proposes concrete fixes as a diff. Never applies changes.",
14732
+ tools: RESEARCH_TOOLS,
14733
+ model: "pro",
14734
+ source: "builtin",
14735
+ prompt: [
14736
+ "You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.",
14737
+ "",
14738
+ "Hard rules:",
14739
+ "- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only",
14740
+ " (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).",
14741
+ " NEVER run anything that mutates infrastructure \u2014 no apply/delete/scale/rollout/restart/push,",
14742
+ " no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.",
14743
+ "- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.",
14744
+ "",
14745
+ "Method:",
14746
+ "1. Read what actually exists \u2014 workflow files, Dockerfiles, manifests, kustomize overlays, the",
14747
+ ' deploy scripts \u2014 before drawing any conclusion. Never reason from what a stack "usually" looks like.',
14748
+ "2. Follow the real path a change takes to production, and name the step that is broken or missing.",
14749
+ "3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually",
14750
+ " needs, image tags that do not match what is deployed, missing health probes, absent resource",
14751
+ " limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and",
14752
+ " migrations that must run before the new image is live.",
14753
+ "",
14754
+ "Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or",
14755
+ "exact file content, and the command a human should run to apply and verify it."
14756
+ ].join("\n")
14428
14757
  }
14429
14758
  ];
14430
14759
  function parseFrontmatter(raw) {
@@ -14443,6 +14772,9 @@ var require_agentTypes = __commonJS({
14443
14772
  const s2 = (v ?? "").toLowerCase();
14444
14773
  return s2 === "turbo" || s2 === "pro" || s2 === "ultra" ? s2 : void 0;
14445
14774
  }
14775
+ function parseBool(v) {
14776
+ return /^(true|yes|1|on)$/i.test((v ?? "").trim());
14777
+ }
14446
14778
  function parseToolList(v) {
14447
14779
  if (!v)
14448
14780
  return void 0;
@@ -14471,7 +14803,12 @@ var require_agentTypes = __commonJS({
14471
14803
  tools: parseToolList(meta.tools),
14472
14804
  model: parseModel(meta.model),
14473
14805
  prompt: body,
14474
- source
14806
+ source,
14807
+ // Exposed to user/plugin definitions too — `test_files_only: true` (or
14808
+ // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
14809
+ // cannot touch production source, rather than only the builtin getting
14810
+ // that guarantee.
14811
+ ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}
14475
14812
  });
14476
14813
  } catch {
14477
14814
  }
@@ -14487,16 +14824,38 @@ var require_agentTypes = __commonJS({
14487
14824
  if (!out.has(agent.name))
14488
14825
  out.set(agent.name, agent);
14489
14826
  }
14490
- return [...out.values()];
14827
+ const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i2) => [a.name, i2]));
14828
+ return [...out.values()].sort((a, b) => {
14829
+ const ai = builtinOrder.get(a.name);
14830
+ const bi = builtinOrder.get(b.name);
14831
+ if (ai !== void 0 && bi !== void 0)
14832
+ return ai - bi;
14833
+ if (ai !== void 0)
14834
+ return -1;
14835
+ if (bi !== void 0)
14836
+ return 1;
14837
+ return a.name.localeCompare(b.name);
14838
+ });
14491
14839
  }
14492
14840
  function summariseAgents(types3) {
14493
14841
  if (!types3.length)
14494
14842
  return "";
14495
14843
  return types3.map((t2) => {
14496
- const tools = t2.tools ? ` (tools: ${t2.tools.join(", ")})` : "";
14497
- return `- ${t2.name}: ${t2.description}${tools}`;
14844
+ const canWrite = !t2.tools || t2.tools.some((x2) => WRITE_TOOL_HINTS.has(x2));
14845
+ const access = t2.testFilesOnly ? "writes TEST files only" : canWrite ? "can modify files" : "read-only";
14846
+ const model = t2.model ? `, ${t2.model} model` : "";
14847
+ return `- ${t2.name} (${access}${model}): ${t2.description}`;
14498
14848
  }).join("\n");
14499
14849
  }
14850
+ var WRITE_TOOL_HINTS = /* @__PURE__ */ new Set([
14851
+ "write_file",
14852
+ "edit_file",
14853
+ "multi_edit",
14854
+ "notebook_edit",
14855
+ "delete_file",
14856
+ "move_file",
14857
+ "copy_file"
14858
+ ]);
14500
14859
  function findAgentType(types3, name) {
14501
14860
  if (!name)
14502
14861
  return void 0;
@@ -14905,6 +15264,7 @@ var require_loop = __commonJS({
14905
15264
  exports.contextWindowFor = contextWindowFor2;
14906
15265
  exports.compactionThresholds = compactionThresholds2;
14907
15266
  exports.estimateBodyBytes = estimateBodyBytes2;
15267
+ exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
14908
15268
  exports.findSafeCutIndex = findSafeCutIndex;
14909
15269
  exports.transcriptOf = transcriptOf;
14910
15270
  exports.createLedger = createLedger;
@@ -15250,6 +15610,8 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15250
15610
  const gatedPermission = async (req) => {
15251
15611
  if (allowed && !allowed.has(req.tool))
15252
15612
  return false;
15613
+ if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
15614
+ return false;
15253
15615
  return options.requestPermission(req);
15254
15616
  };
15255
15617
  const subMessages = [
@@ -15394,6 +15756,15 @@ ${partial}` : "",
15394
15756
  return true;
15395
15757
  }
15396
15758
  exports.WRITE_TOOL_NAMES = /* @__PURE__ */ new Set(["write_file", "edit_file", "multi_edit", "delete_file", "move_file", "copy_file", "notebook_edit"]);
15759
+ function allowsTestOnlyWrite(tool, input) {
15760
+ if (!exports.WRITE_TOOL_NAMES.has(tool))
15761
+ return true;
15762
+ const pathKeys = tool === "notebook_edit" ? ["path"] : ["path", "source", "dest", "destination"];
15763
+ const candidates = pathKeys.map((k) => input?.[k]).filter((v) => typeof v === "string" && v.length > 0);
15764
+ if (candidates.length === 0)
15765
+ return false;
15766
+ return candidates.every((p) => (0, testIntegrity_1.isTestFile)(p));
15767
+ }
15397
15768
  exports.VERIFY_CMD_RE = /\b(npm|yarn|pnpm)\s+(run\s+)?(test|build|lint|typecheck|tsc)\b|\bpytest\b|\bgo\s+(test|vet|build)\b|\btsc\b|\beslint\b|\bcargo\s+(test|build|check)\b/i;
15398
15769
  function findSafeCutIndex(messages, maxIdx) {
15399
15770
  for (let i2 = Math.min(maxIdx, messages.length - 1); i2 >= 2; i2--) {
@@ -17720,6 +18091,7 @@ var require_dist2 = __commonJS({
17720
18091
  __exportStar(require_loop(), exports);
17721
18092
  __exportStar(require_testIntegrity(), exports);
17722
18093
  __exportStar(require_editCompleteness(), exports);
18094
+ __exportStar(require_securityLint(), exports);
17723
18095
  __exportStar(require_crossFile(), exports);
17724
18096
  __exportStar(require_flaky(), exports);
17725
18097
  __exportStar(require_claimEvidence(), exports);
@@ -62794,7 +63166,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
62794
63166
  };
62795
63167
 
62796
63168
  // src/commands/chat.ts
62797
- var CLI_VERSION = "0.5.48";
63169
+ var CLI_VERSION = "0.5.49";
62798
63170
  var MODEL_LABELS = {
62799
63171
  turbo: "Nexrall Turbo",
62800
63172
  pro: "Nexrall Pro",
@@ -64172,6 +64544,12 @@ async function confirmInstall(i2, name, autoYes) {
64172
64544
  if (autoYes && dangerous) {
64173
64545
  console.log(source_default.yellow("\n --yes does not apply to plugins with hooks/MCP; confirmation required."));
64174
64546
  }
64547
+ if (!process.stdin.isTTY) {
64548
+ console.log();
64549
+ console.log(source_default.red(" \u2717 Cannot ask for confirmation: stdin is not a terminal."));
64550
+ console.log(source_default.dim(dangerous ? " This plugin ships hooks/MCP, which always require an interactive confirmation." : " Re-run with --yes to install non-interactively."));
64551
+ return false;
64552
+ }
64175
64553
  console.log();
64176
64554
  const { ok } = await (0, import_prompts.default)({
64177
64555
  type: "confirm",
@@ -64339,7 +64717,7 @@ function pluginListCommand() {
64339
64717
 
64340
64718
  // src/index.ts
64341
64719
  var program2 = new Command();
64342
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.48");
64720
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.49").enablePositionalOptions();
64343
64721
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
64344
64722
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
64345
64723
  program2.command("update").description("Update nex to the latest version").option("-c, --check", "Check for updates without installing").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
@@ -41,7 +41,7 @@
41
41
  "react": "^19.2.8",
42
42
  "readline": "^1.3.0",
43
43
  "string-width": "^7.2.0",
44
- "@nexrall/code-core": "1.4.23"
44
+ "@nexrall/code-core": "1.4.24"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",