@anuraghq/mobius 0.0.0-stage → 0.1.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 (40) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +371 -2
  3. package/bin/mobiusagent-driver-darwin-arm64 +0 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +151 -0
  6. package/dist/core/protocol.d.ts +294 -0
  7. package/dist/core/protocol.js +82 -0
  8. package/dist/execution/connector.d.ts +82 -0
  9. package/dist/execution/connector.js +11 -0
  10. package/dist/execution/drivers/mobiusagent.d.ts +57 -0
  11. package/dist/execution/drivers/mobiusagent.js +165 -0
  12. package/dist/execution/drivers/nodes.d.ts +47 -0
  13. package/dist/execution/drivers/nodes.js +104 -0
  14. package/dist/execution/observe.d.ts +48 -0
  15. package/dist/execution/observe.js +244 -0
  16. package/dist/execution/session.d.ts +101 -0
  17. package/dist/execution/session.js +304 -0
  18. package/dist/infrastructure/plane.d.ts +25 -0
  19. package/dist/infrastructure/plane.js +16 -0
  20. package/dist/infrastructure/registry.d.ts +19 -0
  21. package/dist/infrastructure/registry.js +49 -0
  22. package/dist/interface/engine.d.ts +24 -0
  23. package/dist/interface/engine.js +38 -0
  24. package/dist/interface/http.d.ts +4 -0
  25. package/dist/interface/http.js +85 -0
  26. package/dist/interface/mcp.d.ts +21 -0
  27. package/dist/interface/mcp.js +377 -0
  28. package/dist/interface/mobius-tools.d.ts +10 -0
  29. package/dist/interface/mobius-tools.js +288 -0
  30. package/dist/observability/audit.d.ts +52 -0
  31. package/dist/observability/audit.js +98 -0
  32. package/dist/observability/signing.d.ts +37 -0
  33. package/dist/observability/signing.js +87 -0
  34. package/dist/policy/policy.d.ts +147 -0
  35. package/dist/policy/policy.js +347 -0
  36. package/ios-agent/brand.py +62 -0
  37. package/ios-agent/brand.sh +19 -0
  38. package/ios-agent/setup.sh +26 -0
  39. package/ios-agent/wda-mobius-fast-touch.patch +73 -0
  40. package/package.json +20 -4
