@crosshands/contract 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts CHANGED
@@ -26,41 +26,67 @@ export const ERROR_CATALOG = {
26
26
  accessibility_error: { retry: true, remediation: 'check_accessibility_permission' }
27
27
  } as const
28
28
 
29
+ export const PUBLIC_ERROR_CATALOG = {
30
+ goal_mismatch: { retry: true, remediation: 'refresh_state' },
31
+ policy_unavailable: { retry: true, remediation: 'refresh_state' },
32
+ intent_unavailable: { retry: false, remediation: 'correct_request' }
33
+ } as const
34
+
35
+ const COMBINED_ERROR_CATALOG = {
36
+ ...ERROR_CATALOG,
37
+ ...PUBLIC_ERROR_CATALOG
38
+ } as const
39
+
29
40
  export type ComputerErrorCode = keyof typeof ERROR_CATALOG
30
- export type ComputerErrorRemediation = (typeof ERROR_CATALOG)[ComputerErrorCode]['remediation']
41
+ export type PublicErrorCode = keyof typeof PUBLIC_ERROR_CATALOG
42
+ export type PublicComputerErrorCode = ComputerErrorCode | PublicErrorCode
43
+ export type ComputerErrorRemediation =
44
+ (typeof COMBINED_ERROR_CATALOG)[PublicComputerErrorCode]['remediation']
31
45
 
32
- export const ComputerErrorCodeSchema = z.enum(
33
- Object.keys(ERROR_CATALOG) as [ComputerErrorCode, ...ComputerErrorCode[]]
34
- )
46
+ function errorCodeSchema<T extends string>(catalog: Record<T, unknown>): z.ZodEnum<Record<T, T>> {
47
+ const codes = Object.keys(catalog) as [T, ...T[]]
48
+ return z.enum(codes)
49
+ }
35
50
 
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()
51
+ export const ComputerErrorCodeSchema = errorCodeSchema(ERROR_CATALOG)
52
+ export const PublicComputerErrorCodeSchema = errorCodeSchema(COMBINED_ERROR_CATALOG)
53
+
54
+ function serializedErrorSchema<T extends z.ZodEnum<Record<string, string>>>(code: T) {
55
+ return z
56
+ .object({
57
+ code,
58
+ message: z.string().min(1),
59
+ retry: z.boolean(),
60
+ remediation: z.string().min(1),
61
+ details: z.unknown().optional()
62
+ })
63
+ .strict()
64
+ }
65
+
66
+ export const SerializedComputerErrorSchema = serializedErrorSchema(ComputerErrorCodeSchema)
67
+ export const PublicSerializedComputerErrorSchema = serializedErrorSchema(
68
+ PublicComputerErrorCodeSchema
69
+ )
45
70
 
46
71
  export type SerializedComputerError = z.infer<typeof SerializedComputerErrorSchema>
72
+ export type PublicSerializedComputerError = z.infer<typeof PublicSerializedComputerErrorSchema>
47
73
 
