argus-reviewer-e2e 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -71
- package/action/action.yml +129 -11
- package/action/approval-review.mjs +13 -3
- package/action/bootstrap.mjs +2 -0
- package/action/emit-review.mjs +16 -0
- package/action/runtime.mjs +20 -0
- package/action/sticky-comment.cjs +1260 -479
- package/dist/cli.d.ts +103 -7
- package/dist/cli.js +1202 -186
- package/dist/config.d.ts +96 -11
- package/dist/config.js +102 -4
- package/dist/detect.d.ts +29 -2
- package/dist/detect.js +98 -7
- package/dist/driver/browser.d.ts +32 -0
- package/dist/driver/browser.js +56 -1
- package/dist/driver/target.d.ts +4 -1
- package/dist/driver/target.js +27 -6
- package/dist/engine/actions.d.ts +5 -0
- package/dist/engine/actions.js +8 -0
- package/dist/engine/explore.d.ts +78 -0
- package/dist/engine/explore.js +373 -0
- package/dist/engine/loop.d.ts +2 -2
- package/dist/engine/loop.js +8 -8
- package/dist/engine/prompts.d.ts +28 -1
- package/dist/engine/prompts.js +88 -0
- package/dist/evidence/ci.d.ts +13 -1
- package/dist/evidence/ci.js +38 -3
- package/dist/evidence/gate.d.ts +8 -0
- package/dist/evidence/gate.js +1 -1
- package/dist/evidence/link.js +1 -1
- package/dist/executor/a0.d.ts +114 -1
- package/dist/executor/a0.js +216 -4
- package/dist/fsutil.d.ts +3 -2
- package/dist/fsutil.js +7 -4
- package/dist/journal/schema.d.ts +1 -1
- package/dist/log.d.ts +2 -1
- package/dist/log.js +10 -2
- package/dist/mention.d.ts +45 -0
- package/dist/mention.js +107 -0
- package/dist/pipeline/app.d.ts +126 -0
- package/dist/pipeline/app.js +250 -0
- package/dist/pipeline/budget.d.ts +1 -0
- package/dist/pipeline/budget.js +1 -1
- package/dist/pipeline/verify.d.ts +20 -3
- package/dist/pipeline/verify.js +189 -35
- package/dist/probe/persist.d.ts +68 -0
- package/dist/probe/persist.js +184 -0
- package/dist/probe/queue.d.ts +12 -0
- package/dist/probe/queue.js +10 -2
- package/dist/report/brand-assets.generated.d.ts +9 -0
- package/dist/report/brand-assets.generated.js +8 -0
- package/dist/report/comment.d.ts +99 -6
- package/dist/report/comment.js +292 -103
- package/dist/report/html.d.ts +50 -0
- package/dist/report/html.js +879 -0
- package/dist/report/manifest.d.ts +29 -0
- package/dist/report/manifest.js +37 -0
- package/dist/report/run.d.ts +54 -1
- package/dist/report/run.js +34 -9
- package/dist/report/viewmodel.d.ts +91 -0
- package/dist/report/viewmodel.js +241 -0
- package/dist/review/adjudicate.d.ts +6 -6
- package/dist/review/adjudicate.js +2 -2
- package/dist/review/inline.d.ts +44 -0
- package/dist/review/inline.js +95 -0
- package/dist/review/packs.d.ts +21 -0
- package/dist/review/packs.js +47 -0
- package/dist/review/scope.d.ts +16 -0
- package/dist/review/scope.js +74 -0
- package/dist/review/secrets.d.ts +10 -10
- package/dist/review/secrets.js +7 -7
- package/dist/review/testfiles.d.ts +18 -0
- package/dist/review/testfiles.js +26 -0
- package/dist/review/triage.d.ts +1 -1
- package/dist/review/triage.js +10 -10
- package/dist/review/validate.d.ts +41 -0
- package/dist/review/validate.js +76 -0
- package/dist/ui/errors.d.ts +54 -0
- package/dist/ui/errors.js +236 -0
- package/dist/ui/style.d.ts +34 -0
- package/dist/ui/style.js +48 -0
- package/dist/ui/summary.d.ts +38 -0
- package/dist/ui/summary.js +101 -0
- package/dist/vision/cost.d.ts +1 -1
- package/dist/vision/decisions.d.ts +9 -3
- package/dist/vision/decisions.js +31 -21
- package/dist/vision/openrouter.d.ts +4 -0
- package/dist/vision/openrouter.js +30 -4
- package/package.json +11 -2
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReviewProfile } from './review/packs.js';
|
|
1
2
|
import type { Trust } from './trust.js';
|
|
2
3
|
export interface ProviderRules {
|
|
3
4
|
only?: string[];
|
|
@@ -46,6 +47,51 @@ export interface Sandbox {
|
|
|
46
47
|
*/
|
|
47
48
|
allowForks: boolean;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Exploratory lane (roadmap E2.U4): free runtime capture today
|
|
52
|
+
* (console/pageerror/failed-request taps render as `observed` findings),
|
|
53
|
+
* bounded act policy later. Opt-in — `enabled` defaults to false. On
|
|
54
|
+
* untrusted checkouts the whole block is stripped by the config allowlist.
|
|
55
|
+
*/
|
|
56
|
+
export interface Explore {
|
|
57
|
+
/** Master switch for capture + act. Default false. */
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
/** Step cap for the exploratory act policy (U4b). Default 20. */
|
|
60
|
+
maxSteps: number;
|
|
61
|
+
/** Model budget for the act policy (U4b). Unset = bounded by run budget. */
|
|
62
|
+
budgetUsd: number | undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Verified expected state for `verify --app`: at least one of these must
|
|
66
|
+
* hold for the lane to pass — a page that merely loads is never a pass.
|
|
67
|
+
* All configured conditions are ANDed.
|
|
68
|
+
*/
|
|
69
|
+
export interface AppExpectation {
|
|
70
|
+
/** Substring that must appear in the a11y tree (case-insensitive). */
|
|
71
|
+
text?: string;
|
|
72
|
+
/** Regex source the final page URL must match. */
|
|
73
|
+
url?: string;
|
|
74
|
+
/** Playwright/CSS selector that must resolve at least one node. */
|
|
75
|
+
selector?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `verify --app` lane (follow-through U3): a bounded natural-language task
|
|
79
|
+
* run on the ExploreLoop substrate. Opt-in — the lane is selected by the
|
|
80
|
+
* `--app` flag and this block supplies the task contract. Without `task`
|
|
81
|
+
* and at least one `expected` marker the lane records `blocked`.
|
|
82
|
+
*/
|
|
83
|
+
export interface AppLane {
|
|
84
|
+
/** Natural-language task the lane must accomplish. */
|
|
85
|
+
task: string | undefined;
|
|
86
|
+
/** Expected-state marker(s) the lane verifies deterministically. */
|
|
87
|
+
expected: AppExpectation | undefined;
|
|
88
|
+
/** Step cap for the task loop. Unset → explore.maxSteps. */
|
|
89
|
+
maxSteps: number | undefined;
|
|
90
|
+
/** Lane USD budget. Unset = bounded by run budget. */
|
|
91
|
+
budgetUsd: number | undefined;
|
|
92
|
+
/** Wall-clock cap in ms. Unset → lane default (120s). */
|
|
93
|
+
timeoutMs: number | undefined;
|
|
94
|
+
}
|
|
49
95
|
export interface Config {
|
|
50
96
|
model: string;
|
|
51
97
|
escalation_model: string;
|
|
@@ -61,7 +107,7 @@ export interface Config {
|
|
|
61
107
|
*/
|
|
62
108
|
code_model: string | undefined;
|
|
63
109
|
/**
|
|
64
|
-
* OpenRouter Decisions API model for typed adjudication (
|
|
110
|
+
* OpenRouter Decisions API model for typed adjudication (the confidence model). Defaults
|
|
65
111
|
* to the pinned `typesafe/jev-1.13-20260917` — alias slugs like
|
|
66
112
|
* `~typesafe/jev-latest` drift silently and thresholds are calibrated
|
|
67
113
|
* to a version. Set to `''` to disable adjudication (regex-only mode).
|
|
@@ -80,6 +126,12 @@ export interface Config {
|
|
|
80
126
|
testsDir: string | undefined;
|
|
81
127
|
/** Directory for JUnit XML + JSON run report output. */
|
|
82
128
|
reportDir: string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* How many `verify` run manifests the local history keeps
|
|
131
|
+
* (`<reportDir>/manifests/*.json`) — the dashboard/TUI run list reads it.
|
|
132
|
+
* `0` disables archival; unset defaults to 20 at write time.
|
|
133
|
+
*/
|
|
134
|
+
reportRetention: number | undefined;
|
|
83
135
|
/**
|
|
84
136
|
* Named secrets for `td.type(name, { secret: true })`. The value is typed
|
|
85
137
|
* locally and never sent to the model — the model only resolves the field.
|
|
@@ -140,13 +192,19 @@ export interface Config {
|
|
|
140
192
|
recordStepCap: number | undefined;
|
|
141
193
|
/**
|
|
142
194
|
* Agent Zero instance for delegated tasks (`argus-reviewer delegate`,
|
|
143
|
-
* `heal: 'a0'`). `url` is the instance base URL — leave
|
|
144
|
-
* `a0` CLI resolve it (saved host, AGENT_ZERO_HOST,
|
|
145
|
-
*
|
|
146
|
-
*
|
|
195
|
+
* `heal: 'a0'`, `verify --a0`). `url` is the instance base URL — leave
|
|
196
|
+
* unset to let the `a0` CLI resolve it (saved host, AGENT_ZERO_HOST,
|
|
197
|
+
* Docker discovery). `maxTasks` caps delegations per `verify`/`run`
|
|
198
|
+
* invocation (verify lane default 1, heal default 5); `timeoutMs` is the
|
|
199
|
+
* per-task wall-clock bound for the verify lane. The lane is
|
|
200
|
+
* an explicit opt-in escalation and caps completed delegations at
|
|
201
|
+
* `inconclusive` — the agent's answer is self-reported evidence, never a
|
|
202
|
+
* `passed` verdict (live round-trip proven in #53).
|
|
147
203
|
*/
|
|
148
204
|
a0: {
|
|
149
205
|
url: string | undefined;
|
|
206
|
+
maxTasks: number | undefined;
|
|
207
|
+
timeoutMs: number | undefined;
|
|
150
208
|
} | undefined;
|
|
151
209
|
/**
|
|
152
210
|
* Failure escalation for `run`. 'local' (default) heals via the vision
|
|
@@ -160,9 +218,20 @@ export interface Config {
|
|
|
160
218
|
* `resolveConfig` — `enabled: false` by default so the lane is opt-in.
|
|
161
219
|
*/
|
|
162
220
|
sandbox: Sandbox;
|
|
221
|
+
/**
|
|
222
|
+
* Exploratory lane. Always populated after `resolveConfig` —
|
|
223
|
+
* `enabled: false` by default so capture is opt-in.
|
|
224
|
+
*/
|
|
225
|
+
explore: Explore;
|
|
226
|
+
/**
|
|
227
|
+
* `verify --app` task lane. Always populated after `resolveConfig` —
|
|
228
|
+
* every field unset by default so the lane blocks on missing contract
|
|
229
|
+
* rather than inventing one.
|
|
230
|
+
*/
|
|
231
|
+
app: AppLane;
|
|
163
232
|
/**
|
|
164
233
|
* Code-review policy knobs. Always populated after `resolveConfig`.
|
|
165
|
-
* `secretsThreshold`:
|
|
234
|
+
* `secretsThreshold`: confidence-model `noul` probability at/above which a
|
|
166
235
|
* secret-shaped diff literal is reported as a finding (below →
|
|
167
236
|
* suppressed but audit-recorded). Default 0.3 — tune after dogfooding.
|
|
168
237
|
* `maxComments`: cap on inline review comments posted per run
|
|
@@ -170,19 +239,22 @@ export interface Config {
|
|
|
170
239
|
* `severityGate`: consumer-facing alias over `severity` — 'bug'
|
|
171
240
|
* fails on bugs only, 'risk' fails on bug|risk. Unset → `severity`
|
|
172
241
|
* list is authoritative.
|
|
173
|
-
* `triage`:
|
|
242
|
+
* `triage`: confidence-model pre-review lane — 'off' no call, 'annotate' (default)
|
|
174
243
|
* records risk/deep-review/area into the report + sticky, 'route'
|
|
175
244
|
* additionally swaps the code model to `lowRiskModel` on low-risk
|
|
176
|
-
* diffs.
|
|
245
|
+
* diffs. The confidence model routes/annotates, never gates — coverage is constant.
|
|
177
246
|
* `lowRiskModel`: the cheap code-model slug 'route' falls to; unset →
|
|
178
247
|
* route keeps `code_model` (annotate-equivalent).
|
|
179
248
|
* `findingThreshold`: P(false-positive) required to suppress a nit/q
|
|
180
|
-
* finding after
|
|
249
|
+
* finding after confidence-model adjudication — 1.0 (default) is annotate-only,
|
|
181
250
|
* lowering it suppresses progressively more low-confidence nits.
|
|
182
251
|
* bug/risk are never suppressed.
|
|
183
252
|
* `requestChanges`: allow the review event to escalate to
|
|
184
|
-
* REQUEST_CHANGES for proven blockers (probe-reproduced or
|
|
253
|
+
* REQUEST_CHANGES for proven blockers (probe-reproduced or confidence-model
|
|
185
254
|
* high-confidence). Default true — set false for advisory-only posting.
|
|
255
|
+
* `profiles`: named review lenses appended to the review prompt
|
|
256
|
+
* ('security'|'perf'|'debloat' — see src/review/packs.ts). Unknown names
|
|
257
|
+
* are dropped at config load. Default [] — no extra rubric.
|
|
186
258
|
*/
|
|
187
259
|
review: {
|
|
188
260
|
secretsThreshold: number;
|
|
@@ -192,16 +264,29 @@ export interface Config {
|
|
|
192
264
|
lowRiskModel: string | undefined;
|
|
193
265
|
findingThreshold: number;
|
|
194
266
|
requestChanges: boolean;
|
|
267
|
+
profiles: ReviewProfile[];
|
|
268
|
+
/**
|
|
269
|
+
* Glob list of changed paths kept out of the review input. A configured
|
|
270
|
+
* list replaces the defaults (generated, fixture, golden, vendored
|
|
271
|
+
* paths); `[]` excludes nothing.
|
|
272
|
+
*/
|
|
273
|
+
exclude: string[];
|
|
195
274
|
};
|
|
196
275
|
}
|
|
197
|
-
export type ConfigInput = Partial<Omit<Config, 'provider' | 'sandbox' | 'review'>> & {
|
|
276
|
+
export type ConfigInput = Partial<Omit<Config, 'provider' | 'sandbox' | 'review' | 'explore' | 'app'>> & {
|
|
198
277
|
provider?: Partial<ProviderRules>;
|
|
199
278
|
sandbox?: Partial<Sandbox>;
|
|
200
279
|
review?: Partial<Config['review']>;
|
|
280
|
+
explore?: Partial<Explore>;
|
|
281
|
+
app?: Partial<AppLane>;
|
|
201
282
|
};
|
|
202
283
|
export declare const DEFAULT_RECORD_STEP_CAP = 40;
|
|
284
|
+
export declare const DEFAULT_EXPLORE: Explore;
|
|
285
|
+
export declare const DEFAULT_APP: AppLane;
|
|
203
286
|
export declare const DEFAULT_SANDBOX: Sandbox;
|
|
204
287
|
export declare function defineConfig(input: ConfigInput): ConfigInput;
|
|
288
|
+
/** Keep only non-blank string expected-state markers; all-dropped means unconfigured. */
|
|
289
|
+
export declare function sanitizeExpectation(input: unknown): AppExpectation | undefined;
|
|
205
290
|
/**
|
|
206
291
|
* Which severities fail the review status. `review.severityGate` is the
|
|
207
292
|
* consumer-facing alias over `severity` — 'risk' fails on bug|risk,
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { DEFAULT_REVIEW_EXCLUDE } from './review/scope.js';
|
|
3
|
+
import { isReviewProfile } from './review/packs.js';
|
|
2
4
|
import { JEV_DEFAULT_MODEL } from './vision/decisions.js';
|
|
3
5
|
export const DEFAULT_RECORD_STEP_CAP = 40;
|
|
6
|
+
export const DEFAULT_EXPLORE = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
maxSteps: 20,
|
|
9
|
+
budgetUsd: undefined,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_APP = {
|
|
12
|
+
task: undefined,
|
|
13
|
+
expected: undefined,
|
|
14
|
+
maxSteps: undefined,
|
|
15
|
+
budgetUsd: undefined,
|
|
16
|
+
timeoutMs: undefined,
|
|
17
|
+
};
|
|
4
18
|
export const DEFAULT_SANDBOX = {
|
|
5
19
|
enabled: false,
|
|
6
20
|
image: undefined,
|
|
@@ -26,6 +40,7 @@ const defaults = {
|
|
|
26
40
|
cacheDir: undefined,
|
|
27
41
|
testsDir: undefined,
|
|
28
42
|
reportDir: undefined,
|
|
43
|
+
reportRetention: undefined,
|
|
29
44
|
secrets: undefined,
|
|
30
45
|
pageSetup: undefined,
|
|
31
46
|
openrouter: undefined,
|
|
@@ -40,6 +55,8 @@ const defaults = {
|
|
|
40
55
|
a0: undefined,
|
|
41
56
|
heal: 'local',
|
|
42
57
|
sandbox: { ...DEFAULT_SANDBOX },
|
|
58
|
+
explore: { ...DEFAULT_EXPLORE },
|
|
59
|
+
app: { ...DEFAULT_APP },
|
|
43
60
|
review: {
|
|
44
61
|
secretsThreshold: 0.3,
|
|
45
62
|
maxComments: 20,
|
|
@@ -48,6 +65,8 @@ const defaults = {
|
|
|
48
65
|
lowRiskModel: undefined,
|
|
49
66
|
findingThreshold: 1.0,
|
|
50
67
|
requestChanges: true,
|
|
68
|
+
profiles: [],
|
|
69
|
+
exclude: [...DEFAULT_REVIEW_EXCLUDE],
|
|
51
70
|
},
|
|
52
71
|
};
|
|
53
72
|
export function defineConfig(input) {
|
|
@@ -61,6 +80,32 @@ function posInt(v, dflt) {
|
|
|
61
80
|
function prob01(v, dflt) {
|
|
62
81
|
return v !== undefined && Number.isFinite(v) && v >= 0 && v <= 1 ? v : dflt;
|
|
63
82
|
}
|
|
83
|
+
/** Optional positive-integer config values stay undefined when absent or wrong-typed. */
|
|
84
|
+
function optPosInt(v) {
|
|
85
|
+
return v !== undefined && Number.isInteger(v) && v >= 1 ? v : undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Keep only non-blank string expected-state markers; all-dropped means unconfigured. */
|
|
88
|
+
export function sanitizeExpectation(input) {
|
|
89
|
+
if (typeof input !== 'object' || input === null)
|
|
90
|
+
return undefined;
|
|
91
|
+
const raw = input;
|
|
92
|
+
// Whitespace-only markers are vacuous — a `' '` needle matches every
|
|
93
|
+
// accessibility tree. Trim at the boundary so they drop like ''.
|
|
94
|
+
const marker = (v) => typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined;
|
|
95
|
+
const expected = {};
|
|
96
|
+
const text = marker(raw.text);
|
|
97
|
+
if (text !== undefined)
|
|
98
|
+
expected.text = text;
|
|
99
|
+
const url = marker(raw.url);
|
|
100
|
+
if (url !== undefined)
|
|
101
|
+
expected.url = url;
|
|
102
|
+
const selector = marker(raw.selector);
|
|
103
|
+
if (selector !== undefined)
|
|
104
|
+
expected.selector = selector;
|
|
105
|
+
return expected.text === undefined && expected.url === undefined && expected.selector === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: expected;
|
|
108
|
+
}
|
|
64
109
|
/**
|
|
65
110
|
* Which severities fail the review status. `review.severityGate` is the
|
|
66
111
|
* consumer-facing alias over `severity` — 'risk' fails on bug|risk,
|
|
@@ -103,10 +148,34 @@ export function resolveConfig(input = {}) {
|
|
|
103
148
|
sandbox.maxProbes = posInt(sandbox.maxProbes, DEFAULT_SANDBOX.maxProbes);
|
|
104
149
|
sandbox.timeoutMs = posInt(sandbox.timeoutMs, DEFAULT_SANDBOX.timeoutMs);
|
|
105
150
|
sandbox.pidsLimit = posInt(sandbox.pidsLimit, DEFAULT_SANDBOX.pidsLimit);
|
|
151
|
+
// Same wrong-typed degrade as sandbox — a mis-typed flag must never
|
|
152
|
+
// self-enable the lane.
|
|
153
|
+
const rawExplore = typeof input.explore === 'object' && input.explore !== null ? input.explore : {};
|
|
154
|
+
const explore = { ...defaults.explore, ...rawExplore };
|
|
155
|
+
explore.enabled = rawExplore.enabled === true;
|
|
156
|
+
explore.maxSteps = posInt(explore.maxSteps, DEFAULT_EXPLORE.maxSteps);
|
|
157
|
+
explore.budgetUsd =
|
|
158
|
+
typeof explore.budgetUsd === 'number' &&
|
|
159
|
+
Number.isFinite(explore.budgetUsd) &&
|
|
160
|
+
explore.budgetUsd > 0
|
|
161
|
+
? explore.budgetUsd
|
|
162
|
+
: undefined;
|
|
163
|
+
// Same wrong-typed degrade for the app lane contract — a mis-typed
|
|
164
|
+
// marker must never self-author a passing condition.
|
|
165
|
+
const rawApp = typeof input.app === 'object' && input.app !== null ? input.app : {};
|
|
166
|
+
const app = { ...defaults.app, ...rawApp };
|
|
167
|
+
app.task = typeof app.task === 'string' && app.task.trim() !== '' ? app.task : undefined;
|
|
168
|
+
app.expected = sanitizeExpectation(app.expected);
|
|
169
|
+
app.maxSteps = optPosInt(rawApp.maxSteps);
|
|
170
|
+
app.timeoutMs = optPosInt(rawApp.timeoutMs);
|
|
171
|
+
app.budgetUsd =
|
|
172
|
+
typeof app.budgetUsd === 'number' && Number.isFinite(app.budgetUsd) && app.budgetUsd > 0
|
|
173
|
+
? app.budgetUsd
|
|
174
|
+
: undefined;
|
|
106
175
|
const rawReview = typeof input.review === 'object' && input.review !== null ? input.review : {};
|
|
107
176
|
const review = { ...defaults.review, ...rawReview };
|
|
108
177
|
// Thresholds must be probabilities — anything else (NaN, >1,
|
|
109
|
-
// negative) would silently suppress or flood the
|
|
178
|
+
// negative) would silently suppress or flood the confidence-model lanes.
|
|
110
179
|
review.secretsThreshold = prob01(review.secretsThreshold, defaults.review.secretsThreshold);
|
|
111
180
|
review.maxComments =
|
|
112
181
|
typeof review.maxComments === 'number' &&
|
|
@@ -127,10 +196,39 @@ export function resolveConfig(input = {}) {
|
|
|
127
196
|
// Advisory-only escape hatch — only literal `false` opts out; anything
|
|
128
197
|
// else (mis-typed values included) keeps the default-true posture.
|
|
129
198
|
review.requestChanges = review.requestChanges !== false;
|
|
130
|
-
|
|
199
|
+
// Unknown profile names are rejected at config load — a typo silently
|
|
200
|
+
// disabling a lens is worse than dropping it. Non-array input means the
|
|
201
|
+
// field was mis-typed entirely and also drops to the empty default.
|
|
202
|
+
review.profiles = Array.isArray(rawReview.profiles)
|
|
203
|
+
? [...new Set(rawReview.profiles.filter(isReviewProfile))]
|
|
204
|
+
: [];
|
|
205
|
+
review.exclude =
|
|
206
|
+
Array.isArray(rawReview.exclude) &&
|
|
207
|
+
rawReview.exclude.every((g) => typeof g === 'string' && g !== '')
|
|
208
|
+
? [...rawReview.exclude]
|
|
209
|
+
: [...DEFAULT_REVIEW_EXCLUDE];
|
|
210
|
+
const resolved = { ...defaults, ...input, provider, sandbox, explore, app, review };
|
|
131
211
|
resolved.recordStepCap = posInt(resolved.recordStepCap, DEFAULT_RECORD_STEP_CAP);
|
|
212
|
+
// Retention is a non-negative integer (0 = keep none) — a mis-typed or
|
|
213
|
+
// negative bound degrades to unset, never to "keep everything".
|
|
214
|
+
resolved.reportRetention =
|
|
215
|
+
typeof resolved.reportRetention === 'number' &&
|
|
216
|
+
Number.isInteger(resolved.reportRetention) &&
|
|
217
|
+
resolved.reportRetention >= 0
|
|
218
|
+
? resolved.reportRetention
|
|
219
|
+
: undefined;
|
|
132
220
|
if (resolved.heal !== 'a0')
|
|
133
221
|
resolved.heal = 'local';
|
|
222
|
+
if (resolved.a0 !== undefined) {
|
|
223
|
+
// A0 bounds degrade like every other numeric knob — a hostile or
|
|
224
|
+
// mis-typed cap must not become unlimited tasks or no timeout.
|
|
225
|
+
const a0 = resolved.a0;
|
|
226
|
+
resolved.a0 = {
|
|
227
|
+
url: typeof a0.url === 'string' && a0.url !== '' ? a0.url : undefined,
|
|
228
|
+
maxTasks: optPosInt(a0.maxTasks),
|
|
229
|
+
timeoutMs: optPosInt(a0.timeoutMs),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
134
232
|
// '' is the documented opt-out — an empty slug would send a broken
|
|
135
233
|
// model id to the decisions endpoint on every adjudication call.
|
|
136
234
|
if (resolved.decisionModel === '')
|
|
@@ -188,7 +286,7 @@ export async function loadConfig(cwd, opts) {
|
|
|
188
286
|
// legit consumer debugging "why is my config ignored") is invisible.
|
|
189
287
|
try {
|
|
190
288
|
if ((await fs.stat(path.join(cwd, `${name}.ts`))).isFile()) {
|
|
191
|
-
opts.note?.(`config: ${name}.ts ignored
|
|
289
|
+
opts.note?.(`config: ${name}.ts ignored – untrusted checkouts load JSON config only`);
|
|
192
290
|
}
|
|
193
291
|
}
|
|
194
292
|
catch {
|
|
@@ -208,7 +306,7 @@ export async function loadConfig(cwd, opts) {
|
|
|
208
306
|
const raw = await fs.readFile(file, 'utf8');
|
|
209
307
|
const parsed = JSON.parse(raw);
|
|
210
308
|
if (untrusted) {
|
|
211
|
-
opts.note?.(`config: ${name}.json loaded untrusted
|
|
309
|
+
opts.note?.(`config: ${name}.json loaded untrusted – honoring ${[...UNTRUSTED_CONFIG_KEYS].join(', ')} only`);
|
|
212
310
|
return finish(filterUntrustedConfig(parsed));
|
|
213
311
|
}
|
|
214
312
|
return finish(parsed);
|
package/dist/detect.d.ts
CHANGED
|
@@ -15,16 +15,43 @@ export interface ExecResult {
|
|
|
15
15
|
timedOut?: boolean;
|
|
16
16
|
/** Signal the process was terminated by, when killed (e.g. 'SIGTERM'). */
|
|
17
17
|
signal?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The process never started (ENOENT — binary missing) — set by the real
|
|
20
|
+
* executor so callers don't sniff stderr text for the distinction.
|
|
21
|
+
*/
|
|
22
|
+
spawnError?: boolean | undefined;
|
|
18
23
|
}
|
|
19
24
|
export type ExecFn = (cmd: string, args: string[], timeoutMs: number,
|
|
20
25
|
/** Extra env merged over process.env — keeps secrets out of `ps`/`/proc` argv. */
|
|
21
|
-
env?: Record<string, string
|
|
26
|
+
env?: Record<string, string>,
|
|
27
|
+
/**
|
|
28
|
+
* `baseEnv` replaces the inherited process environment wholesale — the
|
|
29
|
+
* caller's allowlist, not ambient env. Without it the child inherits
|
|
30
|
+
* process.env as before.
|
|
31
|
+
*/
|
|
32
|
+
opts?: {
|
|
33
|
+
baseEnv?: Record<string, string>;
|
|
34
|
+
}) => Promise<ExecResult>;
|
|
22
35
|
export declare const defaultExec: ExecFn;
|
|
23
36
|
export type ProbeFn = (url: string, timeoutMs: number) => Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* The only environment keys an Agent Zero child process may inherit.
|
|
39
|
+
* Provider keys, GitHub tokens, `ARGUS_*`, and npm auth variables never
|
|
40
|
+
* propagate (R12) — the child is a remote agent harness, not an extension
|
|
41
|
+
* of this process's trust. Lives here (not in executor/a0.ts) because every
|
|
42
|
+
* `a0` spawn — delegation, the lane's `--version` preflight, and init's
|
|
43
|
+
* environment probe — must use it or the contract leaks.
|
|
44
|
+
*/
|
|
45
|
+
export declare const A0_CHILD_ENV_KEYS: readonly ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "TERM", "TMPDIR", "XDG_RUNTIME_DIR", "DOCKER_HOST", "AGENT_ZERO_HOST", "A0_USERNAME", "A0_PASSWORD"];
|
|
46
|
+
/** Build the sanitized child env: allowlisted keys that exist in `env`. */
|
|
47
|
+
export declare function buildA0ChildEnv(env: NodeJS.ProcessEnv | Record<string, string | undefined>): Record<string, string>;
|
|
24
48
|
/**
|
|
25
49
|
* Any HTTP response — including a login redirect — means *something* is up,
|
|
26
50
|
* but port 5080 could be an unrelated service. Require an Agent Zero marker
|
|
27
|
-
* in the served HTML before trusting the probe result.
|
|
51
|
+
* in the served HTML before trusting the probe result. Redirects are
|
|
52
|
+
* followed: a login-gated instance 302s `/` to `/login`, and the marker
|
|
53
|
+
* check must apply to the page the host actually serves, not the redirect
|
|
54
|
+
* stub — a hop to a non-Zero page still fails the marker check.
|
|
28
55
|
*/
|
|
29
56
|
export declare const defaultProbe: ProbeFn;
|
|
30
57
|
export interface A0Info {
|
package/dist/detect.js
CHANGED
|
@@ -2,15 +2,19 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { readdir, readFile } from 'node:fs/promises';
|
|
5
|
-
|
|
5
|
+
/** Grace between a timeout's SIGTERM and the escalation SIGKILL. */
|
|
6
|
+
const EXEC_KILL_GRACE_MS = 2_000;
|
|
7
|
+
export const defaultExec = (cmd, args, timeoutMs, env, opts) => new Promise((resolve) => {
|
|
8
|
+
const baseEnv = opts?.baseEnv ?? process.env;
|
|
6
9
|
// 4 MiB headroom — the sandbox caps output itself after capture, and a
|
|
7
10
|
// chatty probe hitting execFile's 1 MiB default would error instead of
|
|
8
11
|
// reaching the harness classifier.
|
|
9
|
-
execFile(cmd, args, {
|
|
12
|
+
const child = execFile(cmd, args, {
|
|
10
13
|
timeout: timeoutMs,
|
|
11
14
|
maxBuffer: 4 * 1024 * 1024,
|
|
12
|
-
|
|
15
|
+
env: { ...baseEnv, ...env },
|
|
13
16
|
}, (err, stdout, stderr) => {
|
|
17
|
+
settled = true;
|
|
14
18
|
if (err) {
|
|
15
19
|
// stderr is '' (not undefined) on spawn ENOENT — fall back to the
|
|
16
20
|
// error message so callers can distinguish "missing" from "failed".
|
|
@@ -23,27 +27,112 @@ export const defaultExec = (cmd, args, timeoutMs, env) => new Promise((resolve)
|
|
|
23
27
|
timedOut: err.killed === true &&
|
|
24
28
|
err.code !== 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
|
|
25
29
|
signal: typeof err.signal === 'string' ? err.signal : undefined,
|
|
30
|
+
spawnError: err.code === 'ENOENT',
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
else {
|
|
29
34
|
resolve({ code: 0, stdout: String(stdout), stderr: String(stderr) });
|
|
30
35
|
}
|
|
31
36
|
});
|
|
37
|
+
let settled = false;
|
|
38
|
+
// execFile's timeout only delivers SIGTERM — a child that traps it (or a
|
|
39
|
+
// descendant holding the stdio pipes open) would keep this promise
|
|
40
|
+
// pending forever. Escalate to SIGKILL after a short grace, and resolve
|
|
41
|
+
// anyway if even that can't make the callback fire.
|
|
42
|
+
const escalate = setTimeout(() => {
|
|
43
|
+
if (settled)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
child.kill('SIGKILL');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// already gone
|
|
50
|
+
}
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (!settled) {
|
|
53
|
+
settled = true;
|
|
54
|
+
resolve({ code: 1, stdout: '', stderr: 'process killed', timedOut: true });
|
|
55
|
+
}
|
|
56
|
+
}, EXEC_KILL_GRACE_MS).unref();
|
|
57
|
+
}, timeoutMs + EXEC_KILL_GRACE_MS);
|
|
58
|
+
escalate.unref();
|
|
59
|
+
child.on('close', () => clearTimeout(escalate));
|
|
32
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* The only environment keys an Agent Zero child process may inherit.
|
|
63
|
+
* Provider keys, GitHub tokens, `ARGUS_*`, and npm auth variables never
|
|
64
|
+
* propagate (R12) — the child is a remote agent harness, not an extension
|
|
65
|
+
* of this process's trust. Lives here (not in executor/a0.ts) because every
|
|
66
|
+
* `a0` spawn — delegation, the lane's `--version` preflight, and init's
|
|
67
|
+
* environment probe — must use it or the contract leaks.
|
|
68
|
+
*/
|
|
69
|
+
export const A0_CHILD_ENV_KEYS = [
|
|
70
|
+
'PATH',
|
|
71
|
+
'HOME',
|
|
72
|
+
'USER',
|
|
73
|
+
'LOGNAME',
|
|
74
|
+
'SHELL',
|
|
75
|
+
'LANG',
|
|
76
|
+
'LC_ALL',
|
|
77
|
+
'TERM',
|
|
78
|
+
'TMPDIR',
|
|
79
|
+
'XDG_RUNTIME_DIR',
|
|
80
|
+
'DOCKER_HOST',
|
|
81
|
+
// The resolved host pointer — a URL, not a credential.
|
|
82
|
+
'AGENT_ZERO_HOST',
|
|
83
|
+
// Headless auth for login-gated instances: the a0 CLI itself consumes
|
|
84
|
+
// these (headless has no other auth path — session cookies only persist
|
|
85
|
+
// via the interactive TUI's remember-host flow). Operator-set only; they
|
|
86
|
+
// scope to the a0 host, not to any provider.
|
|
87
|
+
'A0_USERNAME',
|
|
88
|
+
'A0_PASSWORD',
|
|
89
|
+
];
|
|
90
|
+
/** Build the sanitized child env: allowlisted keys that exist in `env`. */
|
|
91
|
+
export function buildA0ChildEnv(env) {
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const key of A0_CHILD_ENV_KEYS) {
|
|
94
|
+
const value = env[key];
|
|
95
|
+
if (value !== undefined && value !== '')
|
|
96
|
+
out[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
33
100
|
/**
|
|
34
101
|
* Any HTTP response — including a login redirect — means *something* is up,
|
|
35
102
|
* but port 5080 could be an unrelated service. Require an Agent Zero marker
|
|
36
|
-
* in the served HTML before trusting the probe result.
|
|
103
|
+
* in the served HTML before trusting the probe result. Redirects are
|
|
104
|
+
* followed: a login-gated instance 302s `/` to `/login`, and the marker
|
|
105
|
+
* check must apply to the page the host actually serves, not the redirect
|
|
106
|
+
* stub — a hop to a non-Zero page still fails the marker check.
|
|
37
107
|
*/
|
|
38
108
|
export const defaultProbe = async (url, timeoutMs) => {
|
|
39
109
|
try {
|
|
40
110
|
const res = await fetch(url, {
|
|
41
111
|
signal: AbortSignal.timeout(timeoutMs),
|
|
42
|
-
redirect: '
|
|
112
|
+
redirect: 'follow',
|
|
43
113
|
});
|
|
44
114
|
if (res.status >= 500)
|
|
45
115
|
return false;
|
|
46
|
-
|
|
116
|
+
// Bound the bytes actually received — a misbehaving host streaming an
|
|
117
|
+
// unbounded body inside the probe window would otherwise be fully
|
|
118
|
+
// buffered by res.text() before the slice.
|
|
119
|
+
const reader = res.body?.getReader();
|
|
120
|
+
if (reader === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
const chunks = [];
|
|
123
|
+
let received = 0;
|
|
124
|
+
for (;;) {
|
|
125
|
+
const { done, value } = await reader.read();
|
|
126
|
+
if (done)
|
|
127
|
+
break;
|
|
128
|
+
received += value.byteLength;
|
|
129
|
+
chunks.push(value);
|
|
130
|
+
if (received >= 65_536) {
|
|
131
|
+
await reader.cancel();
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const body = new TextDecoder().decode(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
|
|
47
136
|
return /agent.?zero/i.test(body);
|
|
48
137
|
}
|
|
49
138
|
catch {
|
|
@@ -105,7 +194,9 @@ export async function detectEnvironment(env, opts = {}) {
|
|
|
105
194
|
const exec = opts.exec ?? defaultExec;
|
|
106
195
|
const home = opts.home ?? homedir();
|
|
107
196
|
const [a0Version, gh, browsers, a0Host] = await Promise.all([
|
|
108
|
-
|
|
197
|
+
// Even the presence probe gets the allowlisted env — an `a0` binary is
|
|
198
|
+
// third-party code and never sees provider/git secrets.
|
|
199
|
+
exec('a0', ['--version'], 5_000, undefined, { baseEnv: buildA0ChildEnv(env) }),
|
|
109
200
|
exec('gh', ['auth', 'status'], 5_000),
|
|
110
201
|
playwrightBrowsers(home),
|
|
111
202
|
resolveA0Host(env, opts),
|
package/dist/driver/browser.d.ts
CHANGED
|
@@ -16,7 +16,28 @@ export interface BrowserDriverOptions {
|
|
|
16
16
|
browser?: 'chromium' | 'firefox' | 'webkit' | undefined;
|
|
17
17
|
/** Hard limit in ms for Playwright cleanup. */
|
|
18
18
|
browserTimeoutMs?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Exploratory capture (U4a): record page errors, console errors, and
|
|
21
|
+
* failed same-origin requests during the run. Free — no model calls.
|
|
22
|
+
*/
|
|
23
|
+
captureErrors?: boolean;
|
|
19
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* One runtime anomaly observed while the browser was driving the app —
|
|
27
|
+
* becomes an `observed` finding on the run report. Captures never change a
|
|
28
|
+
* verdict; they are evidence, not adjudication.
|
|
29
|
+
*/
|
|
30
|
+
export interface PageCapture {
|
|
31
|
+
kind: 'console-error' | 'pageerror' | 'request-failed';
|
|
32
|
+
/** Normalized, truncated message or failure signature. */
|
|
33
|
+
text: string;
|
|
34
|
+
/** Failing request URL (request-failed only, same-origin only). */
|
|
35
|
+
url?: string;
|
|
36
|
+
/** Collapsed repeat count for this signature. */
|
|
37
|
+
count: number;
|
|
38
|
+
}
|
|
39
|
+
/** Distinct capture signatures kept per browser session. */
|
|
40
|
+
export declare const MAX_CAPTURE_SIGNATURES = 50;
|
|
20
41
|
export interface Observation {
|
|
21
42
|
screenshotJpeg: Buffer;
|
|
22
43
|
a11yYaml: string;
|
|
@@ -38,8 +59,19 @@ export declare class BrowserDriver {
|
|
|
38
59
|
private readonly browserTimeoutMs;
|
|
39
60
|
private video;
|
|
40
61
|
private closed;
|
|
62
|
+
private readonly captures;
|
|
41
63
|
private constructor();
|
|
42
64
|
static launch(options?: BrowserDriverOptions): Promise<BrowserDriver>;
|
|
65
|
+
/**
|
|
66
|
+
* Exploratory capture taps (U4a). Noise controls are applied at collection:
|
|
67
|
+
* identical signatures collapse into one capture with a repeat count,
|
|
68
|
+
* request-failed events drop third-party origins (analytics/tag beacons
|
|
69
|
+
* failing is noise, not signal), and distinct signatures are capped.
|
|
70
|
+
*/
|
|
71
|
+
private _attachCaptureTaps;
|
|
72
|
+
private _addCapture;
|
|
73
|
+
/** Captured page anomalies for this session — empty unless captureErrors. */
|
|
74
|
+
pageCaptures(): PageCapture[];
|
|
43
75
|
get rawPage(): Page;
|
|
44
76
|
get recordingDir(): string;
|
|
45
77
|
goto(url: string): Promise<void>;
|