@moxxy/sdk 0.39.0 → 0.41.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.
Files changed (67) hide show
  1. package/README.md +2 -2
  2. package/dist/computer-control.d.ts +81 -0
  3. package/dist/computer-control.d.ts.map +1 -0
  4. package/dist/computer-control.js +19 -0
  5. package/dist/computer-control.js.map +1 -0
  6. package/dist/events.d.ts +14 -0
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/index.d.ts +6 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/mode/collect-stream.d.ts +3 -0
  13. package/dist/mode/collect-stream.d.ts.map +1 -1
  14. package/dist/mode/collect-stream.js +78 -60
  15. package/dist/mode/collect-stream.js.map +1 -1
  16. package/dist/mode/project-messages.d.ts.map +1 -1
  17. package/dist/mode/project-messages.js +4 -1
  18. package/dist/mode/project-messages.js.map +1 -1
  19. package/dist/mode/provider-timing.d.ts +11 -0
  20. package/dist/mode/provider-timing.d.ts.map +1 -0
  21. package/dist/mode/provider-timing.js +15 -0
  22. package/dist/mode/provider-timing.js.map +1 -0
  23. package/dist/mode/react-loop.d.ts.map +1 -1
  24. package/dist/mode/react-loop.js +5 -1
  25. package/dist/mode/react-loop.js.map +1 -1
  26. package/dist/mode.d.ts +3 -1
  27. package/dist/mode.d.ts.map +1 -1
  28. package/dist/permission.d.ts +3 -0
  29. package/dist/permission.d.ts.map +1 -1
  30. package/dist/provider-utils.js +41 -7
  31. package/dist/provider-utils.js.map +1 -1
  32. package/dist/session-like.d.ts +6 -1
  33. package/dist/session-like.d.ts.map +1 -1
  34. package/dist/tool-dispatch.d.ts +2 -1
  35. package/dist/tool-dispatch.d.ts.map +1 -1
  36. package/dist/tool-dispatch.js +15 -1
  37. package/dist/tool-dispatch.js.map +1 -1
  38. package/dist/tool.d.ts +6 -0
  39. package/dist/tool.d.ts.map +1 -1
  40. package/dist/workflow-approval.d.ts +135 -0
  41. package/dist/workflow-approval.d.ts.map +1 -0
  42. package/dist/workflow-approval.js +18 -0
  43. package/dist/workflow-approval.js.map +1 -0
  44. package/dist/workflow.d.ts +3 -3
  45. package/dist/workflow.d.ts.map +1 -1
  46. package/package.json +6 -2
  47. package/src/computer-control.test.ts +17 -0
  48. package/src/computer-control.ts +31 -0
  49. package/src/events.ts +15 -0
  50. package/src/index.ts +12 -0
  51. package/src/loop-helpers.test.ts +15 -0
  52. package/src/mode/collect-stream.ts +18 -0
  53. package/src/mode/project-messages.ts +4 -1
  54. package/src/mode/provider-timing.test.ts +16 -0
  55. package/src/mode/provider-timing.ts +19 -0
  56. package/src/mode/react-loop.ts +5 -1
  57. package/src/mode.ts +1 -1
  58. package/src/permission.ts +3 -0
  59. package/src/provider-schema-constraints.test.ts +29 -0
  60. package/src/provider-utils.test.ts +1 -1
  61. package/src/provider-utils.ts +33 -5
  62. package/src/session-like.ts +6 -1
  63. package/src/tool-dispatch.ts +27 -3
  64. package/src/tool.ts +7 -0
  65. package/src/workflow-approval.test.ts +12 -0
  66. package/src/workflow-approval.ts +41 -0
  67. package/src/workflow.ts +5 -3
