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.
- package/README.md +325 -28
- package/dist/cli/help.d.ts +22 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +278 -81
- package/dist/core/auth.d.ts +33 -0
- package/dist/core/badge.d.ts +9 -0
- package/dist/core/browser.d.ts +3 -0
- package/dist/core/classifier.d.ts +41 -0
- package/dist/core/cloud/index.d.ts +62 -0
- package/dist/core/cloud/index.js +24 -4
- package/dist/core/devServer.d.ts +90 -0
- package/dist/core/devServer.js +253 -0
- package/dist/core/diagnose.d.ts +18 -0
- package/dist/core/diff.d.ts +31 -0
- package/dist/core/domWalker.d.ts +24 -0
- package/dist/core/domWalker.js +1 -1
- package/dist/core/eventBus.d.ts +59 -0
- package/dist/core/events.d.ts +42 -0
- package/dist/core/fixPr.d.ts +16 -0
- package/dist/core/fixPr.js +11 -1
- package/dist/core/fuzzer.d.ts +24 -0
- package/dist/core/fuzzer.js +1 -1
- package/dist/core/heal/apply.d.ts +28 -0
- package/dist/core/heal/boot.d.ts +74 -0
- package/dist/core/heal/boot.js +126 -23
- package/dist/core/heal/childEnv.d.ts +15 -0
- package/dist/core/heal/gates.d.ts +11 -0
- package/dist/core/heal/index.d.ts +19 -0
- package/dist/core/heal/index.js +53 -23
- package/dist/core/heal/llm.d.ts +43 -0
- package/dist/core/heal/redact.d.ts +22 -0
- package/dist/core/heal/sandbox.d.ts +54 -0
- package/dist/core/heal/sandbox.js +20 -3
- package/dist/core/heal/types.d.ts +102 -0
- package/dist/core/heal/verify.d.ts +37 -0
- package/dist/core/heal/verify.js +28 -14
- package/dist/core/httpFuzzer.d.ts +33 -0
- package/dist/core/init.d.ts +21 -0
- package/dist/core/init.js +5 -6
- package/dist/core/interceptor.d.ts +21 -0
- package/dist/core/llm.d.ts +36 -0
- package/dist/core/llm.js +0 -8
- package/dist/core/minimizer.d.ts +13 -0
- package/dist/core/modernize.d.ts +28 -0
- package/dist/core/modernize.js +2 -2
- package/dist/core/networkGuard.d.ts +13 -0
- package/dist/core/orchestrator.d.ts +86 -0
- package/dist/core/orchestrator.js +24 -9
- package/dist/core/patrol/loop.d.ts +46 -0
- package/dist/core/patrol/pr.d.ts +42 -0
- package/dist/core/patrol/pr.js +9 -6
- package/dist/core/patrol/record.d.ts +51 -0
- package/dist/core/patrol/state.d.ts +39 -0
- package/dist/core/pr.d.ts +3 -0
- package/dist/core/pr.js +15 -11
- package/dist/core/prompt.d.ts +14 -0
- package/dist/core/recorder.d.ts +23 -0
- package/dist/core/recorder.js +1 -1
- package/dist/core/renderMarkdown.d.ts +7 -0
- package/dist/core/replay.d.ts +33 -0
- package/dist/core/replay.js +9 -2
- package/dist/core/report.d.ts +8 -0
- package/dist/core/resolver.d.ts +44 -0
- package/dist/core/resolver.js +51 -13
- package/dist/core/rng.d.ts +2 -0
- package/dist/core/specCompiler.d.ts +17 -0
- package/dist/core/studio.d.ts +6 -0
- package/dist/core/studio.js +0 -3
- package/dist/core/summarize.d.ts +26 -0
- package/dist/core/swarm.d.ts +97 -0
- package/dist/core/telemetry/index.d.ts +33 -0
- package/dist/core/telemetry/index.js +22 -4
- package/dist/core/telemetry/sanitize.d.ts +19 -0
- package/dist/core/telemetry/types.d.ts +26 -0
- package/dist/core/types.d.ts +88 -0
- package/dist/core/ui.d.ts +23 -0
- package/dist/core/ui.js +0 -5
- package/dist/core/validator.d.ts +14 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/hooks/index.d.ts +106 -0
- package/dist/hooks/index.js +349 -0
- package/dist/mcp/index.d.ts +58 -0
- package/dist/mcp/index.js +295 -0
- package/dist/mcp/install.d.ts +52 -0
- package/dist/mcp/install.js +204 -0
- package/dist/mcp/protocol.d.ts +106 -0
- package/dist/mcp/protocol.js +156 -0
- package/dist/mcp/tools.d.ts +107 -0
- package/dist/mcp/tools.js +621 -0
- package/dist/next/index.d.ts +70 -0
- package/dist/next/index.js +103 -0
- package/dist/plugins/scan.d.ts +85 -0
- package/dist/plugins/scan.js +188 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +14 -1
- package/dist/vite/index.d.ts +60 -0
- package/dist/vite/index.js +56 -0
- 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
|
-
|
|
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>;
|
package/dist/core/heal/verify.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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>;
|
package/dist/core/modernize.js
CHANGED
|
@@ -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
|
|
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
|
|
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[]>;
|