autokap 1.8.5 → 1.8.7
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/dist/action-verifier.d.ts +6 -0
- package/dist/action-verifier.js +30 -17
- package/dist/browser.d.ts +59 -0
- package/dist/browser.js +259 -0
- package/dist/cli-contract.d.ts +5 -0
- package/dist/cli-runner.d.ts +0 -1
- package/dist/cli-runner.js +74 -59
- package/dist/clip-capture-loop.d.ts +28 -7
- package/dist/clip-capture-loop.js +102 -19
- package/dist/engine-version.d.ts +24 -0
- package/dist/engine-version.js +25 -0
- package/dist/execution-schema.d.ts +22 -0
- package/dist/execution-schema.js +59 -8
- package/dist/execution-types.d.ts +116 -0
- package/dist/opcode-runner.d.ts +8 -1
- package/dist/opcode-runner.js +120 -29
- package/dist/postcondition.d.ts +18 -3
- package/dist/postcondition.js +75 -27
- package/dist/program-hash.d.ts +11 -0
- package/dist/program-hash.js +28 -0
- package/dist/program-migrations.d.ts +31 -0
- package/dist/program-migrations.js +93 -0
- package/dist/program-signing.d.ts +11 -0
- package/dist/program-signing.js +1 -0
- package/dist/recovery-chain.js +8 -11
- package/dist/scenario-cookie.d.ts +36 -0
- package/dist/scenario-cookie.js +62 -0
- package/dist/security.d.ts +21 -0
- package/dist/security.js +46 -8
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/video-narration-schema.d.ts +3 -0
- package/dist/video-narration-schema.js +3 -0
- package/dist/wait-contract.d.ts +104 -0
- package/dist/wait-contract.js +144 -0
- package/dist/web-playwright-local.d.ts +9 -1
- package/dist/web-playwright-local.js +0 -0
- package/package.json +2 -2
package/dist/postcondition.d.ts
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
* Deterministic evaluation of postconditions after each opcode.
|
|
5
5
|
* No LLM calls — purely structural checks against AKTree, URL, and screenshots.
|
|
6
6
|
*/
|
|
7
|
-
import type { RuntimeAdapter, PostconditionSpec } from './execution-types.js';
|
|
7
|
+
import type { RuntimeAdapter, PostconditionSpec, ProgressSnapshot } from './execution-types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Evaluates whether a postcondition holds.
|
|
10
10
|
* Retries internally up to postcondition.waitMs (polling).
|
|
11
11
|
* Returns true if the condition is satisfied, false otherwise.
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
13
|
+
export interface PostconditionResult {
|
|
14
14
|
passed: boolean;
|
|
15
15
|
reason: string;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* AUT-240 (decision 2): the check could not be verified deterministically
|
|
18
|
+
* (an AKTree probe kept throwing) and was assumed-OK as a last resort. The
|
|
19
|
+
* capture is flagged low-confidence rather than failed.
|
|
20
|
+
*/
|
|
21
|
+
lowConfidence?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function evaluatePostcondition(adapter: RuntimeAdapter, spec: PostconditionSpec): Promise<PostconditionResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Evaluate a postcondition with extend-on-progress (AUT-240, Layer C): the poll
|
|
26
|
+
* gets a generous budget up to the global deadline and the progress watchdog
|
|
27
|
+
* cuts it only when the page is genuinely stuck. Replaces the old clamp-to-
|
|
28
|
+
* remaining-budget that could starve the check to ~1ms after a slow action.
|
|
29
|
+
* Shared by the runner (main path) and the recovery chain (retry re-check).
|
|
30
|
+
*/
|
|
31
|
+
export declare function evaluatePostconditionWithProgress(adapter: RuntimeAdapter, spec: PostconditionSpec, startedAtMs: number, globalDeadlineMs: number, getProgress: (() => Promise<ProgressSnapshot | null>) | undefined): Promise<PostconditionResult>;
|
package/dist/postcondition.js
CHANGED
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
* Deterministic evaluation of postconditions after each opcode.
|
|
5
5
|
* No LLM calls — purely structural checks against AKTree, URL, and screenshots.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
/**
|
|
9
|
-
* Evaluates whether a postcondition holds.
|
|
10
|
-
* Retries internally up to postcondition.waitMs (polling).
|
|
11
|
-
* Returns true if the condition is satisfied, false otherwise.
|
|
12
|
-
*/
|
|
7
|
+
import { runWithProgressBudget } from './wait-contract.js';
|
|
13
8
|
export async function evaluatePostcondition(adapter, spec) {
|
|
14
9
|
const maxWait = spec.waitMs ?? 5000;
|
|
15
10
|
const pollInterval = 500;
|
|
@@ -31,6 +26,29 @@ export async function evaluatePostcondition(adapter, spec) {
|
|
|
31
26
|
// Final check after timeout
|
|
32
27
|
return checkOnce(adapter, spec, context);
|
|
33
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate a postcondition with extend-on-progress (AUT-240, Layer C): the poll
|
|
31
|
+
* gets a generous budget up to the global deadline and the progress watchdog
|
|
32
|
+
* cuts it only when the page is genuinely stuck. Replaces the old clamp-to-
|
|
33
|
+
* remaining-budget that could starve the check to ~1ms after a slow action.
|
|
34
|
+
* Shared by the runner (main path) and the recovery chain (retry re-check).
|
|
35
|
+
*/
|
|
36
|
+
export async function evaluatePostconditionWithProgress(adapter, spec, startedAtMs, globalDeadlineMs, getProgress) {
|
|
37
|
+
// Immediate specs need no adaptive budget.
|
|
38
|
+
if (spec.type === 'always') {
|
|
39
|
+
return evaluatePostcondition(adapter, spec);
|
|
40
|
+
}
|
|
41
|
+
const compiledWaitMs = spec.waitMs ?? 5000;
|
|
42
|
+
const waited = await runWithProgressBudget((budgetMs) => evaluatePostcondition(adapter, { ...spec, waitMs: Math.max(1, Math.round(budgetMs)) }), { startedAtMs, globalDeadlineMs, minBudgetMs: compiledWaitMs, getProgress });
|
|
43
|
+
if (waited.result)
|
|
44
|
+
return waited.result;
|
|
45
|
+
return {
|
|
46
|
+
passed: false,
|
|
47
|
+
reason: waited.cut === 'stuck'
|
|
48
|
+
? `not met (page stuck, no progress for ${Math.round(waited.waitedMs)}ms)`
|
|
49
|
+
: 'not met (global wait deadline reached)',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
34
52
|
async function checkOnce(adapter, spec, context) {
|
|
35
53
|
switch (spec.type) {
|
|
36
54
|
case 'route_matches':
|
|
@@ -117,16 +135,15 @@ async function checkElementVisible(adapter, selector) {
|
|
|
117
135
|
catch {
|
|
118
136
|
// Fall through to AKTree check
|
|
119
137
|
}
|
|
120
|
-
// Fallback:
|
|
138
|
+
// Fallback: a visible node matching the selector in the AKTree.
|
|
139
|
+
// (AUT-240, Layer A: the old `serializeAKTree().includes(selector)` fallback
|
|
140
|
+
// was dropped — a substring match on the serialized tree produced false
|
|
141
|
+
// positives.)
|
|
121
142
|
try {
|
|
122
143
|
const tree = await adapter.getAKTree();
|
|
123
144
|
if (hasVisibleNodeWithSelector(tree, selector)) {
|
|
124
145
|
return { passed: true, reason: `element "${selector}" is visible in AKTree` };
|
|
125
146
|
}
|
|
126
|
-
const serialized = serializeAKTree(tree);
|
|
127
|
-
if (serialized.includes(selector.replace(/[[\]"]/g, ''))) {
|
|
128
|
-
return { passed: true, reason: `element pattern "${selector}" found in serialized AKTree` };
|
|
129
|
-
}
|
|
130
147
|
return { passed: false, reason: `element "${selector}" not visible` };
|
|
131
148
|
}
|
|
132
149
|
catch {
|
|
@@ -147,6 +164,23 @@ async function checkElementAbsent(adapter, selector) {
|
|
|
147
164
|
}
|
|
148
165
|
}
|
|
149
166
|
async function checkTextContains(adapter, selector, expectedText) {
|
|
167
|
+
const expected = normalizeText(expectedText);
|
|
168
|
+
// Playwright-first (AUT-240, Layer A): read the live DOM text.
|
|
169
|
+
if (adapter.getTextContent) {
|
|
170
|
+
try {
|
|
171
|
+
const live = await adapter.getTextContent(selector);
|
|
172
|
+
if (live !== null && normalizeText(live).includes(expected)) {
|
|
173
|
+
return { passed: true, reason: `element "${selector}" contains "${expectedText}" (Playwright)` };
|
|
174
|
+
}
|
|
175
|
+
// Element found but text didn't match (or selector missed): fall through
|
|
176
|
+
// to the AKTree, which may surface label/value/aria text the raw
|
|
177
|
+
// textContent omits.
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Fall through to AKTree.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Fallback: AKTree (label / value / own text).
|
|
150
184
|
try {
|
|
151
185
|
const tree = await adapter.getAKTree();
|
|
152
186
|
const node = findNodeBySelector(tree, selector);
|
|
@@ -158,9 +192,8 @@ async function checkTextContains(adapter, selector, expectedText) {
|
|
|
158
192
|
node.value || '',
|
|
159
193
|
node.attributes.__ownText || '',
|
|
160
194
|
].join(' '));
|
|
161
|
-
const expected = normalizeText(expectedText);
|
|
162
195
|
if (nodeText.includes(expected)) {
|
|
163
|
-
return { passed: true, reason: `element "${selector}" contains "${expectedText}"` };
|
|
196
|
+
return { passed: true, reason: `element "${selector}" contains "${expectedText}" (AKTree)` };
|
|
164
197
|
}
|
|
165
198
|
return { passed: false, reason: `element "${selector}" text "${nodeText}" does not contain "${expectedText}"` };
|
|
166
199
|
}
|
|
@@ -168,24 +201,39 @@ async function checkTextContains(adapter, selector, expectedText) {
|
|
|
168
201
|
return { passed: false, reason: `error checking text: ${err}` };
|
|
169
202
|
}
|
|
170
203
|
}
|
|
204
|
+
function evaluateOverlayTree(tree) {
|
|
205
|
+
if (tree.overlays.length === 0) {
|
|
206
|
+
return { passed: true, reason: 'no overlays detected' };
|
|
207
|
+
}
|
|
208
|
+
const blocking = tree.overlays.filter(o => o.blocksInteraction);
|
|
209
|
+
if (blocking.length === 0) {
|
|
210
|
+
return { passed: true, reason: 'overlays present but none blocking interaction' };
|
|
211
|
+
}
|
|
212
|
+
return { passed: false, reason: `${blocking.length} blocking overlay(s) still present` };
|
|
213
|
+
}
|
|
171
214
|
async function checkOverlayDismissed(adapter) {
|
|
172
215
|
try {
|
|
173
|
-
|
|
174
|
-
// Check if any overlays are reported in the tree
|
|
175
|
-
if (tree.overlays.length === 0) {
|
|
176
|
-
return { passed: true, reason: 'no overlays detected' };
|
|
177
|
-
}
|
|
178
|
-
// Check if remaining overlays are blocking
|
|
179
|
-
const blocking = tree.overlays.filter(o => o.blocksInteraction);
|
|
180
|
-
if (blocking.length === 0) {
|
|
181
|
-
return { passed: true, reason: 'overlays present but none blocking interaction' };
|
|
182
|
-
}
|
|
183
|
-
return { passed: false, reason: `${blocking.length} blocking overlay(s) still present` };
|
|
216
|
+
return evaluateOverlayTree(await adapter.getAKTree());
|
|
184
217
|
}
|
|
185
218
|
catch {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
219
|
+
// AUT-240 (decision 2): "assume OK, but smart". A first `page.evaluate`
|
|
220
|
+
// hiccup (e.g. navigation in flight) is no longer assumed-OK immediately —
|
|
221
|
+
// settle the page and retry the AKTree once.
|
|
222
|
+
try {
|
|
223
|
+
if (adapter.waitForVisuallyStable) {
|
|
224
|
+
await adapter.waitForVisuallyStable({ maxWaitMs: 2000 });
|
|
225
|
+
}
|
|
226
|
+
return evaluateOverlayTree(await adapter.getAKTree());
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Still unverifiable: assume dismissed as a last resort, but flag
|
|
230
|
+
// low-confidence so the post-capture verification scrutinizes it.
|
|
231
|
+
return {
|
|
232
|
+
passed: true,
|
|
233
|
+
reason: 'overlay check unverifiable after settle; assuming dismissed (low-confidence)',
|
|
234
|
+
lowConfidence: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
189
237
|
}
|
|
190
238
|
}
|
|
191
239
|
async function checkScreenshotStable(adapter, threshold, context) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Program content hashing (run provenance)
|
|
3
|
+
*
|
|
4
|
+
* Stable content hash of an ExecutionProgram, persisted as `program_hash` on
|
|
5
|
+
* each run so a screenshot can be traced back to the exact program bytes that
|
|
6
|
+
* produced it. Isolated from program-migrations.ts to keep `node:crypto` out of
|
|
7
|
+
* the schema validation import chain.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExecutionProgram } from './execution-types.js';
|
|
10
|
+
/** sha256 of the canonicalized program (stable across runs of the same program). */
|
|
11
|
+
export declare function hashProgram(program: ExecutionProgram): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Program content hashing (run provenance)
|
|
3
|
+
*
|
|
4
|
+
* Stable content hash of an ExecutionProgram, persisted as `program_hash` on
|
|
5
|
+
* each run so a screenshot can be traced back to the exact program bytes that
|
|
6
|
+
* produced it. Isolated from program-migrations.ts to keep `node:crypto` out of
|
|
7
|
+
* the schema validation import chain.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
/** Deterministic JSON serialization with object keys sorted recursively. */
|
|
11
|
+
function stableStringify(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return 'null';
|
|
14
|
+
if (value === null || typeof value !== 'object')
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
18
|
+
const obj = value;
|
|
19
|
+
const entries = Object.keys(obj)
|
|
20
|
+
.sort()
|
|
21
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
|
|
22
|
+
return `{${entries.join(',')}}`;
|
|
23
|
+
}
|
|
24
|
+
/** sha256 of the canonicalized program (stable across runs of the same program). */
|
|
25
|
+
export function hashProgram(program) {
|
|
26
|
+
return createHash('sha256').update(stableStringify(program)).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=program-hash.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Program FORM migrations (migrate-on-read)
|
|
3
|
+
*
|
|
4
|
+
* Old presets are stored at whatever `programSchemaVersion` (FORM) was current
|
|
5
|
+
* when they were authored. `upgradeProgram` runs a chain of pure
|
|
6
|
+
* `migrate_vN→vN+1` functions to bring any stored program up to the current
|
|
7
|
+
* form BEFORE strict schema validation, so the runner only ever sees one shape.
|
|
8
|
+
*
|
|
9
|
+
* Properties of this layer (decisions locked in AUT-242):
|
|
10
|
+
* - Compat forever: the chain is kept indefinitely; no support window.
|
|
11
|
+
* - Migrate-on-read only: programs are NEVER rewritten back to storage. The
|
|
12
|
+
* stored form changes only when the generator recompiles (create/modify).
|
|
13
|
+
* - Pure + idempotent: a program already at the current form is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* This module is intentionally free of Node-only imports so it can be pulled
|
|
16
|
+
* into the schema validation chain on any runtime. Content hashing
|
|
17
|
+
* (`node:crypto`) lives in program-hash.ts.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Reads the FORM version a raw (pre-migration) program was stored at.
|
|
21
|
+
* Absent / non-finite ⇒ 0 (the oldest form). Used to stamp run provenance
|
|
22
|
+
* (`program_schema_version_origin`) before `upgradeProgram` bumps it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readOriginSchemaVersion(raw: unknown): number;
|
|
25
|
+
/**
|
|
26
|
+
* Brings any stored program up to {@link CURRENT_PROGRAM_SCHEMA_VERSION} (form)
|
|
27
|
+
* before strict validation. Pure: clones, never mutates `raw`. Idempotent: a
|
|
28
|
+
* program already at the current form is returned with only its version stamped.
|
|
29
|
+
* Non-object input is returned untouched so the schema raises a clean error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function upgradeProgram(raw: unknown): unknown;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — Program FORM migrations (migrate-on-read)
|
|
3
|
+
*
|
|
4
|
+
* Old presets are stored at whatever `programSchemaVersion` (FORM) was current
|
|
5
|
+
* when they were authored. `upgradeProgram` runs a chain of pure
|
|
6
|
+
* `migrate_vN→vN+1` functions to bring any stored program up to the current
|
|
7
|
+
* form BEFORE strict schema validation, so the runner only ever sees one shape.
|
|
8
|
+
*
|
|
9
|
+
* Properties of this layer (decisions locked in AUT-242):
|
|
10
|
+
* - Compat forever: the chain is kept indefinitely; no support window.
|
|
11
|
+
* - Migrate-on-read only: programs are NEVER rewritten back to storage. The
|
|
12
|
+
* stored form changes only when the generator recompiles (create/modify).
|
|
13
|
+
* - Pure + idempotent: a program already at the current form is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* This module is intentionally free of Node-only imports so it can be pulled
|
|
16
|
+
* into the schema validation chain on any runtime. Content hashing
|
|
17
|
+
* (`node:crypto`) lives in program-hash.ts.
|
|
18
|
+
*/
|
|
19
|
+
import { CURRENT_PROGRAM_SCHEMA_VERSION } from './engine-version.js';
|
|
20
|
+
/** Canonical video capture/delivery resolution (1920×1080 @1×). */
|
|
21
|
+
const VIDEO_RESOLUTION = { width: 1920, height: 1080 };
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads the FORM version a raw (pre-migration) program was stored at.
|
|
27
|
+
* Absent / non-finite ⇒ 0 (the oldest form). Used to stamp run provenance
|
|
28
|
+
* (`program_schema_version_origin`) before `upgradeProgram` bumps it.
|
|
29
|
+
*/
|
|
30
|
+
export function readOriginSchemaVersion(raw) {
|
|
31
|
+
if (isRecord(raw) &&
|
|
32
|
+
typeof raw.programSchemaVersion === 'number' &&
|
|
33
|
+
Number.isFinite(raw.programSchemaVersion)) {
|
|
34
|
+
return raw.programSchemaVersion;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* v0 → v1: fold the legacy video capture-resolution normalization (formerly the
|
|
40
|
+
* runtime `normalizeVideoCaptureProgram` in cli-runner.ts) into the program
|
|
41
|
+
* FORM. Legacy video presets carrying viewport=2560×1440 / DPR=1.3333 /
|
|
42
|
+
* captureResolution=2560×1440 are normalized to 1920×1080 @1×. Non-video
|
|
43
|
+
* programs pass through unchanged. Defensive on malformed shapes — anything it
|
|
44
|
+
* can't normalize is left for the strict schema to reject with a clean error.
|
|
45
|
+
*/
|
|
46
|
+
function migrate_0_to_1(prog) {
|
|
47
|
+
if (prog.mediaMode !== 'video')
|
|
48
|
+
return prog;
|
|
49
|
+
const artifactPlan = isRecord(prog.artifactPlan) ? prog.artifactPlan : undefined;
|
|
50
|
+
const format = artifactPlan && isRecord(artifactPlan.format) ? artifactPlan.format : {};
|
|
51
|
+
const variants = Array.isArray(prog.variants)
|
|
52
|
+
? prog.variants.map((v) => isRecord(v) ? { ...v, viewport: { ...VIDEO_RESOLUTION }, deviceScaleFactor: 1 } : v)
|
|
53
|
+
: prog.variants;
|
|
54
|
+
return {
|
|
55
|
+
...prog,
|
|
56
|
+
outputScale: 1,
|
|
57
|
+
variants,
|
|
58
|
+
artifactPlan: {
|
|
59
|
+
...(artifactPlan ?? {}),
|
|
60
|
+
format: {
|
|
61
|
+
...format,
|
|
62
|
+
captureResolution: { ...VIDEO_RESOLUTION },
|
|
63
|
+
deliveryResolution: { ...VIDEO_RESOLUTION },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Ordered FORM migrations. `MIGRATIONS[n]` upgrades a vN program to vN+1. */
|
|
69
|
+
const MIGRATIONS = {
|
|
70
|
+
0: migrate_0_to_1,
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Brings any stored program up to {@link CURRENT_PROGRAM_SCHEMA_VERSION} (form)
|
|
74
|
+
* before strict validation. Pure: clones, never mutates `raw`. Idempotent: a
|
|
75
|
+
* program already at the current form is returned with only its version stamped.
|
|
76
|
+
* Non-object input is returned untouched so the schema raises a clean error.
|
|
77
|
+
*/
|
|
78
|
+
export function upgradeProgram(raw) {
|
|
79
|
+
if (!isRecord(raw))
|
|
80
|
+
return raw;
|
|
81
|
+
let prog = { ...raw };
|
|
82
|
+
let v = readOriginSchemaVersion(raw);
|
|
83
|
+
while (v < CURRENT_PROGRAM_SCHEMA_VERSION) {
|
|
84
|
+
const migrate = MIGRATIONS[v];
|
|
85
|
+
if (!migrate)
|
|
86
|
+
break; // no path forward; let the schema reject a wrong shape
|
|
87
|
+
prog = migrate(prog);
|
|
88
|
+
v += 1;
|
|
89
|
+
}
|
|
90
|
+
prog.programSchemaVersion = CURRENT_PROGRAM_SCHEMA_VERSION;
|
|
91
|
+
return prog;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=program-migrations.js.map
|
|
@@ -13,6 +13,13 @@ export interface SignedExecutionProgramEnvelope {
|
|
|
13
13
|
signature: string;
|
|
14
14
|
meta?: {
|
|
15
15
|
stale?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* FORM version the stored program was at BEFORE the server migrated it
|
|
18
|
+
* (0 = legacy / no version field). Debug-only run provenance; lives in the
|
|
19
|
+
* UNSIGNED meta because it carries no security weight — the server reads it
|
|
20
|
+
* from the raw preset config before `extractExecutionProgram` normalizes.
|
|
21
|
+
*/
|
|
22
|
+
programSchemaVersionOrigin?: number;
|
|
16
23
|
};
|
|
17
24
|
}
|
|
18
25
|
export declare const ProgramSecurityMetadataSchema: z.ZodObject<{
|
|
@@ -31,6 +38,8 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
31
38
|
program: z.ZodObject<{
|
|
32
39
|
presetId: z.ZodString;
|
|
33
40
|
programVersion: z.ZodNumber;
|
|
41
|
+
programSchemaVersion: z.ZodOptional<z.ZodNumber>;
|
|
42
|
+
engineVersion: z.ZodOptional<z.ZodNumber>;
|
|
34
43
|
mediaMode: z.ZodEnum<{
|
|
35
44
|
video: "video";
|
|
36
45
|
clip: "clip";
|
|
@@ -141,6 +150,7 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
141
150
|
domain: z.ZodString;
|
|
142
151
|
path: z.ZodOptional<z.ZodString>;
|
|
143
152
|
}, z.core.$strict>>>;
|
|
153
|
+
scenario: z.ZodOptional<z.ZodString>;
|
|
144
154
|
}, z.core.$strict>;
|
|
145
155
|
steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
146
156
|
url: z.ZodString;
|
|
@@ -1133,6 +1143,7 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
1133
1143
|
signature: z.ZodString;
|
|
1134
1144
|
meta: z.ZodOptional<z.ZodObject<{
|
|
1135
1145
|
stale: z.ZodOptional<z.ZodBoolean>;
|
|
1146
|
+
programSchemaVersionOrigin: z.ZodOptional<z.ZodNumber>;
|
|
1136
1147
|
}, z.core.$strict>>;
|
|
1137
1148
|
}, z.core.$strict>;
|
|
1138
1149
|
export declare function normalizeAllowedOrigins(origins: Iterable<string>): string[];
|
package/dist/program-signing.js
CHANGED
|
@@ -30,6 +30,7 @@ export const SignedExecutionProgramEnvelopeSchema = z.object({
|
|
|
30
30
|
signature: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
31
31
|
meta: z.object({
|
|
32
32
|
stale: z.boolean().optional(),
|
|
33
|
+
programSchemaVersionOrigin: z.number().int().nonnegative().optional(),
|
|
33
34
|
}).strict().optional(),
|
|
34
35
|
}).strict();
|
|
35
36
|
function stableNormalize(value) {
|
package/dist/recovery-chain.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. LLM Healer (last resort)
|
|
10
10
|
*/
|
|
11
11
|
import { resolveSelector } from './selector-resolver.js';
|
|
12
|
-
import { evaluatePostcondition } from './postcondition.js';
|
|
12
|
+
import { evaluatePostcondition, evaluatePostconditionWithProgress } from './postcondition.js';
|
|
13
13
|
import { LLMHealer } from './llm-healer.js';
|
|
14
14
|
import { serializeAKTree } from './ak-tree.js';
|
|
15
15
|
import { executeOpcodeCoreAction } from './opcode-actions.js';
|
|
@@ -35,7 +35,7 @@ export class RecoveryChainImpl {
|
|
|
35
35
|
const retryBudget = Math.max(0, Math.min(recovery.retries, options.maxDeterministicRetries ?? recovery.retries));
|
|
36
36
|
if (retryBudget > 0) {
|
|
37
37
|
logger.debug(`[recovery ${opcodeIndex}] strategy 1 (retry x${retryBudget})`);
|
|
38
|
-
const result = await retryOpcode(failedOpcode, adapter, retryBudget, options.remainingTimeMs, options.currentVariant, this.credentials, options.suppressPageReloads);
|
|
38
|
+
const result = await retryOpcode(failedOpcode, adapter, retryBudget, options.remainingTimeMs, options.currentVariant, this.credentials, options.suppressPageReloads, options.globalDeadlineMs, options.getProgress);
|
|
39
39
|
logger.debug(`[recovery ${opcodeIndex}] strategy 1 → recovered=${result.recovered}, reason=${result.reason}`);
|
|
40
40
|
if (result.recovered)
|
|
41
41
|
return result;
|
|
@@ -86,7 +86,7 @@ export class RecoveryChainImpl {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
// ── Strategy 1: Deterministic retry ─────────────────────────────────
|
|
89
|
-
async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, currentVariant, credentials, suppressPageReloads = false) {
|
|
89
|
+
async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, currentVariant, credentials, suppressPageReloads = false, globalDeadlineMs, getProgress) {
|
|
90
90
|
const deadlineMs = typeof remainingTimeMs === 'number'
|
|
91
91
|
? Date.now() + remainingTimeMs
|
|
92
92
|
: Number.POSITIVE_INFINITY;
|
|
@@ -97,7 +97,11 @@ async function retryOpcode(opcode, adapter, maxRetries, remainingTimeMs, current
|
|
|
97
97
|
await sleep(500 * (i + 1)); // backoff
|
|
98
98
|
try {
|
|
99
99
|
await executeRawAction(opcode, adapter, currentVariant, credentials, suppressPageReloads);
|
|
100
|
-
|
|
100
|
+
// AUT-240 (Phase 5): the re-check extends-on-progress up to the global
|
|
101
|
+
// deadline instead of replaying a fixed clamped budget. Without the global
|
|
102
|
+
// deadline (legacy callers), `getProgress` is dropped so it degrades to the
|
|
103
|
+
// compiled postcondition budget.
|
|
104
|
+
const postcondition = await evaluatePostconditionWithProgress(adapter, opcode.postcondition, Date.now(), globalDeadlineMs ?? deadlineMs, globalDeadlineMs !== undefined ? getProgress : undefined);
|
|
101
105
|
if (postcondition.passed) {
|
|
102
106
|
return { recovered: true, strategy: 'retry', reason: `retry ${i + 1}/${maxRetries} succeeded` };
|
|
103
107
|
}
|
|
@@ -363,11 +367,4 @@ async function resolveSelectorCoordinates(adapter, selector) {
|
|
|
363
367
|
function sleep(ms) {
|
|
364
368
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
365
369
|
}
|
|
366
|
-
function clampPostconditionWait(spec, deadlineMs) {
|
|
367
|
-
const remainingTimeMs = Math.max(1, deadlineMs - Date.now());
|
|
368
|
-
return {
|
|
369
|
-
...spec,
|
|
370
|
-
waitMs: Math.max(1, Math.min(spec.waitMs ?? remainingTimeMs, remainingTimeMs)),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
370
|
//# sourceMappingURL=recovery-chain.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PreconditionSpec } from './execution-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* AUT-239 — signing side of the AutoKap Scenario cookie.
|
|
4
|
+
*
|
|
5
|
+
* The runner (this CLI) signs; the client app's `@autokap/scenario` package
|
|
6
|
+
* verifies. At runtime these live in two separate processes/deployments, so the
|
|
7
|
+
* only thing they share is the WIRE FORMAT, not a module. We therefore keep a
|
|
8
|
+
* tiny local signer here rather than taking a runtime dependency on the
|
|
9
|
+
* published `@autokap/scenario` package. `scenario-cookie.test.ts` locks the
|
|
10
|
+
* format against the real resolver so the two can never drift.
|
|
11
|
+
*
|
|
12
|
+
* Format: `<id>.<base64url(HMAC-SHA256(secret, id))>`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SCENARIO_COOKIE_NAME = "__ak_scenario";
|
|
15
|
+
export declare function signScenarioCookie(id: string, secret: string): string;
|
|
16
|
+
/** A cookie ready for Playwright's `context.addCookies`. */
|
|
17
|
+
export interface PreconditionCookie {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
secure: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the cookie set injected before navigation: the preset's seed cookies
|
|
26
|
+
* plus, when configured, the signed scenario switch.
|
|
27
|
+
*
|
|
28
|
+
* `secure` is derived from the target protocol — a Secure cookie is never sent
|
|
29
|
+
* over http://localhost, so the runner must NOT default it to true (that would
|
|
30
|
+
* silently drop seed/scenario cookies in local captures and fall back to real
|
|
31
|
+
* data). Returns a `warning` when a scenario is requested but unsignable.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildPreconditionCookies(preconditions: Pick<PreconditionSpec, 'cookies' | 'scenario'>, baseUrl: string, secret: string | undefined): {
|
|
34
|
+
cookies: PreconditionCookie[];
|
|
35
|
+
warning?: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* AUT-239 — signing side of the AutoKap Scenario cookie.
|
|
4
|
+
*
|
|
5
|
+
* The runner (this CLI) signs; the client app's `@autokap/scenario` package
|
|
6
|
+
* verifies. At runtime these live in two separate processes/deployments, so the
|
|
7
|
+
* only thing they share is the WIRE FORMAT, not a module. We therefore keep a
|
|
8
|
+
* tiny local signer here rather than taking a runtime dependency on the
|
|
9
|
+
* published `@autokap/scenario` package. `scenario-cookie.test.ts` locks the
|
|
10
|
+
* format against the real resolver so the two can never drift.
|
|
11
|
+
*
|
|
12
|
+
* Format: `<id>.<base64url(HMAC-SHA256(secret, id))>`.
|
|
13
|
+
*/
|
|
14
|
+
export const SCENARIO_COOKIE_NAME = '__ak_scenario';
|
|
15
|
+
export function signScenarioCookie(id, secret) {
|
|
16
|
+
const sig = createHmac('sha256', secret).update(id).digest('base64url');
|
|
17
|
+
return `${id}.${sig}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the cookie set injected before navigation: the preset's seed cookies
|
|
21
|
+
* plus, when configured, the signed scenario switch.
|
|
22
|
+
*
|
|
23
|
+
* `secure` is derived from the target protocol — a Secure cookie is never sent
|
|
24
|
+
* over http://localhost, so the runner must NOT default it to true (that would
|
|
25
|
+
* silently drop seed/scenario cookies in local captures and fall back to real
|
|
26
|
+
* data). Returns a `warning` when a scenario is requested but unsignable.
|
|
27
|
+
*/
|
|
28
|
+
export function buildPreconditionCookies(preconditions, baseUrl, secret) {
|
|
29
|
+
let secure = true;
|
|
30
|
+
let hostname = '';
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(baseUrl);
|
|
33
|
+
secure = url.protocol === 'https:';
|
|
34
|
+
hostname = url.hostname;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* baseUrl is validated upstream; fall back to Secure on parse failure */
|
|
38
|
+
}
|
|
39
|
+
const cookies = (preconditions.cookies ?? []).map((c) => ({
|
|
40
|
+
...c,
|
|
41
|
+
secure,
|
|
42
|
+
}));
|
|
43
|
+
let warning;
|
|
44
|
+
if (preconditions.scenario) {
|
|
45
|
+
if (!secret) {
|
|
46
|
+
warning =
|
|
47
|
+
`preconditions.scenario="${preconditions.scenario}" set but AUTOKAP_SCENARIO_SECRET ` +
|
|
48
|
+
`is missing — scenario cookie NOT injected; capture will use real data`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
cookies.push({
|
|
52
|
+
name: SCENARIO_COOKIE_NAME,
|
|
53
|
+
value: signScenarioCookie(preconditions.scenario, secret),
|
|
54
|
+
domain: hostname,
|
|
55
|
+
path: '/',
|
|
56
|
+
secure,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { cookies, warning };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=scenario-cookie.js.map
|
package/dist/security.d.ts
CHANGED
|
@@ -16,6 +16,27 @@ export interface SecurityDecision {
|
|
|
16
16
|
reason?: string;
|
|
17
17
|
target?: InteractiveElement | null;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Is `candidateUrl` part of the same first-party site as `scopeUrl`? Shares the
|
|
21
|
+
* navigation site-scope model (`isWithinProjectScope`): exact-host for IPs /
|
|
22
|
+
* localhost / shared-hosting suffixes (so sibling `*.vercel.app` previews are
|
|
23
|
+
* NOT same-site), sub-domain family match for real registrable domains.
|
|
24
|
+
*
|
|
25
|
+
* Used to scope the adaptive-wait progress signal (AUT-240) to the app's own
|
|
26
|
+
* traffic, so third-party telemetry (PostHog beacons, analytics/ad pixels,
|
|
27
|
+
* Sentry, …) no longer reads as "the page is making progress" and the stuck
|
|
28
|
+
* watchdog can still cut a wait whose condition will never be met.
|
|
29
|
+
*
|
|
30
|
+
* Fail-OPEN by design: a false "first-party" only makes the watchdog slightly
|
|
31
|
+
* more patient (still bounded by the per-media cap), whereas a false "foreign"
|
|
32
|
+
* could suppress a real progress signal and cut a legitimately-slow page early.
|
|
33
|
+
* So when in doubt we count it as first-party:
|
|
34
|
+
* - unparseable / non-http(s) `scopeUrl` (e.g. `about:blank` before the first
|
|
35
|
+
* navigation commits) ⇒ true (don't filter);
|
|
36
|
+
* - non-http(s) `candidateUrl` (`data:` / `blob:` / `about:`) ⇒ true (in-page
|
|
37
|
+
* resource).
|
|
38
|
+
*/
|
|
39
|
+
export declare function isFirstPartyUrl(scopeUrl: string | null | undefined, candidateUrl: string): boolean;
|
|
19
40
|
export declare function evaluateResolvedActionSecurity(action: ActionType, args: Record<string, unknown>, context: SecurityContext, target: InteractiveElement | null): SecurityDecision;
|
|
20
41
|
export declare function evaluateActionSecurity(action: ActionType, args: Record<string, unknown>, context: SecurityContext): SecurityDecision;
|
|
21
42
|
export declare function describeSecurityTarget(target: InteractiveElement | null | undefined): string;
|
package/dist/security.js
CHANGED
|
@@ -109,6 +109,16 @@ function isSharedHostingHostname(hostname) {
|
|
|
109
109
|
function normalizeScopeHostname(hostname) {
|
|
110
110
|
return hostname.startsWith('www.') ? hostname.slice(4) : hostname;
|
|
111
111
|
}
|
|
112
|
+
function scopeFromParsedUrl(parsed) {
|
|
113
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
114
|
+
const exactHost = isLocalHostname(hostname) || isIpv4Hostname(hostname) || isIpv6Hostname(hostname) || isSharedHostingHostname(hostname);
|
|
115
|
+
const scopeHostname = exactHost ? hostname : normalizeScopeHostname(hostname);
|
|
116
|
+
return {
|
|
117
|
+
hostname: scopeHostname,
|
|
118
|
+
port: effectivePort(parsed),
|
|
119
|
+
exactHost,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
112
122
|
function buildNavigationScopes(context) {
|
|
113
123
|
const sources = [
|
|
114
124
|
context.rootUrl,
|
|
@@ -119,18 +129,46 @@ function buildNavigationScopes(context) {
|
|
|
119
129
|
const parsed = parseHttpUrl(value);
|
|
120
130
|
if (!parsed)
|
|
121
131
|
continue;
|
|
122
|
-
const
|
|
123
|
-
const exactHost = isLocalHostname(hostname) || isIpv4Hostname(hostname) || isIpv6Hostname(hostname) || isSharedHostingHostname(hostname);
|
|
124
|
-
const scopeHostname = exactHost ? hostname : normalizeScopeHostname(hostname);
|
|
125
|
-
const scope = {
|
|
126
|
-
hostname: scopeHostname,
|
|
127
|
-
port: effectivePort(parsed),
|
|
128
|
-
exactHost,
|
|
129
|
-
};
|
|
132
|
+
const scope = scopeFromParsedUrl(parsed);
|
|
130
133
|
dedup.set(`${scope.hostname}:${scope.port}:${scope.exactHost ? 'exact' : 'family'}`, scope);
|
|
131
134
|
}
|
|
132
135
|
return Array.from(dedup.values());
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Is `candidateUrl` part of the same first-party site as `scopeUrl`? Shares the
|
|
139
|
+
* navigation site-scope model (`isWithinProjectScope`): exact-host for IPs /
|
|
140
|
+
* localhost / shared-hosting suffixes (so sibling `*.vercel.app` previews are
|
|
141
|
+
* NOT same-site), sub-domain family match for real registrable domains.
|
|
142
|
+
*
|
|
143
|
+
* Used to scope the adaptive-wait progress signal (AUT-240) to the app's own
|
|
144
|
+
* traffic, so third-party telemetry (PostHog beacons, analytics/ad pixels,
|
|
145
|
+
* Sentry, …) no longer reads as "the page is making progress" and the stuck
|
|
146
|
+
* watchdog can still cut a wait whose condition will never be met.
|
|
147
|
+
*
|
|
148
|
+
* Fail-OPEN by design: a false "first-party" only makes the watchdog slightly
|
|
149
|
+
* more patient (still bounded by the per-media cap), whereas a false "foreign"
|
|
150
|
+
* could suppress a real progress signal and cut a legitimately-slow page early.
|
|
151
|
+
* So when in doubt we count it as first-party:
|
|
152
|
+
* - unparseable / non-http(s) `scopeUrl` (e.g. `about:blank` before the first
|
|
153
|
+
* navigation commits) ⇒ true (don't filter);
|
|
154
|
+
* - non-http(s) `candidateUrl` (`data:` / `blob:` / `about:`) ⇒ true (in-page
|
|
155
|
+
* resource).
|
|
156
|
+
*/
|
|
157
|
+
export function isFirstPartyUrl(scopeUrl, candidateUrl) {
|
|
158
|
+
const scopeParsed = parseHttpUrl(scopeUrl ?? undefined);
|
|
159
|
+
if (!scopeParsed)
|
|
160
|
+
return true;
|
|
161
|
+
let candidate;
|
|
162
|
+
try {
|
|
163
|
+
candidate = new URL(candidateUrl);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (candidate.protocol !== 'http:' && candidate.protocol !== 'https:')
|
|
169
|
+
return true;
|
|
170
|
+
return isWithinProjectScope(candidate, [scopeFromParsedUrl(scopeParsed)]);
|
|
171
|
+
}
|
|
134
172
|
function isWithinProjectScope(candidate, scopes) {
|
|
135
173
|
const hostname = candidate.hostname.toLowerCase();
|
|
136
174
|
const port = effectivePort(candidate);
|
package/dist/version.d.ts
CHANGED