@@ -59,7 +59,7 @@ describe('zodToJsonSchema', () => {
59
59
  it('unwraps ZodOptional / ZodDefault / ZodNullable', () => {
60
60
  expect(zodToJsonSchema(z.string().optional())).toEqual({ type: 'string' });
61
61
  expect(zodToJsonSchema(z.number().default(0))).toEqual({ type: 'number' });
62
- expect(zodToJsonSchema(z.string().nullable())).toEqual({ type: 'string' });
62
+ expect(zodToJsonSchema(z.string().nullable())).toEqual({ anyOf: [{ type: 'string' }, { type: 'null' }] });
63
63
  });
64
64
 
65
65
  it('converts ZodEnum to string + enum list', () => {
@@ -122,11 +122,14 @@ function convertSchema(schema: unknown, depth: number): unknown {
122
122
  const inner = (def as unknown as { schema: unknown }).schema;
123
123
  return zodToJsonSchema(inner, depth + 1);
124
124
  }
125
- // ZodOptional / ZodNullable / ZodDefault / ZodBranded / ZodReadonly all
125
+ if (typeName === 'ZodNullable') {
126
+ const inner = (def as unknown as { innerType: unknown }).innerType;
127
+ return { anyOf: [zodToJsonSchema(inner, depth + 1), { type: 'null' }] };
128
+ }
129
+ // ZodOptional / ZodDefault / ZodBranded / ZodReadonly all
126
130
  // wrap an inner schema we want to unwrap for JSON-schema purposes.
127
131
  if (
128
132
  typeName === 'ZodOptional' ||
129
- typeName === 'ZodNullable' ||
130
133
  typeName === 'ZodDefault' ||
131
134
  typeName === 'ZodBranded' ||
132
135
  typeName === 'ZodReadonly'
@@ -176,8 +179,30 @@ function convertSchema(schema: unknown, depth: number): unknown {
176
179
  }
177
180
  return { type: 'object', properties, required };
178
181
  }
179
- if (typeName === 'ZodString') return { type: 'string' };
180
- if (typeName === 'ZodNumber') return { type: 'number' };
182
+ if (typeName === 'ZodString' || typeName === 'ZodNumber') {
183
+ const checks = (def as unknown as { checks: ReadonlyArray<{ kind: string; value?: number; inclusive?: boolean; regex?: RegExp }> }).checks;
184
+ const result: Record<string, unknown> = { type: typeName === 'ZodString' ? 'string' : 'number' };
185
+ const bound = (key: string, value: number, lower: boolean) => {
186
+ const previous = result[key];
187
+ result[key] = typeof previous === 'number' ? (lower ? Math.max(previous, value) : Math.min(previous, value)) : value;
188
+ };
189
+ for (const check of checks) {
190
+ if (check.kind === 'int') result.type = 'integer';
191
+ if (typeName === 'ZodString' && check.regex && !check.regex.flags) {
192
+ // JSON Schema has no JS RegExp flags. Never misrepresent a flagged regex.
193
+ if (result.pattern === undefined) result.pattern = check.regex.source;
194
+ }
195
+ if (check.value === undefined) continue;
196
+ if (typeName === 'ZodString') {
197
+ if (check.kind === 'min' || check.kind === 'length') bound('minLength', check.value, true);
198
+ if (check.kind === 'max' || check.kind === 'length') bound('maxLength', check.value, false);
199
+ } else {
200
+ if (check.kind === 'min') bound(check.inclusive ? 'minimum' : 'exclusiveMinimum', check.value, true);
201
+ if (check.kind === 'max') bound(check.inclusive ? 'maximum' : 'exclusiveMaximum', check.value, false);
202
+ }
203
+ }
204
+ return result;
205
+ }
181
206
  if (typeName === 'ZodBoolean') return { type: 'boolean' };
182
207
  if (typeName === 'ZodEnum') {
183
208
  const values = (def as unknown as { values: ReadonlyArray<string> }).values;
@@ -202,7 +227,10 @@ function convertSchema(schema: unknown, depth: number): unknown {
202
227
  if (typeName === 'ZodArray') {
203
228
  // ZodArray._def.type is the element schema.
204
229
  const items = zodToJsonSchema((def as unknown as { type: unknown }).type, depth + 1);
205
- return { type: 'array', items };
230
+ const limits = def as unknown as { minLength: { value: number } | null; maxLength: { value: number } | null; exactLength: { value: number } | null };
231
+ const minimum = limits.exactLength ?? limits.minLength;
232
+ const maximum = limits.exactLength ?? limits.maxLength;
233
+ return { type: 'array', items, ...(minimum ? { minItems: minimum.value } : {}), ...(maximum ? { maxItems: maximum.value } : {}) };
206
234
  }
207
235
  if (typeName === 'ZodRecord') {
208
236
  // `z.record(keyType, valueType)` — an open-ended object whose values share
@@ -9,6 +9,7 @@ import type { PluginSetupSpec } from './schemas.js';
9
9
  import type { ToolIcon } from './tool-icon.js';
10
10
  import type { ToolCompactPresentation } from './tool.js';
11
11
  import type { ClientChromeItem } from './client-chrome.js';
12
+ import type { ComputerControlService } from './computer-control.js';
12
13
 
13
14
  /**
14
15
  * Options accepted by `SessionLike.runTurn`. Defined here (rather than in
@@ -237,7 +238,7 @@ export interface WorkflowRunView {
237
238
  * with `runId`). Optional for back-compat — absent from older hosts that
238
239
  * never paused. A run/resume that completes or fails reports those.
239
240
  */
240
- readonly status?: 'completed' | 'paused' | 'failed';
241
+ readonly status?: 'completed' | 'paused' | 'failed' | 'cancelled';
241
242
  /** Set when `status` is `paused` — pass to {@link WorkflowsView.resume}. */
242
243
  readonly runId?: string;
243
244
  }
@@ -267,6 +268,8 @@ export interface WorkflowSaveView {
267
268
  * stay capability-detectable — a channel must feature-check before calling.
268
269
  */
269
270
  export interface WorkflowsView {
271
+ delete?(name: string): Promise<void>;
272
+ readonly approvals?: import('./workflow-approval.js').WorkflowApprovalsView;
270
273
  list(): Promise<ReadonlyArray<WorkflowSummaryView>>;
271
274
  setEnabled(name: string, enabled: boolean): Promise<void>;
272
275
  run(name: string): Promise<WorkflowRunView>;
@@ -522,6 +525,8 @@ export interface SessionLike {
522
525
  mcpAdmin?: McpAdminView;
523
526
  /** Provider admin slice — edit stored (runtime-registered) providers. */
524
527
  providerAdmin?: ProviderAdminView;
528
+ /** Optional human control of the session's native Computer Use turns. */
529
+ computerControl?: ComputerControlService;
525
530
  /** Workflows slice backing the `/workflows` modal. */
526
531
  workflows?: WorkflowsView;
527
532
  /** Plugin-management slice backing the `/plugins` picker. */
@@ -8,6 +8,10 @@ import type { ToolCallVerdict } from './hooks.js';
8
8
  import type { CollectedToolUse } from './mode/collect-stream.js';
9
9
  import type { StuckLoopDetector, StuckSignal } from './mode/stuck-loop.js';
10
10
 
11
+ export type ToolDispatchContext = Pick<ModeContext,
12
+ 'sessionId' | 'turnId' | 'cwd' | 'env' | 'services' | 'log' | 'hooks' |
13
+ 'permissions' | 'tools' | 'signal' | 'subagents' | 'emit'>;
14
+
11
15
  /**
12
16
  * Execute a single tool-use end-to-end: dispatch `dispatchToolCall` hooks, run
13
17
  * the permission check, invoke the tool, and emit the approved/denied/result
@@ -23,7 +27,7 @@ import type { StuckLoopDetector, StuckSignal } from './mode/stuck-loop.js';
23
27
  * then rejects on the next turn.
24
28
  */
25
29
  export async function* dispatchToolCall(
26
- ctx: ModeContext,
30
+ ctx: ToolDispatchContext,
27
31
  t: CollectedToolUse,
28
32
  iteration: number,
29
33
  ): AsyncGenerator<MoxxyEvent, void, unknown> {
@@ -56,12 +60,32 @@ export async function* dispatchToolCall(
56
60
 
57
61
  const decision = await ctx.permissions.check(
58
62
  { callId: asToolCallId(t.id), name: t.name, input: actualInput },
59
- { sessionId: String(ctx.sessionId), toolDescription: ctx.tools.get(t.name)?.description },
63
+ { sessionId: String(ctx.sessionId), turnId: String(ctx.turnId), toolDescription: ctx.tools.get(t.name)?.description },
60
64
  );
61
65
  if (decision.mode === 'deny') {
62
66
  yield* emitDenied(ctx, t, decision.reason ?? 'denied by resolver', 'resolver');
63
67
  return;
64
68
  }
69
+ // A deferred approval may outlive its turn, or policy may change while
70
+ // the operator is deciding. Neither permits a late side effect.
71
+ ctx.signal.throwIfAborted();
72
+ const mandatory = await ctx.permissions.mandatoryCheck?.(
73
+ { callId: asToolCallId(t.id), name: t.name, input: actualInput },
74
+ { sessionId: String(ctx.sessionId), turnId: String(ctx.turnId) },
75
+ );
76
+ if (mandatory?.mode === 'deny') {
77
+ yield* emitDenied(ctx, t, mandatory.reason ?? 'workflow approval denied', 'resolver');
78
+ return;
79
+ }
80
+ const policy = await ctx.permissions.policyCheck?.(
81
+ { callId: asToolCallId(t.id), name: t.name, input: actualInput },
82
+ { sessionId: String(ctx.sessionId), turnId: String(ctx.turnId) },
83
+ );
84
+ if (policy?.mode === 'deny') {
85
+ yield* emitDenied(ctx, t, policy.reason ?? 'denied by policy', 'policy');
86
+ return;
87
+ }
88
+ ctx.signal.throwIfAborted();
65
89
  yield await ctx.emit({
66
90
  type: 'tool_call_approved',
67
91
  sessionId: ctx.sessionId,
@@ -129,7 +153,7 @@ function hookDeny(verdict: ToolCallVerdict): string | null {
129
153
  }
130
154
 
131
155
  async function* emitDenied(
132
- ctx: ModeContext,
156
+ ctx: ToolDispatchContext,
133
157
  t: CollectedToolUse,
134
158
  reason: string,
135
159
  by: 'hook' | 'resolver' | 'policy',
package/src/tool.ts CHANGED
@@ -16,6 +16,13 @@ import type { ToolIsolationSpec } from './isolation.js';
16
16
  */
17
17
  export type HostedTool = { readonly type: 'web_search' };
18
18
 
19
+ /** Image pixels and optional model-facing context (for example capture geometry). */
20
+ export interface ToolImageResult {
21
+ readonly mediaType: string;
22
+ readonly base64: string;
23
+ readonly forModel?: string;
24
+ }
25
+
19
26
  /**
20
27
  * Capability-mediated filesystem operations injected by isolators that
21
28
  * support brokering. Handlers can opt in by checking `ctx.fs` at runtime
@@ -0,0 +1,12 @@
1
+ import { expect, it } from 'vitest';
2
+ import { workflowApprovalItemsSchema } from './workflow-approval.js';
3
+
4
+ it('validates approval results at remote boundaries without changing the displayed scope', () => {
5
+ const item = { id: '07a3ed71-ebc0-458f-acb0-9db5a5636632', workflowId: 'workflow', workflowName: 'Test',
6
+ revision: 'revision', runId: 'run', tool: 'Write', input: { path: 'test.txt' }, createdAt: 100, status: 'pending' };
7
+ expect(workflowApprovalItemsSchema.parse([item])).toEqual([item]);
8
+ expect(() => workflowApprovalItemsSchema.parse([{ ...item, status: 'approved-by-default' }])).toThrow();
9
+ expect(() => workflowApprovalItemsSchema.parse([{ ...item, id: '../another' }])).toThrow();
10
+ expect(() => workflowApprovalItemsSchema.parse([{ ...item, createdAt: 0 }])).toThrow();
11
+ expect(() => workflowApprovalItemsSchema.parse([{ ...item, admin: true }])).toThrow();
12
+ });
@@ -0,0 +1,41 @@
1
+ export interface WorkflowApprovalScope {
2
+ readonly workflowId: string;
3
+ readonly workflowName: string;
4
+ readonly revision: string;
5
+ readonly runId: string;
6
+ }
7
+
8
+ export type WorkflowApprovalChoice = 'allow_once' | 'allow_always' | 'deny';
9
+ export interface WorkflowApprovalItem extends WorkflowApprovalScope {
10
+ readonly id: string;
11
+ readonly tool: string;
12
+ readonly input: unknown;
13
+ readonly createdAt: number;
14
+ readonly status: 'pending' | 'allowed' | 'denied' | 'cancelled' | 'interrupted' | 'revoked';
15
+ readonly choice?: WorkflowApprovalChoice;
16
+ }
17
+
18
+ export interface WorkflowApprovalsView {
19
+ list(): Promise<ReadonlyArray<WorkflowApprovalItem>>;
20
+ decide(id: string, choice: WorkflowApprovalChoice): Promise<void>;
21
+ revoke(id: string): Promise<void>;
22
+ cancel(id: string): Promise<void>;
23
+ }
24
+ import { z } from 'zod';
25
+
26
+ export const workflowApprovalItemSchema = z
27
+ .object({
28
+ id: z.string().uuid(),
29
+ workflowId: z.string().min(1),
30
+ workflowName: z.string().min(1),
31
+ revision: z.string().min(1),
32
+ runId: z.string().min(1),
33
+ tool: z.string().min(1),
34
+ input: z.unknown(),
35
+ createdAt: z.number().finite().positive(),
36
+ status: z.enum(['pending', 'allowed', 'denied', 'cancelled', 'interrupted', 'revoked']),
37
+ choice: z.enum(['allow_once', 'allow_always', 'deny']).optional(),
38
+ })
39
+ .strict()
40
+ .transform((item) => ({ ...item, input: item.input }));
41
+ export const workflowApprovalItemsSchema = z.array(workflowApprovalItemSchema);
package/src/workflow.ts CHANGED
@@ -198,11 +198,13 @@ export type WorkflowEventSubtype =
198
198
  | 'workflow_step_completed'
199
199
  | 'workflow_step_skipped'
200
200
  | 'workflow_step_failed'
201
+ | 'workflow_step_cancelled'
201
202
  | 'workflow_step_awaiting_input'
202
203
  | 'workflow_paused'
203
204
  | 'workflow_resumed'
204
205
  | 'workflow_completed'
205
- | 'workflow_failed';
206
+ | 'workflow_failed'
207
+ | 'workflow_cancelled';
206
208
 
207
209
  /**
208
210
  * Everything an executor needs to run a workflow, supplied by the caller.
@@ -231,9 +233,9 @@ export interface WorkflowRunDeps {
231
233
  readonly depth?: number;
232
234
  }
233
235
 
234
- export type WorkflowStepStatus = 'completed' | 'skipped' | 'failed' | 'awaiting_input';
236
+ export type WorkflowStepStatus = 'completed' | 'skipped' | 'failed' | 'awaiting_input' | 'cancelled';
235
237
 
236
- export type WorkflowRunStatus = 'completed' | 'paused' | 'failed';
238
+ export type WorkflowRunStatus = 'completed' | 'paused' | 'failed' | 'cancelled';
237
239
 
238
240
  export interface WorkflowStepResult {
239
241
  readonly id: string;