48
74
  export class ComputerError extends Error {
49
- readonly code: ComputerErrorCode
75
+ readonly code: PublicComputerErrorCode
50
76
  readonly retry: boolean
51
77
  readonly remediation: ComputerErrorRemediation
52
78
  readonly details: unknown
53
79
 
54
- constructor(code: ComputerErrorCode, message: string, details?: unknown) {
80
+ constructor(code: PublicComputerErrorCode, message: string, details?: unknown) {
55
81
  super(message)
56
82
  this.name = 'ComputerError'
57
83
  this.code = code
58
- this.retry = ERROR_CATALOG[code].retry
59
- this.remediation = ERROR_CATALOG[code].remediation
84
+ this.retry = COMBINED_ERROR_CATALOG[code].retry
85
+ this.remediation = COMBINED_ERROR_CATALOG[code].remediation
60
86
  this.details = details
61
87
  }
62
88
 
63
- toJSON(): SerializedComputerError {
89
+ toJSON(): PublicSerializedComputerError {
64
90
  return this.details === undefined
65
91
  ? { code: this.code, message: this.message, retry: this.retry, remediation: this.remediation }
66
92
  : {
@@ -71,10 +97,14 @@ export class ComputerError extends Error {
71
97
  details: this.details
72
98
  }
73
99
  }
100
+
101
+ toBrokerJSON(): SerializedComputerError {
102
+ return SerializedComputerErrorSchema.parse(this.toJSON())
103
+ }
74
104
  }
75
105
 
76
106
  export function createComputerError(
77
- code: ComputerErrorCode,
107
+ code: PublicComputerErrorCode,
78
108
  message: string,
79
109
  details?: unknown
80
110
  ): ComputerError {
@@ -1,11 +1,9 @@
1
- import { COMPUTER_OPERATIONS } from './operations.js'
1
+ import { COMPUTER_OPERATIONS, PUBLIC_OPERATIONS } from './operations.js'
2
2
  import { CONTRACT_VERSIONS } from './versions.js'
3
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]) => [
4
+ function schemaCatalog(operations: typeof COMPUTER_OPERATIONS | typeof PUBLIC_OPERATIONS) {
5
+ return Object.fromEntries(
6
+ Object.entries(operations).map(([name, operation]) => [
9
7
  name,
10
8
  {
11
9
  mutation: operation.mutation,
@@ -14,4 +12,11 @@ export const contractJsonSchemas = {
14
12
  }
15
13
  ])
16
14
  )
15
+ }
16
+
17
+ export const contractJsonSchemas = {
18
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
19
+ versions: CONTRACT_VERSIONS,
20
+ operations: schemaCatalog(COMPUTER_OPERATIONS),
21
+ publicOperations: schemaCatalog(PUBLIC_OPERATIONS)
17
22
  } as const
package/src/operations.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { createComputerError } from './errors.js'
3
4
  import {
4
5
  AppInfoSchema,
6
+ GoalSchema,
7
+ IntentTargetSchema,
5
8
  InteractionContextTokenSchema,
6
9
  MutationResultSchema,
7
10
  ProviderCapabilitiesSchema,
11
+ PublicMutationResultSchema,
12
+ PublicSnapshotResultSchema,
8
13
  ScreenshotSchema,
9
14
  SnapshotResultSchema,
10
15
  TargetReferenceSchema,
@@ -116,13 +121,21 @@ export const COMPUTER_OPERATIONS = {
116
121
  },
117
122
  getAppState: {
118
123
  mutation: false,
119
- input: z
120
- .object({
121
- app: AppQuerySchema,
122
- window: WindowSelectorSchema.optional(),
123
- ...CaptureOptionsShape
124
- })
125
- .strict(),
124
+ input: z.union([
125
+ z
126
+ .object({
127
+ app: AppQuerySchema,
128
+ window: WindowSelectorSchema.optional(),
129
+ ...CaptureOptionsShape
130
+ })
131
+ .strict(),
132
+ z
133
+ .object({
134
+ contextToken: InteractionContextTokenSchema,
135
+ ...CaptureOptionsShape
136
+ })
137
+ .strict()
138
+ ]),
126
139
  output: SnapshotResultSchema
127
140
  },
128
141
  click: {
@@ -212,13 +225,133 @@ export const COMPUTER_OPERATIONS = {
212
225
  } as const
213
226
 
214
227
  export type ComputerOperationName = keyof typeof COMPUTER_OPERATIONS
228
+ export type BrokerOperationInput<K extends ComputerOperationName> = z.infer<
229
+ (typeof COMPUTER_OPERATIONS)[K]['input']
230
+ >
231
+ export type PublicOperationInput<K extends ComputerOperationName> = z.infer<
232
+ (typeof PUBLIC_OPERATIONS)[K]['input']
233
+ >
234
+
235
+ const PublicActionTargetSchema = z.union([ActionTargetSchema, IntentTargetSchema])
236
+ const PublicElementTargetSchema = z.union([ElementActionTargetSchema, IntentTargetSchema])
237
+
238
+ function publicMutationInput(brokerInput: z.ZodObject<z.ZodRawShape>, target: z.ZodType) {
239
+ return brokerInput
240
+ .omit({ target: true })
241
+ .extend({
242
+ target,
243
+ goal: GoalSchema.optional()
244
+ })
245
+ .strict()
246
+ }
247
+
248
+ function withOptionalGoal(schema: z.ZodObject<z.ZodRawShape>) {
249
+ return schema.extend({ goal: GoalSchema.optional() }).strict()
250
+ }
251
+
252
+ const getAppStateArms = COMPUTER_OPERATIONS.getAppState.input.options
253
+
254
+ export const PUBLIC_OPERATIONS = {
255
+ ...COMPUTER_OPERATIONS,
256
+ getAppState: {
257
+ mutation: false,
258
+ input: z.union([withOptionalGoal(getAppStateArms[0]), withOptionalGoal(getAppStateArms[1])]),
259
+ output: PublicSnapshotResultSchema
260
+ },
261
+ click: {
262
+ mutation: true,
263
+ input: publicMutationInput(COMPUTER_OPERATIONS.click.input, PublicActionTargetSchema),
264
+ output: PublicMutationResultSchema
265
+ },
266
+ performSecondaryAction: {
267
+ mutation: true,
268
+ input: publicMutationInput(
269
+ COMPUTER_OPERATIONS.performSecondaryAction.input,
270
+ PublicElementTargetSchema
271
+ ),
272
+ output: PublicMutationResultSchema
273
+ },
274
+ scroll: {
275
+ mutation: true,
276
+ input: publicMutationInput(COMPUTER_OPERATIONS.scroll.input, PublicActionTargetSchema),
277
+ output: PublicMutationResultSchema
278
+ },
279
+ setValue: {
280
+ mutation: true,
281
+ input: publicMutationInput(COMPUTER_OPERATIONS.setValue.input, PublicElementTargetSchema),
282
+ output: PublicMutationResultSchema
283
+ }
284
+ } as const
215
285
 
216
- export function parseOperationInput(operation: ComputerOperationName, input: unknown): unknown {
217
- return COMPUTER_OPERATIONS[operation].input.parse(input)
286
+ export function parseOperationInput<K extends ComputerOperationName>(
287
+ operation: K,
288
+ input: unknown
289
+ ): BrokerOperationInput<K> {
290
+ return COMPUTER_OPERATIONS[operation].input.parse(input) as BrokerOperationInput<K>
218
291
  }
219
292
 
220
293
  export function parseOperationOutput(operation: ComputerOperationName, output: unknown): unknown {
221
294
  return COMPUTER_OPERATIONS[operation].output.parse(output)
222
295
  }
223
296
 
297
+ export function hasIntentTarget(input: unknown): boolean {
298
+ if (input === null || typeof input !== 'object' || Array.isArray(input) || !('target' in input)) {
299
+ return false
300
+ }
301
+ const target = input.target
302
+ return (
303
+ target !== null &&
304
+ typeof target === 'object' &&
305
+ !Array.isArray(target) &&
306
+ 'kind' in target &&
307
+ target.kind === 'intent'
308
+ )
309
+ }
310
+
311
+ function requireGoalForIntent(input: unknown): void {
312
+ if (!hasIntentTarget(input)) return
313
+ const goal = (input as { goal?: unknown }).goal
314
+ if (typeof goal !== 'string' || goal.length === 0) {
315
+ throw createComputerError('invalid_argument', 'intent targeting requires goal')
316
+ }
317
+ }
318
+
319
+ export function parsePublicInput<K extends ComputerOperationName>(
320
+ operation: K,
321
+ input: unknown
322
+ ): PublicOperationInput<K> {
323
+ const parsed = PUBLIC_OPERATIONS[operation].input.parse(input) as PublicOperationInput<K>
324
+ requireGoalForIntent(parsed)
325
+ return parsed
326
+ }
327
+
328
+ export function parsePublicOutput(operation: ComputerOperationName, output: unknown): unknown {
329
+ return PUBLIC_OPERATIONS[operation].output.parse(output)
330
+ }
331
+
332
+ export function splitPublicInput<T extends object>(
333
+ input: T
334
+ ): { goal: string | undefined; rest: Omit<T, 'goal'> } {
335
+ if (!('goal' in input)) return { goal: undefined, rest: input as Omit<T, 'goal'> }
336
+ const { goal, ...rest } = input as T & { goal?: unknown }
337
+ return {
338
+ goal: typeof goal === 'string' && goal.length > 0 ? goal : undefined,
339
+ rest: rest as Omit<T, 'goal'>
340
+ }
341
+ }
342
+
343
+ export function toBrokerInput<K extends ComputerOperationName>(
344
+ operation: K,
345
+ publicInput: object
346
+ ): BrokerOperationInput<K> {
347
+ const { rest } = splitPublicInput(publicInput)
348
+ if (hasIntentTarget(rest)) {
349
+ throw createComputerError(
350
+ 'invalid_argument',
351
+ 'intent targeting must be bound before broker parse'
352
+ )
353
+ }
354
+ return parseOperationInput(operation, rest)
355
+ }
356
+
224
357
  export const ScreenshotOutputSchema = ScreenshotSchema
package/src/schemas.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
 
3
- import { SerializedComputerErrorSchema } from './errors.js'
3
+ import { PublicSerializedComputerErrorSchema, SerializedComputerErrorSchema } from './errors.js'
4
4
 
5
5
  const FiniteNumberSchema = z.number().finite()
6
6
  const NonNegativeIntegerSchema = z.number().int().nonnegative()
@@ -110,16 +110,19 @@ export const SnapshotSchema = z
110
110
  })
111
111
  .strict()
112
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
- ])
113
+ function mutationOutcomeSchema<T extends z.ZodType>(errorSchema: T) {
114
+ return z.discriminatedUnion('state', [
115
+ z.object({ state: z.literal('verified'), evidence: z.unknown().optional() }).strict(),
116
+ z.object({ state: z.literal('indeterminate'), reason: z.string().optional() }).strict(),
117
+ z.object({ state: z.literal('failed'), error: errorSchema.optional() }).strict(),
118
+ z.object({ state: z.literal('not_attempted'), error: errorSchema.optional() }).strict()
119
+ ])
120
+ }
121
+
122
+ export const MutationOutcomeSchema = mutationOutcomeSchema(SerializedComputerErrorSchema)
123
+ export const PublicMutationOutcomeSchema = mutationOutcomeSchema(
124
+ PublicSerializedComputerErrorSchema
125
+ )
123
126
 
124
127
  export type MutationOutcome = z.infer<typeof MutationOutcomeSchema>
125
128
 
@@ -132,6 +135,60 @@ export const SnapshotResultSchema = z
132
135
  })
133
136
  .strict()
134
137
 
138
+ export type SnapshotResult = z.infer<typeof SnapshotResultSchema>
139
+
140
+ export const GoalSchema = z.string().trim().min(1).max(512)
141
+
142
+ export const IntentTargetSchema = z.object({ kind: z.literal('intent') }).strict()
143
+
144
+ export const SuggestionMoveSchema = z.discriminatedUnion('kind', [
145
+ z.object({ kind: z.literal('click'), elementIndex: NonNegativeIntegerSchema }).strict(),
146
+ z.object({ kind: z.literal('setValue'), elementIndex: NonNegativeIntegerSchema }).strict(),
147
+ z
148
+ .object({
149
+ kind: z.literal('secondary'),
150
+ elementIndex: NonNegativeIntegerSchema,
151
+ action: z.string().min(1).max(256)
152
+ })
153
+ .strict(),
154
+ z
155
+ .object({
156
+ kind: z.literal('scroll'),
157
+ elementIndex: NonNegativeIntegerSchema,
158
+ direction: z.enum(['up', 'down', 'left', 'right'])
159
+ })
160
+ .strict(),
161
+ z.object({ kind: z.literal('wait') }).strict(),
162
+ z.object({ kind: z.literal('done') }).strict(),
163
+ z.object({ kind: z.literal('blocked'), reason: z.string().min(1).max(256) }).strict()
164
+ ])
165
+
166
+ export const SuggestionSchema = z
167
+ .object({
168
+ untrusted: z.literal(true),
169
+ snapshotId: IdentifierSchema,
170
+ move: SuggestionMoveSchema,
171
+ confidence: z.number().min(0).max(1).optional(),
172
+ label: z.string().optional()
173
+ })
174
+ .strict()
175
+
176
+ export type Suggestion = z.infer<typeof SuggestionSchema>
177
+
178
+ export const ResolvedTargetSchema = z
179
+ .object({
180
+ kind: z.literal('element'),
181
+ elementIndex: NonNegativeIntegerSchema
182
+ })
183
+ .strict()
184
+
185
+ export const PublicSnapshotResultSchema = SnapshotResultSchema.omit({ issues: true })
186
+ .extend({
187
+ issues: z.array(PublicSerializedComputerErrorSchema).default([]),
188
+ suggestion: SuggestionSchema.optional()
189
+ })
190
+ .strict()
191
+
135
192
  export const MutationResultSchema = z
136
193
  .object({
137
194
  outcome: MutationOutcomeSchema,
@@ -139,6 +196,17 @@ export const MutationResultSchema = z
139
196
  })
140
197
  .strict()
141
198
 
199
+ export type MutationResult = z.infer<typeof MutationResultSchema>
200
+
201
+ export const PublicMutationResultSchema = z
202
+ .object({
203
+ outcome: PublicMutationOutcomeSchema,
204
+ freshState: SnapshotResultSchema.optional(),
205
+ resolvedTarget: ResolvedTargetSchema.optional(),
206
+ suggestion: SuggestionSchema.optional()
207
+ })
208
+ .strict()
209
+
142
210
  const BooleanFlagMapSchema = z.record(z.string(), z.boolean())
143
211
 
144
212
  export const ProviderSupportsSchema = z
package/src/versions.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createComputerError } from './errors.js'
2
2
 
3
3
  export const CONTRACT_VERSIONS = {
4
- product: '0.1.6',
5
- publicContract: '1.1.0',
4
+ product: '0.2.0',
5
+ publicContract: '1.2.0',
6
6
  brokerControl: 1,
7
7
  providerProtocol: 1,
8
8
  mcpProtocol: '2025-11-25'