@crosshands/contract 0.1.2

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/context.d.ts +19 -0
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/context.js +82 -0
  5. package/dist/context.js.map +1 -0
  6. package/dist/errors.d.ts +164 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +65 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/generate-schemas.d.ts +2 -0
  11. package/dist/generate-schemas.d.ts.map +1 -0
  12. package/dist/generate-schemas.js +8 -0
  13. package/dist/generate-schemas.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +8 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json-schema.d.ts +306 -0
  19. package/dist/json-schema.d.ts.map +1 -0
  20. package/dist/json-schema.js +15 -0
  21. package/dist/json-schema.js.map +1 -0
  22. package/dist/operations.d.ts +2115 -0
  23. package/dist/operations.d.ts.map +1 -0
  24. package/dist/operations.js +205 -0
  25. package/dist/operations.js.map +1 -0
  26. package/dist/provider.d.ts +135 -0
  27. package/dist/provider.d.ts.map +1 -0
  28. package/dist/provider.js +56 -0
  29. package/dist/provider.js.map +1 -0
  30. package/dist/schemas.d.ts +499 -0
  31. package/dist/schemas.d.ts.map +1 -0
  32. package/dist/schemas.js +138 -0
  33. package/dist/schemas.js.map +1 -0
  34. package/dist/versions.d.ts +31 -0
  35. package/dist/versions.d.ts.map +1 -0
  36. package/dist/versions.js +28 -0
  37. package/dist/versions.js.map +1 -0
  38. package/package.json +39 -0
  39. package/schemas/contract.json +7572 -0
  40. package/src/context.ts +107 -0
  41. package/src/errors.ts +82 -0
  42. package/src/generate-schemas.ts +9 -0
  43. package/src/index.ts +7 -0
  44. package/src/json-schema.ts +17 -0
  45. package/src/operations.ts +224 -0
  46. package/src/provider.ts +82 -0
  47. package/src/schemas.ts +161 -0
  48. package/src/versions.ts +46 -0
