aztrx-cli 0.4.4 → 0.5.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.
Files changed (98) hide show
  1. package/README.md +325 -28
  2. package/dist/cli/help.d.ts +22 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +278 -81
  5. package/dist/core/auth.d.ts +33 -0
  6. package/dist/core/badge.d.ts +9 -0
  7. package/dist/core/browser.d.ts +3 -0
  8. package/dist/core/classifier.d.ts +41 -0
  9. package/dist/core/cloud/index.d.ts +62 -0
  10. package/dist/core/cloud/index.js +24 -4
  11. package/dist/core/devServer.d.ts +90 -0
  12. package/dist/core/devServer.js +253 -0
  13. package/dist/core/diagnose.d.ts +18 -0
  14. package/dist/core/diff.d.ts +31 -0
  15. package/dist/core/domWalker.d.ts +24 -0
  16. package/dist/core/domWalker.js +1 -1
  17. package/dist/core/eventBus.d.ts +59 -0
  18. package/dist/core/events.d.ts +42 -0
  19. package/dist/core/fixPr.d.ts +16 -0
  20. package/dist/core/fixPr.js +11 -1
  21. package/dist/core/fuzzer.d.ts +24 -0
  22. package/dist/core/fuzzer.js +1 -1
  23. package/dist/core/heal/apply.d.ts +28 -0
  24. package/dist/core/heal/boot.d.ts +74 -0
  25. package/dist/core/heal/boot.js +126 -23
  26. package/dist/core/heal/childEnv.d.ts +15 -0
  27. package/dist/core/heal/gates.d.ts +11 -0
  28. package/dist/core/heal/index.d.ts +19 -0
  29. package/dist/core/heal/index.js +53 -23
  30. package/dist/core/heal/llm.d.ts +43 -0
  31. package/dist/core/heal/redact.d.ts +22 -0
  32. package/dist/core/heal/sandbox.d.ts +54 -0
  33. package/dist/core/heal/sandbox.js +20 -3
  34. package/dist/core/heal/types.d.ts +102 -0
  35. package/dist/core/heal/verify.d.ts +37 -0
  36. package/dist/core/heal/verify.js +28 -14
  37. package/dist/core/httpFuzzer.d.ts +33 -0
  38. package/dist/core/init.d.ts +21 -0
  39. package/dist/core/init.js +5 -6
  40. package/dist/core/interceptor.d.ts +21 -0
  41. package/dist/core/llm.d.ts +36 -0
  42. package/dist/core/llm.js +0 -8
  43. package/dist/core/minimizer.d.ts +13 -0
  44. package/dist/core/modernize.d.ts +28 -0
  45. package/dist/core/modernize.js +2 -2
  46. package/dist/core/networkGuard.d.ts +13 -0
  47. package/dist/core/orchestrator.d.ts +86 -0
  48. package/dist/core/orchestrator.js +24 -9
  49. package/dist/core/patrol/loop.d.ts +46 -0
  50. package/dist/core/patrol/pr.d.ts +42 -0
  51. package/dist/core/patrol/pr.js +9 -6
  52. package/dist/core/patrol/record.d.ts +51 -0
  53. package/dist/core/patrol/state.d.ts +39 -0
  54. package/dist/core/pr.d.ts +3 -0
  55. package/dist/core/pr.js +15 -11
  56. package/dist/core/prompt.d.ts +14 -0
  57. package/dist/core/recorder.d.ts +23 -0
  58. package/dist/core/recorder.js +1 -1
  59. package/dist/core/renderMarkdown.d.ts +7 -0
  60. package/dist/core/replay.d.ts +33 -0
  61. package/dist/core/replay.js +9 -2
  62. package/dist/core/report.d.ts +8 -0
  63. package/dist/core/resolver.d.ts +44 -0
  64. package/dist/core/resolver.js +51 -13
  65. package/dist/core/rng.d.ts +2 -0
  66. package/dist/core/specCompiler.d.ts +17 -0
  67. package/dist/core/studio.d.ts +6 -0
  68. package/dist/core/studio.js +0 -3
  69. package/dist/core/summarize.d.ts +26 -0
  70. package/dist/core/swarm.d.ts +97 -0
  71. package/dist/core/telemetry/index.d.ts +33 -0
  72. package/dist/core/telemetry/index.js +22 -4
  73. package/dist/core/telemetry/sanitize.d.ts +19 -0
  74. package/dist/core/telemetry/types.d.ts +26 -0
  75. package/dist/core/types.d.ts +88 -0
  76. package/dist/core/ui.d.ts +23 -0
  77. package/dist/core/ui.js +0 -5
  78. package/dist/core/validator.d.ts +14 -0
  79. package/dist/core/version.d.ts +1 -0
  80. package/dist/hooks/index.d.ts +106 -0
  81. package/dist/hooks/index.js +349 -0
  82. package/dist/mcp/index.d.ts +58 -0
  83. package/dist/mcp/index.js +295 -0
  84. package/dist/mcp/install.d.ts +52 -0
  85. package/dist/mcp/install.js +204 -0
  86. package/dist/mcp/protocol.d.ts +106 -0
  87. package/dist/mcp/protocol.js +156 -0
  88. package/dist/mcp/tools.d.ts +107 -0
  89. package/dist/mcp/tools.js +621 -0
  90. package/dist/next/index.d.ts +70 -0
  91. package/dist/next/index.js +103 -0
  92. package/dist/plugins/scan.d.ts +85 -0
  93. package/dist/plugins/scan.js +188 -0
  94. package/dist/ui/app.d.ts +11 -0
  95. package/dist/ui/app.js +14 -1
  96. package/dist/vite/index.d.ts +60 -0
  97. package/dist/vite/index.js +56 -0
  98. package/package.json +34 -4
