diff --git a/packages/contracts/src/embeddedBrowser.test.ts b/packages/contracts/src/embeddedBrowser.test.ts new file mode 100644 index 00000000..3958f9d7 --- /dev/null +++ b/packages/contracts/src/embeddedBrowser.test.ts @@ -0,0 +1,293 @@ +import * as Schema from "effect/Schema"; +import { describe, expect, it } from "vitest"; + +import { + EMBEDDED_BROWSER_MAX_TYPE_CHARS, + EMBEDDED_BROWSER_MAX_URL_CHARS, + EMBEDDED_BROWSER_MAX_SNAPSHOT_TARGETS, + EMBEDDED_BROWSER_MAX_SNAPSHOT_TEXT_CHARS, + EMBEDDED_BROWSER_MAX_IMAGE_REGIONS, + AgentBrowserGrantInputSchema, + AgentBrowserCompleteInputSchema, + AgentBrowserActionSchema, + AgentBrowserRequestSchema, + AgentBrowserSessionContextSchema, + EmbeddedBrowserClickInputSchema, + EmbeddedBrowserNavigateInputSchema, + EmbeddedBrowserSetBoundsInputSchema, + EmbeddedBrowserSnapshotSchema, + EmbeddedBrowserTypeInputSchema, +} from "./embeddedBrowser.js"; + +const decodeNavigate = Schema.decodeUnknownSync(EmbeddedBrowserNavigateInputSchema); +const decodeBounds = Schema.decodeUnknownSync(EmbeddedBrowserSetBoundsInputSchema); +const decodeClick = Schema.decodeUnknownSync(EmbeddedBrowserClickInputSchema); +const decodeType = Schema.decodeUnknownSync(EmbeddedBrowserTypeInputSchema); +const encodeSnapshot = Schema.encodeUnknownSync(EmbeddedBrowserSnapshotSchema); +const decodeAgentGrant = Schema.decodeUnknownSync(AgentBrowserGrantInputSchema); +const decodeAgentComplete = Schema.decodeUnknownSync(AgentBrowserCompleteInputSchema); +const decodeAgentAction = Schema.decodeUnknownSync(AgentBrowserActionSchema); +const decodeSessionContext = Schema.decodeUnknownSync(AgentBrowserSessionContextSchema); +const decodeRequest = Schema.decodeUnknownSync(AgentBrowserRequestSchema); + +describe("embedded browser IPC contracts", () => { + it("bounds URL and view geometry inputs", () => { + expect( + decodeNavigate({ + tabId: "tab_1", + url: "https://portal.example", + }), + ).toEqual({ + tabId: "tab_1", + url: "https://portal.example", + }); + expect(() => + decodeNavigate({ + tabId: "tab_1", + url: "x".repeat(EMBEDDED_BROWSER_MAX_URL_CHARS + 1), + }), + ).toThrow(); + expect(() => + decodeBounds({ + tabId: "tab_1", + bounds: { x: -1, y: 0, width: 100, height: 100 }, + }), + ).toThrow(); + }); + + it("accepts only opaque tab/snapshot IDs and enumerated target IDs", () => { + expect( + decodeClick({ + tabId: "tab-1", + snapshotId: "snapshot_1", + targetId: "e12", + }), + ).toEqual({ + tabId: "tab-1", + snapshotId: "snapshot_1", + targetId: "e12", + }); + expect(() => + decodeClick({ + tabId: "../other-tab", + snapshotId: "snapshot_1", + targetId: "#password", + }), + ).toThrow(); + }); + + it("bounds transient typing values and requires an explicit sensitivity flag", () => { + expect( + decodeType({ + tabId: "tab-1", + snapshotId: "snapshot-1", + targetId: "e1", + value: "one-time", + sensitive: true, + }), + ).toMatchObject({ value: "one-time", sensitive: true }); + expect(() => + decodeType({ + tabId: "tab-1", + snapshotId: "snapshot-1", + targetId: "e1", + value: "x".repeat(EMBEDDED_BROWSER_MAX_TYPE_CHARS + 1), + sensitive: true, + }), + ).toThrow(); + expect(() => + decodeType({ + tabId: "tab-1", + snapshotId: "snapshot-1", + targetId: "e1", + value: "one-time", + }), + ).toThrow(); + }); + + it("encodes snapshot timestamps across the IPC boundary as ISO strings", () => { + expect( + encodeSnapshot({ + snapshotId: "snapshot-1", + mode: "dom-accessibility", + displayUrl: "https://portal.example/account", + title: "Account", + capturedAt: "2026-07-23T12:00:00.000Z", + text: "Visible text", + targets: [], + imageRegions: [], + ocr: null, + redactionNotice: "Likely secrets are omitted.", + }), + ).toMatchObject({ capturedAt: "2026-07-23T12:00:00.000Z" }); + }); + + it("gates offline OCR to the packaged English and Japanese language identifiers", () => { + expect(decodeAgentAction({ type: "ocr", language: "eng" })).toEqual({ + type: "ocr", + language: "eng", + }); + expect(decodeAgentAction({ type: "ocr", language: "jpn" })).toEqual({ + type: "ocr", + language: "jpn", + }); + expect(() => decodeAgentAction({ type: "ocr", language: "deu" })).toThrow(); + expect(() => + encodeSnapshot({ + snapshotId: "snapshot-ocr", + mode: "ocr", + displayUrl: "https://portal.example/", + title: "Portal", + capturedAt: "2026-07-23T12:00:00.000Z", + text: "DOM text", + targets: [], + imageRegions: [], + ocr: { + status: "completed", + engine: "tesseract.js@7.0.0", + language: "eng", + confidence: 101, + truncated: false, + text: "OCR text", + }, + redactionNotice: "Likely secrets are omitted.", + }), + ).toThrow(); + }); + + it("bounds agent grants and requires exact renderer completion correlation", () => { + expect( + decodeAgentGrant({ + threadId: "thread-1", + providerInstanceId: "codex", + tabId: "tab-1", + origin: "https://portal.example", + durationSeconds: 300, + }), + ).toMatchObject({ durationSeconds: 300, origin: "https://portal.example" }); + expect(() => + decodeAgentGrant({ + threadId: "thread-1", + providerInstanceId: "codex", + tabId: "tab-1", + origin: "https://portal.example", + durationSeconds: 3_600, + }), + ).toThrow(); + expect(() => + decodeAgentComplete({ + context: { tabId: "tab-1", origin: "https://portal.example" }, + requestId: "../wrong", + result: { type: "snapshot", snapshot: null }, + }), + ).toThrow(); + }); + + it("retains visibility independently of tab identity so minimizing need not close a tab", () => { + const input = { + tabId: "tab-1", + bounds: { x: 0, y: 40, width: 800, height: 600 }, + }; + expect(decodeBounds({ ...input, visible: false })).toEqual({ ...input, visible: false }); + expect(decodeBounds({ ...input, visible: true })).toEqual({ ...input, visible: true }); + expect(decodeBounds(input)).toEqual(input); + expect(() => decodeBounds({ ...input, visible: "false" })).toThrow(); + }); + + it("carries explicit thread access policy with a bounded exclusion list", () => { + const context = { tabId: "tab-1", origin: "https://portal.example" }; + expect(decodeSessionContext(context)).toEqual(context); + expect( + decodeSessionContext({ ...context, defaultAccess: false, disabledThreadIds: ["thread-1"] }), + ).toEqual({ ...context, defaultAccess: false, disabledThreadIds: ["thread-1"] }); + expect(() => + decodeSessionContext({ ...context, disabledThreadIds: Array(10_001).fill("thread-1") }), + ).toThrow(); + expect(() => decodeSessionContext({ ...context, disabledThreadIds: [""] })).toThrow(); + for (const threadId of ["x".repeat(513), "thread\u0000other", "thread\nother"]) { + expect(() => decodeSessionContext({ ...context, disabledThreadIds: [threadId] })).toThrow(); + } + }); + + it("requires real canonical UTC deadlines before consumers compare request expiry", () => { + const request = { + requestId: "request-1", + grantId: "grant-1", + threadId: "thread-1", + providerInstanceId: "codex", + tabId: "tab-1", + origin: "https://portal.example", + action: { type: "snapshot" }, + summary: "Read page", + createdAt: "2026-09-11T12:00:00.000Z", + expiresAt: "2026-09-11T12:01:30.000Z", + }; + expect(decodeRequest(request)).toEqual(request); + for (const expiresAt of [ + "not-a-date", + "x".repeat(100_000), + "2026-02-30T12:00:00.000Z", + "2026-09-11", + "2026-09-11T12:01:30", + ]) { + expect(() => decodeRequest({ ...request, expiresAt })).toThrow(); + } + }); + + it("rejects oversized snapshot text, targets, and image descriptions at the transport boundary", () => { + const snapshot = { + snapshotId: "snapshot-1", + mode: "dom-accessibility", + displayUrl: "https://portal.example/", + title: "Portal", + capturedAt: "2026-09-11T12:00:00.000Z", + text: "Visible text", + targets: [], + imageRegions: [], + ocr: null, + redactionNotice: "Likely secrets are omitted.", + }; + expect(encodeSnapshot(snapshot)).toEqual(snapshot); + const target = { targetId: "e1", role: "button", name: "Open", text: "", sensitive: false }; + expect(() => + encodeSnapshot({ + ...snapshot, + targets: Array(EMBEDDED_BROWSER_MAX_SNAPSHOT_TARGETS + 1).fill(target), + }), + ).toThrow(); + expect(() => + encodeSnapshot({ + ...snapshot, + text: "x".repeat(EMBEDDED_BROWSER_MAX_SNAPSHOT_TEXT_CHARS + 1), + }), + ).toThrow(); + expect(() => + encodeSnapshot({ + ...snapshot, + imageRegions: Array(EMBEDDED_BROWSER_MAX_IMAGE_REGIONS + 1).fill({ + index: 0, + alt: "Image", + labelledBy: "", + }), + }), + ).toThrow(); + }); + + it("accepts only the defined agent actions and requires snapshot correlation for mutations", () => { + expect(() => decodeAgentAction({ type: "evaluate", script: "document.cookie" })).toThrow(); + expect(() => decodeAgentAction({ type: "click", targetId: "e1" })).toThrow(); + expect(() => + decodeAgentAction({ + type: "type", + snapshotId: "snapshot-1", + targetId: "e1", + value: "x".repeat(1_025), + }), + ).toThrow(); + expect(decodeAgentAction({ type: "history", action: "stop" })).toEqual({ + type: "history", + action: "stop", + }); + expect(() => decodeAgentAction({ type: "history", action: "execute" })).toThrow(); + }); +}); diff --git a/packages/contracts/src/embeddedBrowser.ts b/packages/contracts/src/embeddedBrowser.ts new file mode 100644 index 00000000..4be982a6 --- /dev/null +++ b/packages/contracts/src/embeddedBrowser.ts @@ -0,0 +1,365 @@ +import * as Schema from "effect/Schema"; + +import { IsoDateTime, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { ProviderInstanceId } from "./providerInstance.ts"; + +// These transport schemas define browser messages only. Decoding a message does +// not authorize a URL, origin, tab, thread, provider, or snapshot target. The +// native browser and agent bridge must enforce ownership and current grants. +export const EMBEDDED_BROWSER_MAX_URL_CHARS = 4_096; +export const EMBEDDED_BROWSER_MAX_TABS = 8; +export const EMBEDDED_BROWSER_MAX_TYPE_CHARS = 4_096; +export const EMBEDDED_BROWSER_MAX_SNAPSHOT_TEXT_CHARS = 24_000; +export const EMBEDDED_BROWSER_MAX_SNAPSHOT_TARGETS = 200; +export const EMBEDDED_BROWSER_MAX_IMAGE_REGIONS = 100; +export const EMBEDDED_BROWSER_OCR_MAX_CAPTURE_EDGE = 4_096; +export const EMBEDDED_BROWSER_OCR_MAX_CAPTURE_PIXELS = 16_777_216; +export const EMBEDDED_BROWSER_OCR_MAX_INPUT_EDGE = 2_048; +export const EMBEDDED_BROWSER_OCR_MAX_INPUT_PIXELS = 2_097_152; +export const EMBEDDED_BROWSER_OCR_MAX_PNG_BYTES = 8 * 1_024 * 1_024; +export const EMBEDDED_BROWSER_OCR_TIMEOUT_MS = 30_000; +export const AGENT_BROWSER_GRANT_MIN_SECONDS = 60; +export const AGENT_BROWSER_GRANT_MAX_SECONDS = 600; +export const AGENT_BROWSER_GRANT_DEFAULT_SECONDS = 300; +export const AGENT_BROWSER_MAX_REQUESTS_PER_GRANT = 40; +export const AGENT_BROWSER_MAX_QUEUED_REQUESTS = 4; +export const AGENT_BROWSER_REQUEST_TIMEOUT_MS = 90_000; + +// Keep browser deadlines as canonical UTC strings. The shared IsoDateTime +// primitive is intentionally broad and does not validate Date.parse inputs. +const EmbeddedBrowserTimestampSchema = IsoDateTime.check( + Schema.isMaxLength(24), + Schema.makeFilter( + (value) => Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value, + ), +); +const EmbeddedBrowserThreadIdSchema = ThreadId.check( + Schema.isMaxLength(512), + Schema.isPattern(/^[^\u0000-\u001f\u007f]+$/), +); + +export const EmbeddedBrowserTabIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(128), + Schema.isPattern(/^[A-Za-z0-9_-]+$/), +); +export type EmbeddedBrowserTabId = typeof EmbeddedBrowserTabIdSchema.Type; + +export const EmbeddedBrowserSnapshotIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(128), + Schema.isPattern(/^[A-Za-z0-9_-]+$/), +); +export type EmbeddedBrowserSnapshotId = typeof EmbeddedBrowserSnapshotIdSchema.Type; + +export const EmbeddedBrowserTargetIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(32), + Schema.isPattern(/^e[0-9]+$/), +); +export type EmbeddedBrowserTargetId = typeof EmbeddedBrowserTargetIdSchema.Type; + +const EmbeddedBrowserUrlInputSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(EMBEDDED_BROWSER_MAX_URL_CHARS), +); + +export const EmbeddedBrowserBoundsSchema = Schema.Struct({ + x: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100_000 })), + y: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100_000 })), + width: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100_000 })), + height: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100_000 })), +}); +export type EmbeddedBrowserBounds = typeof EmbeddedBrowserBoundsSchema.Type; + +export const EmbeddedBrowserStateSchema = Schema.Struct({ + status: Schema.Literals(["open", "closed"]), + tabId: Schema.NullOr(EmbeddedBrowserTabIdSchema), + displayUrl: Schema.String.check(Schema.isMaxLength(EMBEDDED_BROWSER_MAX_URL_CHARS)), + title: Schema.String.check(Schema.isMaxLength(512)), + loading: Schema.Boolean, + canGoBack: Schema.Boolean, + canGoForward: Schema.Boolean, + shared: Schema.Boolean, + sharedOrigin: Schema.NullOr(Schema.String.check(Schema.isMaxLength(512))), +}); +export type EmbeddedBrowserState = typeof EmbeddedBrowserStateSchema.Type; + +export const EmbeddedBrowserOpenInputSchema = Schema.Struct({ + initialUrl: Schema.optionalKey(EmbeddedBrowserUrlInputSchema), +}); +export type EmbeddedBrowserOpenInput = typeof EmbeddedBrowserOpenInputSchema.Type; + +export const EmbeddedBrowserTabInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, +}); +export type EmbeddedBrowserTabInput = typeof EmbeddedBrowserTabInputSchema.Type; + +export const EmbeddedBrowserSetBoundsInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + bounds: EmbeddedBrowserBoundsSchema, + visible: Schema.optionalKey(Schema.Boolean), +}); +export type EmbeddedBrowserSetBoundsInput = typeof EmbeddedBrowserSetBoundsInputSchema.Type; + +export const EmbeddedBrowserShareInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + shared: Schema.Boolean, +}); +export type EmbeddedBrowserShareInput = typeof EmbeddedBrowserShareInputSchema.Type; + +export const EmbeddedBrowserNavigateInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + url: EmbeddedBrowserUrlInputSchema, +}); +export type EmbeddedBrowserNavigateInput = typeof EmbeddedBrowserNavigateInputSchema.Type; + +export const EmbeddedBrowserHistoryActionInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + action: Schema.Literals(["back", "forward", "reload", "stop"]), +}); +export type EmbeddedBrowserHistoryActionInput = typeof EmbeddedBrowserHistoryActionInputSchema.Type; + +export const EmbeddedBrowserSnapshotInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + mode: Schema.Literals(["dom-accessibility", "ocr"]), + ocrLanguage: Schema.optionalKey(Schema.Literals(["eng", "jpn"])), +}); +export type EmbeddedBrowserSnapshotInput = typeof EmbeddedBrowserSnapshotInputSchema.Type; + +export const EmbeddedBrowserSnapshotTargetSchema = Schema.Struct({ + targetId: EmbeddedBrowserTargetIdSchema, + role: Schema.String.check(Schema.isMaxLength(64)), + name: Schema.String.check(Schema.isMaxLength(512)), + text: Schema.String.check(Schema.isMaxLength(1_024)), + sensitive: Schema.Boolean, +}); +export type EmbeddedBrowserSnapshotTarget = typeof EmbeddedBrowserSnapshotTargetSchema.Type; + +export const EmbeddedBrowserImageRegionSchema = Schema.Struct({ + index: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 10_000 })), + alt: Schema.String.check(Schema.isMaxLength(1_024)), + labelledBy: Schema.String.check(Schema.isMaxLength(1_024)), +}); +export type EmbeddedBrowserImageRegion = typeof EmbeddedBrowserImageRegionSchema.Type; + +export const EmbeddedBrowserOcrResultSchema = Schema.Union([ + Schema.Struct({ + status: Schema.Literal("completed"), + engine: Schema.String.check(Schema.isMaxLength(128)), + language: Schema.Literals(["eng", "jpn"]), + confidence: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 100 })), + truncated: Schema.Boolean, + text: Schema.String.check(Schema.isMaxLength(EMBEDDED_BROWSER_MAX_SNAPSHOT_TEXT_CHARS)), + }), + Schema.Struct({ + status: Schema.Literal("unavailable"), + reason: Schema.String.check(Schema.isMaxLength(512)), + }), +]); +export type EmbeddedBrowserOcrResult = typeof EmbeddedBrowserOcrResultSchema.Type; + +export const EmbeddedBrowserSnapshotSchema = Schema.Struct({ + snapshotId: EmbeddedBrowserSnapshotIdSchema, + mode: Schema.Literals(["dom-accessibility", "ocr"]), + displayUrl: Schema.String.check(Schema.isMaxLength(EMBEDDED_BROWSER_MAX_URL_CHARS)), + title: Schema.String.check(Schema.isMaxLength(512)), + capturedAt: EmbeddedBrowserTimestampSchema, + text: Schema.String.check(Schema.isMaxLength(EMBEDDED_BROWSER_MAX_SNAPSHOT_TEXT_CHARS)), + targets: Schema.Array(EmbeddedBrowserSnapshotTargetSchema).check( + Schema.isMaxLength(EMBEDDED_BROWSER_MAX_SNAPSHOT_TARGETS), + ), + imageRegions: Schema.Array(EmbeddedBrowserImageRegionSchema).check( + Schema.isMaxLength(EMBEDDED_BROWSER_MAX_IMAGE_REGIONS), + ), + ocr: Schema.NullOr(EmbeddedBrowserOcrResultSchema), + redactionNotice: Schema.String.check(Schema.isMaxLength(512)), +}); +export type EmbeddedBrowserSnapshot = typeof EmbeddedBrowserSnapshotSchema.Type; + +export const EmbeddedBrowserClickInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + snapshotId: EmbeddedBrowserSnapshotIdSchema, + targetId: EmbeddedBrowserTargetIdSchema, +}); +export type EmbeddedBrowserClickInput = typeof EmbeddedBrowserClickInputSchema.Type; + +export const EmbeddedBrowserTypeInputSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + snapshotId: EmbeddedBrowserSnapshotIdSchema, + targetId: EmbeddedBrowserTargetIdSchema, + value: Schema.String.check( + Schema.isMinLength(1), + Schema.isMaxLength(EMBEDDED_BROWSER_MAX_TYPE_CHARS), + ), + sensitive: Schema.Boolean, +}); +export type EmbeddedBrowserTypeInput = typeof EmbeddedBrowserTypeInputSchema.Type; + +export const EmbeddedBrowserActionResultSchema = Schema.Struct({ + status: Schema.Literals(["completed", "denied", "stale", "failed"]), + message: Schema.String.check(Schema.isMaxLength(512)), + state: EmbeddedBrowserStateSchema, +}); +export type EmbeddedBrowserActionResult = typeof EmbeddedBrowserActionResultSchema.Type; + +export const AgentBrowserGrantIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(128), + Schema.isPattern(/^[A-Za-z0-9_-]+$/), +); +export type AgentBrowserGrantId = typeof AgentBrowserGrantIdSchema.Type; + +export const AgentBrowserRequestIdSchema = TrimmedNonEmptyString.check( + Schema.isMaxLength(128), + Schema.isPattern(/^[A-Za-z0-9_-]+$/), +); +export type AgentBrowserRequestId = typeof AgentBrowserRequestIdSchema.Type; + +export const AgentBrowserGrantInputSchema = Schema.Struct({ + threadId: EmbeddedBrowserThreadIdSchema, + providerInstanceId: ProviderInstanceId, + tabId: EmbeddedBrowserTabIdSchema, + origin: TrimmedNonEmptyString.check(Schema.isMaxLength(512)), + durationSeconds: Schema.Int.check( + Schema.isBetween({ + minimum: AGENT_BROWSER_GRANT_MIN_SECONDS, + maximum: AGENT_BROWSER_GRANT_MAX_SECONDS, + }), + ), +}); +export type AgentBrowserGrantInput = typeof AgentBrowserGrantInputSchema.Type; + +export const AgentBrowserSessionContextSchema = Schema.Struct({ + tabId: EmbeddedBrowserTabIdSchema, + origin: TrimmedNonEmptyString.check(Schema.isMaxLength(512)), + defaultAccess: Schema.optionalKey(Schema.Boolean), + disabledThreadIds: Schema.optionalKey( + Schema.Array(EmbeddedBrowserThreadIdSchema).check(Schema.isMaxLength(10_000)), + ), +}); +export type AgentBrowserSessionContext = typeof AgentBrowserSessionContextSchema.Type; + +export const AgentBrowserRevokeInputSchema = Schema.Struct({ + reason: Schema.Literals(["operator", "origin-changed", "tab-closed", "thread-changed"]), + threadId: Schema.optionalKey(EmbeddedBrowserThreadIdSchema), +}); +export type AgentBrowserRevokeInput = typeof AgentBrowserRevokeInputSchema.Type; + +const AgentBrowserSnapshotActionSchema = Schema.Struct({ + type: Schema.Literal("snapshot"), +}); + +const AgentBrowserOcrActionSchema = Schema.Struct({ + type: Schema.Literal("ocr"), + language: Schema.Literals(["eng", "jpn"]), +}); + +const AgentBrowserNavigateActionSchema = Schema.Struct({ + type: Schema.Literal("navigate"), + url: EmbeddedBrowserUrlInputSchema, +}); + +const AgentBrowserClickActionSchema = Schema.Struct({ + type: Schema.Literal("click"), + snapshotId: EmbeddedBrowserSnapshotIdSchema, + targetId: EmbeddedBrowserTargetIdSchema, +}); + +const AgentBrowserTypeActionSchema = Schema.Struct({ + type: Schema.Literal("type"), + snapshotId: EmbeddedBrowserSnapshotIdSchema, + targetId: EmbeddedBrowserTargetIdSchema, + value: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1_024)), +}); + +const AgentBrowserHistoryActionSchema = Schema.Struct({ + type: Schema.Literal("history"), + action: Schema.Literals(["back", "forward", "reload", "stop"]), +}); + +export const AgentBrowserActionSchema = Schema.Union([ + AgentBrowserSnapshotActionSchema, + AgentBrowserOcrActionSchema, + AgentBrowserNavigateActionSchema, + AgentBrowserClickActionSchema, + AgentBrowserTypeActionSchema, + AgentBrowserHistoryActionSchema, +]); +export type AgentBrowserAction = typeof AgentBrowserActionSchema.Type; + +export const AgentBrowserRequestSchema = Schema.Struct({ + requestId: AgentBrowserRequestIdSchema, + grantId: AgentBrowserGrantIdSchema, + threadId: EmbeddedBrowserThreadIdSchema, + providerInstanceId: ProviderInstanceId, + tabId: EmbeddedBrowserTabIdSchema, + origin: TrimmedNonEmptyString.check(Schema.isMaxLength(512)), + action: AgentBrowserActionSchema, + summary: Schema.String.check(Schema.isMaxLength(512)), + createdAt: EmbeddedBrowserTimestampSchema, + expiresAt: EmbeddedBrowserTimestampSchema, +}); +export type AgentBrowserRequest = typeof AgentBrowserRequestSchema.Type; + +export const AgentBrowserExecutionResultSchema = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("snapshot"), + snapshot: Schema.NullOr(EmbeddedBrowserSnapshotSchema), + }), + Schema.Struct({ + type: Schema.Literal("action"), + result: EmbeddedBrowserActionResultSchema, + }), +]); +export type AgentBrowserExecutionResult = typeof AgentBrowserExecutionResultSchema.Type; + +export const AgentBrowserCompleteInputSchema = Schema.Struct({ + context: AgentBrowserSessionContextSchema, + requestId: AgentBrowserRequestIdSchema, + result: AgentBrowserExecutionResultSchema, +}); +export type AgentBrowserCompleteInput = typeof AgentBrowserCompleteInputSchema.Type; + +const AgentBrowserInactiveGrantStateSchema = Schema.Struct({ + status: Schema.Literal("inactive"), + reason: Schema.String.check(Schema.isMaxLength(512)), +}); + +const AgentBrowserActiveGrantStateSchema = Schema.Struct({ + status: Schema.Literal("active"), + grantId: AgentBrowserGrantIdSchema, + threadId: EmbeddedBrowserThreadIdSchema, + providerInstanceId: ProviderInstanceId, + tabId: EmbeddedBrowserTabIdSchema, + origin: TrimmedNonEmptyString.check(Schema.isMaxLength(512)), + grantedAt: EmbeddedBrowserTimestampSchema, + expiresAt: EmbeddedBrowserTimestampSchema, + requestCount: Schema.Int.check( + Schema.isBetween({ minimum: 0, maximum: AGENT_BROWSER_MAX_REQUESTS_PER_GRANT }), + ), + requestLimit: Schema.Literal(AGENT_BROWSER_MAX_REQUESTS_PER_GRANT), + pendingAction: Schema.NullOr(Schema.String.check(Schema.isMaxLength(512))), +}); + +export const AgentBrowserGrantStateSchema = Schema.Union([ + AgentBrowserInactiveGrantStateSchema, + AgentBrowserActiveGrantStateSchema, +]); +export type AgentBrowserGrantState = typeof AgentBrowserGrantStateSchema.Type; + +export const AgentBrowserPollResultSchema = Schema.Struct({ + grant: AgentBrowserGrantStateSchema, + request: Schema.NullOr(AgentBrowserRequestSchema), +}); +export type AgentBrowserPollResult = typeof AgentBrowserPollResultSchema.Type; + +export const AgentBrowserCompleteResultSchema = Schema.Struct({ + accepted: Schema.Boolean, + grant: AgentBrowserGrantStateSchema, +}); +export type AgentBrowserCompleteResult = typeof AgentBrowserCompleteResultSchema.Type; + +export class AgentBrowserRpcError extends Schema.TaggedErrorClass()( + "AgentBrowserRpcError", + { reason: Schema.String.check(Schema.isMaxLength(512)) }, +) { + override get message(): string { + return this.reason; + } +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index d9ff1b41..a38c7cf0 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -2,6 +2,7 @@ export * from "./baseSchemas.ts"; export * from "./auth.ts"; export * from "./environment.ts"; export * from "./desktopBootstrap.ts"; +export * from "./embeddedBrowser.ts"; export * from "./dictation.ts"; export * from "./remoteAccess.ts"; export * from "./ipc.ts";