package/src/context.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ import { createComputerError } from './errors.js'
4
+ import type { InteractionContext, ReferenceBindings, TargetReference } from './schemas.js'
5
+
6
+ export const DEFAULT_INTERACTION_CONTEXT_TTL_MS = 120_000
7
+ export const DEFAULT_INTERACTION_CONTEXT_LIMIT = 32
8
+
9
+ type InteractionContextStoreOptions = {
10
+ ttlMs?: number
11
+ limit?: number
12
+ now?: () => number
13
+ tokenFactory?: () => string
14
+ }
15
+
16
+ export class InteractionContextStore {
17
+ readonly #ttlMs: number
18
+ readonly #limit: number
19
+ readonly #now: () => number
20
+ readonly #tokenFactory: () => string
21
+ readonly #contexts = new Map<string, InteractionContext>()
22
+
23
+ constructor(options: InteractionContextStoreOptions = {}) {
24
+ this.#ttlMs = options.ttlMs ?? DEFAULT_INTERACTION_CONTEXT_TTL_MS
25
+ this.#limit = options.limit ?? DEFAULT_INTERACTION_CONTEXT_LIMIT
26
+ this.#now = options.now ?? Date.now
27
+ this.#tokenFactory =
28
+ options.tokenFactory ?? (() => `ctx_${randomBytes(24).toString('base64url')}`)
29
+ if (this.#ttlMs <= 0 || this.#limit <= 0)
30
+ throw new RangeError('Context TTL and limit must be positive')
31
+ }
32
+
33
+ issue(bindings: ReferenceBindings): InteractionContext {
34
+ const issuedAtMs = this.#now()
35
+ const context: InteractionContext = {
36
+ token: this.#tokenFactory(),
37
+ ...bindings,
38
+ issuedAt: new Date(issuedAtMs).toISOString(),
39
+ expiresAt: new Date(issuedAtMs + this.#ttlMs).toISOString()
40
+ }
41
+ this.#contexts.delete(context.token)
42
+ this.#contexts.set(context.token, context)
43
+ while (this.#contexts.size > this.#limit) {
44
+ const oldest = this.#contexts.keys().next().value as string | undefined
45
+ if (oldest === undefined) break
46
+ this.#contexts.delete(oldest)
47
+ }
48
+ return context
49
+ }
50
+
51
+ resolve(token: string, now = this.#now()): InteractionContext {
52
+ const context = this.#contexts.get(token)
53
+ if (context === undefined) {
54
+ throw createComputerError(
55
+ 'interaction_context_invalid',
56
+ 'Interaction context is unknown or forged'
57
+ )
58
+ }
59
+ if (Date.parse(context.expiresAt) < now) {
60
+ this.#contexts.delete(token)
61
+ throw createComputerError('interaction_context_expired', 'Interaction context has expired')
62
+ }
63
+ return context
64
+ }
65
+
66
+ invalidateAll(): void {
67
+ this.#contexts.clear()
68
+ }
69
+ }
70
+
71
+ function sameProcess(a: ReferenceBindings['process'], b: ReferenceBindings['process']): boolean {
72
+ return a.pid === b.pid && a.startedAt === b.startedAt && a.executableId === b.executableId
73
+ }
74
+
75
+ function sameWindow(a: ReferenceBindings['window'], b: ReferenceBindings['window']): boolean {
76
+ return a.id === b.id && a.ownerPid === b.ownerPid
77
+ }
78
+
79
+ export function assertReferenceFresh(
80
+ reference: TargetReference,
81
+ context: InteractionContext,
82
+ current: ReferenceBindings,
83
+ now = Date.now()
84
+ ): void {
85
+ const stale =
86
+ reference.contextToken !== context.token ||
87
+ reference.brokerGeneration !== context.brokerGeneration ||
88
+ reference.providerGeneration !== context.providerGeneration ||
89
+ reference.graphicalSessionId !== context.graphicalSessionId ||
90
+ reference.brokerGeneration !== current.brokerGeneration ||
91
+ reference.providerGeneration !== current.providerGeneration ||
92
+ reference.graphicalSessionId !== current.graphicalSessionId ||
93
+ !sameProcess(reference.process, context.process) ||
94
+ !sameProcess(reference.process, current.process) ||
95
+ reference.appId !== context.appId ||
96
+ reference.appId !== current.appId ||
97
+ !sameWindow(reference.window, context.window) ||
98
+ !sameWindow(reference.window, current.window) ||
99
+ reference.snapshotId !== context.snapshotId ||
100
+ reference.snapshotId !== current.snapshotId ||
101
+ reference.desktopEpoch !== context.desktopEpoch ||
102
+ reference.desktopEpoch !== current.desktopEpoch ||
103
+ Date.parse(reference.expiresAt) < now ||
104
+ Date.parse(context.expiresAt) < now
105
+
106
+ if (stale) throw createComputerError('stale_target', 'Target reference is no longer fresh')
107
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod'
2
+
3
+ export const ERROR_CATALOG = {
4
+ app_not_found: { retry: true, remediation: 'refresh_apps' },
5
+ app_unavailable: { retry: true, remediation: 'refresh_apps' },
6
+ app_blocked: { retry: false, remediation: 'choose_non_sensitive_target' },
7
+ window_not_found: { retry: true, remediation: 'refresh_windows' },
8
+ window_unavailable: { retry: true, remediation: 'refresh_windows' },
9
+ window_not_focused: { retry: true, remediation: 'focus_window' },
10
+ permission_denied: { retry: false, remediation: 'grant_permission' },
11
+ stale_target: { retry: true, remediation: 'refresh_state' },
12
+ element_not_found: { retry: true, remediation: 'refresh_state' },
13
+ element_not_clickable: { retry: true, remediation: 'choose_actionable_target' },
14
+ action_not_supported: { retry: false, remediation: 'choose_supported_operation' },
15
+ value_not_settable: { retry: false, remediation: 'choose_settable_target' },
16
+ invalid_argument: { retry: false, remediation: 'correct_request' },
17
+ timeout: { retry: false, remediation: 'inspect_state_before_retry' },
18
+ unsupported_capability: { retry: false, remediation: 'choose_supported_operation' },
19
+ provider_unavailable: { retry: true, remediation: 'run_doctor' },
20
+ provider_crashed: { retry: true, remediation: 'refresh_state' },
21
+ version_incompatible: { retry: false, remediation: 'upgrade_or_downgrade' },
22
+ interaction_context_invalid: { retry: true, remediation: 'refresh_state' },
23
+ interaction_context_expired: { retry: true, remediation: 'refresh_state' },
24
+ session_unavailable: { retry: true, remediation: 'unlock_graphical_session' },
25
+ screenshot_failed: { retry: true, remediation: 'check_screenshot_permission' },
26
+ accessibility_error: { retry: true, remediation: 'check_accessibility_permission' }
27
+ } as const
28
+
29
+ export type ComputerErrorCode = keyof typeof ERROR_CATALOG
30
+ export type ComputerErrorRemediation = (typeof ERROR_CATALOG)[ComputerErrorCode]['remediation']
31
+
32
+ export const ComputerErrorCodeSchema = z.enum(
33
+ Object.keys(ERROR_CATALOG) as [ComputerErrorCode, ...ComputerErrorCode[]]
34
+ )
35
+
36
+ export const SerializedComputerErrorSchema = z
37
+ .object({
38
+ code: ComputerErrorCodeSchema,
39
+ message: z.string().min(1),
40
+ retry: z.boolean(),
41
+ remediation: z.string().min(1),
42
+ details: z.unknown().optional()
43
+ })
44
+ .strict()
45
+
46
+ export type SerializedComputerError = z.infer<typeof SerializedComputerErrorSchema>
47
+
48
+ export class ComputerError extends Error {
49
+ readonly code: ComputerErrorCode
50
+ readonly retry: boolean
51
+ readonly remediation: ComputerErrorRemediation
52
+ readonly details: unknown
53
+
54
+ constructor(code: ComputerErrorCode, message: string, details?: unknown) {
55
+ super(message)
56
+ this.name = 'ComputerError'
57
+ this.code = code
58
+ this.retry = ERROR_CATALOG[code].retry
59
+ this.remediation = ERROR_CATALOG[code].remediation
60
+ this.details = details
61
+ }
62
+
63
+ toJSON(): SerializedComputerError {
64
+ return this.details === undefined
65
+ ? { code: this.code, message: this.message, retry: this.retry, remediation: this.remediation }
66
+ : {
67
+ code: this.code,
68
+ message: this.message,
69
+ retry: this.retry,
70
+ remediation: this.remediation,
71
+ details: this.details
72
+ }
73
+ }
74
+ }
75
+
76
+ export function createComputerError(
77
+ code: ComputerErrorCode,
78
+ message: string,
79
+ details?: unknown
80
+ ): ComputerError {
81
+ return new ComputerError(code, message, details)
82
+ }
@@ -0,0 +1,9 @@
1
+ import { writeFile } from 'node:fs/promises'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import { contractJsonSchemas } from './json-schema.js'
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url))
8
+ const output = resolve(here, '../schemas/contract.json')
9
+ await writeFile(output, `${JSON.stringify(contractJsonSchemas, null, 2)}\n`, 'utf8')
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './context.js'
2
+ export * from './errors.js'
3
+ export * from './json-schema.js'
4
+ export * from './operations.js'
5
+ export * from './provider.js'
6
+ export * from './schemas.js'
7
+ export * from './versions.js'
@@ -0,0 +1,17 @@
1
+ import { COMPUTER_OPERATIONS } from './operations.js'
2
+ import { CONTRACT_VERSIONS } from './versions.js'
3
+
4
+ export const contractJsonSchemas = {
5
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
6
+ versions: CONTRACT_VERSIONS,
7
+ operations: Object.fromEntries(
8
+ Object.entries(COMPUTER_OPERATIONS).map(([name, operation]) => [
9
+ name,
10
+ {
11
+ mutation: operation.mutation,
12
+ input: operation.input.toJSONSchema({ target: 'draft-2020-12' }),
13
+ output: operation.output.toJSONSchema({ target: 'draft-2020-12' })
14
+ }
15
+ ])
16
+ )
17
+ } as const
@@ -0,0 +1,224 @@
1
+ import { z } from 'zod'
2
+
3
+ import {
4
+ AppInfoSchema,
5
+ InteractionContextTokenSchema,
6
+ MutationResultSchema,
7
+ ProviderCapabilitiesSchema,
8
+ ScreenshotSchema,
9
+ SnapshotResultSchema,
10
+ TargetReferenceSchema,
11
+ WindowInfoSchema
12
+ } from './schemas.js'
13
+
14
+ const EmptyInputSchema = z.object({}).strict()
15
+ const AppQuerySchema = z.string().min(1).max(512)
16
+ const WindowSelectorSchema = z.union([
17
+ z.object({ id: z.string().min(1) }).strict(),
18
+ z.object({ index: z.number().int().nonnegative() }).strict()
19
+ ])
20
+ const CaptureOptionsShape = {
21
+ captureScreenshot: z.boolean().optional(),
22
+ restoreWindow: z.boolean().optional()
23
+ }
24
+ const ContextWindowTargetSchema = z.object({ kind: z.literal('context-window') }).strict()
25
+ const ElementIndexTargetSchema = z
26
+ .object({ kind: z.literal('element'), elementIndex: z.number().int().nonnegative() })
27
+ .strict()
28
+ const CoordinateTargetSchema = z
29
+ .object({
30
+ kind: z.literal('coordinate'),
31
+ window: TargetReferenceSchema.optional(),
32
+ x: z.number().finite(),
33
+ y: z.number().finite()
34
+ })
35
+ .strict()
36
+ const ElementTargetSchema = z
37
+ .object({ kind: z.literal('element'), ref: TargetReferenceSchema })
38
+ .strict()
39
+ const ElementActionTargetSchema = z.union([ElementTargetSchema, ElementIndexTargetSchema])
40
+ const ContextWindowMutationBaseShape = {
41
+ contextToken: InteractionContextTokenSchema,
42
+ app: AppQuerySchema.optional(),
43
+ target: ContextWindowTargetSchema,
44
+ ...CaptureOptionsShape
45
+ }
46
+ const ElementMutationBaseShape = {
47
+ contextToken: InteractionContextTokenSchema,
48
+ app: AppQuerySchema.optional(),
49
+ target: ElementActionTargetSchema,
50
+ ...CaptureOptionsShape
51
+ }
52
+ const ActionTargetSchema = z.union([
53
+ ElementTargetSchema,
54
+ ElementIndexTargetSchema,
55
+ CoordinateTargetSchema,
56
+ ContextWindowTargetSchema
57
+ ])
58
+
59
+ export const CLICK_MODIFIER_TOKENS = [
60
+ 'Shift',
61
+ 'Ctrl',
62
+ 'Control',
63
+ 'Alt',
64
+ 'Option',
65
+ 'Meta',
66
+ 'Cmd',
67
+ 'Command',
68
+ 'Super',
69
+ 'Win',
70
+ 'CmdOrCtrl',
71
+ 'CommandOrControl',
72
+ 'shift',
73
+ 'ctrl',
74
+ 'control',
75
+ 'alt',
76
+ 'option',
77
+ 'meta',
78
+ 'cmd',
79
+ 'command',
80
+ 'super',
81
+ 'win',
82
+ 'cmdorctrl',
83
+ 'commandorcontrol'
84
+ ] as const
85
+
86
+ const ClickModifierTokenSchema = z.enum(CLICK_MODIFIER_TOKENS)
87
+
88
+ const PermissionsResultSchema = z
89
+ .object({
90
+ permissions: z.record(z.string(), z.enum(['granted', 'denied', 'unknown', 'not_required']))
91
+ })
92
+ .strict()
93
+ const AppListResultSchema = z.object({ apps: z.array(AppInfoSchema) }).strict()
94
+ const WindowListResultSchema = z.object({ windows: z.array(WindowInfoSchema) }).strict()
95
+
96
+ export const COMPUTER_OPERATIONS = {
97
+ capabilities: {
98
+ mutation: false,
99
+ input: EmptyInputSchema,
100
+ output: ProviderCapabilitiesSchema
101
+ },
102
+ permissions: {
103
+ mutation: false,
104
+ input: z.object({ id: z.enum(['accessibility', 'screenshots']).optional() }).strict(),
105
+ output: PermissionsResultSchema
106
+ },
107
+ listApps: {
108
+ mutation: false,
109
+ input: EmptyInputSchema,
110
+ output: AppListResultSchema
111
+ },
112
+ listWindows: {
113
+ mutation: false,
114
+ input: z.object({ app: AppQuerySchema }).strict(),
115
+ output: WindowListResultSchema
116
+ },
117
+ getAppState: {
118
+ mutation: false,
119
+ input: z
120
+ .object({
121
+ app: AppQuerySchema,
122
+ window: WindowSelectorSchema.optional(),
123
+ ...CaptureOptionsShape
124
+ })
125
+ .strict(),
126
+ output: SnapshotResultSchema
127
+ },
128
+ click: {
129
+ mutation: true,
130
+ input: z
131
+ .object({
132
+ contextToken: InteractionContextTokenSchema,
133
+ app: AppQuerySchema.optional(),
134
+ target: ActionTargetSchema,
135
+ clickCount: z.number().int().min(1).max(3).optional(),
136
+ button: z.enum(['left', 'right', 'middle']).optional(),
137
+ modifiers: z.array(ClickModifierTokenSchema).min(1).max(4).optional(),
138
+ ...CaptureOptionsShape
139
+ })
140
+ .strict(),
141
+ output: MutationResultSchema
142
+ },
143
+ performSecondaryAction: {
144
+ mutation: true,
145
+ input: z.object({ ...ElementMutationBaseShape, action: z.string().min(1).max(256) }).strict(),
146
+ output: MutationResultSchema
147
+ },
148
+ scroll: {
149
+ mutation: true,
150
+ input: z
151
+ .object({
152
+ contextToken: InteractionContextTokenSchema,
153
+ app: AppQuerySchema.optional(),
154
+ target: ActionTargetSchema,
155
+ direction: z.enum(['up', 'down', 'left', 'right']),
156
+ pages: z.number().int().min(1).max(100).optional(),
157
+ ...CaptureOptionsShape
158
+ })
159
+ .strict(),
160
+ output: MutationResultSchema
161
+ },
162
+ drag: {
163
+ mutation: true,
164
+ input: z
165
+ .object({
166
+ contextToken: InteractionContextTokenSchema,
167
+ app: AppQuerySchema.optional(),
168
+ from: ActionTargetSchema,
169
+ to: ActionTargetSchema,
170
+ durationMs: z.number().int().min(50).max(30_000).optional(),
171
+ ...CaptureOptionsShape
172
+ })
173
+ .strict(),
174
+ output: MutationResultSchema
175
+ },
176
+ typeText: {
177
+ mutation: true,
178
+ input: z
179
+ .object({ ...ContextWindowMutationBaseShape, text: z.string().max(1_000_000) })
180
+ .strict(),
181
+ output: MutationResultSchema
182
+ },
183
+ pressKey: {
184
+ mutation: true,
185
+ input: z
186
+ .object({ ...ContextWindowMutationBaseShape, key: z.string().min(1).max(128) })
187
+ .strict(),
188
+ output: MutationResultSchema
189
+ },
190
+ hotkey: {
191
+ mutation: true,
192
+ input: z
193
+ .object({
194
+ ...ContextWindowMutationBaseShape,
195
+ keys: z.array(z.string().min(1).max(128)).min(2).max(5)
196
+ })
197
+ .strict(),
198
+ output: MutationResultSchema
199
+ },
200
+ pasteText: {
201
+ mutation: true,
202
+ input: z
203
+ .object({ ...ContextWindowMutationBaseShape, text: z.string().max(1_000_000) })
204
+ .strict(),
205
+ output: MutationResultSchema
206
+ },
207
+ setValue: {
208
+ mutation: true,
209
+ input: z.object({ ...ElementMutationBaseShape, value: z.string().max(1_000_000) }).strict(),
210
+ output: MutationResultSchema
211
+ }
212
+ } as const
213
+
214
+ export type ComputerOperationName = keyof typeof COMPUTER_OPERATIONS
215
+
216
+ export function parseOperationInput(operation: ComputerOperationName, input: unknown): unknown {
217
+ return COMPUTER_OPERATIONS[operation].input.parse(input)
218
+ }
219
+
220
+ export function parseOperationOutput(operation: ComputerOperationName, output: unknown): unknown {
221
+ return COMPUTER_OPERATIONS[operation].output.parse(output)
222
+ }
223
+
224
+ export const ScreenshotOutputSchema = ScreenshotSchema
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { SerializedComputerError } from './errors.js'
4
+ import type { ComputerOperationName } from './operations.js'
5
+ import { ProviderHandshakeSchema } from './schemas.js'
6
+
7
+ export const ProviderRequestSchema = z
8
+ .object({
9
+ requestId: z.string().min(1),
10
+ operation: z.enum([
11
+ 'capabilities',
12
+ 'permissions',
13
+ 'listApps',
14
+ 'listWindows',
15
+ 'getAppState',
16
+ 'click',
17
+ 'performSecondaryAction',
18
+ 'scroll',
19
+ 'drag',
20
+ 'typeText',
21
+ 'pressKey',
22
+ 'hotkey',
23
+ 'pasteText',
24
+ 'setValue'
25
+ ]),
26
+ input: z.unknown(),
27
+ deadlineAt: z.number()
28
+ })
29
+ .strict()
30
+
31
+ export const ProviderResponseSchema = z.union([
32
+ z
33
+ .object({
34
+ requestId: z.string().min(1),
35
+ dispatched: z.boolean(),
36
+ result: z.unknown()
37
+ })
38
+ .strict(),
39
+ z
40
+ .object({
41
+ requestId: z.string().min(1),
42
+ dispatched: z.boolean(),
43
+ error: z
44
+ .object({
45
+ code: z.string().min(1),
46
+ message: z.string().min(1),
47
+ retry: z.boolean(),
48
+ remediation: z.string().min(1),
49
+ details: z.unknown().optional()
50
+ })
51
+ .strict()
52
+ })
53
+ .strict()
54
+ ])
55
+
56
+ export const ProviderProtocolFrameSchema = z.discriminatedUnion('type', [
57
+ z.object({ type: z.literal('handshake'), payload: ProviderHandshakeSchema }).strict(),
58
+ z.object({ type: z.literal('request'), payload: ProviderRequestSchema }).strict(),
59
+ z.object({ type: z.literal('response'), payload: ProviderResponseSchema }).strict(),
60
+ z.object({ type: z.literal('cancel'), requestId: z.string().min(1) }).strict()
61
+ ])
62
+
63
+ export type ProviderRequest = {
64
+ requestId: string
65
+ operation: ComputerOperationName
66
+ input: unknown
67
+ deadlineAt: number
68
+ }
69
+
70
+ export type ProviderResponse =
71
+ | { requestId: string; dispatched: boolean; result: unknown; error?: never }
72
+ | { requestId: string; dispatched: boolean; result?: never; error: SerializedComputerError }
73
+
74
+ export type ProviderHandshake = z.infer<typeof ProviderHandshakeSchema>
75
+
76
+ export interface ComputerProvider {
77
+ readonly generation: string
78
+ start(): Promise<ProviderHandshake>
79
+ dispatch(request: ProviderRequest): Promise<ProviderResponse>
80
+ cancel(requestId: string): Promise<void>
81
+ close(): Promise<void>
82
+ }
package/src/schemas.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { z } from 'zod'
2
+
3
+ import { SerializedComputerErrorSchema } from './errors.js'
4
+
5
+ const FiniteNumberSchema = z.number().finite()
6
+ const NonNegativeIntegerSchema = z.number().int().nonnegative()
7
+ const PositiveIntegerSchema = z.number().int().positive()
8
+ const IdentifierSchema = z.string().min(1).max(512)
9
+ export const InteractionContextTokenSchema = z.string().regex(/^ctx_[A-Za-z0-9_-]{32,}$/)
10
+
11
+ export const ProcessIdentitySchema = z
12
+ .object({
13
+ pid: PositiveIntegerSchema,
14
+ startedAt: z.iso.datetime(),
15
+ executableId: IdentifierSchema
16
+ })
17
+ .strict()
18
+
19
+ export const WindowIdentitySchema = z
20
+ .object({
21
+ id: IdentifierSchema,
22
+ ownerPid: PositiveIntegerSchema
23
+ })
24
+ .strict()
25
+
26
+ export const ReferenceBindingsSchema = z
27
+ .object({
28
+ brokerGeneration: IdentifierSchema,
29
+ providerGeneration: IdentifierSchema,
30
+ graphicalSessionId: IdentifierSchema,
31
+ process: ProcessIdentitySchema,
32
+ appId: IdentifierSchema,
33
+ window: WindowIdentitySchema,
34
+ snapshotId: IdentifierSchema,
35
+ desktopEpoch: NonNegativeIntegerSchema
36
+ })
37
+ .strict()
38
+
39
+ export type ReferenceBindings = z.infer<typeof ReferenceBindingsSchema>
40
+
41
+ export const InteractionContextSchema = ReferenceBindingsSchema.extend({
42
+ token: InteractionContextTokenSchema,
43
+ issuedAt: z.iso.datetime(),
44
+ expiresAt: z.iso.datetime()
45
+ }).strict()
46
+
47
+ export type InteractionContext = z.infer<typeof InteractionContextSchema>
48
+
49
+ export const TargetReferenceSchema = ReferenceBindingsSchema.extend({
50
+ ref: IdentifierSchema,
51
+ kind: z.enum(['app', 'window', 'element']),
52
+ contextToken: InteractionContextTokenSchema,
53
+ expiresAt: z.iso.datetime()
54
+ }).strict()
55
+
56
+ export type TargetReference = z.infer<typeof TargetReferenceSchema>
57
+
58
+ export const AppInfoSchema = z
59
+ .object({
60
+ id: IdentifierSchema,
61
+ name: z.string().min(1),
62
+ bundleId: z.string().min(1).nullable(),
63
+ pid: PositiveIntegerSchema,
64
+ isRunning: z.boolean()
65
+ })
66
+ .strict()
67
+
68
+ export const WindowInfoSchema = z
69
+ .object({
70
+ id: IdentifierSchema,
71
+ appId: IdentifierSchema,
72
+ title: z.string(),
73
+ index: NonNegativeIntegerSchema,
74
+ bounds: z
75
+ .object({
76
+ x: FiniteNumberSchema,
77
+ y: FiniteNumberSchema,
78
+ width: z.number().finite().positive(),
79
+ height: z.number().finite().positive()
80
+ })
81
+ .strict(),
82
+ minimized: z.boolean()
83
+ })
84
+ .strict()
85
+
86
+ export const ScreenshotSchema = z
87
+ .object({
88
+ format: z.literal('png'),
89
+ width: PositiveIntegerSchema,
90
+ height: PositiveIntegerSchema,
91
+ scale: z.number().finite().positive(),
92
+ data: z.string().min(1).optional(),
93
+ path: z.string().min(1).optional(),
94
+ expiresAt: z.iso.datetime().optional()
95
+ })
96
+ .strict()
97
+ .refine((value) => value.data !== undefined || value.path !== undefined, {
98
+ message: 'Screenshot must include data or path'
99
+ })
100
+
101
+ export const SnapshotSchema = z
102
+ .object({
103
+ id: IdentifierSchema,
104
+ app: AppInfoSchema,
105
+ window: WindowInfoSchema,
106
+ treeText: z.string(),
107
+ elementCount: NonNegativeIntegerSchema,
108
+ focusedElementRef: z.string().nullable(),
109
+ desktopEpoch: NonNegativeIntegerSchema
110
+ })
111
+ .strict()
112
+
113
+ export const MutationOutcomeSchema = z.discriminatedUnion('state', [
114
+ z.object({ state: z.literal('verified'), evidence: z.unknown().optional() }).strict(),
115
+ z.object({ state: z.literal('indeterminate'), reason: z.string().optional() }).strict(),
116
+ z
117
+ .object({ state: z.literal('failed'), error: SerializedComputerErrorSchema.optional() })
118
+ .strict(),
119
+ z
120
+ .object({ state: z.literal('not_attempted'), error: SerializedComputerErrorSchema.optional() })
121
+ .strict()
122
+ ])
123
+
124
+ export type MutationOutcome = z.infer<typeof MutationOutcomeSchema>
125
+
126
+ export const SnapshotResultSchema = z
127
+ .object({
128
+ context: InteractionContextSchema,
129
+ snapshot: SnapshotSchema,
130
+ screenshot: ScreenshotSchema.nullable(),
131
+ issues: z.array(SerializedComputerErrorSchema).default([])
132
+ })
133
+ .strict()
134
+
135
+ export const MutationResultSchema = z
136
+ .object({
137
+ outcome: MutationOutcomeSchema,
138
+ freshState: SnapshotResultSchema.optional()
139
+ })
140
+ .strict()
141
+
142
+ export const ProviderCapabilitiesSchema = z
143
+ .object({
144
+ platform: z.enum(['darwin', 'win32', 'linux']),
145
+ provider: IdentifierSchema,
146
+ providerVersion: z.string().min(1),
147
+ operations: z.record(z.string(), z.boolean()),
148
+ permissions: z.record(z.string(), z.enum(['granted', 'denied', 'unknown', 'not_required']))
149
+ })
150
+ .strict()
151
+
152
+ export const ProviderHandshakeSchema = z
153
+ .object({
154
+ provider: IdentifierSchema,
155
+ generation: IdentifierSchema,
156
+ graphicalSessionId: IdentifierSchema,
157
+ providerProtocol: PositiveIntegerSchema,
158
+ publicContract: z.string().min(1),
159
+ capabilities: ProviderCapabilitiesSchema
160
+ })
161
+ .strict()