@@ -0,0 +1,54 @@
1
+ /**
2
+ * F10 gate #3 — isolated Git worktree sandbox. A patch is never applied to the
3
+ * user's working tree: it lands in a detached `git worktree`, is verified there,
4
+ * and the only artifact that escapes is a `.patch` file for a human to review
5
+ * and apply. Aztrx never commits — humans do.
6
+ */
7
+ import type { PatchHunk, TestGateResult } from "./types.js";
8
+ export interface Worktree {
9
+ dir: string;
10
+ cleanup: () => Promise<void>;
11
+ }
12
+ export interface ApplyResult {
13
+ ok: boolean;
14
+ patched: string;
15
+ applied: number;
16
+ errors: string[];
17
+ }
18
+ /** Create a detached worktree at HEAD in a temp dir (outside the repo). */
19
+ export declare function createWorktree(repoRoot: string, label: string): Promise<Worktree>;
20
+ /** Apply Search & Replace hunks to an in-memory file. Each `search` must match
21
+ * exactly once; ambiguous or missing matches fail the whole apply (no partial
22
+ * writes). Pure — the caller decides where the result lands. */
23
+ export declare function applyHunks(content: string, hunks: PatchHunk[]): ApplyResult;
24
+ /** Write the patched file into the worktree, refusing to escape it. */
25
+ export declare function writeWorktreeFile(worktreeDir: string, repoRelativePath: string, content: string): string | null;
26
+ /** Produce a unified diff of the patched file against HEAD in the worktree. */
27
+ export declare function diffWorktree(worktreeDir: string, repoRelativePath: string): Promise<string>;
28
+ /** Run `tsc --noEmit` against the patched worktree — the full type check that
29
+ * follows the AST syntax gate. Best-effort: passes (skips) when the repo has no
30
+ * TypeScript or the worktree has no tsconfig, so non-TS projects aren't blocked.
31
+ * The worktree has no node_modules; a symlink to the root's is created first and
32
+ * removed with the worktree on cleanup.
33
+ *
34
+ * Unlike every other gate here, this one is whole-project work: `tsc` type-checks
35
+ * the entire repository, so its cost scales with the repo, and on a very large
36
+ * one it is minutes. It therefore carries a ceiling — and a timeout is reported
37
+ * as *skipped*, not failed. "The compiler never reached a verdict" is a different
38
+ * sentence from "the patch is broken", and conflating them would silently discard
39
+ * good fixes on exactly the large repos this ceiling exists to protect. */
40
+ export declare function typecheckWorktree(worktreeDir: string, repoRoot: string, opts?: {
41
+ timeoutMs?: number;
42
+ }): Promise<{
43
+ ok: boolean;
44
+ ran: boolean;
45
+ output: string;
46
+ }>;
47
+ /** Run the repo's own test suite inside the patched worktree. Best-effort: skips
48
+ * (passes by omission) when there is no `test` script to run, so untested or
49
+ * non-JS projects are never blocked. `CI=true` is set so watch-mode runners exit
50
+ * instead of hanging until the timeout. */
51
+ export declare function runTests(worktreeDir: string, repoRoot: string, opts?: {
52
+ command?: string;
53
+ timeoutMs?: number;
54
+ }): Promise<TestGateResult>;
@@ -76,8 +76,15 @@ export async function diffWorktree(worktreeDir, repoRelativePath) {
76
76
  * follows the AST syntax gate. Best-effort: passes (skips) when the repo has no
77
77
  * TypeScript or the worktree has no tsconfig, so non-TS projects aren't blocked.
78
78
  * The worktree has no node_modules; a symlink to the root's is created first and
79
- * removed with the worktree on cleanup. */
80
- export async function typecheckWorktree(worktreeDir, repoRoot) {
79
+ * removed with the worktree on cleanup.
80
+ *
81
+ * Unlike every other gate here, this one is whole-project work: `tsc` type-checks
82
+ * the entire repository, so its cost scales with the repo, and on a very large
83
+ * one it is minutes. It therefore carries a ceiling — and a timeout is reported
84
+ * as *skipped*, not failed. "The compiler never reached a verdict" is a different
85
+ * sentence from "the patch is broken", and conflating them would silently discard
86
+ * good fixes on exactly the large repos this ceiling exists to protect. */
87
+ export async function typecheckWorktree(worktreeDir, repoRoot, opts = {}) {
81
88
  const tscBin = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc");
82
89
  const hasTsconfig = fs.existsSync(path.join(worktreeDir, "tsconfig.json"));
83
90
  if (!fs.existsSync(tscBin) || !hasTsconfig) {
@@ -93,12 +100,22 @@ export async function typecheckWorktree(worktreeDir, repoRoot) {
93
100
  /* symlink failed — tsc reports its own resolution errors below */
94
101
  }
95
102
  }
103
+ const timeoutMs = opts.timeoutMs ?? 300000;
96
104
  try {
97
- const { stdout } = await execFileP(process.execPath, [tscBin, "--noEmit", "-p", worktreeDir], { cwd: worktreeDir, maxBuffer: 10 * 1024 * 1024, env: buildChildEnv() });
105
+ const { stdout } = await execFileP(process.execPath, [tscBin, "--noEmit", "-p", worktreeDir], { cwd: worktreeDir, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, env: buildChildEnv() });
98
106
  return { ok: true, ran: true, output: stdout.trim() };
99
107
  }
100
108
  catch (e) {
101
109
  const err = e;
110
+ // A timeout kills the child; that is not a compile error. Skip the gate and
111
+ // say why, rather than reporting a verdict the compiler never gave.
112
+ if (err.killed || err.signal) {
113
+ return {
114
+ ok: true,
115
+ ran: false,
116
+ output: `tsc did not finish within ${timeoutMs}ms — type check skipped`,
117
+ };
118
+ }
102
119
  return { ok: false, ran: true, output: ((err.stdout ?? "") + (err.stderr ?? "")).trim() };
103
120
  }
104
121
  }
@@ -0,0 +1,102 @@
1
+ import type { Finding, RecordedAction } from "../types.js";
2
+ export type HealStatus = "healed" | "unfixed" | "rejected" | "compile-failed" | "apply-failed" | "test-failed" | "skipped" | "no-llm" | "budget-exhausted";
3
+ /** One Search & Replace edit. `search` is an exact, unique substring of the
4
+ * source file; `replace` is its replacement. */
5
+ export interface PatchHunk {
6
+ search: string;
7
+ replace: string;
8
+ }
9
+ export interface Patch {
10
+ explanation: string;
11
+ hunks: PatchHunk[];
12
+ }
13
+ export interface GateViolation {
14
+ rule: string;
15
+ detail: string;
16
+ }
17
+ export interface GateResult {
18
+ ok: boolean;
19
+ violations: GateViolation[];
20
+ }
21
+ /** Context handed to the patch generator. `redactedContent` is what may leave
22
+ * the machine; `fileContent` is the raw bytes used for applying the edit. */
23
+ export interface HealContext {
24
+ finding: Finding;
25
+ filePath: string;
26
+ fileContent: string;
27
+ redactedContent: string;
28
+ }
29
+ /** Shared, mutable cap on paid LLM generations across a whole patrol session.
30
+ * `generatePatch` decrements `remaining` each time it pays for a completion; once
31
+ * it hits 0 generation stops and heal reports `budget-exhausted`. The free
32
+ * rule-based fixer never touches it. One object is threaded through every
33
+ * `run()`/`heal()` of a session so the cap spans cycles, not just one run. */
34
+ export interface SpendBudget {
35
+ remaining: number;
36
+ }
37
+ export interface VerifyResult {
38
+ runs: number;
39
+ reproductions: number;
40
+ /** Of `runs`, how many actually loaded the app. `fixed` requires this to be
41
+ * non-zero: a run whose page never loaded proves nothing, and counting it as a
42
+ * non-reproduction is how a patch gets declared verified without being tried. */
43
+ loaded: number;
44
+ fixed: boolean;
45
+ }
46
+ export interface TestGateResult {
47
+ /** False when there was no test script to run — the gate passes by omission. */
48
+ ran: boolean;
49
+ ok: boolean;
50
+ command: string;
51
+ /** Tail of the test output, already truncated. */
52
+ output: string;
53
+ }
54
+ export interface HealOptions {
55
+ repoRoot: string;
56
+ url: string;
57
+ /** Minimized repro actions (F7 output). */
58
+ actions: RecordedAction[];
59
+ fingerprint: string;
60
+ allowHosts: string[];
61
+ /** Fallback model (the last tier tried). Defaults to `claude-sonnet-5`. */
62
+ model?: string;
63
+ /** Fast/cheap first tier. Defaults to `claude-haiku-4-5` (`AZTRX_FAST_MODEL`). */
64
+ fastModel?: string;
65
+ /** Inject a patch generator for testing/demo (bypasses the network LLM). */
66
+ patchFn?: (ctx: HealContext) => Promise<Patch>;
67
+ /** Shared session budget for paid generations (see `SpendBudget`). */
68
+ budget?: SpendBudget;
69
+ /** Inject an app server for the patched code. Default: static file server. */
70
+ serve?: (worktreeDir: string, filePath: string) => Promise<{
71
+ url: string;
72
+ close: () => Promise<void>;
73
+ }>;
74
+ /** How to boot the patched app for server (network_5xx) findings. Auto-detected
75
+ * from package.json `scripts.dev` / `scripts.start` when omitted. */
76
+ startCommand?: string;
77
+ verifyRuns?: number;
78
+ /** Override the test command run against a healed patch (default: `npm test`,
79
+ * auto-detected from package.json). */
80
+ testCommand?: string;
81
+ /** Timeout for the test gate, ms (default 300000). */
82
+ testTimeoutMs?: number;
83
+ /** Skip the test gate entirely. */
84
+ skipTest?: boolean;
85
+ }
86
+ export interface HealResult {
87
+ status: HealStatus;
88
+ findingId: string;
89
+ filePath: string;
90
+ explanation?: string;
91
+ hunks: PatchHunk[];
92
+ violations: GateViolation[];
93
+ verification?: VerifyResult;
94
+ test?: TestGateResult;
95
+ /** Path to the saved unified-diff patch artifact (.aztrx/heal/…, gitignored). */
96
+ patchPath?: string;
97
+ error?: string;
98
+ /** Model that produced the returned patch (the winning tier). */
99
+ model?: string;
100
+ /** All tiers attempted, in order, for observability in the PR bot. */
101
+ tiers?: string[];
102
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * F10 — automated verification. Reuses the repro engine (F9) but inverts the
3
+ * question: the bug must *stop* reproducing against the patched code. If the
4
+ * fingerprint is still seen after the fix, the loop rejects the patch rather
5
+ * than handing the human a lie.
6
+ */
7
+ import type { ReplayResult } from "../replay.js";
8
+ import type { FindingType, RecordedAction } from "../types.js";
9
+ import type { VerifyResult } from "./types.js";
10
+ /** The slice of the replay engine verification depends on. Production passes a
11
+ * real browser-backed `ReplayEngine`; tests inject a stub so the pass/fail logic
12
+ * is exercised without launching Chromium. */
13
+ export interface Verifier {
14
+ run(url: string, actions: RecordedAction[], targetFingerprint: string, opts?: {
15
+ targetType?: FindingType;
16
+ }): Promise<ReplayResult>;
17
+ close(): Promise<void>;
18
+ }
19
+ export interface VerifyOptions {
20
+ /** The URL the finding was recorded against. Retained for callers/reporting;
21
+ * replays run against the served URL, never this origin. */
22
+ url: string;
23
+ actions: RecordedAction[];
24
+ fingerprint: string;
25
+ runs?: number;
26
+ /** Start the patched app and return its URL + a close hook. */
27
+ serve: () => Promise<{
28
+ url: string;
29
+ close: () => Promise<void>;
30
+ }>;
31
+ /** For network findings: verify by signal type (origin-agnostic) instead of the
32
+ * exact fingerprint. */
33
+ targetType?: FindingType;
34
+ /** Replay engine override — tests only. Defaults to a real browser-backed one. */
35
+ engine?: Verifier;
36
+ }
37
+ export declare function verifyFix(opts: VerifyOptions): Promise<VerifyResult>;
@@ -23,29 +23,43 @@ function rewriteOrigin(raw, serveUrl) {
23
23
  }
24
24
  export async function verifyFix(opts) {
25
25
  const { url: serveUrl, close } = await opts.serve();
26
- const engine = new ReplayEngine();
26
+ const engine = opts.engine ?? new ReplayEngine();
27
27
  try {
28
- const runs = opts.runs ?? 3;
28
+ // Never zero. `fixed` is derived from the reproduction count, so a caller that
29
+ // asks for 0 runs would otherwise get "fixed: true" out of *no attempts at
30
+ // all* — a patch declared verified on the strength of never having been run.
31
+ const runs = Math.max(1, Math.trunc(opts.runs ?? 3));
29
32
  let reproductions = 0;
30
- const actions = opts.targetType
31
- ? opts.actions.map((a) => {
32
- if (a.type === "request" && a.request) {
33
- return { ...a, request: { ...a.request, url: rewriteOrigin(a.request.url, serveUrl) } };
34
- }
35
- if (a.type === "navigate" && a.value) {
36
- return { ...a, value: rewriteOrigin(a.value, serveUrl) };
37
- }
38
- return a;
39
- })
40
- : opts.actions;
33
+ let loaded = 0;
34
+ // Every recorded URL is rewritten to the served origin — for client findings
35
+ // too, not just network ones. The repro was recorded against the original
36
+ // app, so a `navigate` action left as-is would send the replay straight back
37
+ // to the *unpatched* server, where the bug reproduces no matter what the
38
+ // patch says. That reads as "unfixed" at best; when the fingerprint happens
39
+ // to be absent there, it reads as a verified fix that was never exercised.
40
+ const actions = opts.actions.map((a) => {
41
+ if (a.type === "request" && a.request) {
42
+ return { ...a, request: { ...a.request, url: rewriteOrigin(a.request.url, serveUrl) } };
43
+ }
44
+ if (a.type === "navigate" && a.value) {
45
+ return { ...a, value: rewriteOrigin(a.value, serveUrl) };
46
+ }
47
+ return a;
48
+ });
41
49
  for (let i = 0; i < runs; i++) {
42
50
  const res = opts.targetType
43
51
  ? await engine.run(serveUrl, actions, opts.fingerprint, { targetType: opts.targetType })
44
52
  : await engine.run(serveUrl, actions, opts.fingerprint);
53
+ if (res.loaded)
54
+ loaded += 1;
45
55
  if (res.reproduced)
46
56
  reproductions += 1;
47
57
  }
48
- return { runs, reproductions, fixed: reproductions === 0 };
58
+ // "Did not reproduce" only means "fixed" if the app was actually there to
59
+ // reproduce against. If no run loaded, this verification proved nothing about
60
+ // the patch — so it must not report success, or the caller writes an
61
+ // unverified patch into the user's working tree on the strength of it.
62
+ return { runs, reproductions, loaded, fixed: loaded > 0 && reproductions === 0 };
49
63
  }
50
64
  finally {
51
65
  await engine.close();
@@ -0,0 +1,33 @@
1
+ import type { Page } from "playwright";
2
+ import type { EventBus } from "./eventBus.js";
3
+ export interface HttpFuzzOptions {
4
+ maxRequests?: number;
5
+ dryRun?: boolean;
6
+ /** Hostnames the fuzzer may target — the Node-side self-enforcement of the
7
+ * deny-by-default policy (the browser `networkGuard` only wraps the `Page`). */
8
+ allowHosts?: Set<string>;
9
+ /** Opt-in for POST/PUT body mutations. Default false: the fuzzer sends only
10
+ * GET requests (query mutations + hostile GET headers), which can't mutate
11
+ * server state. Set this to also probe JSON-body type-confusion and method
12
+ * confusion — only on endpoints you own and trust to be non-destructive. */
13
+ mutations?: boolean;
14
+ /** Opt-in: also probe endpoints the deny-list skips (delete/pay/logout/…).
15
+ * Off by default — these can mutate real state. */
16
+ allowDestructive?: boolean;
17
+ /** Endpoints observed live during a walk/fuzz pass (e.g. a `fetch()` fired
18
+ * from a click handler) that `collectEndpoints` can't see on a fresh page.
19
+ * Each is pushed through the same origin/static/destructive filters. */
20
+ seedUrls?: string[];
21
+ /** When false, skip the `page.goto` — the page is already loaded and
22
+ * positioned by the caller (folded post-walk pass). Default true. */
23
+ navigate?: boolean;
24
+ }
25
+ /**
26
+ * F5-http — server-side mutation fuzzer. Harvests the endpoints the app really
27
+ * uses, then throws seeded hostile requests at them (query overflow, JSON type
28
+ * confusion, header injection, method confusion). A response `status >= 500`
29
+ * becomes a `network_5xx` finding — same type and message format the interceptor
30
+ * emits for in-browser requests — so it flows through the existing
31
+ * ddmin → spec → validate → heal pipeline unchanged.
32
+ */
33
+ export declare function httpFuzz(page: Page, targetUrl: string, bus: EventBus, opts?: HttpFuzzOptions): Promise<number>;
@@ -0,0 +1,21 @@
1
+ export interface InitOptions {
2
+ repoRoot: string;
3
+ url?: string;
4
+ framework?: string;
5
+ }
6
+ export interface InitResult {
7
+ framework: string;
8
+ url: string;
9
+ configPath: string;
10
+ gitignoreUpdated: boolean;
11
+ }
12
+ export interface FrameworkMeta {
13
+ framework: string;
14
+ version?: string;
15
+ }
16
+ /** Detect the framework name plus its installed version range. */
17
+ export declare function detectFrameworkMeta(repoRoot: string): FrameworkMeta;
18
+ /** The port a framework's dev server listens on by default. */
19
+ export declare function defaultPort(framework: string): number;
20
+ /** `aztrx-cli init` — detect framework + port, scaffold aztrx.config.ts, seed .gitignore. */
21
+ export declare function initProject(opts: InitOptions): Promise<InitResult>;
package/dist/core/init.js CHANGED
@@ -31,7 +31,8 @@ export function detectFrameworkMeta(repoRoot) {
31
31
  function detectFramework(repoRoot) {
32
32
  return detectFrameworkMeta(repoRoot).framework;
33
33
  }
34
- function defaultPort(framework) {
34
+ /** The port a framework's dev server listens on by default. */
35
+ export function defaultPort(framework) {
35
36
  if (["Vite", "Svelte", "SvelteKit", "Vue", "Astro"].includes(framework))
36
37
  return 5173;
37
38
  return 3000;
@@ -44,13 +45,11 @@ export default {
44
45
  // Dev server Aztrx should attack.
45
46
  url: ${JSON.stringify(url)},
46
47
 
47
- // Repo root for sourcemap source resolution (defaults to this directory).
48
- repo: ".",
49
-
50
- // Max interactions per pass.
48
+ // Max interactions per pass (default: 100). --max-actions overrides this.
51
49
  maxActions: 100,
52
50
 
53
- // Deny-by-default network allow-list for fuzz runs — add your API host here.
51
+ // Deny-by-default network allow-list — add your API host here, or every
52
+ // cross-origin request the app makes is refused. --allow-host adds to this.
54
53
  allowHosts: [],
55
54
  };
56
55
  `;
@@ -0,0 +1,21 @@
1
+ import type { Page } from "playwright";
2
+ import type { EventBus } from "./eventBus.js";
3
+ /**
4
+ * Canonical 5xx finding message. Shared by the interceptor (passive in-browser
5
+ * capture) and the HTTP mutation fuzzer (active Node-side capture) so the two
6
+ * produce byte-identical `rawMessage`s — otherwise a finding's fingerprint and
7
+ * its replay fingerprint would diverge and repro would never match.
8
+ */
9
+ export declare function network5xxMessage(status: number, url: string): string;
10
+ /**
11
+ * F1 — CDP interceptor. Attaches capture to a page and emits typed
12
+ * `telemetry` events on the bus. No framework hooks: works on React
13
+ * (17/18/19), Next.js, Vite, Svelte, Remix, and Vue alike.
14
+ *
15
+ * The subtle part: `console.error` is the ONLY runtime-level way to see
16
+ * errors a React Error Boundary swallows, because the boundary logs them
17
+ * there instead of rethrowing — so `window.onerror` / `pageerror` never fire
18
+ * for them. We pull the real throw-site stack off the Error *object* via
19
+ * `msg.args()`, not `msg.text()` (which is just the message, no stack).
20
+ */
21
+ export declare function attachInterceptor(page: Page, bus: EventBus): void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Provider-agnostic LLM client. Two transports behind one `complete()`:
3
+ *
4
+ * - Anthropic Messages API (default) — key from `ANTHROPIC_API_KEY` (or
5
+ * `AZTRX_API_KEY`), primary model defaults to `claude-sonnet-5`.
6
+ * - Any OpenAI-compatible `/chat/completions` endpoint — selected by setting
7
+ * `AZTRX_API_BASE`. That single flag unlocks OpenAI, Grok, DeepSeek, Gemini,
8
+ * Kimi, Mistral, OpenRouter, and local models (Ollama / vLLM / LM Studio)
9
+ * with their existing keys.
10
+ *
11
+ * `AZTRX_MODEL` / `AZTRX_FAST_MODEL` pick the model(s); they default sensibly
12
+ * for Anthropic and must be set explicitly when a custom base URL is used.
13
+ */
14
+ export type Provider = "anthropic" | "openai";
15
+ export interface LlmSettings {
16
+ provider: Provider;
17
+ apiKey?: string;
18
+ baseUrl?: string;
19
+ }
20
+ /** Resolve the active provider from the environment. */
21
+ export declare function resolveSettings(): LlmSettings;
22
+ /** Is any provider configured? Used to decide whether to attempt an LLM call. */
23
+ export declare function hasLlmKey(): boolean;
24
+ /** The primary model for the active provider. */
25
+ export declare function primaryModel(): string;
26
+ /** The cheap/fast first tier, or undefined when the provider has none. */
27
+ export declare function fastModel(): string | undefined;
28
+ export interface CompleteOptions {
29
+ system: string;
30
+ prompt: string;
31
+ model?: string;
32
+ maxTokens?: number;
33
+ temperature?: number;
34
+ }
35
+ /** Run one completion against the active provider and return the text. */
36
+ export declare function complete(opts: CompleteOptions): Promise<string>;
package/dist/core/llm.js CHANGED
@@ -44,14 +44,6 @@ export function fastModel() {
44
44
  return process.env.AZTRX_FAST_MODEL || "claude-haiku-4-5-20251001";
45
45
  return process.env.AZTRX_FAST_MODEL || undefined;
46
46
  }
47
- /** Human-readable description of the active provider + model, e.g. `grok-2 via https://api.x.ai/v1`. */
48
- export function describeLlm(model) {
49
- const s = resolveSettings();
50
- const m = model || primaryModel();
51
- if (s.provider === "anthropic")
52
- return `${m} (Anthropic)`;
53
- return `${m} via ${s.baseUrl}`;
54
- }
55
47
  // Announce the resolved model once per distinct (provider, model), so the two-tier
56
48
  // router shows each tier as it's tried without spamming. Written to stderr so it never
57
49
  // corrupts the Ink TUI (which renders on stdout).
@@ -0,0 +1,13 @@
1
+ import type { RecordedAction } from "./types.js";
2
+ import type { ReplayEngine } from "./replay.js";
3
+ export interface MinimizeOptions {
4
+ url: string;
5
+ fingerprint: string;
6
+ maxReplays?: number;
7
+ }
8
+ /**
9
+ * F7 — ddmin (delta debugging). Shrinks the failing action sequence to a
10
+ * minimal subset that still reproduces `fingerprint`. Best-effort under a
11
+ * replay budget; a sequence shorter than 2 actions is returned as-is.
12
+ */
13
+ export declare function minimize(engine: ReplayEngine, actions: RecordedAction[], opts: MinimizeOptions): Promise<RecordedAction[]>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * F-modernize — the "code translator". Rewrites a legacy JS/TS file into modern
3
+ * idiomatic form (const/let over var, async/await over callbacks and promise
4
+ * chains, arrow functions, optional chaining) while preserving behavior. This is
5
+ * a *static* transform, unlike the rest of Aztrx's runtime detection, so it's its
6
+ * own command rather than a `run` flag.
7
+ *
8
+ * Safety model: the model's output is gated by a re-parse (`ts.transpileModule`
9
+ * reports syntax errors without running a full tsc), and the caller applies it to
10
+ * the working tree only after the user confirms — never automatically.
11
+ */
12
+ export type Lang = "ts" | "js";
13
+ export declare function detectLang(filePath: string): Lang | null;
14
+ export interface ParseGateResult {
15
+ ok: boolean;
16
+ errors: string[];
17
+ }
18
+ /** Syntax gate: does the output still parse? In-process (no tsc subprocess). */
19
+ export declare function parseGate(source: string): ParseGateResult;
20
+ export interface ModernizeResult {
21
+ ok: boolean;
22
+ original: string;
23
+ modernized?: string;
24
+ changes: string[];
25
+ lang?: Lang;
26
+ error?: string;
27
+ }
28
+ export declare function modernizeFile(repoRoot: string, filePath: string): Promise<ModernizeResult>;
@@ -22,7 +22,7 @@ export function detectLang(filePath) {
22
22
  return null;
23
23
  }
24
24
  /** Syntax gate: does the output still parse? In-process (no tsc subprocess). */
25
- export function parseGate(source, lang) {
25
+ export function parseGate(source) {
26
26
  const result = ts.transpileModule(source, {
27
27
  compilerOptions: {
28
28
  target: ts.ScriptTarget.ES2022,
@@ -110,7 +110,7 @@ export async function modernizeFile(repoRoot, filePath) {
110
110
  if (!parsed.modernized.trim()) {
111
111
  return { ok: false, original, changes: [], lang, error: "model returned an empty file" };
112
112
  }
113
- const gate = parseGate(parsed.modernized, lang);
113
+ const gate = parseGate(parsed.modernized);
114
114
  if (!gate.ok) {
115
115
  return {
116
116
  ok: false,
@@ -0,0 +1,13 @@
1
+ import type { Page } from "playwright";
2
+ export interface NetworkGuardOptions {
3
+ allowHosts: Set<string>;
4
+ onBlock?: (url: string) => void;
5
+ }
6
+ /**
7
+ * F6 — deny-by-default network guard (PRD §6.2). Aborts every request whose
8
+ * host isn't allow-listed, so a fuzz/replay pass can't reach payment, delete,
9
+ * or analytics endpoints. Loopback is always allowed (this is a local tool).
10
+ */
11
+ export declare function attachNetworkGuard(page: Page, opts: NetworkGuardOptions): Promise<void>;
12
+ /** Builds the allow-list: target origin + each `--allow-host`. */
13
+ export declare function allowHostsFrom(url: string, extra: string[]): Set<string>;
@@ -0,0 +1,86 @@
1
+ import { EventBus } from "./eventBus.js";
2
+ import type { Finding } from "./types.js";
3
+ import type { SpendBudget } from "./heal/types.js";
4
+ export interface RunOptions {
5
+ url: string;
6
+ repoRoot: string;
7
+ maxActions?: number;
8
+ dryRun?: boolean;
9
+ crashTest?: boolean;
10
+ fuzz?: boolean;
11
+ /** F5-http: mutate the target origin with hostile HTTP requests (server-side
12
+ * attack surface). Runs after the walk/fuzz pass, before repro/heal. */
13
+ httpFuzz?: boolean;
14
+ /** With `httpFuzz`, also send POST/PUT body mutations (default: GET-only). */
15
+ httpFuzzMutations?: boolean;
16
+ /** Opt-in: include destructive controls/endpoints (delete/pay/logout/checkout…).
17
+ * Off by default — these can mutate real state, so they're refused unless the
18
+ * caller explicitly accepts the risk (loudly warned). */
19
+ allowDestructive?: boolean;
20
+ /** Language for the per-finding diagnosis headline (en/ru). Default: en. */
21
+ lang?: string;
22
+ repro?: boolean;
23
+ seed?: number;
24
+ /** F-swarm: number of parallel detection workers (default 1). `--swarm` = auto. */
25
+ workers?: number;
26
+ allowHosts?: string[];
27
+ reproRuns?: number;
28
+ /** Path to a Playwright storage-state JSON (cookies + localStorage) so the
29
+ * session starts authenticated. Produced by `playwright codegen --save-storage`. */
30
+ storageState?: string;
31
+ /** F-auth: auto-login before the pass. Needs `loginEmail` + `loginPassword`. */
32
+ login?: boolean;
33
+ loginEmail?: string;
34
+ loginPassword?: string;
35
+ /** Explicit login page URL (default: the current page, after any auth-wall redirect). */
36
+ loginUrl?: string;
37
+ /** F10: attempt closed-loop healing for crash/error findings. Needs
38
+ * `ANTHROPIC_API_KEY` (or an injected patchFn) and `repro: true`. */
39
+ heal?: boolean;
40
+ /** Fingerprints already handled (PR opened / marked unfixable) by an
41
+ * autonomous supervisor — skip healing them so a re-scan doesn't re-burn the
42
+ * LLM on a bug we've already acted on. */
43
+ skipHealFingerprints?: string[];
44
+ /** Shared, mutable session cap on paid LLM generations (see `SpendBudget`).
45
+ * Passed through to every heal call; a patrol session creates one budget and
46
+ * reuses it across cycles. */
47
+ budget?: SpendBudget;
48
+ /** LLM model override for healing (the fallback tier). */
49
+ healModel?: string;
50
+ /** Fast/cheap first tier for the Smart Cloud Router (`AZTRX_FAST_MODEL`). */
51
+ healFastModel?: string;
52
+ /** Override the test command run against a healed patch (default: `npm test`). */
53
+ testCommand?: string;
54
+ /** Timeout for the heal test gate, ms. */
55
+ testTimeoutMs?: number;
56
+ /** Skip the heal test gate. */
57
+ skipTest?: boolean;
58
+ /** How to boot the app for server-side healing (network_5xx findings).
59
+ * Auto-detected from package.json `scripts.dev` / `scripts.start` when omitted. */
60
+ startCommand?: string;
61
+ /** F11: collect + persist anonymized telemetry locally (opt-in). */
62
+ telemetry?: boolean;
63
+ /** F11: additionally upload the sanitized tuple to the telemetry endpoint. */
64
+ shareData?: boolean;
65
+ /** Override the telemetry endpoint (`AZTRX_TELEMETRY_URL`). */
66
+ telemetryUrl?: string;
67
+ /** F12: upload the run's findings to the Aztrx AI cloud dashboard (opt-in). */
68
+ upload?: boolean;
69
+ /** API key for cloud + telemetry uploads (`AZTRX_CLOUD_API_KEY`). */
70
+ apiKey?: string;
71
+ /** Override the cloud ingest base URL (`AZTRX_CLOUD_URL`). */
72
+ cloudUrl?: string;
73
+ /** Inject an external bus (the TUI subscribes to it). */
74
+ bus?: EventBus;
75
+ /** When true, suppress console output — the caller renders from bus events. */
76
+ ui?: boolean;
77
+ }
78
+ /**
79
+ * The run's finite state machine: launch → (guard) → intercept → act (walk or
80
+ * fuzz) → classify → map → report → repro (minimize/compile/validate). Modules
81
+ * communicate only through the EventBus; the orchestrator is the single place
82
+ * that wires them together. In `ui` mode it emits structured events (phase,
83
+ * action, finding, repro, route, noise) and stays silent on stdout, so a
84
+ * terminal renderer (Ink) can draw the live panel instead of log lines.
85
+ */
86
+ export declare function run(options: RunOptions): Promise<Finding[]>;