@hraness/direct 0.7.5
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 +21 -0
- package/README.md +436 -0
- package/dist/core/index.js +162 -0
- package/dist/index-1csg00w4.js +1167 -0
- package/dist/index-6mdfd2ey.js +464 -0
- package/dist/index-7n1h75n6.js +616 -0
- package/dist/index.js +232 -0
- package/dist/react.js +32 -0
- package/dist/testing/index.js +1069 -0
- package/dist/tooling/bombadil.js +2117 -0
- package/dist/tooling/browser-verification-entry.js +1499 -0
- package/dist/tooling/bundle-boundary.js +119 -0
- package/dist/web.js +605 -0
- package/package.json +179 -0
- package/skills/direct/AGENTS.md +13 -0
- package/skills/direct/SKILL.md +49 -0
- package/skills/direct/agents/openai.yaml +4 -0
- package/skills/direct/references/adoption.md +131 -0
- package/skills/direct/references/install.md +91 -0
- package/skills/direct/references/verification.md +247 -0
- package/src/core/coverage.ts +336 -0
- package/src/core/definition.ts +378 -0
- package/src/core/effects.ts +88 -0
- package/src/core/fixture.ts +185 -0
- package/src/core/ids.ts +77 -0
- package/src/core/index.ts +13 -0
- package/src/core/json-value.ts +7 -0
- package/src/core/json.ts +593 -0
- package/src/core/query.ts +230 -0
- package/src/core/reason.ts +16 -0
- package/src/core/resource.ts +10 -0
- package/src/core/result.ts +19 -0
- package/src/core/runtime.ts +229 -0
- package/src/core/scenario.ts +149 -0
- package/src/core/store.ts +784 -0
- package/src/index.ts +51 -0
- package/src/react.ts +54 -0
- package/src/testing/activity.ts +228 -0
- package/src/testing/coverage-binding.ts +99 -0
- package/src/testing/evidence.ts +59 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/manifest.ts +559 -0
- package/src/testing/probe.ts +446 -0
- package/src/testing/scripted-transport.ts +775 -0
- package/src/testing/session.ts +525 -0
- package/src/tooling/bombadil-campaign.ts +288 -0
- package/src/tooling/bombadil-internal.d.ts +46 -0
- package/src/tooling/bombadil-runner.ts +1424 -0
- package/src/tooling/bombadil.ts +27 -0
- package/src/tooling/browser-verification-entry.ts +32 -0
- package/src/tooling/browser-verification.ts +916 -0
- package/src/tooling/bundle-boundary.ts +159 -0
- package/src/web/browser-bridge.ts +296 -0
- package/src/web/browser.ts +277 -0
- package/src/web/fetch-firewall.ts +251 -0
- package/src/web.ts +27 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
|
2
|
+
/// <reference path="./bombadil-internal.d.ts" />
|
|
3
|
+
/* eslint-enable @typescript-eslint/triple-slash-reference */
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
actions,
|
|
7
|
+
always,
|
|
8
|
+
eventually,
|
|
9
|
+
extract,
|
|
10
|
+
weighted,
|
|
11
|
+
type ActionGenerator,
|
|
12
|
+
type Formula,
|
|
13
|
+
type JSON as BombadilJson,
|
|
14
|
+
type Tree,
|
|
15
|
+
} from "@antithesishq/bombadil";
|
|
16
|
+
import type {
|
|
17
|
+
ActionTemplate,
|
|
18
|
+
State as BombadilBrowserState,
|
|
19
|
+
} from "@antithesishq/bombadil/browser";
|
|
20
|
+
import {
|
|
21
|
+
clicks,
|
|
22
|
+
inputs,
|
|
23
|
+
scroll,
|
|
24
|
+
} from "@antithesishq/bombadil/browser/defaults/actions";
|
|
25
|
+
|
|
26
|
+
const DIRECT_BROWSER_BRIDGE_SCHEMA = "direct.browser-bridge/v2";
|
|
27
|
+
const DIRECT_SESSION_MANIFEST_SCHEMA = "direct.session-manifest/v1";
|
|
28
|
+
const DIRECT_PROBE_SCHEMA = "direct.probe/v1";
|
|
29
|
+
const MAX_RAW_CONTRACT_CHARACTERS = 2_000_000;
|
|
30
|
+
const BRIDGE_KEYS = new Set(["manifest", "reset", "schema", "snapshot"]);
|
|
31
|
+
const UNSAFE_CLICK_INPUT_TYPES = new Set(["image", "reset", "submit"]);
|
|
32
|
+
|
|
33
|
+
export interface DirectBombadilObservation {
|
|
34
|
+
readonly [key: string | number | symbol]: BombadilJson;
|
|
35
|
+
readonly activationHash: string;
|
|
36
|
+
readonly activeRoute: string;
|
|
37
|
+
readonly activeScenario: string;
|
|
38
|
+
readonly activeSource: string;
|
|
39
|
+
readonly bridgePresent: boolean;
|
|
40
|
+
readonly bridgeSchema: string;
|
|
41
|
+
readonly catalogHash: string;
|
|
42
|
+
readonly contractValid: boolean;
|
|
43
|
+
readonly isQuiescent: boolean;
|
|
44
|
+
readonly manifest: BombadilJson;
|
|
45
|
+
readonly probe: BombadilJson;
|
|
46
|
+
readonly violations: number[];
|
|
47
|
+
readonly violationsValid: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DirectBombadilProperties {
|
|
51
|
+
readonly exactContract: Formula;
|
|
52
|
+
readonly stableCatalog: Formula;
|
|
53
|
+
readonly noDeclaredViolations: Formula;
|
|
54
|
+
readonly eventualQuiescence: Formula;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeClickAction(action: ActionTemplate): boolean {
|
|
58
|
+
if (typeof action !== "object" || action === null) return false;
|
|
59
|
+
const candidate = "Click" in action
|
|
60
|
+
? action.Click
|
|
61
|
+
: "DoubleClick" in action
|
|
62
|
+
? action.DoubleClick
|
|
63
|
+
: null;
|
|
64
|
+
if (candidate === null) return false;
|
|
65
|
+
const { fingerprint } = candidate;
|
|
66
|
+
const tag = fingerprint.tag.toLowerCase();
|
|
67
|
+
const inputType = fingerprint.inputType?.toLowerCase() ?? "";
|
|
68
|
+
const labels = [fingerprint.accessibleName, fingerprint.textContent]
|
|
69
|
+
.filter((label): label is string => label !== null)
|
|
70
|
+
.map((label) => label.trim().toLowerCase());
|
|
71
|
+
return fingerprint.href === null
|
|
72
|
+
&& tag !== "a"
|
|
73
|
+
&& fingerprint.role?.toLowerCase() !== "link"
|
|
74
|
+
&& !labels.includes("reset")
|
|
75
|
+
&& !UNSAFE_CLICK_INPUT_TYPES.has(inputType)
|
|
76
|
+
&& (tag !== "button" || inputType === "button");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeInputAction(action: ActionTemplate): boolean {
|
|
80
|
+
return !(
|
|
81
|
+
typeof action === "object"
|
|
82
|
+
&& action !== null
|
|
83
|
+
&& "PressKey" in action
|
|
84
|
+
&& action.PressKey.code === 13
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pruneActionTree<Action>(
|
|
89
|
+
tree: Tree<Action>,
|
|
90
|
+
keep: (action: Action) => boolean,
|
|
91
|
+
): Tree<Action> | null {
|
|
92
|
+
if ("value" in tree) return keep(tree.value) ? tree : null;
|
|
93
|
+
const branches: [number, Tree<Action>][] = [];
|
|
94
|
+
for (const [weight, child] of tree.branches) {
|
|
95
|
+
const filtered = pruneActionTree(child, keep);
|
|
96
|
+
if (filtered !== null) branches.push([weight, filtered]);
|
|
97
|
+
}
|
|
98
|
+
return branches.length === 0 ? null : { branches };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds a browser action generator without reload/history actions or visible
|
|
103
|
+
* navigation and submission click targets, keeping Direct continuously bound.
|
|
104
|
+
*/
|
|
105
|
+
export function createDirectBombadilActions(): ActionGenerator<ActionTemplate> {
|
|
106
|
+
const safeClicks = actions(() => pruneActionTree(clicks.generate(), safeClickAction) ?? []);
|
|
107
|
+
const safeInputs = actions(() => pruneActionTree(inputs.generate(), safeInputAction) ?? []);
|
|
108
|
+
const wait = actions<ActionTemplate>(() => ["Wait"]);
|
|
109
|
+
return weighted([
|
|
110
|
+
[4, safeClicks],
|
|
111
|
+
[3, safeInputs],
|
|
112
|
+
[2, scroll],
|
|
113
|
+
[1, wait],
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
118
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasExactKeys(
|
|
122
|
+
value: Readonly<Record<string, unknown>>,
|
|
123
|
+
expected: ReadonlySet<string>,
|
|
124
|
+
): boolean {
|
|
125
|
+
const keys = Object.keys(value);
|
|
126
|
+
return keys.length === expected.size && keys.every((key) => expected.has(key));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function invalidObservation(bridgePresent = false): DirectBombadilObservation {
|
|
130
|
+
return {
|
|
131
|
+
activationHash: "",
|
|
132
|
+
activeRoute: "",
|
|
133
|
+
activeScenario: "",
|
|
134
|
+
activeSource: "",
|
|
135
|
+
bridgePresent,
|
|
136
|
+
bridgeSchema: "",
|
|
137
|
+
catalogHash: "",
|
|
138
|
+
contractValid: false,
|
|
139
|
+
isQuiescent: false,
|
|
140
|
+
manifest: null,
|
|
141
|
+
probe: null,
|
|
142
|
+
violations: [],
|
|
143
|
+
violationsValid: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function boundedJsonClone(value: unknown): BombadilJson | null {
|
|
148
|
+
const source = JSON.stringify(value);
|
|
149
|
+
if (source === undefined || source.length > MAX_RAW_CONTRACT_CHARACTERS) return null;
|
|
150
|
+
return JSON.parse(source) as BombadilJson;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readNonNegativeCounters(value: unknown): {
|
|
154
|
+
readonly valid: boolean;
|
|
155
|
+
readonly values: number[];
|
|
156
|
+
} {
|
|
157
|
+
if (!isRecord(value)) return { valid: false, values: [] };
|
|
158
|
+
const values = Object.values(value);
|
|
159
|
+
if (!values.every((candidate) =>
|
|
160
|
+
typeof candidate === "number"
|
|
161
|
+
&& Number.isSafeInteger(candidate)
|
|
162
|
+
&& candidate >= 0
|
|
163
|
+
)) {
|
|
164
|
+
return { valid: false, values: [] };
|
|
165
|
+
}
|
|
166
|
+
return { valid: true, values: values as number[] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reads the foreign Direct browser boundary without allowing a hostile getter,
|
|
171
|
+
* proxy, or snapshot callback to escape the extractor.
|
|
172
|
+
*
|
|
173
|
+
* The extractor intentionally retains bounded raw contracts. The host runner
|
|
174
|
+
* applies Direct's canonical manifest and probe parsers to every trace sample.
|
|
175
|
+
*/
|
|
176
|
+
export function readDirectBombadilObservation(
|
|
177
|
+
windowValue: unknown,
|
|
178
|
+
): DirectBombadilObservation {
|
|
179
|
+
let bridgePresent = false;
|
|
180
|
+
try {
|
|
181
|
+
if (!isRecord(windowValue)) return invalidObservation();
|
|
182
|
+
bridgePresent = Object.hasOwn(windowValue, "__direct");
|
|
183
|
+
if (!bridgePresent) return invalidObservation();
|
|
184
|
+
const bridge = Reflect.get(windowValue, "__direct");
|
|
185
|
+
if (!isRecord(bridge) || !hasExactKeys(bridge, BRIDGE_KEYS)) {
|
|
186
|
+
return invalidObservation(true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
bridge.schema !== DIRECT_BROWSER_BRIDGE_SCHEMA
|
|
191
|
+
|| typeof bridge.reset !== "function"
|
|
192
|
+
|| typeof bridge.snapshot !== "function"
|
|
193
|
+
) {
|
|
194
|
+
return invalidObservation(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const manifest = boundedJsonClone(Reflect.get(bridge, "manifest"));
|
|
198
|
+
const snapshot = Reflect.get(bridge, "snapshot");
|
|
199
|
+
if (manifest === null || typeof snapshot !== "function") return invalidObservation(true);
|
|
200
|
+
const probe: unknown = Reflect.apply(snapshot, bridge, []) as unknown;
|
|
201
|
+
const clonedProbe = boundedJsonClone(probe);
|
|
202
|
+
if (clonedProbe === null || !isRecord(manifest) || !isRecord(clonedProbe)) {
|
|
203
|
+
return invalidObservation(true);
|
|
204
|
+
}
|
|
205
|
+
const active: unknown = Reflect.get(manifest, "active");
|
|
206
|
+
if (!isRecord(active)) return invalidObservation(true);
|
|
207
|
+
const violations = readNonNegativeCounters(Reflect.get(clonedProbe, "violations"));
|
|
208
|
+
const activationHash: unknown = Reflect.get(active, "activationHash");
|
|
209
|
+
const probeActivationHash: unknown = Reflect.get(clonedProbe, "activationHash");
|
|
210
|
+
const activeRoute: unknown = Reflect.get(active, "route");
|
|
211
|
+
const activeScenario: unknown = Reflect.get(active, "scenario");
|
|
212
|
+
const activeSource: unknown = Reflect.get(active, "source");
|
|
213
|
+
const catalogHash: unknown = Reflect.get(manifest, "catalogHash");
|
|
214
|
+
const isQuiescent: unknown = Reflect.get(clonedProbe, "isQuiescent");
|
|
215
|
+
const contractValid = Reflect.get(manifest, "schema") === DIRECT_SESSION_MANIFEST_SCHEMA
|
|
216
|
+
&& Reflect.get(clonedProbe, "schema") === DIRECT_PROBE_SCHEMA
|
|
217
|
+
&& typeof activationHash === "string"
|
|
218
|
+
&& activationHash.length > 0
|
|
219
|
+
&& probeActivationHash === activationHash
|
|
220
|
+
&& typeof activeRoute === "string"
|
|
221
|
+
&& activeRoute.length > 0
|
|
222
|
+
&& typeof activeScenario === "string"
|
|
223
|
+
&& activeScenario.length > 0
|
|
224
|
+
&& (activeSource === "scenario" || activeSource === "fixture")
|
|
225
|
+
&& typeof catalogHash === "string"
|
|
226
|
+
&& catalogHash.length > 0
|
|
227
|
+
&& typeof isQuiescent === "boolean"
|
|
228
|
+
&& violations.valid;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
activationHash: typeof activationHash === "string" ? activationHash : "",
|
|
232
|
+
activeRoute: typeof activeRoute === "string" ? activeRoute : "",
|
|
233
|
+
activeScenario: typeof activeScenario === "string" ? activeScenario : "",
|
|
234
|
+
activeSource: typeof activeSource === "string" ? activeSource : "",
|
|
235
|
+
bridgePresent: true,
|
|
236
|
+
bridgeSchema: DIRECT_BROWSER_BRIDGE_SCHEMA,
|
|
237
|
+
catalogHash: typeof catalogHash === "string" ? catalogHash : "",
|
|
238
|
+
contractValid,
|
|
239
|
+
isQuiescent: isQuiescent === true,
|
|
240
|
+
manifest,
|
|
241
|
+
probe: clonedProbe,
|
|
242
|
+
violations: violations.values,
|
|
243
|
+
violationsValid: violations.valid,
|
|
244
|
+
};
|
|
245
|
+
} catch {
|
|
246
|
+
return invalidObservation(bridgePresent);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Builds the four Direct invariants used by Bombadil browser campaigns. */
|
|
251
|
+
export function createDirectBombadilProperties(): DirectBombadilProperties {
|
|
252
|
+
const direct = extract<BombadilBrowserState, DirectBombadilObservation>((state) =>
|
|
253
|
+
readDirectBombadilObservation(state.window)
|
|
254
|
+
).named("direct");
|
|
255
|
+
|
|
256
|
+
const exactContract = always(
|
|
257
|
+
eventually(() =>
|
|
258
|
+
direct.current.contractValid
|
|
259
|
+
&& direct.current.activeSource === "scenario"
|
|
260
|
+
&& direct.current.activeScenario.length > 0
|
|
261
|
+
&& direct.current.activeRoute.length > 0
|
|
262
|
+
&& direct.current.activationHash.length > 0
|
|
263
|
+
).within(10, "seconds"),
|
|
264
|
+
);
|
|
265
|
+
const stableCatalog = always(
|
|
266
|
+
eventually(() =>
|
|
267
|
+
direct.current.contractValid
|
|
268
|
+
&& direct.current.catalogHash.length > 0
|
|
269
|
+
).within(10, "seconds"),
|
|
270
|
+
);
|
|
271
|
+
const noDeclaredViolations = always(
|
|
272
|
+
eventually(() =>
|
|
273
|
+
direct.current.contractValid
|
|
274
|
+
&& direct.current.violationsValid
|
|
275
|
+
&& direct.current.violations.every((value: number) => value === 0)
|
|
276
|
+
).within(10, "seconds"),
|
|
277
|
+
);
|
|
278
|
+
const eventualQuiescence = always(
|
|
279
|
+
eventually(() => direct.current.isQuiescent).within(10, "seconds"),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return Object.freeze({
|
|
283
|
+
exactContract,
|
|
284
|
+
stableCatalog,
|
|
285
|
+
noDeclaredViolations,
|
|
286
|
+
eventualQuiescence,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bombadil 0.7.2's public declarations reference this subpath, but its npm
|
|
3
|
+
* export map omits it. Keep this ambient declaration pinned to that release's
|
|
4
|
+
* public type dependency until the package exports the declaration itself.
|
|
5
|
+
*/
|
|
6
|
+
declare module "@antithesishq/bombadil/internal" {
|
|
7
|
+
export type TimeUnit = "milliseconds" | "seconds";
|
|
8
|
+
|
|
9
|
+
export interface Cell<T> {
|
|
10
|
+
readonly current: T;
|
|
11
|
+
update(snapshot: T): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type JSON =
|
|
15
|
+
| string
|
|
16
|
+
| number
|
|
17
|
+
| boolean
|
|
18
|
+
| null
|
|
19
|
+
| JSON[]
|
|
20
|
+
| { [key: string | number | symbol]: JSON }
|
|
21
|
+
| { toJSON(): JSON };
|
|
22
|
+
|
|
23
|
+
export class ExtractorCell<T extends JSON, S> implements Cell<T> {
|
|
24
|
+
readonly current: T;
|
|
25
|
+
readonly index: number;
|
|
26
|
+
name: string | null;
|
|
27
|
+
constructor(runtime: Runtime<S>, extract: (state: S) => T);
|
|
28
|
+
named(name: string): this;
|
|
29
|
+
run(state: S): T;
|
|
30
|
+
update(snapshot: T): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Runtime<S> {
|
|
34
|
+
readonly extractors: readonly ExtractorCell<JSON, S>[];
|
|
35
|
+
checkNotExtracting(): void;
|
|
36
|
+
recordAccess(index: number): void;
|
|
37
|
+
registerExtractor(cell: ExtractorCell<JSON, S>): number;
|
|
38
|
+
runExtractors(state: S): readonly {
|
|
39
|
+
readonly index: number;
|
|
40
|
+
readonly name: string | null;
|
|
41
|
+
readonly value: JSON;
|
|
42
|
+
}[];
|
|
43
|
+
startTracking(): void;
|
|
44
|
+
stopTracking(): number[];
|
|
45
|
+
}
|
|
46
|
+
}
|