@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.
- package/README.md +2 -2
- package/dist/computer-control.d.ts +81 -0
- package/dist/computer-control.d.ts.map +1 -0
- package/dist/computer-control.js +19 -0
- package/dist/computer-control.js.map +1 -0
- package/dist/events.d.ts +14 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mode/collect-stream.d.ts +3 -0
- package/dist/mode/collect-stream.d.ts.map +1 -1
- package/dist/mode/collect-stream.js +78 -60
- package/dist/mode/collect-stream.js.map +1 -1
- package/dist/mode/project-messages.d.ts.map +1 -1
- package/dist/mode/project-messages.js +4 -1
- package/dist/mode/project-messages.js.map +1 -1
- package/dist/mode/provider-timing.d.ts +11 -0
- package/dist/mode/provider-timing.d.ts.map +1 -0
- package/dist/mode/provider-timing.js +15 -0
- package/dist/mode/provider-timing.js.map +1 -0
- package/dist/mode/react-loop.d.ts.map +1 -1
- package/dist/mode/react-loop.js +5 -1
- package/dist/mode/react-loop.js.map +1 -1
- package/dist/mode.d.ts +3 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/permission.d.ts +3 -0
- package/dist/permission.d.ts.map +1 -1
- package/dist/provider-utils.js +41 -7
- package/dist/provider-utils.js.map +1 -1
- package/dist/session-like.d.ts +6 -1
- package/dist/session-like.d.ts.map +1 -1
- package/dist/tool-dispatch.d.ts +2 -1
- package/dist/tool-dispatch.d.ts.map +1 -1
- package/dist/tool-dispatch.js +15 -1
- package/dist/tool-dispatch.js.map +1 -1
- package/dist/tool.d.ts +6 -0
- package/dist/tool.d.ts.map +1 -1
- package/dist/workflow-approval.d.ts +135 -0
- package/dist/workflow-approval.d.ts.map +1 -0
- package/dist/workflow-approval.js +18 -0
- package/dist/workflow-approval.js.map +1 -0
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/computer-control.test.ts +17 -0
- package/src/computer-control.ts +31 -0
- package/src/events.ts +15 -0
- package/src/index.ts +12 -0
- package/src/loop-helpers.test.ts +15 -0
- package/src/mode/collect-stream.ts +18 -0
- package/src/mode/project-messages.ts +4 -1
- package/src/mode/provider-timing.test.ts +16 -0
- package/src/mode/provider-timing.ts +19 -0
- package/src/mode/react-loop.ts +5 -1
- package/src/mode.ts +1 -1
- package/src/permission.ts +3 -0
- package/src/provider-schema-constraints.test.ts +29 -0
- package/src/provider-utils.test.ts +1 -1
- package/src/provider-utils.ts +33 -5
- package/src/session-like.ts +6 -1
- package/src/tool-dispatch.ts +27 -3
- package/src/tool.ts +7 -0
- package/src/workflow-approval.test.ts +12 -0
- package/src/workflow-approval.ts +41 -0
- 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', () => {
|
package/src/provider-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
180
|
-
|
|
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
|
-
|
|
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
|
package/src/session-like.ts
CHANGED
|
@@ -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. */
|
package/src/tool-dispatch.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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;
|