package/dist/cli.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { AuditLog } from "./observability/audit.js";
4
+ import { createServer } from "./interface/mcp.js";
5
+ import { createEngine } from "./interface/engine.js";
6
+ import { createHttpServer } from "./interface/http.js";
7
+ import { randomBytes } from 'node:crypto';
8
+ import { appendFileSync, existsSync, readFileSync } from 'node:fs';
9
+ import { issueSession, rotateInstallKey, verifySession } from "./observability/signing.js";
10
+ /** Session tokens are verified with this install's key, or an operator key (MOBIUS_SESSION_PUBKEY = path to a PEM). */
11
+ const operatorKey = process.env.MOBIUS_SESSION_PUBKEY ? readFileSync(process.env.MOBIUS_SESSION_PUBKEY, 'utf8') : undefined;
12
+ import { driverBuilt, driverPath, iosAgentReady, MobiusAgentConnector, MobiusAgentDriver } from "./execution/drivers/mobiusagent.js";
13
+ /** Every online device through the Mobius driver. iOS devices also need the Mobius Agent runner set up once. */
14
+ const connectors = (driver) => async () => {
15
+ if (!driverBuilt())
16
+ throw new Error(`Mobius driver not built: run \`cargo build --release\` in drivers/mobiusagent-driver (expected ${driverPath()})`);
17
+ const ios = iosAgentReady();
18
+ return (await driver.devices()).filter(d => d.state === 'online' && (d.platform === 'android' || ios)).map(d => new MobiusAgentConnector(driver, d));
19
+ };
20
+ import { defaultApprover, Policy } from "./policy/policy.js";
21
+ import { writeFileSync, rmSync } from 'node:fs';
22
+ import { killSwitchPath, revokedPath } from "./infrastructure/plane.js";
23
+ import { mobiusHome } from "./observability/audit.js";
24
+ import { Mobius } from "./infrastructure/registry.js";
25
+ const log = (level, msg, extra = {}) => process.stderr.write(JSON.stringify({ ts: new Date().toISOString(), level, msg, ...extra }) + '\n'); // stdout is the MCP channel
26
+ const cmd = process.argv[2] ?? 'mcp';
27
+ if (cmd === 'mcp') {
28
+ // ponytail: stdio server owns devices directly. The shared daemon + bridge from DESIGN.md arrives
29
+ // with the Android app connector, when a long-lived WebSocket owner is actually needed.
30
+ const driver = new MobiusAgentDriver();
31
+ const audit = new AuditLog();
32
+ const mobius = new Mobius(connectors(driver), audit, new Policy(), defaultApprover());
33
+ mobius.persistIds = true;
34
+ // A signed agent session handed to this MCP server by its operator (identity + policy version for every action).
35
+ const tok = process.env.MOBIUS_SESSION_TOKEN;
36
+ if (tok) {
37
+ const check = () => verifySession(tok, operatorKey ?? audit.signer.publicKeyPem);
38
+ const v = check();
39
+ // Re-verified on every action: a key rotation or expiry takes effect mid-session (revocation is checked in the gate).
40
+ mobius.plane.session = () => { const r = check(); return r.ok ? r.claims : { invalid: r.reason }; };
41
+ log(v.ok ? 'info' : 'error', v.ok ? 'agent session verified' : 'agent session token rejected', v.ok ? { agent: v.claims.sub, policy: v.claims.policy_version ?? null } : { reason: v.reason });
42
+ }
43
+ const server = createServer(mobius);
44
+ await server.connect(new StdioServerTransport());
45
+ log('info', 'mobius mcp ready');
46
+ const shutdown = async () => { driver.close(); process.exit(0); };
47
+ process.on('SIGINT', shutdown);
48
+ process.on('SIGTERM', shutdown);
49
+ process.stdin.on('close', shutdown);
50
+ }
51
+ else if (cmd === 'serve') {
52
+ // REST front door (same engine as MCP). Local by default; put it behind your own TLS proxy to expose it.
53
+ const args = process.argv.slice(3);
54
+ const opt = (k, d) => { const i = args.indexOf(`--${k}`); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
55
+ const port = Number(opt('port', '7777')), host = opt('host', '127.0.0.1');
56
+ let token = process.env.MOBIUS_API_TOKEN ?? '';
57
+ if (!token) {
58
+ token = randomBytes(24).toString('base64url');
59
+ process.stderr.write(`MOBIUS_API_TOKEN not set; generated one for this run:\n ${token}\n`);
60
+ }
61
+ const driver = new MobiusAgentDriver();
62
+ const audit = new AuditLog();
63
+ const mobius = new Mobius(connectors(driver), audit, new Policy(), defaultApprover());
64
+ mobius.persistIds = true;
65
+ const engine = await createEngine(mobius);
66
+ createHttpServer(engine, audit, token, operatorKey).listen(port, host, () => log('info', 'mobius rest ready', { url: `http://${host}:${port}/v1` }));
67
+ const shutdown = async () => { await engine.close().catch(() => { }); driver.close(); process.exit(0); };
68
+ process.on('SIGINT', shutdown);
69
+ process.on('SIGTERM', shutdown);
70
+ }
71
+ else if (cmd === 'keys' && process.argv[3] === 'rotate') {
72
+ // Retire the install key (its public half is kept so old audit entries still verify) and start a new one.
73
+ rotateInstallKey(mobiusHome());
74
+ process.stdout.write('Key rotated. Running Mobius servers switch to the new key on their next action.\nEvery session token signed by the old key is now refused: issue new ones with `mobius session issue`.\n');
75
+ }
76
+ else if (cmd === 'keys') {
77
+ // The public half of this install's key: verifies its audit logs and the session tokens it issues.
78
+ // --all: every key it has used, oldest first, as JSON (what an auditor needs to verify a log across rotations).
79
+ const a = new AuditLog();
80
+ process.stdout.write(process.argv[3] === '--all' ? JSON.stringify(a.keyring(), null, 2) + '\n' : a.signer.publicKeyPem);
81
+ }
82
+ else if (cmd === 'session' && process.argv[3] === 'revoke') {
83
+ // mobius session revoke <jti>: the jti is printed at issue time and recorded on every audit entry.
84
+ const jti = process.argv[4];
85
+ if (!jti || !/^[\w.-]{1,128}$/.test(jti)) {
86
+ process.stderr.write('usage: mobius session revoke <session id (jti), e.g. ses_1a2b3c>\n');
87
+ process.exit(2);
88
+ }
89
+ appendFileSync(revokedPath(), jti + '\n', { mode: 0o600 });
90
+ process.stdout.write(`Session ${jti} revoked: every Mobius on this machine refuses its actions from now on.\n`);
91
+ }
92
+ else if (cmd === 'session' && process.argv[3] === 'issue') {
93
+ // mobius session issue --agent acme-bot/1.0 [--ttl 8h] [--policy-version v3] [--issuer ops-team]
94
+ const args = process.argv.slice(4);
95
+ const opt = (k) => { const i = args.indexOf(`--${k}`); return i >= 0 ? args[i + 1] : undefined; };
96
+ const agent = opt('agent');
97
+ if (!agent) {
98
+ process.stderr.write('usage: mobius session issue --agent <name/version> [--ttl 8h] [--policy-version <v>] [--issuer <who>]\n');
99
+ process.exit(2);
100
+ }
101
+ const m = /^(\d+)(m|h|d)$/.exec(opt('ttl') ?? '8h');
102
+ if (!m) {
103
+ process.stderr.write('--ttl: e.g. 30m, 8h, 7d\n');
104
+ process.exit(2);
105
+ }
106
+ const now = Math.floor(Date.now() / 1000);
107
+ const pv = opt('policy-version'), iss = opt('issuer');
108
+ const jti = `ses_${randomBytes(6).toString('hex')}`;
109
+ process.stderr.write(`session id ${jti} (revoke with: mobius session revoke ${jti})\n`);
110
+ process.stdout.write(issueSession(new AuditLog().signer, {
111
+ sub: agent, jti, iat: now, exp: now + Number(m[1]) * { m: 60, h: 3600, d: 86400 }[m[2]],
112
+ ...(pv ? { policy_version: pv } : {}), ...(iss ? { iss } : {}),
113
+ }) + '\n');
114
+ }
115
+ else if (cmd === 'setup-ios') {
116
+ // One-time iOS setup: build, patch, sign and install the Mobius Agent runner with your Apple developer team.
117
+ const { spawnSync } = await import('node:child_process');
118
+ const { fileURLToPath } = await import('node:url');
119
+ const script = [new URL('../ios-agent/setup.sh', import.meta.url), new URL('../../../drivers/mobiusagent-driver/ios-agent/setup.sh', import.meta.url)]
120
+ .map(u => fileURLToPath(u)).find(p => existsSync(p));
121
+ if (!script) {
122
+ process.stderr.write('ios-agent/setup.sh not found in this install\n');
123
+ process.exit(1);
124
+ }
125
+ if (!process.env.MOBIUS_TEAM_ID) {
126
+ process.stderr.write('Set MOBIUS_TEAM_ID to your Apple developer team id (the OU of your "Apple Development" certificate):\n MOBIUS_TEAM_ID=XXXXXXXXXX mobius setup-ios\n');
127
+ process.exit(2);
128
+ }
129
+ process.exit(spawnSync('sh', [script], { stdio: 'inherit' }).status ?? 1);
130
+ }
131
+ else if (cmd === 'stop') {
132
+ // Kill switch: every running Mobius refuses state-changing actions until `mobius resume`.
133
+ writeFileSync(killSwitchPath(), new Date().toISOString());
134
+ process.stdout.write('Kill switch engaged: all agent actions are stopped. Run `mobius resume` to allow them again.\n');
135
+ }
136
+ else if (cmd === 'resume') {
137
+ rmSync(killSwitchPath(), { force: true });
138
+ process.stdout.write('Kill switch released.\n');
139
+ }
140
+ else if (cmd === 'audit' && process.argv[3] === 'export') {
141
+ process.stdout.write(new AuditLog().exportJsonl() + '\n');
142
+ }
143
+ else if (cmd === 'audit' && process.argv[3] === 'verify') {
144
+ const r = new AuditLog().verify();
145
+ process.stdout.write(r.ok ? `Audit log intact: ${r.rows} entries, chain and signatures verified${r.legacy_unsigned ? ` (${r.legacy_unsigned} older unsigned entries)` : ''}.\n` : `TAMPERED at ${r.broken_at}: ${r.reason} (${r.rows} entries).\n`);
146
+ process.exit(r.ok ? 0 : 1);
147
+ }
148
+ else {
149
+ process.stderr.write('usage: mobius mcp | serve [--port 7777] [--host 127.0.0.1] | session issue --agent <name> | session revoke <jti> | keys [--all] | keys rotate | setup-ios | stop | resume | audit export | audit verify\n');
150
+ process.exit(2);
151
+ }
@@ -0,0 +1,294 @@
1
+ import { z } from 'zod';
2
+ export declare const PROTOCOL_VERSION = 1;
3
+ export declare const ErrorCode: z.ZodEnum<{
4
+ INVALID_INPUT: "INVALID_INPUT";
5
+ DEVICE_NOT_FOUND: "DEVICE_NOT_FOUND";
6
+ DEVICE_OFFLINE: "DEVICE_OFFLINE";
7
+ DEVICE_LOCKED: "DEVICE_LOCKED";
8
+ SERVICE_DISABLED: "SERVICE_DISABLED";
9
+ PERMISSION_REQUIRED: "PERMISSION_REQUIRED";
10
+ UNSUPPORTED_CAPABILITY: "UNSUPPORTED_CAPABILITY";
11
+ ELEMENT_NOT_FOUND: "ELEMENT_NOT_FOUND";
12
+ AMBIGUOUS_TARGET: "AMBIGUOUS_TARGET";
13
+ STALE_ELEMENT: "STALE_ELEMENT";
14
+ SECURE_WINDOW: "SECURE_WINDOW";
15
+ APP_NOT_INSTALLED: "APP_NOT_INSTALLED";
16
+ POLICY_BLOCKED: "POLICY_BLOCKED";
17
+ APPROVAL_REQUIRED: "APPROVAL_REQUIRED";
18
+ APPROVAL_REJECTED: "APPROVAL_REJECTED";
19
+ APPROVAL_EXPIRED: "APPROVAL_EXPIRED";
20
+ BUDGET_EXCEEDED: "BUDGET_EXCEEDED";
21
+ LOOP_DETECTED: "LOOP_DETECTED";
22
+ RATE_LIMITED: "RATE_LIMITED";
23
+ DUPLICATE_ACTION: "DUPLICATE_ACTION";
24
+ TIMEOUT: "TIMEOUT";
25
+ CANCELLED: "CANCELLED";
26
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN";
27
+ FOREGROUND_REQUIRED: "FOREGROUND_REQUIRED";
28
+ INTERNAL: "INTERNAL";
29
+ }>;
30
+ export type ErrorCode = z.infer<typeof ErrorCode>;
31
+ export declare const Platform: z.ZodEnum<{
32
+ android: "android";
33
+ ios: "ios";
34
+ }>;
35
+ export declare const Framework: z.ZodEnum<{
36
+ native: "native";
37
+ flutter: "flutter";
38
+ react_native: "react_native";
39
+ unknown: "unknown";
40
+ }>;
41
+ export type Framework = z.infer<typeof Framework>;
42
+ export declare const TreeQuality: z.ZodEnum<{
43
+ good: "good";
44
+ sparse: "sparse";
45
+ empty: "empty";
46
+ }>;
47
+ export declare const Bounds: z.ZodObject<{
48
+ x: z.ZodNumber;
49
+ y: z.ZodNumber;
50
+ width: z.ZodNumber;
51
+ height: z.ZodNumber;
52
+ }, z.core.$strip>;
53
+ export type Bounds = z.infer<typeof Bounds>;
54
+ export declare const Role: z.ZodEnum<{
55
+ button: "button";
56
+ textbox: "textbox";
57
+ checkbox: "checkbox";
58
+ switch: "switch";
59
+ radio: "radio";
60
+ image: "image";
61
+ text: "text";
62
+ list: "list";
63
+ tab: "tab";
64
+ view: "view";
65
+ }>;
66
+ export declare const Element: z.ZodObject<{
67
+ id: z.ZodString;
68
+ role: z.ZodEnum<{
69
+ button: "button";
70
+ textbox: "textbox";
71
+ checkbox: "checkbox";
72
+ switch: "switch";
73
+ radio: "radio";
74
+ image: "image";
75
+ text: "text";
76
+ list: "list";
77
+ tab: "tab";
78
+ view: "view";
79
+ }>;
80
+ text: z.ZodNullable<z.ZodString>;
81
+ label: z.ZodNullable<z.ZodString>;
82
+ resource_id: z.ZodNullable<z.ZodString>;
83
+ clickable: z.ZodBoolean;
84
+ long_clickable: z.ZodBoolean;
85
+ enabled: z.ZodBoolean;
86
+ focused: z.ZodBoolean;
87
+ scrollable: z.ZodBoolean;
88
+ editable: z.ZodBoolean;
89
+ checked: z.ZodNullable<z.ZodBoolean>;
90
+ selected: z.ZodBoolean;
91
+ sensitive: z.ZodBoolean;
92
+ bounds: z.ZodObject<{
93
+ x: z.ZodNumber;
94
+ y: z.ZodNumber;
95
+ width: z.ZodNumber;
96
+ height: z.ZodNumber;
97
+ }, z.core.$strip>;
98
+ dup_index: z.ZodNumber;
99
+ }, z.core.$strip>;
100
+ export type Element = z.infer<typeof Element>;
101
+ export declare const Observation: z.ZodObject<{
102
+ observation_id: z.ZodString;
103
+ device_id: z.ZodString;
104
+ platform: z.ZodEnum<{
105
+ android: "android";
106
+ ios: "ios";
107
+ }>;
108
+ foreground_app: z.ZodNullable<z.ZodString>;
109
+ screen: z.ZodObject<{
110
+ width: z.ZodNumber;
111
+ height: z.ZodNumber;
112
+ rotation: z.ZodNumber;
113
+ }, z.core.$strip>;
114
+ screen_hash: z.ZodString;
115
+ framework: z.ZodEnum<{
116
+ native: "native";
117
+ flutter: "flutter";
118
+ react_native: "react_native";
119
+ unknown: "unknown";
120
+ }>;
121
+ tree_quality: z.ZodEnum<{
122
+ good: "good";
123
+ sparse: "sparse";
124
+ empty: "empty";
125
+ }>;
126
+ sources: z.ZodArray<z.ZodEnum<{
127
+ a11y: "a11y";
128
+ flutter_vm: "flutter_vm";
129
+ vision: "vision";
130
+ }>>;
131
+ elements: z.ZodArray<z.ZodObject<{
132
+ id: z.ZodString;
133
+ role: z.ZodEnum<{
134
+ button: "button";
135
+ textbox: "textbox";
136
+ checkbox: "checkbox";
137
+ switch: "switch";
138
+ radio: "radio";
139
+ image: "image";
140
+ text: "text";
141
+ list: "list";
142
+ tab: "tab";
143
+ view: "view";
144
+ }>;
145
+ text: z.ZodNullable<z.ZodString>;
146
+ label: z.ZodNullable<z.ZodString>;
147
+ resource_id: z.ZodNullable<z.ZodString>;
148
+ clickable: z.ZodBoolean;
149
+ long_clickable: z.ZodBoolean;
150
+ enabled: z.ZodBoolean;
151
+ focused: z.ZodBoolean;
152
+ scrollable: z.ZodBoolean;
153
+ editable: z.ZodBoolean;
154
+ checked: z.ZodNullable<z.ZodBoolean>;
155
+ selected: z.ZodBoolean;
156
+ sensitive: z.ZodBoolean;
157
+ bounds: z.ZodObject<{
158
+ x: z.ZodNumber;
159
+ y: z.ZodNumber;
160
+ width: z.ZodNumber;
161
+ height: z.ZodNumber;
162
+ }, z.core.$strip>;
163
+ dup_index: z.ZodNumber;
164
+ }, z.core.$strip>>;
165
+ untrusted_content: z.ZodLiteral<true>;
166
+ truncated: z.ZodBoolean;
167
+ keyboard: z.ZodNullable<z.ZodObject<{
168
+ x: z.ZodNumber;
169
+ y: z.ZodNumber;
170
+ width: z.ZodNumber;
171
+ height: z.ZodNumber;
172
+ }, z.core.$strip>>;
173
+ }, z.core.$strip>;
174
+ export type Observation = z.infer<typeof Observation>;
175
+ export declare const MobiusErrorBody: z.ZodObject<{
176
+ code: z.ZodEnum<{
177
+ INVALID_INPUT: "INVALID_INPUT";
178
+ DEVICE_NOT_FOUND: "DEVICE_NOT_FOUND";
179
+ DEVICE_OFFLINE: "DEVICE_OFFLINE";
180
+ DEVICE_LOCKED: "DEVICE_LOCKED";
181
+ SERVICE_DISABLED: "SERVICE_DISABLED";
182
+ PERMISSION_REQUIRED: "PERMISSION_REQUIRED";
183
+ UNSUPPORTED_CAPABILITY: "UNSUPPORTED_CAPABILITY";
184
+ ELEMENT_NOT_FOUND: "ELEMENT_NOT_FOUND";
185
+ AMBIGUOUS_TARGET: "AMBIGUOUS_TARGET";
186
+ STALE_ELEMENT: "STALE_ELEMENT";
187
+ SECURE_WINDOW: "SECURE_WINDOW";
188
+ APP_NOT_INSTALLED: "APP_NOT_INSTALLED";
189
+ POLICY_BLOCKED: "POLICY_BLOCKED";
190
+ APPROVAL_REQUIRED: "APPROVAL_REQUIRED";
191
+ APPROVAL_REJECTED: "APPROVAL_REJECTED";
192
+ APPROVAL_EXPIRED: "APPROVAL_EXPIRED";
193
+ BUDGET_EXCEEDED: "BUDGET_EXCEEDED";
194
+ LOOP_DETECTED: "LOOP_DETECTED";
195
+ RATE_LIMITED: "RATE_LIMITED";
196
+ DUPLICATE_ACTION: "DUPLICATE_ACTION";
197
+ TIMEOUT: "TIMEOUT";
198
+ CANCELLED: "CANCELLED";
199
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN";
200
+ FOREGROUND_REQUIRED: "FOREGROUND_REQUIRED";
201
+ INTERNAL: "INTERNAL";
202
+ }>;
203
+ message: z.ZodString;
204
+ retryable: z.ZodBoolean;
205
+ hint: z.ZodOptional<z.ZodString>;
206
+ }, z.core.$strip>;
207
+ export declare const ActionStatus: z.ZodEnum<{
208
+ succeeded: "succeeded";
209
+ failed: "failed";
210
+ blocked: "blocked";
211
+ pending_approval: "pending_approval";
212
+ outcome_unknown: "outcome_unknown";
213
+ }>;
214
+ export declare const ActionResult: z.ZodObject<{
215
+ ok: z.ZodBoolean;
216
+ action_id: z.ZodString;
217
+ status: z.ZodEnum<{
218
+ succeeded: "succeeded";
219
+ failed: "failed";
220
+ blocked: "blocked";
221
+ pending_approval: "pending_approval";
222
+ outcome_unknown: "outcome_unknown";
223
+ }>;
224
+ duration_ms: z.ZodNumber;
225
+ state_changed: z.ZodNullable<z.ZodBoolean>;
226
+ foreground_app: z.ZodNullable<z.ZodString>;
227
+ observation_id: z.ZodNullable<z.ZodString>;
228
+ verified: z.ZodNullable<z.ZodBoolean>;
229
+ message: z.ZodString;
230
+ error: z.ZodOptional<z.ZodObject<{
231
+ code: z.ZodEnum<{
232
+ INVALID_INPUT: "INVALID_INPUT";
233
+ DEVICE_NOT_FOUND: "DEVICE_NOT_FOUND";
234
+ DEVICE_OFFLINE: "DEVICE_OFFLINE";
235
+ DEVICE_LOCKED: "DEVICE_LOCKED";
236
+ SERVICE_DISABLED: "SERVICE_DISABLED";
237
+ PERMISSION_REQUIRED: "PERMISSION_REQUIRED";
238
+ UNSUPPORTED_CAPABILITY: "UNSUPPORTED_CAPABILITY";
239
+ ELEMENT_NOT_FOUND: "ELEMENT_NOT_FOUND";
240
+ AMBIGUOUS_TARGET: "AMBIGUOUS_TARGET";
241
+ STALE_ELEMENT: "STALE_ELEMENT";
242
+ SECURE_WINDOW: "SECURE_WINDOW";
243
+ APP_NOT_INSTALLED: "APP_NOT_INSTALLED";
244
+ POLICY_BLOCKED: "POLICY_BLOCKED";
245
+ APPROVAL_REQUIRED: "APPROVAL_REQUIRED";
246
+ APPROVAL_REJECTED: "APPROVAL_REJECTED";
247
+ APPROVAL_EXPIRED: "APPROVAL_EXPIRED";
248
+ BUDGET_EXCEEDED: "BUDGET_EXCEEDED";
249
+ LOOP_DETECTED: "LOOP_DETECTED";
250
+ RATE_LIMITED: "RATE_LIMITED";
251
+ DUPLICATE_ACTION: "DUPLICATE_ACTION";
252
+ TIMEOUT: "TIMEOUT";
253
+ CANCELLED: "CANCELLED";
254
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN";
255
+ FOREGROUND_REQUIRED: "FOREGROUND_REQUIRED";
256
+ INTERNAL: "INTERNAL";
257
+ }>;
258
+ message: z.ZodString;
259
+ retryable: z.ZodBoolean;
260
+ hint: z.ZodOptional<z.ZodString>;
261
+ }, z.core.$strip>>;
262
+ }, z.core.$strip>;
263
+ export type ActionResult = z.infer<typeof ActionResult>;
264
+ /** Post-action expectation; the host waits (state-based) until it holds or times out. */
265
+ export declare const Expect: z.ZodObject<{
266
+ text_visible: z.ZodOptional<z.ZodString>;
267
+ app: z.ZodOptional<z.ZodString>;
268
+ element_gone: z.ZodOptional<z.ZodString>;
269
+ }, z.core.$strict>;
270
+ export type Expect = z.infer<typeof Expect>;
271
+ export declare const Capability: z.ZodEnum<{
272
+ type: "type";
273
+ observe: "observe";
274
+ screenshot: "screenshot";
275
+ tap: "tap";
276
+ scroll: "scroll";
277
+ swipe: "swipe";
278
+ press_key: "press_key";
279
+ list_apps: "list_apps";
280
+ open_app: "open_app";
281
+ open_url: "open_url";
282
+ wait_for: "wait_for";
283
+ run_shortcut: "run_shortcut";
284
+ device_action: "device_action";
285
+ }>;
286
+ export type Capability = z.infer<typeof Capability>;
287
+ export declare const Key: z.ZodEnum<{
288
+ back: "back";
289
+ home: "home";
290
+ recents: "recents";
291
+ enter: "enter";
292
+ notifications: "notifications";
293
+ }>;
294
+ export type Key = z.infer<typeof Key>;
@@ -0,0 +1,82 @@
1
+ // Mobius protocol: the contract shared by host, Android app and iOS app.
2
+ // Kotlin/Swift models must round-trip the golden fixtures in tests/fixtures.
3
+ import { z } from 'zod';
4
+ export const PROTOCOL_VERSION = 1;
5
+ export const ErrorCode = z.enum([
6
+ 'INVALID_INPUT', 'DEVICE_NOT_FOUND', 'DEVICE_OFFLINE', 'DEVICE_LOCKED', 'SERVICE_DISABLED',
7
+ 'PERMISSION_REQUIRED', 'UNSUPPORTED_CAPABILITY', 'ELEMENT_NOT_FOUND', 'AMBIGUOUS_TARGET',
8
+ 'STALE_ELEMENT', 'SECURE_WINDOW', 'APP_NOT_INSTALLED', 'POLICY_BLOCKED', 'APPROVAL_REQUIRED',
9
+ 'APPROVAL_REJECTED', 'APPROVAL_EXPIRED', 'BUDGET_EXCEEDED', 'LOOP_DETECTED', 'RATE_LIMITED',
10
+ 'DUPLICATE_ACTION', 'TIMEOUT', 'CANCELLED', 'OUTCOME_UNKNOWN', 'FOREGROUND_REQUIRED', 'INTERNAL',
11
+ ]);
12
+ export const Platform = z.enum(['android', 'ios']);
13
+ export const Framework = z.enum(['native', 'flutter', 'react_native', 'unknown']);
14
+ export const TreeQuality = z.enum(['good', 'sparse', 'empty']);
15
+ export const Bounds = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() });
16
+ export const Role = z.enum([
17
+ 'button', 'textbox', 'checkbox', 'switch', 'radio', 'image', 'text', 'list', 'tab', 'view',
18
+ ]);
19
+ export const Element = z.object({
20
+ id: z.string(), // "o12:e5" — valid only against observation o12 (or re-resolvable)
21
+ role: Role,
22
+ text: z.string().nullable(),
23
+ label: z.string().nullable(), // content-desc / accessibility label
24
+ resource_id: z.string().nullable(),
25
+ clickable: z.boolean(),
26
+ long_clickable: z.boolean(),
27
+ enabled: z.boolean(),
28
+ focused: z.boolean(),
29
+ scrollable: z.boolean(),
30
+ editable: z.boolean(),
31
+ checked: z.boolean().nullable(),
32
+ selected: z.boolean(),
33
+ sensitive: z.boolean(), // true → text redacted at source
34
+ bounds: Bounds,
35
+ dup_index: z.number().int(), // nth element with identical role+text+label
36
+ });
37
+ export const Observation = z.object({
38
+ observation_id: z.string(),
39
+ device_id: z.string(),
40
+ platform: Platform,
41
+ foreground_app: z.string().nullable(),
42
+ screen: z.object({ width: z.number(), height: z.number(), rotation: z.number() }),
43
+ screen_hash: z.string(),
44
+ framework: Framework,
45
+ tree_quality: TreeQuality,
46
+ sources: z.array(z.enum(['a11y', 'flutter_vm', 'vision'])),
47
+ elements: z.array(Element),
48
+ untrusted_content: z.literal(true),
49
+ truncated: z.boolean(),
50
+ /** On-screen keyboard area when visible. Its keys are not listed as elements. */
51
+ keyboard: Bounds.nullable(),
52
+ });
53
+ export const MobiusErrorBody = z.object({
54
+ code: ErrorCode,
55
+ message: z.string(),
56
+ retryable: z.boolean(),
57
+ hint: z.string().optional(),
58
+ });
59
+ export const ActionStatus = z.enum(['succeeded', 'failed', 'blocked', 'pending_approval', 'outcome_unknown']);
60
+ export const ActionResult = z.object({
61
+ ok: z.boolean(),
62
+ action_id: z.string(),
63
+ status: ActionStatus,
64
+ duration_ms: z.number(),
65
+ state_changed: z.boolean().nullable(),
66
+ foreground_app: z.string().nullable(),
67
+ observation_id: z.string().nullable(),
68
+ verified: z.boolean().nullable(),
69
+ message: z.string(),
70
+ error: MobiusErrorBody.optional(),
71
+ });
72
+ /** Post-action expectation; the host waits (state-based) until it holds or times out. */
73
+ export const Expect = z.object({
74
+ text_visible: z.string().optional(),
75
+ app: z.string().optional(),
76
+ element_gone: z.string().optional(),
77
+ }).strict();
78
+ export const Capability = z.enum([
79
+ 'observe', 'screenshot', 'tap', 'type', 'scroll', 'swipe', 'press_key',
80
+ 'list_apps', 'open_app', 'open_url', 'wait_for', 'run_shortcut', 'device_action',
81
+ ]);
82
+ export const Key = z.enum(['back', 'home', 'recents', 'enter', 'notifications']);
@@ -0,0 +1,82 @@
1
+ import type { Bounds, Capability, ErrorCode, Framework, Key } from '../core/protocol.ts';
2
+ /** Platform-neutral raw accessibility node, as produced by a connector before normalization. */
3
+ export interface RawNode {
4
+ cls: string;
5
+ pkg: string;
6
+ text: string;
7
+ desc: string;
8
+ resourceId: string;
9
+ clickable: boolean;
10
+ longClickable: boolean;
11
+ enabled: boolean;
12
+ focused: boolean;
13
+ scrollable: boolean;
14
+ checkable: boolean;
15
+ checked: boolean;
16
+ selected: boolean;
17
+ password: boolean;
18
+ bounds: Bounds;
19
+ path: string;
20
+ children: RawNode[];
21
+ }
22
+ export interface RawScreen {
23
+ root: RawNode | null;
24
+ width: number;
25
+ height: number;
26
+ rotation: number;
27
+ foregroundApp: string | null;
28
+ }
29
+ export interface DeviceInfo {
30
+ device_id: string;
31
+ platform: 'android' | 'ios';
32
+ connector: string;
33
+ name: string;
34
+ model: string;
35
+ os_version: string;
36
+ online: boolean;
37
+ screen_on: boolean | null;
38
+ locked: boolean | null;
39
+ }
40
+ export type TapMode = 'single' | 'long' | 'double';
41
+ export type Direction = 'up' | 'down' | 'left' | 'right';
42
+ /** What every device backend implements. Add a platform = add a Connector. */
43
+ export interface Connector {
44
+ readonly deviceId: string;
45
+ readonly platform: 'android' | 'ios';
46
+ /** Emulator or simulator (no real accounts or money on it). Undefined = unknown, treated as a real phone. */
47
+ readonly virtual?: boolean;
48
+ readonly capabilities: ReadonlySet<Capability>;
49
+ /** Known limitations surfaced to agents via device_capabilities. */
50
+ readonly limitations: readonly string[];
51
+ info(): Promise<DeviceInfo>;
52
+ /** Accessibility tree of the current screen; should return once the UI is idle. */
53
+ dump(): Promise<RawScreen>;
54
+ framework(pkg: string): Promise<Framework>;
55
+ screenshot(maxSize: number): Promise<{
56
+ data: string;
57
+ mimeType: string;
58
+ }>;
59
+ tap(x: number, y: number, mode: TapMode): Promise<void>;
60
+ type(text: string, submit: boolean): Promise<void>;
61
+ /** `direction` is finger movement; start defaults to screen centre. */
62
+ swipe(direction: Direction, from: {
63
+ x: number;
64
+ y: number;
65
+ } | null, distance: number | null): Promise<void>;
66
+ key(key: Key): Promise<void>;
67
+ openApp(appId: string): Promise<void>;
68
+ openUrl(url: string): Promise<void>;
69
+ listApps(): Promise<string[]>;
70
+ /** Driver operations beyond the core set (terminate, install, uninstall, clear_data, orientation, location, clipboard, info).
71
+ * Optional: connectors without it report UNSUPPORTED_CAPABILITY. */
72
+ extra?(op: string, args?: Record<string, unknown>): Promise<any>;
73
+ }
74
+ export declare class MobiusError extends Error {
75
+ readonly code: ErrorCode;
76
+ readonly retryable: boolean;
77
+ readonly hint: string | undefined;
78
+ constructor(code: ErrorCode, message: string, opts?: {
79
+ retryable?: boolean;
80
+ hint?: string;
81
+ });
82
+ }
@@ -0,0 +1,11 @@
1
+ export class MobiusError extends Error {
2
+ code;
3
+ retryable;
4
+ hint;
5
+ constructor(code, message, opts = {}) {
6
+ super(message);
7
+ this.code = code;
8
+ this.retryable = opts.retryable ?? false;
9
+ this.hint = opts.hint;
10
+ }
11
+ }
@@ -0,0 +1,57 @@
1
+ import type { Capability, Framework, Key } from '../../core/protocol.ts';
2
+ import { type Connector, type DeviceInfo, type Direction, type RawScreen, type TapMode } from '../connector.ts';
3
+ /** The driver binary: $MOBIUS_DRIVER_BIN, the one bundled in the npm package for this platform, or a source build. */
4
+ export declare const driverPath: () => string;
5
+ /** The driver binary is built (`cargo build --release` in drivers/mobiusagent-driver). */
6
+ export declare const driverBuilt: () => boolean;
7
+ /** iOS needs WebDriverAgent built and signed once (drivers/mobiusagent-driver/ios-agent/setup.sh). */
8
+ export declare const iosAgentReady: () => boolean;
9
+ export interface AgentDevice {
10
+ id: string;
11
+ platform: 'android' | 'ios';
12
+ type: 'emulator' | 'simulator' | 'real';
13
+ name: string;
14
+ model: string;
15
+ version: string;
16
+ state: string;
17
+ }
18
+ export declare class MobiusAgentDriver {
19
+ private proc;
20
+ private next;
21
+ private waiting;
22
+ private start;
23
+ call<T = any>(op: string, args?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
24
+ devices(): Promise<AgentDevice[]>;
25
+ close(): void;
26
+ }
27
+ export declare class MobiusAgentConnector implements Connector {
28
+ readonly platform: 'android' | 'ios';
29
+ readonly deviceId: string;
30
+ readonly capabilities: ReadonlySet<Capability>;
31
+ readonly limitations: readonly string[];
32
+ private size;
33
+ private fwCache;
34
+ private readonly drv;
35
+ private readonly dev;
36
+ constructor(drv: MobiusAgentDriver, dev: AgentDevice);
37
+ get virtual(): boolean;
38
+ private get d();
39
+ info(): Promise<DeviceInfo>;
40
+ dump(): Promise<RawScreen>;
41
+ framework(pkg: string): Promise<Framework>;
42
+ screenshot(maxSize: number): Promise<{
43
+ data: string;
44
+ mimeType: string;
45
+ }>;
46
+ tap(x: number, y: number, mode: TapMode): Promise<void>;
47
+ type(text: string, submit: boolean): Promise<void>;
48
+ swipe(direction: Direction, from: {
49
+ x: number;
50
+ y: number;
51
+ } | null, distance: number | null): Promise<void>;
52
+ key(key: Key): Promise<void>;
53
+ openApp(appId: string): Promise<void>;
54
+ openUrl(url: string): Promise<void>;
55
+ listApps(): Promise<string[]>;
56
+ extra(op: string, args?: Record<string, unknown>): Promise<any>;
57
+ }