@nikelyh/offscript 0.0.0-stage → 1.1.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/LICENSE +201 -0
- package/README.md +183 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/dist/packages/application/assign-investigation/execute.js +34 -0
- package/dist/packages/application/assign-investigation/ports.js +1 -0
- package/dist/packages/application/assign-investigation/resolve-context.js +41 -0
- package/dist/packages/application/check-target.js +4 -0
- package/dist/packages/application/complete-investigation/execute.js +54 -0
- package/dist/packages/application/complete-investigation/ports.js +1 -0
- package/dist/packages/application/complete-investigation/resolve-context.js +48 -0
- package/dist/packages/application/configure-simulation/execute.js +29 -0
- package/dist/packages/application/configure-simulation/ports.js +1 -0
- package/dist/packages/application/configure-simulation/profiles.js +16 -0
- package/dist/packages/application/delete-configured-simulation.js +11 -0
- package/dist/packages/application/errors.js +8 -0
- package/dist/packages/application/index.js +11 -0
- package/dist/packages/application/outbox/dispatch.js +48 -0
- package/dist/packages/application/outbox/ports.js +16 -0
- package/dist/packages/application/recover-simulation/execute.js +31 -0
- package/dist/packages/application/recover-simulation/ports.js +1 -0
- package/dist/packages/application/request-investigation/execute.js +38 -0
- package/dist/packages/application/request-investigation/ports.js +1 -0
- package/dist/packages/application/request-investigation/resolve-context.js +41 -0
- package/dist/packages/browser/browser-manager.js +91 -0
- package/dist/packages/browser/dom-snapshot.js +186 -0
- package/dist/packages/browser/index.js +5 -0
- package/dist/packages/browser/scope-guard.js +73 -0
- package/dist/packages/browser/session.js +209 -0
- package/dist/packages/browser/tools/browser-tools.js +192 -0
- package/dist/packages/browser/types.js +1 -0
- package/dist/packages/contracts/errors.js +8 -0
- package/dist/packages/contracts/event-envelope.js +28 -0
- package/dist/packages/contracts/event-validation.js +44 -0
- package/dist/packages/contracts/events.js +1 -0
- package/dist/packages/contracts/finding-assessment.js +128 -0
- package/dist/packages/contracts/index.js +15 -0
- package/dist/packages/contracts/investigation-events.js +43 -0
- package/dist/packages/contracts/investigation-summary.js +60 -0
- package/dist/packages/contracts/mappers.js +17 -0
- package/dist/packages/contracts/observation-summary.js +25 -0
- package/dist/packages/contracts/semantic-events.js +41 -0
- package/dist/packages/contracts/serialization.js +18 -0
- package/dist/packages/contracts/simulation-configuration.js +17 -0
- package/dist/packages/contracts/simulation-execution.js +62 -0
- package/dist/packages/contracts/simulation-results.js +52 -0
- package/dist/packages/contracts/simulation-setup.js +60 -0
- package/dist/packages/contracts/summaries.js +42 -0
- package/dist/packages/contracts/target-connection.js +36 -0
- package/dist/packages/contracts/validation.js +42 -0
- package/dist/packages/domain/agent.js +23 -0
- package/dist/packages/domain/environment.js +1 -0
- package/dist/packages/domain/errors.js +14 -0
- package/dist/packages/domain/identity.js +16 -0
- package/dist/packages/domain/index.js +10 -0
- package/dist/packages/domain/investigation-result.js +19 -0
- package/dist/packages/domain/investigation.js +70 -0
- package/dist/packages/domain/observation.js +19 -0
- package/dist/packages/domain/profile.js +19 -0
- package/dist/packages/domain/simulation.js +61 -0
- package/dist/packages/domain/timestamp.js +9 -0
- package/dist/packages/orchestration/browser-exploration-factory.js +135 -0
- package/dist/packages/orchestration/controlled-demo-factory.js +26 -0
- package/dist/packages/orchestration/deterministic-browser-runner.js +92 -0
- package/dist/packages/orchestration/deterministic-runner.js +69 -0
- package/dist/packages/orchestration/event-bridge.js +27 -0
- package/dist/packages/orchestration/guard-inference.js +44 -0
- package/dist/packages/orchestration/index.js +7 -0
- package/dist/packages/orchestration/investigation-reaction.js +53 -0
- package/dist/packages/orchestration/investigation-store.js +96 -0
- package/dist/packages/orchestration/memory-outbox.js +63 -0
- package/dist/packages/orchestration/multi-mode-factory.js +12 -0
- package/dist/packages/orchestration/outbox-publisher.js +13 -0
- package/dist/packages/orchestration/session-storage.js +52 -0
- package/dist/packages/orchestration/session.js +176 -0
- package/dist/packages/orchestration/situations.js +26 -0
- package/dist/packages/orchestration/state.js +32 -0
- package/dist/packages/orchestration/tools/arguments.js +13 -0
- package/dist/packages/orchestration/tools/browser-exploration-tools.js +154 -0
- package/dist/packages/orchestration/tools/browser-interaction-tools.js +74 -0
- package/dist/packages/orchestration/tools/exploration-tools.js +119 -0
- package/dist/packages/persistence/codecs/investigation.js +30 -0
- package/dist/packages/persistence/codecs/simulation.js +69 -0
- package/dist/packages/persistence/codecs/values.js +24 -0
- package/dist/packages/persistence/connection.js +29 -0
- package/dist/packages/persistence/errors.js +8 -0
- package/dist/packages/persistence/index.js +22 -0
- package/dist/packages/persistence/migrate.js +40 -0
- package/dist/packages/persistence/migrations/0000_initial_domain_storage.sql +92 -0
- package/dist/packages/persistence/migrations/0001_transactional_outbox.sql +26 -0
- package/dist/packages/persistence/migrations/0002_simulation_configuration.sql +7 -0
- package/dist/packages/persistence/migrations/meta/0000_snapshot.json +637 -0
- package/dist/packages/persistence/migrations/meta/0001_snapshot.json +812 -0
- package/dist/packages/persistence/migrations/meta/0002_snapshot.json +859 -0
- package/dist/packages/persistence/migrations/meta/_journal.json +27 -0
- package/dist/packages/persistence/outbox/committer.js +54 -0
- package/dist/packages/persistence/outbox/store.js +66 -0
- package/dist/packages/persistence/repositories/configurations.js +41 -0
- package/dist/packages/persistence/repositories/delete-configuration.js +20 -0
- package/dist/packages/persistence/repositories/investigation-writes.js +72 -0
- package/dist/packages/persistence/repositories/investigations.js +27 -0
- package/dist/packages/persistence/repositories/observations.js +36 -0
- package/dist/packages/persistence/repositories/simulation-writes.js +39 -0
- package/dist/packages/persistence/repositories/simulations.js +23 -0
- package/dist/packages/persistence/schema/configurations.js +10 -0
- package/dist/packages/persistence/schema/index.js +5 -0
- package/dist/packages/persistence/schema/investigations.js +53 -0
- package/dist/packages/persistence/schema/observations.js +17 -0
- package/dist/packages/persistence/schema/outbox.js +26 -0
- package/dist/packages/persistence/schema/simulations.js +29 -0
- package/dist/packages/projects/index.js +2 -0
- package/dist/packages/projects/inspect.js +45 -0
- package/dist/packages/projects/suggest-target.js +34 -0
- package/dist/packages/projects/target-probe.js +61 -0
- package/dist/packages/runtime/deadline.js +11 -0
- package/dist/packages/runtime/evidence-store.js +76 -0
- package/dist/packages/runtime/execution-coordinator.js +223 -0
- package/dist/packages/runtime/execution-settlement.js +50 -0
- package/dist/packages/runtime/execution-types.js +1 -0
- package/dist/packages/runtime/host-config.js +26 -0
- package/dist/packages/runtime/host-storage.js +46 -0
- package/dist/packages/runtime/host.js +2 -0
- package/dist/packages/runtime/index.js +1 -0
- package/dist/packages/runtime/local-host.js +280 -0
- package/dist/packages/runtime/outbox-dispatcher.js +61 -0
- package/dist/packages/runtime/session-registry.js +42 -0
- package/dist/packages/server/configuration-routes.js +85 -0
- package/dist/packages/server/contracts.js +13 -0
- package/dist/packages/server/index.js +2 -0
- package/dist/packages/server/security.js +38 -0
- package/dist/packages/server/server.js +44 -0
- package/dist/packages/server/studio-assets.js +44 -0
- package/dist/packages/server/studio-session.js +28 -0
- package/dist/scripts/server/access-file.js +21 -0
- package/dist/scripts/server/cli-results.js +126 -0
- package/dist/scripts/server/open-browser.js +13 -0
- package/dist/scripts/server/run.js +76 -0
- package/dist/scripts/server/startup-error.js +17 -0
- package/dist/scripts/server/studio-entry-file.js +22 -0
- package/dist/studio/assets/index-DMnHlGCr.js +9 -0
- package/dist/studio/assets/index-Dh5KHl3J.css +1 -0
- package/dist/studio/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/dist/studio/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- package/dist/studio/brand/logo-perspectives.svg +1 -0
- package/dist/studio/favicon.svg +1 -0
- package/dist/studio/index.html +14 -0
- package/dist/studio/licenses/jetbrains-mono.txt +93 -0
- package/dist/studio/licenses/space-grotesk.txt +93 -0
- package/dist/studio/participants/badger.png +0 -0
- package/dist/studio/participants/compass.png +0 -0
- package/dist/studio/participants/fox.png +0 -0
- package/dist/studio/participants/owl.png +0 -0
- package/dist/studio/participants/raven.png +0 -0
- package/dist/studio/participants/sparrow.png +0 -0
- package/offscript.mjs +178 -0
- package/package.json +100 -4
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { assertInScope, isAllowedSubresource, isUrlInScope, normalizeTargetUrl } from "./scope-guard.js";
|
|
2
|
+
import { capturePageSnapshot } from "./dom-snapshot.js";
|
|
3
|
+
export class BrowserUserSession {
|
|
4
|
+
context;
|
|
5
|
+
page;
|
|
6
|
+
targetUrl;
|
|
7
|
+
actionTimeoutMs;
|
|
8
|
+
navigationTimeoutMs;
|
|
9
|
+
currentEpoch = 0;
|
|
10
|
+
closed = false;
|
|
11
|
+
constructor(context, page, targetUrl, actionTimeoutMs = 5000, navigationTimeoutMs = 10000) {
|
|
12
|
+
this.context = context;
|
|
13
|
+
this.page = page;
|
|
14
|
+
this.targetUrl = targetUrl;
|
|
15
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
16
|
+
this.navigationTimeoutMs = navigationTimeoutMs;
|
|
17
|
+
}
|
|
18
|
+
static async create(context, options) {
|
|
19
|
+
const targetUrl = normalizeTargetUrl(options.targetUrl);
|
|
20
|
+
const actionTimeoutMs = options.actionTimeoutMs ?? 5000;
|
|
21
|
+
const navigationTimeoutMs = options.navigationTimeoutMs ?? 10000;
|
|
22
|
+
await context.route("**", (route) => {
|
|
23
|
+
const requestUrl = route.request().url();
|
|
24
|
+
if (!isAllowedSubresource(requestUrl, targetUrl)) {
|
|
25
|
+
void route.abort("accessdenied");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
void route.continue();
|
|
29
|
+
});
|
|
30
|
+
const page = await context.newPage();
|
|
31
|
+
page.setDefaultTimeout(actionTimeoutMs);
|
|
32
|
+
page.setDefaultNavigationTimeout(navigationTimeoutMs);
|
|
33
|
+
return new BrowserUserSession(context, page, targetUrl, actionTimeoutMs, navigationTimeoutMs);
|
|
34
|
+
}
|
|
35
|
+
isClosed() {
|
|
36
|
+
return this.closed;
|
|
37
|
+
}
|
|
38
|
+
getTargetUrl() {
|
|
39
|
+
return this.targetUrl;
|
|
40
|
+
}
|
|
41
|
+
getCurrentUrl() {
|
|
42
|
+
return this.page.url();
|
|
43
|
+
}
|
|
44
|
+
async navigate(url) {
|
|
45
|
+
if (this.closed)
|
|
46
|
+
return { success: false, action: "navigate", url: this.getCurrentUrl(), error: "SESSION_CLOSED" };
|
|
47
|
+
let resolvedUrl;
|
|
48
|
+
try {
|
|
49
|
+
resolvedUrl = assertInScope(url, this.targetUrl);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
action: "navigate",
|
|
55
|
+
url: this.getCurrentUrl(),
|
|
56
|
+
error: error instanceof Error ? error.message : "TARGET_OUT_OF_SCOPE",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
this.currentEpoch++;
|
|
60
|
+
try {
|
|
61
|
+
await this.page.goto(resolvedUrl, {
|
|
62
|
+
timeout: this.navigationTimeoutMs,
|
|
63
|
+
waitUntil: "domcontentloaded",
|
|
64
|
+
});
|
|
65
|
+
return { success: true, action: "navigate", url: this.page.url() };
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
action: "navigate",
|
|
71
|
+
url: this.page.url(),
|
|
72
|
+
error: cause instanceof Error ? cause.message : "NAVIGATION_FAILED",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async inspect() {
|
|
77
|
+
if (this.closed)
|
|
78
|
+
throw new Error("Session is closed.");
|
|
79
|
+
this.currentEpoch++;
|
|
80
|
+
return await capturePageSnapshot(this.page, this.currentEpoch);
|
|
81
|
+
}
|
|
82
|
+
resolveLocator(ref) {
|
|
83
|
+
if (!/^elem_\d+$/.test(ref))
|
|
84
|
+
return null;
|
|
85
|
+
return this.page.locator(`[data-offscript-ref="${ref}"][data-offscript-epoch="${this.currentEpoch}"]`);
|
|
86
|
+
}
|
|
87
|
+
async click(ref) {
|
|
88
|
+
if (this.closed)
|
|
89
|
+
return { success: false, action: "click", url: this.getCurrentUrl(), error: "SESSION_CLOSED" };
|
|
90
|
+
const locator = this.resolveLocator(ref);
|
|
91
|
+
if (!locator)
|
|
92
|
+
return { success: false, action: "click", url: this.getCurrentUrl(), error: "INVALID_ELEMENT_REF" };
|
|
93
|
+
try {
|
|
94
|
+
if ((await locator.count()) === 0) {
|
|
95
|
+
return { success: false, action: "click", url: this.page.url(), error: "STALE_OR_MISSING_ELEMENT_REF" };
|
|
96
|
+
}
|
|
97
|
+
if (await locator.first().isDisabled().catch(() => false)) {
|
|
98
|
+
return { success: false, action: "click", url: this.page.url(), error: "ELEMENT_IS_DISABLED" };
|
|
99
|
+
}
|
|
100
|
+
await locator.first().click({ timeout: this.actionTimeoutMs });
|
|
101
|
+
const newUrl = this.page.url();
|
|
102
|
+
if (!isUrlInScope(newUrl, this.targetUrl)) {
|
|
103
|
+
return { success: false, action: "click", url: newUrl, error: `Navigation resulted in out-of-scope URL "${newUrl}".` };
|
|
104
|
+
}
|
|
105
|
+
return { success: true, action: "click", url: newUrl };
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
return { success: false, action: "click", url: this.page.url(), error: cause instanceof Error ? cause.message : "CLICK_FAILED" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async type(ref, text) {
|
|
112
|
+
if (this.closed)
|
|
113
|
+
return { success: false, action: "type", url: this.getCurrentUrl(), error: "SESSION_CLOSED" };
|
|
114
|
+
const locator = this.resolveLocator(ref);
|
|
115
|
+
if (!locator)
|
|
116
|
+
return { success: false, action: "type", url: this.getCurrentUrl(), error: "INVALID_ELEMENT_REF" };
|
|
117
|
+
try {
|
|
118
|
+
if ((await locator.count()) === 0) {
|
|
119
|
+
return { success: false, action: "type", url: this.page.url(), error: "STALE_OR_MISSING_ELEMENT_REF" };
|
|
120
|
+
}
|
|
121
|
+
if (await locator.first().isDisabled().catch(() => false)) {
|
|
122
|
+
return { success: false, action: "type", url: this.page.url(), error: "ELEMENT_IS_DISABLED" };
|
|
123
|
+
}
|
|
124
|
+
await locator.first().fill(text, { timeout: this.actionTimeoutMs });
|
|
125
|
+
return { success: true, action: "type", url: this.page.url() };
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
return { success: false, action: "type", url: this.page.url(), error: cause instanceof Error ? cause.message : "TYPE_FAILED" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async scroll(direction = "down", ref) {
|
|
132
|
+
if (this.closed)
|
|
133
|
+
return { success: false, action: "scroll", url: this.getCurrentUrl(), error: "SESSION_CLOSED" };
|
|
134
|
+
try {
|
|
135
|
+
if (ref) {
|
|
136
|
+
const locator = this.resolveLocator(ref);
|
|
137
|
+
if (!locator)
|
|
138
|
+
return { success: false, action: "scroll", url: this.getCurrentUrl(), error: "INVALID_ELEMENT_REF" };
|
|
139
|
+
if ((await locator.count()) === 0) {
|
|
140
|
+
return { success: false, action: "scroll", url: this.page.url(), error: "STALE_OR_MISSING_ELEMENT_REF" };
|
|
141
|
+
}
|
|
142
|
+
await locator.first().scrollIntoViewIfNeeded({ timeout: this.actionTimeoutMs });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const delta = direction === "up" ? -400 : 400;
|
|
146
|
+
await this.page.evaluate((d) => window.scrollBy(0, d), delta);
|
|
147
|
+
}
|
|
148
|
+
return { success: true, action: "scroll", url: this.page.url() };
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
return { success: false, action: "scroll", url: this.page.url(), error: cause instanceof Error ? cause.message : "SCROLL_FAILED" };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async select(ref, value) {
|
|
155
|
+
if (this.closed)
|
|
156
|
+
return { success: false, action: "select", url: this.getCurrentUrl(), error: "SESSION_CLOSED" };
|
|
157
|
+
const locator = this.resolveLocator(ref);
|
|
158
|
+
if (!locator)
|
|
159
|
+
return { success: false, action: "select", url: this.getCurrentUrl(), error: "INVALID_ELEMENT_REF" };
|
|
160
|
+
try {
|
|
161
|
+
if ((await locator.count()) === 0) {
|
|
162
|
+
return { success: false, action: "select", url: this.page.url(), error: "STALE_OR_MISSING_ELEMENT_REF" };
|
|
163
|
+
}
|
|
164
|
+
if (await locator.first().isDisabled().catch(() => false)) {
|
|
165
|
+
return { success: false, action: "select", url: this.page.url(), error: "ELEMENT_IS_DISABLED" };
|
|
166
|
+
}
|
|
167
|
+
await locator.first().selectOption(value, { timeout: this.actionTimeoutMs });
|
|
168
|
+
const newUrl = this.page.url();
|
|
169
|
+
if (!isUrlInScope(newUrl, this.targetUrl)) {
|
|
170
|
+
return { success: false, action: "select", url: newUrl, error: `Navigation resulted in out-of-scope URL "${newUrl}".` };
|
|
171
|
+
}
|
|
172
|
+
return { success: true, action: "select", url: newUrl };
|
|
173
|
+
}
|
|
174
|
+
catch (cause) {
|
|
175
|
+
return { success: false, action: "select", url: this.page.url(), error: cause instanceof Error ? cause.message : "SELECT_FAILED" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Captures the rendered viewport of the active in-scope page as a PNG image.
|
|
180
|
+
* Password fields (<input type="password">) are rendered with native masking.
|
|
181
|
+
* Universal optical redaction on arbitrary rendered text/canvas is not performed.
|
|
182
|
+
*/
|
|
183
|
+
async screenshot() {
|
|
184
|
+
if (this.closed)
|
|
185
|
+
return { success: false, error: "SESSION_CLOSED" };
|
|
186
|
+
try {
|
|
187
|
+
const currentUrl = this.page.url();
|
|
188
|
+
if (!isUrlInScope(currentUrl, this.targetUrl)) {
|
|
189
|
+
return { success: false, error: `OUT_OF_SCOPE: Cannot capture screenshot from "${currentUrl}".` };
|
|
190
|
+
}
|
|
191
|
+
const data = await this.page.screenshot({ type: "png" });
|
|
192
|
+
return { success: true, data, mimeType: "image/png" };
|
|
193
|
+
}
|
|
194
|
+
catch (cause) {
|
|
195
|
+
return { success: false, error: cause instanceof Error ? cause.message : "SCREENSHOT_FAILED" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async close() {
|
|
199
|
+
if (this.closed)
|
|
200
|
+
return;
|
|
201
|
+
this.closed = true;
|
|
202
|
+
try {
|
|
203
|
+
await this.context.close();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Ignore errors on closing already terminated contexts
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createObservation, observationId } from "../../domain/index.js";
|
|
3
|
+
function asObject(val, expectedKeys) {
|
|
4
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) {
|
|
5
|
+
throw new Error("Tool arguments must be an object.");
|
|
6
|
+
}
|
|
7
|
+
const obj = val;
|
|
8
|
+
for (const k of Object.keys(obj)) {
|
|
9
|
+
if (!expectedKeys.includes(k))
|
|
10
|
+
throw new Error(`Unexpected argument: ${k}`);
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
function asText(val, field) {
|
|
15
|
+
if (typeof val !== "string" || val.trim().length === 0) {
|
|
16
|
+
throw new Error(`Argument "${field}" must be non-empty text.`);
|
|
17
|
+
}
|
|
18
|
+
return val.trim();
|
|
19
|
+
}
|
|
20
|
+
export function createBrowserTools(session, agentId, simulationId, onObservation) {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
name: "browser_navigate",
|
|
24
|
+
description: "Navigate to an authorized URL within the target application scope.",
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
url: { type: "string", description: "Relative path or full URL within authorized target scope" },
|
|
29
|
+
},
|
|
30
|
+
required: ["url"],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
async invoke(rawArgs) {
|
|
34
|
+
const args = asObject(rawArgs, ["url"]);
|
|
35
|
+
const url = asText(args.url, "url");
|
|
36
|
+
const result = await session.navigate(url);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
return { error: result.error || "Navigation failed" };
|
|
39
|
+
}
|
|
40
|
+
return { currentUrl: result.url };
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "browser_inspect",
|
|
45
|
+
description: "Inspect the current page state, returning accessible ARIA structure and interactive element references.",
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {},
|
|
49
|
+
required: [],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
async invoke() {
|
|
53
|
+
const snapshot = await session.inspect();
|
|
54
|
+
return {
|
|
55
|
+
url: snapshot.url,
|
|
56
|
+
title: snapshot.title,
|
|
57
|
+
textSummary: snapshot.textSummary,
|
|
58
|
+
ariaSnapshot: snapshot.ariaSnapshot,
|
|
59
|
+
elements: snapshot.elements.map((el) => ({
|
|
60
|
+
ref: el.ref,
|
|
61
|
+
role: el.role,
|
|
62
|
+
name: el.name,
|
|
63
|
+
text: el.text,
|
|
64
|
+
disabled: el.disabled,
|
|
65
|
+
...(el.type ? { type: el.type } : {}),
|
|
66
|
+
...(el.placeholder ? { placeholder: el.placeholder } : {}),
|
|
67
|
+
...(el.checked !== undefined ? { checked: el.checked } : {}),
|
|
68
|
+
...(el.expanded !== undefined ? { expanded: el.expanded } : {}),
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "browser_click",
|
|
75
|
+
description: "Click an interactive element identified by its opaque reference (e.g. 'elem_1') from the latest inspection.",
|
|
76
|
+
parameters: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
ref: { type: "string", description: "Opaque element reference identifier (e.g. 'elem_1')" },
|
|
80
|
+
},
|
|
81
|
+
required: ["ref"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
async invoke(rawArgs) {
|
|
85
|
+
const args = asObject(rawArgs, ["ref"]);
|
|
86
|
+
const ref = asText(args.ref, "ref");
|
|
87
|
+
const result = await session.click(ref);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
return { error: result.error || "Click failed" };
|
|
90
|
+
}
|
|
91
|
+
return { success: true, currentUrl: result.url };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "browser_type",
|
|
96
|
+
description: "Type text into an interactive input element identified by its opaque reference.",
|
|
97
|
+
parameters: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
ref: { type: "string", description: "Opaque element reference identifier (e.g. 'elem_1')" },
|
|
101
|
+
text: { type: "string", description: "Text to type into the input element" },
|
|
102
|
+
},
|
|
103
|
+
required: ["ref", "text"],
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
async invoke(rawArgs) {
|
|
107
|
+
const args = asObject(rawArgs, ["ref", "text"]);
|
|
108
|
+
const ref = asText(args.ref, "ref");
|
|
109
|
+
const text = typeof args.text === "string" ? args.text : "";
|
|
110
|
+
const result = await session.type(ref, text);
|
|
111
|
+
if (!result.success) {
|
|
112
|
+
return { error: result.error || "Type failed" };
|
|
113
|
+
}
|
|
114
|
+
return { success: true, currentUrl: result.url };
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "browser_scroll",
|
|
119
|
+
description: "Scroll the page up or down to reveal hidden content or elements.",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
direction: { type: "string", enum: ["up", "down"], description: "Scroll direction: up or down" },
|
|
124
|
+
},
|
|
125
|
+
required: ["direction"],
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
},
|
|
128
|
+
async invoke(rawArgs) {
|
|
129
|
+
const args = asObject(rawArgs, ["direction"]);
|
|
130
|
+
const direction = args.direction === "up" ? "up" : "down";
|
|
131
|
+
const result = await session.scroll(direction);
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
return { error: result.error || "Scroll failed" };
|
|
134
|
+
}
|
|
135
|
+
return { success: true, currentUrl: result.url };
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "browser_select",
|
|
140
|
+
description: "Select an option in a dropdown or select element by its opaque reference and option value.",
|
|
141
|
+
parameters: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
ref: { type: "string", description: "Opaque element reference identifier (e.g. 'elem_1')" },
|
|
145
|
+
value: { type: "string", description: "Option value to select" },
|
|
146
|
+
},
|
|
147
|
+
required: ["ref", "value"],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
async invoke(rawArgs) {
|
|
151
|
+
const args = asObject(rawArgs, ["ref", "value"]);
|
|
152
|
+
const ref = asText(args.ref, "ref");
|
|
153
|
+
const value = asText(args.value, "value");
|
|
154
|
+
const result = await session.select(ref, value);
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
return { error: result.error || "Select failed" };
|
|
157
|
+
}
|
|
158
|
+
return { success: true, currentUrl: result.url };
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "browser_record_observation",
|
|
163
|
+
description: "Record an observed behavior on the current page as untrusted observation data attributed to this user.",
|
|
164
|
+
parameters: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
description: { type: "string", description: "Description of the observed behavior or anomaly" },
|
|
168
|
+
},
|
|
169
|
+
required: ["description"],
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
},
|
|
172
|
+
async invoke(rawArgs) {
|
|
173
|
+
const args = asObject(rawArgs, ["description"]);
|
|
174
|
+
const description = asText(args.description, "description");
|
|
175
|
+
const location = session.getCurrentUrl() || session.getTargetUrl();
|
|
176
|
+
const observation = createObservation({
|
|
177
|
+
id: observationId(randomUUID()),
|
|
178
|
+
simulationId,
|
|
179
|
+
agentId,
|
|
180
|
+
observedAt: new Date().toISOString(),
|
|
181
|
+
description,
|
|
182
|
+
context: { location },
|
|
183
|
+
evidenceIds: [],
|
|
184
|
+
});
|
|
185
|
+
if (onObservation) {
|
|
186
|
+
await onObservation(observation);
|
|
187
|
+
}
|
|
188
|
+
return { observationId: observation.id, location: observation.context.location };
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { readAgentId, readSimulationId, record, text, timestamp } from "./validation.js";
|
|
3
|
+
function origin(value) {
|
|
4
|
+
const input = record(value, "event.origin", ["kind", "agentId"]);
|
|
5
|
+
if (input.kind === "agent") {
|
|
6
|
+
return Object.freeze({ kind: "agent", agentId: readAgentId(input.agentId, "event.origin.agentId") });
|
|
7
|
+
}
|
|
8
|
+
if (input.kind !== "system" || Object.hasOwn(input, "agentId")) {
|
|
9
|
+
throw new ContractValidationError("event.origin", "Expected a system origin or an agent origin with an ID.");
|
|
10
|
+
}
|
|
11
|
+
return Object.freeze({ kind: "system" });
|
|
12
|
+
}
|
|
13
|
+
export function parseEventEnvelope(value) {
|
|
14
|
+
const input = record(value, "event", [
|
|
15
|
+
"eventId", "type", "schemaVersion", "occurredAt", "simulationId", "origin", "correlationId", "payload",
|
|
16
|
+
]);
|
|
17
|
+
if (input.schemaVersion !== 1) {
|
|
18
|
+
throw new ContractValidationError("event.schemaVersion", "Only schema version 1 is supported.");
|
|
19
|
+
}
|
|
20
|
+
const metadata = {
|
|
21
|
+
eventId: text(input.eventId, "event.eventId"),
|
|
22
|
+
occurredAt: timestamp(input.occurredAt, "event.occurredAt"),
|
|
23
|
+
simulationId: readSimulationId(input.simulationId, "event.simulationId"),
|
|
24
|
+
origin: origin(input.origin),
|
|
25
|
+
correlationId: text(input.correlationId, "event.correlationId"),
|
|
26
|
+
};
|
|
27
|
+
return { input, base: { ...metadata, schemaVersion: 1 } };
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ContractValidationError } from "./errors.js";
|
|
2
|
+
import { parseAgentSummary, parseSimulationSummary } from "./summaries.js";
|
|
3
|
+
import { record } from "./validation.js";
|
|
4
|
+
import { parseEventEnvelope } from "./event-envelope.js";
|
|
5
|
+
function simulationPayload(value, status, simulationId) {
|
|
6
|
+
const input = record(value, "event.payload", ["simulation"]);
|
|
7
|
+
const simulation = parseSimulationSummary(input.simulation);
|
|
8
|
+
if (simulation.lifecycle.status !== status || simulation.id !== simulationId) {
|
|
9
|
+
throw new ContractValidationError("event.payload.simulation", "Lifecycle or simulation ID does not match the envelope.");
|
|
10
|
+
}
|
|
11
|
+
// The equality checks above narrow both the subject and its event-specific status.
|
|
12
|
+
return Object.freeze({ simulation });
|
|
13
|
+
}
|
|
14
|
+
function agentPayload(value, status, simulationId) {
|
|
15
|
+
const input = record(value, "event.payload", ["agent"]);
|
|
16
|
+
const agent = parseAgentSummary(input.agent);
|
|
17
|
+
if (agent.lifecycle.status !== status || agent.simulationId !== simulationId) {
|
|
18
|
+
throw new ContractValidationError("event.payload.agent", "Lifecycle or simulation ID does not match the envelope.");
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({ agent });
|
|
21
|
+
}
|
|
22
|
+
export function parseLifecycleEvent(value) {
|
|
23
|
+
const { input, base } = parseEventEnvelope(value);
|
|
24
|
+
switch (input.type) {
|
|
25
|
+
case "simulation.started":
|
|
26
|
+
return Object.freeze({ ...base, type: input.type, payload: simulationPayload(input.payload, "running", base.simulationId) });
|
|
27
|
+
case "simulation.completed":
|
|
28
|
+
return Object.freeze({ ...base, type: input.type, payload: simulationPayload(input.payload, "completed", base.simulationId) });
|
|
29
|
+
case "simulation.failed":
|
|
30
|
+
return Object.freeze({ ...base, type: input.type, payload: simulationPayload(input.payload, "failed", base.simulationId) });
|
|
31
|
+
case "simulation.cancelled":
|
|
32
|
+
return Object.freeze({ ...base, type: input.type, payload: simulationPayload(input.payload, "cancelled", base.simulationId) });
|
|
33
|
+
case "agent.started":
|
|
34
|
+
return Object.freeze({ ...base, type: input.type, payload: agentPayload(input.payload, "running", base.simulationId) });
|
|
35
|
+
case "agent.completed":
|
|
36
|
+
return Object.freeze({ ...base, type: input.type, payload: agentPayload(input.payload, "completed", base.simulationId) });
|
|
37
|
+
case "agent.failed":
|
|
38
|
+
return Object.freeze({ ...base, type: input.type, payload: agentPayload(input.payload, "failed", base.simulationId) });
|
|
39
|
+
case "agent.cancelled":
|
|
40
|
+
return Object.freeze({ ...base, type: input.type, payload: agentPayload(input.payload, "cancelled", base.simulationId) });
|
|
41
|
+
default:
|
|
42
|
+
throw new ContractValidationError("event.type", "Unknown lifecycle event type.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function evaluateFindingAssessment(investigation, observations = []) {
|
|
2
|
+
const lifecycle = investigation.lifecycle;
|
|
3
|
+
const outcome = lifecycle.result.outcome;
|
|
4
|
+
const explanation = lifecycle.result.explanation;
|
|
5
|
+
const evidenceIds = lifecycle.result.evidenceIds;
|
|
6
|
+
const participants = Array.from(new Set([investigation.requestedBy, ...lifecycle.agentIds, lifecycle.completedBy]));
|
|
7
|
+
if (outcome === "inconclusive") {
|
|
8
|
+
return Object.freeze({
|
|
9
|
+
investigationId: investigation.id,
|
|
10
|
+
hypothesis: investigation.hypothesis,
|
|
11
|
+
outcome,
|
|
12
|
+
confidence: "inconclusive",
|
|
13
|
+
explanation,
|
|
14
|
+
isConfirmedFinding: false,
|
|
15
|
+
participatingAgents: Object.freeze(participants),
|
|
16
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
17
|
+
uncertaintyReason: "Investigation did not establish either a supporting or contradictory conclusion.",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (outcome === "contradicted") {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
investigationId: investigation.id,
|
|
23
|
+
hypothesis: investigation.hypothesis,
|
|
24
|
+
outcome,
|
|
25
|
+
confidence: "refuted",
|
|
26
|
+
explanation,
|
|
27
|
+
isConfirmedFinding: false,
|
|
28
|
+
participatingAgents: Object.freeze(participants),
|
|
29
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
30
|
+
uncertaintyReason: "The hypothesis was contradicted by investigative assessment.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// Outcome is "supported": apply strict verification and independent corroboration criteria.
|
|
34
|
+
// 1. Evidence presence: conclusion must cite positive supporting evidence.
|
|
35
|
+
if (evidenceIds.length === 0) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
investigationId: investigation.id,
|
|
38
|
+
hypothesis: investigation.hypothesis,
|
|
39
|
+
outcome,
|
|
40
|
+
confidence: "provisional",
|
|
41
|
+
explanation,
|
|
42
|
+
isConfirmedFinding: false,
|
|
43
|
+
participatingAgents: Object.freeze(participants),
|
|
44
|
+
evidenceIds: Object.freeze([]),
|
|
45
|
+
uncertaintyReason: "Provisional assessment: supported conclusion does not reference any supporting evidence.",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// 2. Observations presence: simulation observations must be available.
|
|
49
|
+
const simulationObservations = observations.filter((obs) => obs.simulationId === investigation.simulationId);
|
|
50
|
+
if (simulationObservations.length === 0) {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
investigationId: investigation.id,
|
|
53
|
+
hypothesis: investigation.hypothesis,
|
|
54
|
+
outcome,
|
|
55
|
+
confidence: "provisional",
|
|
56
|
+
explanation,
|
|
57
|
+
isConfirmedFinding: false,
|
|
58
|
+
participatingAgents: Object.freeze(participants),
|
|
59
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
60
|
+
uncertaintyReason: "Provisional assessment: no simulation observations are available to verify evidence or corroborate findings.",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// 3. Evidence verification: all cited evidenceIds must exist in observed simulation data.
|
|
64
|
+
const observedEvidenceMap = new Map(); // evidenceId -> agentId
|
|
65
|
+
for (const obs of simulationObservations) {
|
|
66
|
+
for (const eid of obs.evidenceIds) {
|
|
67
|
+
observedEvidenceMap.set(eid, obs.agentId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const hasUnverifiedEvidence = evidenceIds.some((eid) => !observedEvidenceMap.has(eid));
|
|
71
|
+
if (hasUnverifiedEvidence) {
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
investigationId: investigation.id,
|
|
74
|
+
hypothesis: investigation.hypothesis,
|
|
75
|
+
outcome,
|
|
76
|
+
confidence: "provisional",
|
|
77
|
+
explanation,
|
|
78
|
+
isConfirmedFinding: false,
|
|
79
|
+
participatingAgents: Object.freeze(participants),
|
|
80
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
81
|
+
uncertaintyReason: "Provisional assessment: one or more referenced evidence identifiers are unverified in observed simulation data.",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// 4. Independent corroboration:
|
|
85
|
+
// Requester/assignee counts, roster membership, or completion authorship alone do NOT constitute corroboration.
|
|
86
|
+
// Independent corroboration requires observations contributed by at least two distinct artificial users.
|
|
87
|
+
const observationMap = new Map();
|
|
88
|
+
for (const obs of simulationObservations) {
|
|
89
|
+
observationMap.set(obs.id, obs);
|
|
90
|
+
}
|
|
91
|
+
const observingAgents = new Set();
|
|
92
|
+
for (const obsId of investigation.observationIds) {
|
|
93
|
+
const obs = observationMap.get(obsId);
|
|
94
|
+
if (obs) {
|
|
95
|
+
observingAgents.add(obs.agentId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const eid of evidenceIds) {
|
|
99
|
+
const agentId = observedEvidenceMap.get(eid);
|
|
100
|
+
if (agentId) {
|
|
101
|
+
observingAgents.add(agentId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const isCorroboratedByDistinctObservers = observingAgents.size >= 2;
|
|
105
|
+
if (!isCorroboratedByDistinctObservers) {
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
investigationId: investigation.id,
|
|
108
|
+
hypothesis: investigation.hypothesis,
|
|
109
|
+
outcome,
|
|
110
|
+
confidence: "provisional",
|
|
111
|
+
explanation,
|
|
112
|
+
isConfirmedFinding: false,
|
|
113
|
+
participatingAgents: Object.freeze(participants),
|
|
114
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
115
|
+
uncertaintyReason: "Provisional assessment: conclusion was reached without independent collaborator corroboration across distinct observing agents.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
investigationId: investigation.id,
|
|
120
|
+
hypothesis: investigation.hypothesis,
|
|
121
|
+
outcome,
|
|
122
|
+
confidence: "corroborated",
|
|
123
|
+
explanation,
|
|
124
|
+
isConfirmedFinding: true,
|
|
125
|
+
participatingAgents: Object.freeze(participants),
|
|
126
|
+
evidenceIds: Object.freeze([...evidenceIds]),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { ContractValidationError } from "./errors.js";
|
|
2
|
+
export { parseAgentSummary, parseSimulationSummary } from "./summaries.js";
|
|
3
|
+
export { toAgentSummary, toSimulationSummary } from "./mappers.js";
|
|
4
|
+
export { parseLifecycleEvent } from "./event-validation.js";
|
|
5
|
+
export { createLifecycleEvent, serializeLifecycleEvent, deserializeLifecycleEvent } from "./serialization.js";
|
|
6
|
+
export { parseObservationSummary } from "./observation-summary.js";
|
|
7
|
+
export { parseSemanticEvent, createSemanticEvent, serializeSemanticEvent, deserializeSemanticEvent } from "./semantic-events.js";
|
|
8
|
+
export { parseInvestigationSummary, parseInvestigationResult } from "./investigation-summary.js";
|
|
9
|
+
export { parseInvestigationEvent } from "./investigation-events.js";
|
|
10
|
+
export { evaluateFindingAssessment } from "./finding-assessment.js";
|
|
11
|
+
export { parseSimulationSetup, parseExecutionLimits, defaultExecutionLimits, userProfileIds } from "./simulation-setup.js";
|
|
12
|
+
export { parseSimulationConfiguration, parseProjectDescriptor } from "./simulation-configuration.js";
|
|
13
|
+
export { parseLocalTargetUrl, parseTargetConnection, parseTargetSuggestion } from "./target-connection.js";
|
|
14
|
+
export { simulationExecutionModes, simulationExecutionPhases, parseStartSimulationRequest, parseSimulationExecutionStatus, } from "./simulation-execution.js";
|
|
15
|
+
export { parseSimulationResults } from "./simulation-results.js";
|