@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.
- package/LICENSE +21 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +82 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +164 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +65 -0
- package/dist/errors.js.map +1 -0
- package/dist/generate-schemas.d.ts +2 -0
- package/dist/generate-schemas.d.ts.map +1 -0
- package/dist/generate-schemas.js +8 -0
- package/dist/generate-schemas.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +306 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +15 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/operations.d.ts +2115 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +205 -0
- package/dist/operations.js.map +1 -0
- package/dist/provider.d.ts +135 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +56 -0
- package/dist/provider.js.map +1 -0
- package/dist/schemas.d.ts +499 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +138 -0
- package/dist/schemas.js.map +1 -0
- package/dist/versions.d.ts +31 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +28 -0
- package/dist/versions.js.map +1 -0
- package/package.json +39 -0
- package/schemas/contract.json +7572 -0
- package/src/context.ts +107 -0
- package/src/errors.ts +82 -0
- package/src/generate-schemas.ts +9 -0
- package/src/index.ts +7 -0
- package/src/json-schema.ts +17 -0
- package/src/operations.ts +224 -0
- package/src/provider.ts +82 -0
- package/src/schemas.ts +161 -0
- 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,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
|
package/src/provider.ts
ADDED
|
@@ -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()
|