agent-device-lab 0.0.0-stage → 0.3.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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +150 -2
- package/assets/dashboard/app.css +238 -0
- package/assets/dashboard/app.js +976 -0
- package/assets/dashboard/index.html +120 -0
- package/bin/agentlab.js +8 -0
- package/dist/cli/ci.js +443 -0
- package/dist/cli/flow.js +127 -0
- package/dist/cli/main.js +855 -0
- package/dist/cli/signals.js +38 -0
- package/dist/core/action-log.js +125 -0
- package/dist/core/auth.js +175 -0
- package/dist/core/bundle.js +432 -0
- package/dist/core/checks.js +137 -0
- package/dist/core/ci.js +389 -0
- package/dist/core/commands.js +366 -0
- package/dist/core/control.js +217 -0
- package/dist/core/detectors.js +392 -0
- package/dist/core/devices.js +83 -0
- package/dist/core/doctor.js +203 -0
- package/dist/core/explore-safety.js +110 -0
- package/dist/core/extract.js +1445 -0
- package/dist/core/feed.js +493 -0
- package/dist/core/findings.js +179 -0
- package/dist/core/format.js +311 -0
- package/dist/core/init.js +607 -0
- package/dist/core/lab.js +1979 -0
- package/dist/core/migrate.js +35 -0
- package/dist/core/observation.js +161 -0
- package/dist/core/ownership.js +142 -0
- package/dist/core/process-group.js +206 -0
- package/dist/core/process-identity.js +56 -0
- package/dist/core/profile.js +816 -0
- package/dist/core/project-runner.js +359 -0
- package/dist/core/replay.js +303 -0
- package/dist/core/reproduction.js +36 -0
- package/dist/core/scan-policy.js +156 -0
- package/dist/core/scan-report.js +145 -0
- package/dist/core/scan.js +455 -0
- package/dist/core/schema.js +39 -0
- package/dist/core/services.js +134 -0
- package/dist/core/steps.js +80 -0
- package/dist/core/sweep.js +138 -0
- package/dist/core/trace-sanitize.js +641 -0
- package/dist/core/url-redact.js +25 -0
- package/dist/core/versions.js +29 -0
- package/dist/core/zip.js +264 -0
- package/dist/daemon/main.js +160 -0
- package/dist/daemon/state.js +93 -0
- package/dist/dashboard/open.js +25 -0
- package/dist/dashboard/server.js +366 -0
- package/dist/mcp/server.js +89 -0
- package/package.json +63 -4
package/dist/cli/flow.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { redactTree } from '../core/bundle.js';
|
|
4
|
+
import { formatAction, formatClose, formatError, formatStart } from '../core/format.js';
|
|
5
|
+
import { Lab } from '../core/lab.js';
|
|
6
|
+
import { LabError } from '../core/schema.js';
|
|
7
|
+
import { check, describeStep, stepRequest } from '../core/steps.js';
|
|
8
|
+
export { check } from '../core/steps.js';
|
|
9
|
+
export async function runFlow(flowPath, opts) {
|
|
10
|
+
return (await runFlowDetailed(flowPath, opts)).passed;
|
|
11
|
+
}
|
|
12
|
+
export async function runFlowDetailed(flowPath, opts) {
|
|
13
|
+
const file = resolve(flowPath);
|
|
14
|
+
const flow = JSON.parse(await readFile(file, 'utf8'));
|
|
15
|
+
const write = opts.write ?? ((s) => { process.stdout.write(s); });
|
|
16
|
+
const out = (s) => { if (!opts.json)
|
|
17
|
+
write(s + '\n'); };
|
|
18
|
+
const indent = (s) => s.split('\n').map((l) => ` ${l}`).join('\n');
|
|
19
|
+
const lab = new Lab({ ...opts.lab, stateDir: opts.stateDir });
|
|
20
|
+
opts.onLab?.(lab);
|
|
21
|
+
const t0 = Date.now();
|
|
22
|
+
const reports = [];
|
|
23
|
+
let closeResult;
|
|
24
|
+
let runDir;
|
|
25
|
+
let startError;
|
|
26
|
+
out(`flow: ${flow.name}`);
|
|
27
|
+
try {
|
|
28
|
+
const start = await lab.start({
|
|
29
|
+
project: opts.project ?? resolve(dirname(file), flow.project),
|
|
30
|
+
device: opts.device ?? flow.device,
|
|
31
|
+
headed: opts.headed,
|
|
32
|
+
slowMoMs: opts.slowMoMs,
|
|
33
|
+
auth: flow.auth ?? opts.auth ?? 'fresh',
|
|
34
|
+
...(opts.authState && (flow.auth ?? 'saved') !== 'fresh' ? { authState: opts.authState } : {}),
|
|
35
|
+
...(opts.trace ? { trace: true } : {}),
|
|
36
|
+
});
|
|
37
|
+
runDir = start.session.runDir;
|
|
38
|
+
out(indent(formatStart(start)));
|
|
39
|
+
for (const [i, step] of flow.steps.entries()) {
|
|
40
|
+
const label = step.label ?? describeStep(step);
|
|
41
|
+
out(`\nstep ${i + 1}/${flow.steps.length}: ${label}`);
|
|
42
|
+
const report = { index: i + 1, label, ok: true, checks: [] };
|
|
43
|
+
reports.push(report);
|
|
44
|
+
try {
|
|
45
|
+
// A person watching may pause or take over between steps. A flow has no one to return an error
|
|
46
|
+
// to, so it waits for control to come back, then observes afresh before resolving the next target.
|
|
47
|
+
if (lab.control.mode !== 'agent') {
|
|
48
|
+
out(` waiting: ${lab.control.mode === 'human' ? 'a person has control of the browser' : 'paused from the dashboard'}`);
|
|
49
|
+
await lab.control.waitForTurn();
|
|
50
|
+
}
|
|
51
|
+
if (lab.control.state.observeRequired)
|
|
52
|
+
await lab.observe();
|
|
53
|
+
if (step.do) {
|
|
54
|
+
report.action = await lab.act(stepRequest(lab, step));
|
|
55
|
+
out(indent(formatAction(report.action)));
|
|
56
|
+
if (report.action.outcome === 'error')
|
|
57
|
+
report.ok = false;
|
|
58
|
+
}
|
|
59
|
+
if (report.ok && step.expect) {
|
|
60
|
+
report.checks = check(step.expect, lab.lastObservation, report.action, lab.inspect().findings);
|
|
61
|
+
for (const c of report.checks)
|
|
62
|
+
out(` ${c.ok ? 'PASS' : 'FAIL'} ${c.text}`);
|
|
63
|
+
report.ok = report.checks.every((c) => c.ok);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
report.ok = false;
|
|
68
|
+
report.error = LabError.from(err).toJSON();
|
|
69
|
+
out(indent(`ERROR ${formatError(report.error)}`));
|
|
70
|
+
}
|
|
71
|
+
if (!report.ok)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
startError = LabError.from(err).toJSON();
|
|
77
|
+
out(`start failed: ${formatError(startError)}`);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (opts.beforeClose && lab.active) {
|
|
81
|
+
const bad = reports.find((r) => !r.ok);
|
|
82
|
+
const failure = bad ? failureOf(bad) : undefined;
|
|
83
|
+
const failedStep = bad ? failedStepOf(bad) : undefined;
|
|
84
|
+
const passed = !bad && reports.length === flow.steps.length;
|
|
85
|
+
await opts.beforeClose(lab, {
|
|
86
|
+
passed, ...(failure ? { failure, reason: `flow "${flow.name}" failed at step ${failedStep.index}: ${failedStep.text}` } : {}),
|
|
87
|
+
}).catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
closeResult = await lab.close('flow finished');
|
|
90
|
+
out(`\n${formatClose(closeResult)}`);
|
|
91
|
+
}
|
|
92
|
+
const passed = !startError && reports.length === flow.steps.length && reports.every((r) => r.ok);
|
|
93
|
+
// The page can echo what was typed (a console message, an error): every secret the session knows is redacted.
|
|
94
|
+
const secrets = lab.secretValues().filter((s) => s.length >= 4).sort((a, b) => b.length - a.length);
|
|
95
|
+
const summary = redactTree({
|
|
96
|
+
flow: flow.name, passed, steps: flow.steps.length, completed: reports.filter((r) => r.ok).length,
|
|
97
|
+
elapsedMs: Date.now() - t0, session: lab.status().session?.id, runDir, startError, close: closeResult,
|
|
98
|
+
findings: lab.status().session ? lab.inspect().findings : [], reports,
|
|
99
|
+
}, secrets);
|
|
100
|
+
if (runDir)
|
|
101
|
+
await writeFile(join(runDir, 'flow-result.json'), JSON.stringify(summary, null, 2));
|
|
102
|
+
if (opts.json)
|
|
103
|
+
write(JSON.stringify(summary, null, 2) + '\n');
|
|
104
|
+
else
|
|
105
|
+
out(`${passed ? 'PASS' : 'FAIL'} ${flow.name}: ${summary.completed}/${summary.steps} steps in ${(summary.elapsedMs / 1000).toFixed(1)}s${runDir ? ` (report: ${join(runDir, 'flow-result.json')})` : ''}`);
|
|
106
|
+
const bad = reports.find((r) => !r.ok);
|
|
107
|
+
return {
|
|
108
|
+
name: flow.name, file, passed, steps: flow.steps.length, completed: summary.completed, elapsedMs: summary.elapsedMs, ...(runDir ? { runDir } : {}),
|
|
109
|
+
...(bad ? { failedStep: failedStepOf(bad) } : {}), ...(startError ? { startError } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The bundle's classification of a failed step: an action that errored, or an expectation that did not hold. */
|
|
113
|
+
function failureOf(r) {
|
|
114
|
+
if (r.error)
|
|
115
|
+
return { kind: 'action-error', step: r.index, code: r.error.code };
|
|
116
|
+
if (r.action?.outcome === 'error')
|
|
117
|
+
return { kind: 'action-error', step: r.index, code: r.action.error?.code ?? 'unknown' };
|
|
118
|
+
return { kind: 'expectation', step: r.index, text: r.checks.find((c) => !c.ok)?.text ?? r.label };
|
|
119
|
+
}
|
|
120
|
+
function failedStepOf(r) {
|
|
121
|
+
const f = failureOf(r);
|
|
122
|
+
if (f.kind === 'expectation')
|
|
123
|
+
return { index: r.index, label: r.label, kind: 'expectation', text: f.text };
|
|
124
|
+
const e = r.error ?? r.action?.error;
|
|
125
|
+
return { index: r.index, label: r.label, kind: r.error ? 'error' : 'action-error', text: e ? formatError(e) : f.code };
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=flow.js.map
|