@openchamber/sdk 1.24.2 → 2.0.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/API.md +7 -3
- package/DOCUMENTATION.md +2 -0
- package/GUEST_SERVICES.md +84 -1
- package/README.md +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/manifest.d.ts +36 -0
- package/dist/manifest.js +20 -0
- package/dist/parse.d.ts +22 -0
- package/dist/parse.js +27 -3
- package/dist/schemas.d.ts +3 -0
- package/dist/schemas.js +2 -0
- package/dist/service-provider-schemas.d.ts +25 -0
- package/dist/service-provider-schemas.js +12 -0
- package/dist/service-providers.d.ts +218 -0
- package/dist/service-providers.js +73 -0
- package/dist/service-surface-schemas.d.ts +173 -0
- package/dist/service-surface-schemas.js +59 -0
- package/dist/service-surface.d.ts +116 -0
- package/dist/service-surface.js +154 -0
- package/dist/workspace.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared surface: a service shows the host a live picture of something an
|
|
3
|
+
* agent is working in (a browser, a simulator, a desktop app) and takes the
|
|
4
|
+
* user's pointer and keyboard back, so a person can watch, step in for a
|
|
5
|
+
* manual step, and hand control back. The host draws it in the extension's
|
|
6
|
+
* rail panel; the service only produces frames and consumes input.
|
|
7
|
+
*
|
|
8
|
+
* Declared with `contributes.service.surface: true`. The host keeps the
|
|
9
|
+
* service running while a viewer is attached and talks plain HTTP to it:
|
|
10
|
+
*
|
|
11
|
+
* - `GET /surface/frame?after=<seq>&wait=<ms>`: the next frame newer than
|
|
12
|
+
* `seq`, held up to `wait` ms. 200 with the image bytes (`Content-Type`
|
|
13
|
+
* `image/jpeg` or `image/png`) and the `x-surface-*` headers below; 204 when
|
|
14
|
+
* nothing newer arrived in time. Sequence numbers start at 1, so `after=0`
|
|
15
|
+
* is "the current picture, now". The host asks for one frame at a time per
|
|
16
|
+
* viewer, so a slow viewer never piles up frames: that is the backpressure.
|
|
17
|
+
* - `POST /surface/input`: `{ events: SurfaceInputEvent[] }`, coordinates in
|
|
18
|
+
* frame pixels. Sent only while the user holds control.
|
|
19
|
+
* - `POST /surface/control`: `{ controller }` whenever control changes, so the
|
|
20
|
+
* service can pause its own automation while the user is in.
|
|
21
|
+
* - `POST /surface/resize`: `{ width, height }` the panel can show; the
|
|
22
|
+
* service answers the size it settled on, or 400 to keep its own.
|
|
23
|
+
* - `GET /surface/clipboard`: `{ text }`, what the user copied inside the
|
|
24
|
+
* surface; the host puts it on their clipboard.
|
|
25
|
+
*/
|
|
26
|
+
export const SURFACE_FRAME_PATH = '/surface/frame';
|
|
27
|
+
export const SURFACE_INPUT_PATH = '/surface/input';
|
|
28
|
+
export const SURFACE_CONTROL_PATH = '/surface/control';
|
|
29
|
+
export const SURFACE_RESIZE_PATH = '/surface/resize';
|
|
30
|
+
export const SURFACE_CLIPBOARD_PATH = '/surface/clipboard';
|
|
31
|
+
export const SURFACE_SEQ_HEADER = 'x-surface-seq';
|
|
32
|
+
export const SURFACE_WIDTH_HEADER = 'x-surface-width';
|
|
33
|
+
export const SURFACE_HEIGHT_HEADER = 'x-surface-height';
|
|
34
|
+
export const SURFACE_TITLE_HEADER = 'x-surface-title';
|
|
35
|
+
/** `1` while the service's own automation (an agent) is driving the surface. */
|
|
36
|
+
export const SURFACE_AGENT_ACTIVE_HEADER = 'x-surface-agent-active';
|
|
37
|
+
export const SURFACE_FRAME_MIMES = ['image/jpeg', 'image/png'];
|
|
38
|
+
/** How long the host lets one frame request wait before asking again. */
|
|
39
|
+
export const SURFACE_FRAME_WAIT_MS = 25_000;
|
|
40
|
+
export const SURFACE_FRAME_MAX_BYTES = 8_000_000;
|
|
41
|
+
export const SURFACE_INPUT_BATCH_MAX = 256;
|
|
42
|
+
export const SURFACE_TEXT_MAX = 64_000;
|
|
43
|
+
export const SURFACE_TITLE_MAX = 200;
|
|
44
|
+
export const SURFACE_DIMENSION_MAX = 16_384;
|
|
45
|
+
/** After an agent action the agent counts as controlling for this long. */
|
|
46
|
+
export const SURFACE_AGENT_HOLD_MS = 30_000;
|
|
47
|
+
export const SURFACE_CONTROLLERS = ['none', 'agent', 'user'];
|
|
48
|
+
const isFiniteNumber = (value) => (Number(value) === value && Number.isFinite(value));
|
|
49
|
+
const isBool = (value) => value === true || value === false;
|
|
50
|
+
const isText = (value) => String(value) === value;
|
|
51
|
+
const readModifiers = (value) => {
|
|
52
|
+
if (Object(value) !== value || value === null)
|
|
53
|
+
return null;
|
|
54
|
+
// SAFETY: a non-null object; each field is checked below before use.
|
|
55
|
+
const wire = value;
|
|
56
|
+
if (!isBool(wire.alt) || !isBool(wire.ctrl) || !isBool(wire.meta) || !isBool(wire.shift))
|
|
57
|
+
return null;
|
|
58
|
+
return { alt: wire.alt, ctrl: wire.ctrl, meta: wire.meta, shift: wire.shift };
|
|
59
|
+
};
|
|
60
|
+
const readEvent = (value) => {
|
|
61
|
+
if (value.type === 'text') {
|
|
62
|
+
if (!isText(value.text) || value.text.length > SURFACE_TEXT_MAX)
|
|
63
|
+
return null;
|
|
64
|
+
return { type: 'text', text: value.text };
|
|
65
|
+
}
|
|
66
|
+
const modifiers = readModifiers(value.modifiers);
|
|
67
|
+
if (!modifiers)
|
|
68
|
+
return null;
|
|
69
|
+
if (value.type === 'pointer') {
|
|
70
|
+
if (value.action !== 'down' && value.action !== 'up' && value.action !== 'move')
|
|
71
|
+
return null;
|
|
72
|
+
if (!isFiniteNumber(value.x) || !isFiniteNumber(value.y) || !isFiniteNumber(value.button) || !isFiniteNumber(value.buttons))
|
|
73
|
+
return null;
|
|
74
|
+
return { type: 'pointer', action: value.action, x: value.x, y: value.y, button: value.button, buttons: value.buttons, modifiers };
|
|
75
|
+
}
|
|
76
|
+
if (value.type === 'wheel') {
|
|
77
|
+
if (!isFiniteNumber(value.x) || !isFiniteNumber(value.y) || !isFiniteNumber(value.deltaX) || !isFiniteNumber(value.deltaY))
|
|
78
|
+
return null;
|
|
79
|
+
return { type: 'wheel', x: value.x, y: value.y, deltaX: value.deltaX, deltaY: value.deltaY, modifiers };
|
|
80
|
+
}
|
|
81
|
+
if (value.type === 'key') {
|
|
82
|
+
if (value.action !== 'down' && value.action !== 'up')
|
|
83
|
+
return null;
|
|
84
|
+
if (!isText(value.key) || !isText(value.code) || value.key.length > 64 || value.code.length > 64)
|
|
85
|
+
return null;
|
|
86
|
+
return { type: 'key', action: value.action, key: value.key, code: value.code, modifiers };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* A service's read of the `POST /surface/input` body. Every event is checked
|
|
92
|
+
* field by field; one bad event refuses the whole batch (`null`), which the
|
|
93
|
+
* service answers with HTTP 400. No schema library needed.
|
|
94
|
+
*/
|
|
95
|
+
export const readSurfaceInputBatch = (body) => {
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(body);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (Object(parsed) !== parsed || parsed === null || !Array.isArray(parsed.events))
|
|
104
|
+
return null;
|
|
105
|
+
if (parsed.events.length > SURFACE_INPUT_BATCH_MAX)
|
|
106
|
+
return null;
|
|
107
|
+
const events = [];
|
|
108
|
+
for (const item of parsed.events) {
|
|
109
|
+
if (Object(item) !== item || item === null)
|
|
110
|
+
return null;
|
|
111
|
+
// SAFETY: a non-null object; readEvent checks every field it reads.
|
|
112
|
+
const event = readEvent(item);
|
|
113
|
+
if (!event)
|
|
114
|
+
return null;
|
|
115
|
+
events.push(event);
|
|
116
|
+
}
|
|
117
|
+
return { events };
|
|
118
|
+
};
|
|
119
|
+
const CONTROLLERS = new Set(SURFACE_CONTROLLERS);
|
|
120
|
+
/** A service's read of the `POST /surface/control` body. */
|
|
121
|
+
export const readSurfaceControlNotice = (body) => {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(body);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (Object(parsed) !== parsed || parsed === null)
|
|
130
|
+
return null;
|
|
131
|
+
const { controller } = parsed;
|
|
132
|
+
if (!isText(controller) || !CONTROLLERS.has(controller))
|
|
133
|
+
return null;
|
|
134
|
+
// SAFETY: membership in SURFACE_CONTROLLERS was just checked.
|
|
135
|
+
return { controller: controller };
|
|
136
|
+
};
|
|
137
|
+
/** A service's read of the `POST /surface/resize` body. */
|
|
138
|
+
export const readSurfaceResizeRequest = (body) => {
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(body);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (Object(parsed) !== parsed || parsed === null)
|
|
147
|
+
return null;
|
|
148
|
+
const { width, height } = parsed;
|
|
149
|
+
if (!isFiniteNumber(width) || !isFiniteNumber(height))
|
|
150
|
+
return null;
|
|
151
|
+
if (width < 1 || height < 1 || width > SURFACE_DIMENSION_MAX || height > SURFACE_DIMENSION_MAX)
|
|
152
|
+
return null;
|
|
153
|
+
return { width: Math.round(width), height: Math.round(height) };
|
|
154
|
+
};
|
package/dist/workspace.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type GuestWorktree = {
|
|
|
11
11
|
branch: string;
|
|
12
12
|
status: 'ready' | 'pending' | 'invalid' | 'missing';
|
|
13
13
|
};
|
|
14
|
+
/** `waiting-question` means the agent is waiting on an answer — a form it put to the user. */
|
|
14
15
|
export type GuestSessionActivity = 'unknown' | 'idle' | 'running' | 'retrying' | 'waiting-permission' | 'waiting-question';
|
|
15
16
|
export type GuestSessionRecord = {
|
|
16
17
|
id: string;
|