@reticlehq/server 3.8.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +1 -19
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +30 -23
- package/dist/command/cli/cli-flow-commands.js +7 -1
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +1 -10
- package/dist/command/cli/cli-parse.js +62 -121
- package/dist/command/cli/cli-usage.js +5 -2
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/ports/port-holder.js +6 -1
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/daemon/daemon.d.ts +6 -0
- package/dist/command/daemon/daemon.js +52 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/version/version-skew.d.ts +2 -0
- package/dist/command/version/version-skew.js +7 -3
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/license/license.d.ts +2 -14
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/fields/flow-secret-field.js +1 -1
- package/dist/language/flows/flow-expect-grammar.js +14 -6
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +88 -283
- package/dist/language/flows/flow-replayable.d.ts +1 -1
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +34 -66
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/machine/git-tracks.js +1 -0
- package/dist/memory/cloud/cloud-sync.d.ts +2 -24
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/portal/input/viewport-tools.d.ts +19 -15
- package/dist/portal/pool/browser-pool.d.ts +5 -55
- package/dist/portal/pool/browser-pool.js +121 -100
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/production/production-activation.d.ts +5 -27
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/zero-install-first-call.d.ts +3 -0
- package/dist/portal/session/zero-install-first-call.js +33 -12
- package/dist/surface/mcp/mcp.d.ts +3 -3
- package/dist/surface/mcp/mcp.js +4 -6
- package/dist/surface/tools/act/act-danger.js +32 -17
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -3
- package/dist/surface/tools/act-tools.js +13 -14
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +36 -16
- package/dist/surface/tools/empty-session-list.d.ts +5 -29
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/gap-novelty.d.ts +1 -1
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-gate.d.ts +1 -1
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +23 -1
- package/dist/surface/tools/lease-readiness.js +75 -9
- package/dist/surface/tools/lease-tools.d.ts +1 -52
- package/dist/surface/tools/lease-tools.js +79 -142
- package/dist/surface/tools/observe-tools.js +3 -1
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +5 -3
- package/dist/surface/tools/tools.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +2 -0
- package/docs/agent-cheatsheet.md +12 -0
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/verify.mdx +8 -1
- package/docs/first-drive.md +7 -0
- package/docs/multi-agent-testing.md +2 -0
- package/docs/predicates.mdx +6 -0
- package/docs/skill.md +3 -3
- package/docs/troubleshooting.mdx +10 -0
- package/docs/usage.md +1 -1
- package/package.json +8 -9
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
* Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
|
|
11
11
|
*/
|
|
12
12
|
import * as production from '../../portal/production/production-activation.js';
|
|
13
|
-
import { fixturePortFor, seedFromStorageState, } from '../../portal/input/storage-fixture.js';
|
|
14
|
-
import { mutationPortFor } from '../../portal/input/network-mutation.js';
|
|
15
13
|
import { z } from 'zod';
|
|
16
14
|
import { leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
|
|
17
|
-
import { connectOrInject, probeLeaseAlive,
|
|
15
|
+
import { connectOrInject, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
|
|
18
16
|
import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
|
|
19
17
|
import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
|
|
20
18
|
import { diagnoseObservedWebCsp } from '@reticlehq/init';
|
|
@@ -31,6 +29,7 @@ import { asString } from '@reticlehq/core';
|
|
|
31
29
|
import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
32
30
|
import { LEASE_PERMISSIONS_ARG, hintOf, notificationReadBack, parseLeasePermissions, permissionRefusal, refuseRegrant, } from './lease-permissions.js';
|
|
33
31
|
import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport-tools.js';
|
|
32
|
+
export { sessionMutationPort, sessionPerturbationPort, suiteFixturePort, suiteFixtureSeed, } from './session-ports.js';
|
|
34
33
|
/**
|
|
35
34
|
* Everything the daemon already knows about why a leased tab might not have dialled in.
|
|
36
35
|
*
|
|
@@ -54,11 +53,13 @@ import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport
|
|
|
54
53
|
*/
|
|
55
54
|
async function observedCspBlock(url, port) {
|
|
56
55
|
const document = await observeWebDocument(url);
|
|
57
|
-
if (document === undefined)
|
|
56
|
+
if (document === undefined) {
|
|
58
57
|
return undefined;
|
|
58
|
+
}
|
|
59
59
|
const refusesSocket = diagnoseObservedWebCsp(document, port).some((finding) => `${finding.problem} ${finding.fix}`.includes(CONNECT_SRC));
|
|
60
|
-
if (!refusesSocket)
|
|
60
|
+
if (!refusesSocket) {
|
|
61
61
|
return undefined;
|
|
62
|
+
}
|
|
62
63
|
const allow = `ws://localhost:${String(port)} ws://127.0.0.1:${String(port)}`;
|
|
63
64
|
return {
|
|
64
65
|
problem: `its ${CONNECT_SRC} does not allow ws://localhost:${String(port)}`,
|
|
@@ -102,8 +103,9 @@ export function appendReticleParams(url, session, projectId, hud) {
|
|
|
102
103
|
if (projectId !== undefined && projectId.length > 0) {
|
|
103
104
|
u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
|
|
104
105
|
}
|
|
105
|
-
if (hud !== undefined)
|
|
106
|
+
if (hud !== undefined) {
|
|
106
107
|
u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
|
|
108
|
+
}
|
|
107
109
|
return u.toString();
|
|
108
110
|
}
|
|
109
111
|
catch {
|
|
@@ -137,8 +139,9 @@ function newLeaseId() {
|
|
|
137
139
|
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
138
140
|
*/
|
|
139
141
|
export function resolveLeasedSessionId(sessions, leaseId) {
|
|
140
|
-
if (sessions.get(leaseId) !== undefined)
|
|
142
|
+
if (sessions.get(leaseId) !== undefined) {
|
|
141
143
|
return leaseId;
|
|
144
|
+
}
|
|
142
145
|
return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
|
|
143
146
|
}
|
|
144
147
|
/**
|
|
@@ -156,17 +159,20 @@ export function resolveLeasedSessionId(sessions, leaseId) {
|
|
|
156
159
|
*/
|
|
157
160
|
export function carryReticleIdentity(fromUrl, toUrl) {
|
|
158
161
|
const session = sessionParamOf(fromUrl);
|
|
159
|
-
if (session === undefined)
|
|
162
|
+
if (session === undefined) {
|
|
160
163
|
return toUrl;
|
|
164
|
+
}
|
|
161
165
|
// Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
|
|
162
|
-
if (sessionParamOf(toUrl) !== undefined)
|
|
166
|
+
if (sessionParamOf(toUrl) !== undefined) {
|
|
163
167
|
return toUrl;
|
|
168
|
+
}
|
|
164
169
|
const projectId = projectParamOf(fromUrl);
|
|
165
170
|
return appendReticleParams(toUrl, session, projectId);
|
|
166
171
|
}
|
|
167
172
|
function projectParamOf(url) {
|
|
168
|
-
if (url === undefined)
|
|
173
|
+
if (url === undefined) {
|
|
169
174
|
return undefined;
|
|
175
|
+
}
|
|
170
176
|
try {
|
|
171
177
|
return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
|
|
172
178
|
}
|
|
@@ -175,8 +181,9 @@ function projectParamOf(url) {
|
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
function sessionParamOf(url) {
|
|
178
|
-
if (url === undefined)
|
|
184
|
+
if (url === undefined) {
|
|
179
185
|
return undefined;
|
|
186
|
+
}
|
|
180
187
|
try {
|
|
181
188
|
return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
|
|
182
189
|
}
|
|
@@ -209,8 +216,9 @@ export async function acquireLeasedSession(pool, sessions, url, projectId, seedS
|
|
|
209
216
|
registeredId = resolveLeasedSessionId(sessions, lease.sessionId);
|
|
210
217
|
return registeredId !== undefined;
|
|
211
218
|
});
|
|
212
|
-
if (registeredId !== undefined)
|
|
219
|
+
if (registeredId !== undefined) {
|
|
213
220
|
pool.alias?.(registeredId, lease.sessionId);
|
|
221
|
+
}
|
|
214
222
|
const effectiveId = registeredId ?? lease.sessionId;
|
|
215
223
|
if (seedStorage !== undefined) {
|
|
216
224
|
const session = sessions.get(effectiveId);
|
|
@@ -244,7 +252,8 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
244
252
|
inputSchema: {
|
|
245
253
|
url: z
|
|
246
254
|
.string()
|
|
247
|
-
.
|
|
255
|
+
.optional()
|
|
256
|
+
.describe('URL of the already-running app to drive (e.g. http://localhost:3000/dashboard). Omit it to lease the one dev server that runs in this project folder.'),
|
|
248
257
|
projectId: z
|
|
249
258
|
.string()
|
|
250
259
|
.optional()
|
|
@@ -311,6 +320,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
311
320
|
.boolean()
|
|
312
321
|
.optional()
|
|
313
322
|
.describe('True when this acquire returned an existing live lease on the same origin rather than minting a second context.'),
|
|
323
|
+
navigated: z
|
|
324
|
+
.boolean()
|
|
325
|
+
.optional()
|
|
326
|
+
.describe('Present when a reused lease was on another page. True: its tab was moved to url. False: it could not be moved, and url is the page it is on.'),
|
|
314
327
|
hint: z.string().optional(),
|
|
315
328
|
production: production.PRODUCTION_LEASE_OUTPUT,
|
|
316
329
|
versionSkew: z
|
|
@@ -322,11 +335,14 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
322
335
|
},
|
|
323
336
|
handler: async (deps, args) => {
|
|
324
337
|
const pool = deps.pool;
|
|
325
|
-
if (pool === undefined)
|
|
338
|
+
if (pool === undefined) {
|
|
326
339
|
throw new Error(POOL_UNAVAILABLE);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
340
|
+
}
|
|
341
|
+
const given = asString(args['url']);
|
|
342
|
+
const url = given === undefined || 0 === given.length ? await deps.projectAppUrl?.() : given;
|
|
343
|
+
if (url === undefined) {
|
|
344
|
+
throw new Error('reticle_lease{action:"acquire"} requires a url: no single dev server runs in this project folder. Start the app, or pass the url it serves.');
|
|
345
|
+
}
|
|
330
346
|
// Preflight the browser before spending the round trip. Without it a missing Playwright Chromium
|
|
331
347
|
// only surfaces inside pool.acquire, where the launch failure is caught and reported as
|
|
332
348
|
// "could not open <url> — is the app running?" — sending the caller to debug an app that is
|
|
@@ -334,8 +350,9 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
334
350
|
// installed" so error-recovery routes it to the NO_POOL fix (install + drive a human tab).
|
|
335
351
|
if (deps.browserProbe !== undefined) {
|
|
336
352
|
const refusal = chromiumPreflightRefusal(await deps.browserProbe());
|
|
337
|
-
if (refusal !== undefined)
|
|
353
|
+
if (refusal !== undefined) {
|
|
338
354
|
throw new Error(refusal);
|
|
355
|
+
}
|
|
339
356
|
}
|
|
340
357
|
const projectId = asString(args['projectId']);
|
|
341
358
|
const root = asString(args['root']);
|
|
@@ -438,16 +455,16 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
438
455
|
// old, or there may be none at all — `unresponsive` is set by past commands failing, and
|
|
439
456
|
// its own contract says absence means "answering, OR NOT ASKED YET". So the happy path of
|
|
440
457
|
// a first acquire pays nothing for this.
|
|
458
|
+
const tabUrl = () => deps.sessions.get(resolved)?.url;
|
|
459
|
+
const moved = await moveReusedLease(tabUrl, url, () => pool.gotoLease(existing, carryReticleIdentity(tabUrl(), url)), deps.now);
|
|
441
460
|
const reusedSession = deps.sessions.get(resolved);
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
: tabHidden(reusedSession)
|
|
446
|
-
? LeaseNotReadyReason.TAB_HIDDEN
|
|
447
|
-
: undefined;
|
|
461
|
+
const reuseReason = false === moved?.arrived
|
|
462
|
+
? LeaseNotReadyReason.SDK_NEVER_DIALLED
|
|
463
|
+
: notReadyReason(await probeLeaseAlive(reusedSession), reusedSession);
|
|
448
464
|
return {
|
|
449
465
|
sessionId: resolved,
|
|
450
|
-
url,
|
|
466
|
+
url: moved?.url ?? url,
|
|
467
|
+
...(moved === undefined ? {} : { navigated: moved.navigated }),
|
|
451
468
|
ready: reuseReason === undefined,
|
|
452
469
|
...(reuseReason === undefined ? {} : { notReadyReason: reuseReason }),
|
|
453
470
|
reused: true,
|
|
@@ -470,8 +487,9 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
470
487
|
}
|
|
471
488
|
const sessionId = newLeaseId();
|
|
472
489
|
// Before the page can dial in: the session is stamped with its root the moment it registers.
|
|
473
|
-
if (root !== undefined)
|
|
490
|
+
if (root !== undefined) {
|
|
474
491
|
claimArtifactRoot(sessionId, root);
|
|
492
|
+
}
|
|
475
493
|
const navUrl = appendReticleParams(activated?.url ?? url, sessionId, projectId, hud);
|
|
476
494
|
let lease;
|
|
477
495
|
try {
|
|
@@ -490,11 +508,13 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
490
508
|
});
|
|
491
509
|
}
|
|
492
510
|
catch (err) {
|
|
493
|
-
if (err instanceof PoolAtCapacityError)
|
|
511
|
+
if (err instanceof PoolAtCapacityError) {
|
|
494
512
|
throw err;
|
|
513
|
+
}
|
|
495
514
|
const refusal = permissionRefusal(err);
|
|
496
|
-
if (refusal !== undefined)
|
|
515
|
+
if (refusal !== undefined) {
|
|
497
516
|
throw refusal;
|
|
517
|
+
}
|
|
498
518
|
if (err instanceof Error && err.message.startsWith('Storage seeding failed')) {
|
|
499
519
|
throw new Error(scrubSeedFromError(err.message, validatedSeed));
|
|
500
520
|
}
|
|
@@ -516,8 +536,9 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
516
536
|
// under the id we are about to hand back, and the pool is keyed by the id it navigated with;
|
|
517
537
|
// without this the touches miss, the lease ages out despite continuous activity, and the
|
|
518
538
|
// reaper closes the context mid-flow. See BrowserPool.alias and #157.
|
|
519
|
-
if (registeredId !== undefined)
|
|
539
|
+
if (registeredId !== undefined) {
|
|
520
540
|
pool.alias(registeredId, lease.sessionId);
|
|
541
|
+
}
|
|
521
542
|
const effectiveSessionId = registeredId ?? lease.sessionId;
|
|
522
543
|
const session = deps.sessions.get(effectiveSessionId);
|
|
523
544
|
if (validatedSeed !== undefined && session !== undefined) {
|
|
@@ -528,17 +549,29 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
528
549
|
}
|
|
529
550
|
// The lease now exists, so any HUD a human is watching has just gone dark. Say so. A lease whose
|
|
530
551
|
// reader failed is already released, so there is nothing to say.
|
|
531
|
-
if (readerFailure === undefined)
|
|
552
|
+
if (readerFailure === undefined) {
|
|
532
553
|
tellWatchers(deps, projectId, AGENT_DRIVING_ELSEWHERE);
|
|
554
|
+
}
|
|
533
555
|
// ready means the SDK dialled in — not that contracts match. Carry the skew warning on acquire
|
|
534
556
|
// so the agent does not learn it only after a CDP tool invents a closed page (#688).
|
|
535
557
|
const mintReason = readerFailure?.reason ??
|
|
536
|
-
(
|
|
537
|
-
? LeaseNotReadyReason.SDK_NEVER_DIALLED
|
|
538
|
-
: tabHidden(session)
|
|
539
|
-
? LeaseNotReadyReason.TAB_HIDDEN
|
|
540
|
-
: undefined);
|
|
558
|
+
notReadyReason(ready, session, LeaseNotReadyReason.SDK_NEVER_DIALLED);
|
|
541
559
|
const versionSkew = registeredId === undefined ? undefined : deps.sessions.get(registeredId)?.versionSkew;
|
|
560
|
+
let notReadyHint;
|
|
561
|
+
if (!ready) {
|
|
562
|
+
if (readerFailure !== undefined) {
|
|
563
|
+
notReadyHint = leaseReaderFailedHint(url, readerFailure);
|
|
564
|
+
}
|
|
565
|
+
else if (true === stillLoading) {
|
|
566
|
+
notReadyHint = leaseStillLoadingHint(url, Date.now() - waitStartedAt);
|
|
567
|
+
}
|
|
568
|
+
else if (activated !== undefined) {
|
|
569
|
+
notReadyHint = production.notActivatedHint(url);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
notReadyHint = await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
542
575
|
return {
|
|
543
576
|
sessionId: registeredId ?? lease.sessionId,
|
|
544
577
|
url,
|
|
@@ -563,15 +596,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
563
596
|
...(versionSkew === undefined ? {} : { versionSkew }),
|
|
564
597
|
...production.summary(activated),
|
|
565
598
|
...(initScript === undefined ? {} : { hostStub: HOST_STUB_NOTE }),
|
|
566
|
-
...hintOf(ready
|
|
567
|
-
? undefined
|
|
568
|
-
: readerFailure !== undefined
|
|
569
|
-
? leaseReaderFailedHint(url, readerFailure)
|
|
570
|
-
: true === stillLoading
|
|
571
|
-
? leaseStillLoadingHint(url, Date.now() - waitStartedAt)
|
|
572
|
-
: activated !== undefined
|
|
573
|
-
? production.notActivatedHint(url)
|
|
574
|
-
: await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)), ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
|
|
599
|
+
...hintOf(notReadyHint, ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
|
|
575
600
|
};
|
|
576
601
|
}
|
|
577
602
|
finally {
|
|
@@ -605,8 +630,9 @@ const LEASE_RELEASE_TOOL = {
|
|
|
605
630
|
},
|
|
606
631
|
handler: async (deps, args) => {
|
|
607
632
|
const pool = deps.pool;
|
|
608
|
-
if (pool === undefined)
|
|
633
|
+
if (pool === undefined) {
|
|
609
634
|
throw new Error(POOL_UNAVAILABLE);
|
|
635
|
+
}
|
|
610
636
|
const sessionId = asString(args['sessionId']);
|
|
611
637
|
if (sessionId === undefined || 0 === sessionId.length) {
|
|
612
638
|
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
@@ -635,8 +661,9 @@ export async function releaseLease(deps, pool, sessionId) {
|
|
|
635
661
|
dropArtifactRootClaim(sessionId);
|
|
636
662
|
// Only once the LAST lease is gone. Announcing "live again" while another lease still drives
|
|
637
663
|
// would be a lie, and a HUD that says the wrong thing is worse than one that says nothing.
|
|
638
|
-
if (0 === pool.activeCount())
|
|
664
|
+
if (0 === pool.activeCount()) {
|
|
639
665
|
tellWatchers(deps, projectId, AGENT_DRIVING_HERE_AGAIN);
|
|
666
|
+
}
|
|
640
667
|
return pool.activeCount();
|
|
641
668
|
}
|
|
642
669
|
/**
|
|
@@ -664,11 +691,13 @@ const PREFER_EXISTING_NOTE = 'a non-leased tab for this app was already connecte
|
|
|
664
691
|
*/
|
|
665
692
|
function liveTabFor(deps, projectId, url) {
|
|
666
693
|
const pool = deps.pool;
|
|
667
|
-
if (pool === undefined)
|
|
694
|
+
if (pool === undefined) {
|
|
668
695
|
return undefined;
|
|
696
|
+
}
|
|
669
697
|
const wanted = originOf(url);
|
|
670
|
-
if (wanted === undefined)
|
|
698
|
+
if (wanted === undefined) {
|
|
671
699
|
return undefined;
|
|
700
|
+
}
|
|
672
701
|
try {
|
|
673
702
|
const candidates = deps.sessions
|
|
674
703
|
.all()
|
|
@@ -692,8 +721,9 @@ function liveTabFor(deps, projectId, url) {
|
|
|
692
721
|
*/
|
|
693
722
|
function tellWatchers(deps, projectId, text) {
|
|
694
723
|
const pool = deps.pool;
|
|
695
|
-
if (pool === undefined)
|
|
724
|
+
if (pool === undefined) {
|
|
696
725
|
return;
|
|
726
|
+
}
|
|
697
727
|
try {
|
|
698
728
|
const candidates = deps.sessions
|
|
699
729
|
.all()
|
|
@@ -707,96 +737,3 @@ function tellWatchers(deps, projectId, text) {
|
|
|
707
737
|
}
|
|
708
738
|
}
|
|
709
739
|
export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
|
|
710
|
-
/**
|
|
711
|
-
* The state a suite should boot its flows from: whatever the agent is already sitting in.
|
|
712
|
-
*
|
|
713
|
-
* This is the join the fixture work was building toward. A suite's flows each start from cold, so
|
|
714
|
-
* fifty of them prove the login works fifty times — and the flow that LOGS OUT leaves every flow
|
|
715
|
-
* after it signed out, which no navigation repairs, because the problem is not where the subject is
|
|
716
|
-
* but what it holds. A seed is applied to an isolated context BEFORE the first navigation, which is
|
|
717
|
-
* exactly and only when a fixture can work.
|
|
718
|
-
*
|
|
719
|
-
* Best-effort, deliberately. A fixture is an OPTIMISATION over running from cold, and running from
|
|
720
|
-
* cold is correct: a suite that refused to start because a cookie jar could not be read would have
|
|
721
|
-
* traded a slow answer for no answer. Every "no" here — no provider, no storage support, nothing
|
|
722
|
-
* driven, a browser that threw, nothing worth seeding — produces the same honest `undefined`, and
|
|
723
|
-
* the suite runs exactly as it did before any of this existed.
|
|
724
|
-
*/
|
|
725
|
-
export async function suiteFixtureSeed(realInput, appUrl) {
|
|
726
|
-
if (realInput === undefined)
|
|
727
|
-
return undefined;
|
|
728
|
-
try {
|
|
729
|
-
const port = await fixturePortFor(realInput, appUrl);
|
|
730
|
-
if (port === undefined)
|
|
731
|
-
return undefined;
|
|
732
|
-
return seedFromStorageState(await port.capture(), appUrl);
|
|
733
|
-
}
|
|
734
|
-
catch {
|
|
735
|
-
return undefined;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* A way to make this session's page SLOW on purpose, or nothing.
|
|
740
|
-
*
|
|
741
|
-
* The third of these seams, for the same reason as the other two: a feature reaching into the input
|
|
742
|
-
* layer directly is a feature depending on a driver, and the boundary guard asks about it.
|
|
743
|
-
*
|
|
744
|
-
* Installs delays and NOTHING else. A rule that carried a status or a body would substitute a
|
|
745
|
-
* response the server never sent, and an app raced against invented data produces invented races —
|
|
746
|
-
* a fabricated defect handed to somebody at 3am, which is worse than finding nothing.
|
|
747
|
-
*/
|
|
748
|
-
export async function sessionPerturbationPort(realInput, appUrl) {
|
|
749
|
-
if (realInput === undefined || appUrl === undefined)
|
|
750
|
-
return undefined;
|
|
751
|
-
const setMocks = realInput.setMocks?.bind(realInput);
|
|
752
|
-
if (setMocks === undefined)
|
|
753
|
-
return undefined;
|
|
754
|
-
if (!(await realInput.isAvailableFor(appUrl)))
|
|
755
|
-
return undefined;
|
|
756
|
-
return {
|
|
757
|
-
slow: async (rules) => {
|
|
758
|
-
await setMocks(appUrl, [...rules]);
|
|
759
|
-
},
|
|
760
|
-
/** Clearing is the undo. A page left slowed is damage the next run inherits. */
|
|
761
|
-
clear: async () => {
|
|
762
|
-
await setMocks(appUrl, []);
|
|
763
|
-
},
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Save/restore for this session's page, or nothing.
|
|
768
|
-
*
|
|
769
|
-
* The same job as `suiteFixtureSeed` next door, stopping one step earlier: that one captures and
|
|
770
|
-
* converts, this hands back the PORT so a caller can re-apply what it captured later. It lives here
|
|
771
|
-
* for the reason written above — a feature reaching into the input layer directly is a feature
|
|
772
|
-
* depending on a driver, and the boundary guard asks about it. It asked.
|
|
773
|
-
*/
|
|
774
|
-
export async function suiteFixturePort(realInput, appUrl) {
|
|
775
|
-
if (realInput === undefined || appUrl === undefined)
|
|
776
|
-
return undefined;
|
|
777
|
-
try {
|
|
778
|
-
return await fixturePortFor(realInput, appUrl);
|
|
779
|
-
}
|
|
780
|
-
catch {
|
|
781
|
-
return undefined;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* A way to break this session's page, or nothing.
|
|
786
|
-
*
|
|
787
|
-
* Sits beside `suiteFixtureSeed` because it is the same shape of job: turning a provider capability
|
|
788
|
-
* into something a feature can use, on the one side of the graph that is allowed to know about both.
|
|
789
|
-
* A flow tool reaching into the input layer directly would be a feature depending on a driver, which
|
|
790
|
-
* is the reach the boundary guard exists to ask about.
|
|
791
|
-
*/
|
|
792
|
-
export async function sessionMutationPort(realInput, appUrl) {
|
|
793
|
-
if (realInput === undefined || appUrl === undefined)
|
|
794
|
-
return undefined;
|
|
795
|
-
try {
|
|
796
|
-
return await mutationPortFor(realInput, appUrl);
|
|
797
|
-
}
|
|
798
|
-
catch {
|
|
799
|
-
// A provider that cannot answer is the same answer as a provider that cannot break anything.
|
|
800
|
-
return undefined;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
@@ -705,7 +705,9 @@ export const OBSERVE_TOOLS = [
|
|
|
705
705
|
.optional()
|
|
706
706
|
.describe('Total matches before `limit` — present only when capped.'),
|
|
707
707
|
droppedOldest: z.number().optional().describe('How many older matches `limit` dropped.'),
|
|
708
|
-
hint: z
|
|
708
|
+
hint: z
|
|
709
|
+
.object({ totalInWindow: z.number(), byLevel: z.record(z.string(), z.number()) })
|
|
710
|
+
.optional(),
|
|
709
711
|
cost: z.object({ bytes: z.number(), tokens: z.number() }).optional(),
|
|
710
712
|
...bufferOutputShape,
|
|
711
713
|
},
|
|
@@ -199,7 +199,7 @@ export const READ_TOOLS = [
|
|
|
199
199
|
},
|
|
200
200
|
outputSchema: {
|
|
201
201
|
recordingName: z.string(),
|
|
202
|
-
program: z.unknown(),
|
|
202
|
+
program: z.unknown().optional(),
|
|
203
203
|
warning: z.string().optional(),
|
|
204
204
|
proposedConsequences: z
|
|
205
205
|
.array(z.unknown())
|
|
@@ -386,7 +386,7 @@ export const READ_TOOLS = [
|
|
|
386
386
|
...sessionIdShape,
|
|
387
387
|
},
|
|
388
388
|
outputSchema: {
|
|
389
|
-
stores: z.record(z.unknown()).optional(),
|
|
389
|
+
stores: z.record(z.string(), z.unknown()).optional(),
|
|
390
390
|
storeNames: z.array(z.string()).optional(),
|
|
391
391
|
found: z.boolean().optional(),
|
|
392
392
|
value: z.unknown().optional(),
|
|
@@ -598,9 +598,9 @@ export const READ_TOOLS = [
|
|
|
598
598
|
...sessionIdShape,
|
|
599
599
|
},
|
|
600
600
|
outputSchema: {
|
|
601
|
-
local: z.record(z.string()).optional(),
|
|
602
|
-
session: z.record(z.string()).optional(),
|
|
603
|
-
cookies: z.record(z.string()).optional(),
|
|
601
|
+
local: z.record(z.string(), z.string()).optional(),
|
|
602
|
+
session: z.record(z.string(), z.string()).optional(),
|
|
603
|
+
cookies: z.record(z.string(), z.string()).optional(),
|
|
604
604
|
area: z.string().optional(),
|
|
605
605
|
key: z.string().optional(),
|
|
606
606
|
value: z.string().optional(),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider capabilities handed to features as ports: a suite fixture to seed from, save/restore,
|
|
3
|
+
* slowing the page and breaking it. Each is `undefined` when the provider cannot do it. They sit on
|
|
4
|
+
* the tool side because a feature reaching into the input layer directly would depend on a driver.
|
|
5
|
+
*/
|
|
6
|
+
import type { RealInputProvider } from '../../portal/input/real-input.js';
|
|
7
|
+
import { type StorageFixturePort } from '../../portal/input/storage-fixture.js';
|
|
8
|
+
import { type NetworkMutationPort } from '../../portal/input/network-mutation.js';
|
|
9
|
+
import type { Perturbation, SeedStorage } from '@reticlehq/core';
|
|
10
|
+
/**
|
|
11
|
+
* The state a suite should boot its flows from: whatever the agent is already sitting in.
|
|
12
|
+
*
|
|
13
|
+
* This is the join the fixture work was building toward. A suite's flows each start from cold, so
|
|
14
|
+
* fifty of them prove the login works fifty times — and the flow that LOGS OUT leaves every flow
|
|
15
|
+
* after it signed out, which no navigation repairs, because the problem is not where the subject is
|
|
16
|
+
* but what it holds. A seed is applied to an isolated context BEFORE the first navigation, which is
|
|
17
|
+
* exactly and only when a fixture can work.
|
|
18
|
+
*
|
|
19
|
+
* Best-effort, deliberately. A fixture is an OPTIMISATION over running from cold, and running from
|
|
20
|
+
* cold is correct: a suite that refused to start because a cookie jar could not be read would have
|
|
21
|
+
* traded a slow answer for no answer. Every "no" here — no provider, no storage support, nothing
|
|
22
|
+
* driven, a browser that threw, nothing worth seeding — produces the same honest `undefined`, and
|
|
23
|
+
* the suite runs exactly as it did before any of this existed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function suiteFixtureSeed(realInput: RealInputProvider | undefined, appUrl: string): Promise<SeedStorage | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* A way to make this session's page SLOW on purpose, or nothing.
|
|
28
|
+
*
|
|
29
|
+
* The third of these seams, for the same reason as the other two: a feature reaching into the input
|
|
30
|
+
* layer directly is a feature depending on a driver, and the boundary guard asks about it.
|
|
31
|
+
*
|
|
32
|
+
* Installs delays and NOTHING else. A rule that carried a status or a body would substitute a
|
|
33
|
+
* response the server never sent, and an app raced against invented data produces invented races —
|
|
34
|
+
* a fabricated defect handed to somebody at 3am, which is worse than finding nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sessionPerturbationPort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<{
|
|
37
|
+
slow(rules: readonly Perturbation[]): Promise<void>;
|
|
38
|
+
clear(): Promise<void>;
|
|
39
|
+
} | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Save/restore for this session's page, or nothing.
|
|
42
|
+
*
|
|
43
|
+
* The same job as `suiteFixtureSeed` next door, stopping one step earlier: that one captures and
|
|
44
|
+
* converts, this hands back the PORT so a caller can re-apply what it captured later. It lives here
|
|
45
|
+
* for the reason written above — a feature reaching into the input layer directly is a feature
|
|
46
|
+
* depending on a driver, and the boundary guard asks about it. It asked.
|
|
47
|
+
*/
|
|
48
|
+
export declare function suiteFixturePort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<StorageFixturePort | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* A way to break this session's page, or nothing.
|
|
51
|
+
*
|
|
52
|
+
* Sits beside `suiteFixtureSeed` because it is the same shape of job: turning a provider capability
|
|
53
|
+
* into something a feature can use, on the one side of the graph that is allowed to know about both.
|
|
54
|
+
* A flow tool reaching into the input layer directly would be a feature depending on a driver, which
|
|
55
|
+
* is the reach the boundary guard exists to ask about.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sessionMutationPort(realInput: RealInputProvider | undefined, appUrl: string | undefined): Promise<NetworkMutationPort | undefined>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { fixturePortFor, seedFromStorageState, } from '../../portal/input/storage-fixture.js';
|
|
2
|
+
import { mutationPortFor } from '../../portal/input/network-mutation.js';
|
|
3
|
+
/**
|
|
4
|
+
* The state a suite should boot its flows from: whatever the agent is already sitting in.
|
|
5
|
+
*
|
|
6
|
+
* This is the join the fixture work was building toward. A suite's flows each start from cold, so
|
|
7
|
+
* fifty of them prove the login works fifty times — and the flow that LOGS OUT leaves every flow
|
|
8
|
+
* after it signed out, which no navigation repairs, because the problem is not where the subject is
|
|
9
|
+
* but what it holds. A seed is applied to an isolated context BEFORE the first navigation, which is
|
|
10
|
+
* exactly and only when a fixture can work.
|
|
11
|
+
*
|
|
12
|
+
* Best-effort, deliberately. A fixture is an OPTIMISATION over running from cold, and running from
|
|
13
|
+
* cold is correct: a suite that refused to start because a cookie jar could not be read would have
|
|
14
|
+
* traded a slow answer for no answer. Every "no" here — no provider, no storage support, nothing
|
|
15
|
+
* driven, a browser that threw, nothing worth seeding — produces the same honest `undefined`, and
|
|
16
|
+
* the suite runs exactly as it did before any of this existed.
|
|
17
|
+
*/
|
|
18
|
+
export async function suiteFixtureSeed(realInput, appUrl) {
|
|
19
|
+
if (realInput === undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const port = await fixturePortFor(realInput, appUrl);
|
|
24
|
+
if (port === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return seedFromStorageState(await port.capture(), appUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A way to make this session's page SLOW on purpose, or nothing.
|
|
35
|
+
*
|
|
36
|
+
* The third of these seams, for the same reason as the other two: a feature reaching into the input
|
|
37
|
+
* layer directly is a feature depending on a driver, and the boundary guard asks about it.
|
|
38
|
+
*
|
|
39
|
+
* Installs delays and NOTHING else. A rule that carried a status or a body would substitute a
|
|
40
|
+
* response the server never sent, and an app raced against invented data produces invented races —
|
|
41
|
+
* a fabricated defect handed to somebody at 3am, which is worse than finding nothing.
|
|
42
|
+
*/
|
|
43
|
+
export async function sessionPerturbationPort(realInput, appUrl) {
|
|
44
|
+
if (realInput === undefined || appUrl === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const setMocks = realInput.setMocks?.bind(realInput);
|
|
48
|
+
if (setMocks === undefined) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (!(await realInput.isAvailableFor(appUrl))) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
slow: async (rules) => {
|
|
56
|
+
await setMocks(appUrl, [...rules]);
|
|
57
|
+
},
|
|
58
|
+
/** Clearing is the undo. A page left slowed is damage the next run inherits. */
|
|
59
|
+
clear: async () => {
|
|
60
|
+
await setMocks(appUrl, []);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Save/restore for this session's page, or nothing.
|
|
66
|
+
*
|
|
67
|
+
* The same job as `suiteFixtureSeed` next door, stopping one step earlier: that one captures and
|
|
68
|
+
* converts, this hands back the PORT so a caller can re-apply what it captured later. It lives here
|
|
69
|
+
* for the reason written above — a feature reaching into the input layer directly is a feature
|
|
70
|
+
* depending on a driver, and the boundary guard asks about it. It asked.
|
|
71
|
+
*/
|
|
72
|
+
export async function suiteFixturePort(realInput, appUrl) {
|
|
73
|
+
if (realInput === undefined || appUrl === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return await fixturePortFor(realInput, appUrl);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A way to break this session's page, or nothing.
|
|
85
|
+
*
|
|
86
|
+
* Sits beside `suiteFixtureSeed` because it is the same shape of job: turning a provider capability
|
|
87
|
+
* into something a feature can use, on the one side of the graph that is allowed to know about both.
|
|
88
|
+
* A flow tool reaching into the input layer directly would be a feature depending on a driver, which
|
|
89
|
+
* is the reach the boundary guard exists to ask about.
|
|
90
|
+
*/
|
|
91
|
+
export async function sessionMutationPort(realInput, appUrl) {
|
|
92
|
+
if (realInput === undefined || appUrl === undefined) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return await mutationPortFor(realInput, appUrl);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// A provider that cannot answer is the same answer as a provider that cannot break anything.
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -94,6 +94,8 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
94
94
|
url: string;
|
|
95
95
|
notice: string;
|
|
96
96
|
} | undefined>;
|
|
97
|
+
/** The one dev server running in the caller's project folder, for a lease that names no url. */
|
|
98
|
+
projectAppUrl?: () => Promise<string | undefined>;
|
|
97
99
|
/** cross-run outcome memory (.reticle/project.json). */
|
|
98
100
|
project: ProjectStore;
|
|
99
101
|
/** optional native-input provider. undefined ⇒ everything stays synthetic. */
|
|
@@ -170,13 +172,13 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
170
172
|
export interface ToolDef<Ext = unknown> {
|
|
171
173
|
name: string;
|
|
172
174
|
description: string;
|
|
173
|
-
inputSchema: z.
|
|
175
|
+
inputSchema: Record<string, z.ZodType>;
|
|
174
176
|
/**
|
|
175
177
|
* JSON Schema-compatible output schema for this tool. When present, the MCP server advertises it
|
|
176
178
|
* in the tools/list response so schema-aware clients (like @reticlehq/cli) can validate outputs and
|
|
177
179
|
* compose tool calls safely. Also drives TOON encoding for snapshot/query results.
|
|
178
180
|
*/
|
|
179
|
-
outputSchema?: z.
|
|
181
|
+
outputSchema?: Record<string, z.ZodType>;
|
|
180
182
|
/**
|
|
181
183
|
* One concrete, valid call — the shape, not the prose.
|
|
182
184
|
*
|
|
@@ -369,7 +371,7 @@ export declare const SCREENSHOT_DOC = " `screenshot.path` is a picture of the pa
|
|
|
369
371
|
* Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
|
|
370
372
|
* whole release, and every key that went missing was a channel nobody could see was missing.
|
|
371
373
|
*/
|
|
372
|
-
export declare const sessionEnvelopeShape: z.
|
|
374
|
+
export declare const sessionEnvelopeShape: Record<string, z.ZodType>;
|
|
373
375
|
/** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
|
|
374
376
|
export declare function commandOrThrow(deps: ToolDeps, sessionId: string | undefined, name: ReticleCommand, args: Record<string, unknown>): Promise<unknown>;
|
|
375
377
|
/** Full DOM snapshot → normalized tree lines + route, for tools that diff or scan the page. */
|
|
@@ -324,7 +324,7 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
324
324
|
states: z.array(z.string()),
|
|
325
325
|
visible: z.boolean(),
|
|
326
326
|
attrs: z
|
|
327
|
-
.record(z.string())
|
|
327
|
+
.record(z.string(), z.string())
|
|
328
328
|
.optional()
|
|
329
329
|
.describe('Present only for attributes requested via `attrs` and found on the element.'),
|
|
330
330
|
source: z
|