@proagentstore/cli 0.4.125 → 0.4.127
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/dist/browser-runner/coding/engine-check.js +4 -1
- package/dist/browser-runner/coding/headless.js +11 -3
- package/dist/browser-runner/coding/launcher-wrapper.js +25 -0
- package/dist/browser-runner/native/contract.js +17 -0
- package/dist/browser-runner/native/preflight.js +172 -0
- package/dist/browser-runner/runner.js +1 -0
- package/dist/browser-runner/server.js +6 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { unwrapLauncher } from "./launcher-wrapper.js";
|
|
2
3
|
import { statSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -66,7 +67,9 @@ export function detectEngineLogin(engine, deps = {}) {
|
|
|
66
67
|
*/
|
|
67
68
|
export function checkEngine(input, deps = {}) {
|
|
68
69
|
const engine = (["claude", "gemini", "codex", "grok", "generic"].includes(input.clientType ?? "") ? input.clientType : "claude");
|
|
69
|
-
|
|
70
|
+
// The ENGINE is what must be installed — after a `pas mcp-auth exec --` wrapper (#1006), not the wrapper.
|
|
71
|
+
const command = parseCommand(input.command);
|
|
72
|
+
const parsed = unwrapLauncher(command.bin, command.args);
|
|
70
73
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
71
74
|
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
72
75
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InputLedger } from "./input-ledger.js";
|
|
2
|
+
import { spawnTarget, unwrapLauncher } from "./launcher-wrapper.js";
|
|
2
3
|
import { runObjectiveBlock } from "./run-objective.js";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { classifyCommand, commandFromToolInput, fillTargetFromResult, toolCallOk, toolResultMark } from "./engine-acts.js";
|
|
@@ -83,6 +84,8 @@ export class HeadlessSession {
|
|
|
83
84
|
mode;
|
|
84
85
|
adapter;
|
|
85
86
|
cmdBin;
|
|
87
|
+
/** A launcher wrapper spawned in front of the engine (`pas mcp-auth exec --`), or empty. */
|
|
88
|
+
cmdPrefix;
|
|
86
89
|
cmdArgs;
|
|
87
90
|
binName;
|
|
88
91
|
/** Wall-clock of the last stdout/stderr byte — drives the persistent-raw idle heuristic. */
|
|
@@ -242,7 +245,10 @@ export class HeadlessSession {
|
|
|
242
245
|
constructor(config) {
|
|
243
246
|
this.config = config;
|
|
244
247
|
this.engineLabel = `${config.clientType}:${config.id}`;
|
|
245
|
-
|
|
248
|
+
// `pas mcp-auth exec -- codex …` (#1006): spawn the wrapper, but drive the engine after `--`.
|
|
249
|
+
const parsed = parseCommand(config.command);
|
|
250
|
+
const { prefix, bin, args } = unwrapLauncher(parsed.bin, parsed.args);
|
|
251
|
+
this.cmdPrefix = prefix;
|
|
246
252
|
// When no explicit command is configured, fall back to THIS engine's default
|
|
247
253
|
// command (codex/gemini/grok/…) — not a hard-coded "claude", which would drive a
|
|
248
254
|
// non-Claude session with the wrong CLI (and mode is already "raw" for it).
|
|
@@ -428,7 +434,8 @@ export class HeadlessSession {
|
|
|
428
434
|
this.buf = "";
|
|
429
435
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
430
436
|
const env = this.spawnEnv;
|
|
431
|
-
const
|
|
437
|
+
const target = spawnTarget(this.cmdPrefix, this.cmdBin, args);
|
|
438
|
+
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
432
439
|
const proc = spawn(launch.command, launch.args, {
|
|
433
440
|
cwd: this.config.workDir,
|
|
434
441
|
env,
|
|
@@ -587,7 +594,8 @@ export class HeadlessSession {
|
|
|
587
594
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
588
595
|
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
589
596
|
const env = this.spawnEnv;
|
|
590
|
-
const
|
|
597
|
+
const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
|
|
598
|
+
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
591
599
|
const proc = spawn(launch.command, launch.args, {
|
|
592
600
|
cwd: this.config.workDir,
|
|
593
601
|
env,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pas mcp-auth exec -- <engine …>` (#1006, PAS #356) on the runner.
|
|
3
|
+
*
|
|
4
|
+
* The ProAppStore CLI runs a command with the owner's remote PAS MCP credential in that process's
|
|
5
|
+
* environment only. So the runner must SPAWN the wrapper (it is what supplies the token) while
|
|
6
|
+
* everything that understands an engine — the adapter choice (`codex exec` → structured), the turn
|
|
7
|
+
* and resume argument builders, the engine-installed check — works on the ENGINE command after `--`.
|
|
8
|
+
*
|
|
9
|
+
* Matched exactly, as in the API (`workers/api/src/lib/coding-command.ts`) and the SDK
|
|
10
|
+
* (`packages/sdk/src/engine-posture.ts`): `pas` followed by anything other than `mcp-auth exec --`
|
|
11
|
+
* is not a wrapper, and stays the binary.
|
|
12
|
+
*/
|
|
13
|
+
export const PAS_MCP_AUTH_WRAPPER = ["mcp-auth", "exec", "--"];
|
|
14
|
+
export function unwrapLauncher(bin, args) {
|
|
15
|
+
const isPas = (bin.split("/").pop() || "").toLowerCase() === "pas";
|
|
16
|
+
if (!isPas || !PAS_MCP_AUTH_WRAPPER.every((t, k) => args[k] === t) || args.length <= PAS_MCP_AUTH_WRAPPER.length) {
|
|
17
|
+
return { prefix: [], bin, args: [...args] };
|
|
18
|
+
}
|
|
19
|
+
const rest = args.slice(PAS_MCP_AUTH_WRAPPER.length);
|
|
20
|
+
return { prefix: [bin, ...PAS_MCP_AUTH_WRAPPER], bin: rest[0], args: rest.slice(1) };
|
|
21
|
+
}
|
|
22
|
+
/** What to actually spawn: the wrapper (if any) carrying the engine and its built args. */
|
|
23
|
+
export function spawnTarget(prefix, bin, args) {
|
|
24
|
+
return prefix.length ? { command: prefix[0], args: [...prefix.slice(1), bin, ...args] } : { command: bin, args: [...args] };
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native desktop / Maestro runtime (`local_native`, #1057) — the wire contract between the API
|
|
3
|
+
* Worker and the runner.
|
|
4
|
+
*
|
|
5
|
+
* This file is copied BYTE-FOR-BYTE between `packages/browser-runner/src/native/contract.ts` and
|
|
6
|
+
* `workers/api/src/lib/native/contract.ts` (the two packages cannot import each other), and
|
|
7
|
+
* `workers/api/src/lib/native/contract.test.ts` fails if they differ. It imports nothing.
|
|
8
|
+
*
|
|
9
|
+
* Slice 1 is the FOUNDATION only: a read-only preflight that reports what this machine could do,
|
|
10
|
+
* plus the run/event shapes the later slices will use. Nothing here drives a screen, a mouse, a
|
|
11
|
+
* keyboard or a Maestro flow yet.
|
|
12
|
+
*/
|
|
13
|
+
/** The runner route the API calls (over the relay) to probe a machine. Read-only. */
|
|
14
|
+
export const NATIVE_PREFLIGHT_PATH = "/native/preflight";
|
|
15
|
+
/** The capability a runner advertises once it can answer {@link NATIVE_PREFLIGHT_PATH}. */
|
|
16
|
+
export const NATIVE_PREFLIGHT_CAPABILITY = "native.preflight";
|
|
17
|
+
export const NATIVE_BACKENDS = ["desktop", "maestro"];
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/native/preflight` (#1057, slice 1): what this machine could do for the `local_native` runtime
|
|
3
|
+
* — desktop control permissions and Maestro devices — WITHOUT doing any of it.
|
|
4
|
+
*
|
|
5
|
+
* Rules this module keeps:
|
|
6
|
+
* - READ-ONLY. It runs fixed probe commands and installs nothing. It never prompts for an OS
|
|
7
|
+
* permission either: on macOS the two checks are the "preflight" APIs that only report state
|
|
8
|
+
* (`CGPreflightScreenCaptureAccess`, `AXIsProcessTrusted`), not the "request" ones that open a
|
|
9
|
+
* system dialog. (`screencapture` is NOT used as the probe: without Screen Recording it still
|
|
10
|
+
* exits 0 and captures only the wallpaper, so its exit code proves nothing.)
|
|
11
|
+
* - NEVER THROWS. A missing tool, a timeout or garbage output becomes a state (`unknown`, not
|
|
12
|
+
* installed, no devices) and a stable error code with a sentence the owner can act on.
|
|
13
|
+
* - Parsing is pure and exported, so it is tested on recorded outputs — no live device needed.
|
|
14
|
+
*
|
|
15
|
+
* `adb devices` starts the adb server if it is not already running; that is adb's own behaviour
|
|
16
|
+
* when asked to list devices, not something installed or configured here.
|
|
17
|
+
*/
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { arch as osArch, platform as osPlatform } from "node:os";
|
|
20
|
+
/** Run one probe command; never rejects. */
|
|
21
|
+
export const execProbe = (cmd, args, timeoutMs = 8000) => new Promise((resolve) => {
|
|
22
|
+
execFile(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
23
|
+
const e = err;
|
|
24
|
+
if (e?.code === "ENOENT")
|
|
25
|
+
return resolve({ found: false, code: null, stdout: "", stderr: "" });
|
|
26
|
+
resolve({
|
|
27
|
+
found: true,
|
|
28
|
+
code: e ? (typeof e.code === "number" ? e.code : 1) : 0,
|
|
29
|
+
stdout: String(stdout ?? ""),
|
|
30
|
+
stderr: String(stderr ?? ""),
|
|
31
|
+
...(e?.killed ? { timedOut: true } : {}),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// ── Pure parsers ────────────────────────────────────────────────────────────
|
|
36
|
+
/** `osascript -l JavaScript` printing `true`/`false` → a permission state. */
|
|
37
|
+
export function parseBooleanProbe(r) {
|
|
38
|
+
if (!r.found || r.timedOut || r.code !== 0)
|
|
39
|
+
return "unknown";
|
|
40
|
+
const out = r.stdout.trim().toLowerCase();
|
|
41
|
+
return out === "true" ? "granted" : out === "false" ? "denied" : "unknown";
|
|
42
|
+
}
|
|
43
|
+
/** `maestro --version` → its version, or null when it printed nothing recognisable. */
|
|
44
|
+
export function parseMaestroVersion(r) {
|
|
45
|
+
if (!r.found || r.code !== 0)
|
|
46
|
+
return null;
|
|
47
|
+
const m = r.stdout.match(/\d+\.\d+(?:\.\d+)?/);
|
|
48
|
+
if (m)
|
|
49
|
+
return m[0];
|
|
50
|
+
// No version number: keep a short first line verbatim, and refuse a long one rather than cut it.
|
|
51
|
+
const first = r.stdout.trim().split("\n")[0] ?? "";
|
|
52
|
+
return first && first.length <= 40 ? first : null;
|
|
53
|
+
}
|
|
54
|
+
/** `adb devices -l` → attached devices (any state; `device` is the usable one). */
|
|
55
|
+
export function parseAdbDevices(stdout) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const line of stdout.split("\n")) {
|
|
58
|
+
const t = line.trim();
|
|
59
|
+
if (!t || t.startsWith("List of devices") || t.startsWith("*"))
|
|
60
|
+
continue;
|
|
61
|
+
const [id, state, ...rest] = t.split(/\s+/);
|
|
62
|
+
if (!id || !state)
|
|
63
|
+
continue;
|
|
64
|
+
const model = rest.find((p) => p.startsWith("model:"))?.slice("model:".length);
|
|
65
|
+
out.push({ id, kind: "android", name: model ? model.replace(/_/g, " ") : id, state });
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** `xcrun simctl list devices booted --json` → booted simulators. */
|
|
70
|
+
export function parseSimctlBooted(stdout) {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(stdout);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const devices = parsed?.devices;
|
|
79
|
+
if (!devices || typeof devices !== "object")
|
|
80
|
+
return [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const list of Object.values(devices)) {
|
|
83
|
+
if (!Array.isArray(list))
|
|
84
|
+
continue;
|
|
85
|
+
for (const d of list) {
|
|
86
|
+
if (typeof d?.udid !== "string" || d.state !== "Booted")
|
|
87
|
+
continue;
|
|
88
|
+
out.push({ id: d.udid, kind: "ios_simulator", name: typeof d.name === "string" ? d.name : d.udid, state: "Booted" });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** Combine the parts into one result with its error codes. Pure. */
|
|
94
|
+
export function summarizePreflight(input) {
|
|
95
|
+
const { screenRecording, accessibility } = input;
|
|
96
|
+
const states = [screenRecording, accessibility];
|
|
97
|
+
const permissionState = states.includes("unsupported")
|
|
98
|
+
? "unsupported"
|
|
99
|
+
: states.includes("denied")
|
|
100
|
+
? "denied"
|
|
101
|
+
: states.every((s) => s === "granted")
|
|
102
|
+
? "granted"
|
|
103
|
+
: "unknown";
|
|
104
|
+
const errors = [];
|
|
105
|
+
const add = (code, message) => errors.push({ code, message });
|
|
106
|
+
if (permissionState === "unsupported")
|
|
107
|
+
add("NATIVE_UNSUPPORTED_OS", `Desktop control is not supported on ${input.os} yet (macOS and Windows only).`);
|
|
108
|
+
if (permissionState === "denied") {
|
|
109
|
+
const missing = [screenRecording === "denied" ? "Screen Recording" : null, accessibility === "denied" ? "Accessibility" : null].filter(Boolean).join(" and ");
|
|
110
|
+
add("NATIVE_PERMISSIONS_MISSING", `This machine has not granted ${missing}. Grant it to the app that runs \`pags up\` (e.g. Terminal) in System Settings → Privacy & Security, then restart \`pags up\`.`);
|
|
111
|
+
}
|
|
112
|
+
if (!input.maestroInstalled)
|
|
113
|
+
add("NATIVE_MAESTRO_NOT_INSTALLED", "Maestro is not installed on this machine. The runner never installs it; install it yourself (https://maestro.mobile.dev) if you want phone or emulator automation.");
|
|
114
|
+
const usable = input.devices.filter((d) => d.state === "device" || d.state === "Booted");
|
|
115
|
+
if (input.maestroInstalled && usable.length === 0) {
|
|
116
|
+
add("NATIVE_DEVICE_NOT_FOUND", input.devices.length ? `No usable device: ${input.devices.map((d) => `${d.name} (${d.state})`).join(", ")}. Authorise or boot one.` : "No Android device/emulator or booted iOS simulator is connected.");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
os: input.os,
|
|
120
|
+
osVersion: input.osVersion,
|
|
121
|
+
arch: input.arch,
|
|
122
|
+
desktop: { screenRecording, accessibility, permissionState },
|
|
123
|
+
maestro: { installed: input.maestroInstalled, version: input.maestroVersion, adbInstalled: input.adbInstalled, simctlAvailable: input.simctlAvailable, connectedDevices: input.devices },
|
|
124
|
+
errors,
|
|
125
|
+
checkedAt: input.checkedAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ── The probe ───────────────────────────────────────────────────────────────
|
|
129
|
+
// CGPreflightScreenCaptureAccess is not in JXA's bridge metadata, so it is bound explicitly —
|
|
130
|
+
// without the bind the call is "not a function" and the state would read `unknown`.
|
|
131
|
+
const JXA_SCREEN = 'ObjC.import("CoreGraphics"); ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []]); $.CGPreflightScreenCaptureAccess()';
|
|
132
|
+
const JXA_AX = 'ObjC.import("ApplicationServices"); $.AXIsProcessTrusted()';
|
|
133
|
+
const PS_UIA = "Add-Type -AssemblyName UIAutomationClient; if ([System.Windows.Automation.AutomationElement]::RootElement) { 'true' } else { 'false' }";
|
|
134
|
+
export async function runNativePreflight(deps = {}) {
|
|
135
|
+
const exec = deps.exec ?? execProbe;
|
|
136
|
+
const platform = deps.platform ?? osPlatform();
|
|
137
|
+
const safe = (p) => p.catch(() => ({ found: false, code: null, stdout: "", stderr: "" }));
|
|
138
|
+
let osVersion = null;
|
|
139
|
+
let screenRecording = "unsupported";
|
|
140
|
+
let accessibility = "unsupported";
|
|
141
|
+
if (platform === "darwin") {
|
|
142
|
+
const [ver, screen, ax] = await Promise.all([safe(exec("sw_vers", ["-productVersion"])), safe(exec("osascript", ["-l", "JavaScript", "-e", JXA_SCREEN])), safe(exec("osascript", ["-l", "JavaScript", "-e", JXA_AX]))]);
|
|
143
|
+
osVersion = ver.code === 0 ? ver.stdout.trim() || null : null;
|
|
144
|
+
screenRecording = parseBooleanProbe(screen);
|
|
145
|
+
accessibility = parseBooleanProbe(ax);
|
|
146
|
+
}
|
|
147
|
+
else if (platform === "win32") {
|
|
148
|
+
// Windows has no per-app permission for screen capture or UI Automation: one availability probe.
|
|
149
|
+
const uia = parseBooleanProbe(await safe(exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_UIA], 15000)));
|
|
150
|
+
screenRecording = uia;
|
|
151
|
+
accessibility = uia;
|
|
152
|
+
}
|
|
153
|
+
const [maestro, adb, simctl] = await Promise.all([
|
|
154
|
+
safe(exec("maestro", ["--version"], 15000)),
|
|
155
|
+
safe(exec("adb", ["devices", "-l"])),
|
|
156
|
+
platform === "darwin" ? safe(exec("xcrun", ["simctl", "list", "devices", "booted", "--json"])) : Promise.resolve({ found: false, code: null, stdout: "", stderr: "" }),
|
|
157
|
+
]);
|
|
158
|
+
const devices = [...(adb.found && adb.code === 0 ? parseAdbDevices(adb.stdout) : []), ...(simctl.found && simctl.code === 0 ? parseSimctlBooted(simctl.stdout) : [])];
|
|
159
|
+
return summarizePreflight({
|
|
160
|
+
os: platform,
|
|
161
|
+
osVersion,
|
|
162
|
+
arch: deps.arch ?? osArch(),
|
|
163
|
+
screenRecording,
|
|
164
|
+
accessibility,
|
|
165
|
+
maestroVersion: parseMaestroVersion(maestro),
|
|
166
|
+
maestroInstalled: maestro.found && maestro.code === 0,
|
|
167
|
+
adbInstalled: adb.found,
|
|
168
|
+
simctlAvailable: simctl.found && simctl.code === 0,
|
|
169
|
+
devices,
|
|
170
|
+
checkedAt: (deps.now ?? Date.now)(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -5,6 +5,7 @@ import { probeGitSshIdentity } from "./coding/repo.js";
|
|
|
5
5
|
import { checkEngine } from "./coding/engine-check.js";
|
|
6
6
|
import { listGithubOrgs, listGithubRepos, searchGithubRepos, getGithubRepoDetail, getGithubCredentialScope } from "./coding/github-browse.js";
|
|
7
7
|
import { routeLocalApplyHandoff } from "./local-apply/handoff-routes.js";
|
|
8
|
+
import { runNativePreflight } from "./native/preflight.js";
|
|
8
9
|
import { routeLocalApplyReconciliation } from "./local-apply/reconciliation-routes.js";
|
|
9
10
|
export function createRunnerServer(runner) {
|
|
10
11
|
return createServer(async (req, res) => {
|
|
@@ -105,6 +106,11 @@ async function route(runner, req, res) {
|
|
|
105
106
|
if ((req.method === "GET" || req.method === "POST") && path === "/capabilities") {
|
|
106
107
|
return json(res, 200, runner.capabilities());
|
|
107
108
|
}
|
|
109
|
+
// #1057 slice 1: a READ-ONLY probe of what native desktop / Maestro control could do on this
|
|
110
|
+
// machine. Runs fixed probe commands, installs nothing, prompts for nothing, never throws.
|
|
111
|
+
if ((req.method === "GET" || req.method === "POST") && path === "/native/preflight") {
|
|
112
|
+
return json(res, 200, await runNativePreflight());
|
|
113
|
+
}
|
|
108
114
|
if (req.method === "GET" && path === "/sessions") {
|
|
109
115
|
return json(res, 200, { sessions: runner.store.listSessions() });
|
|
110
116
|
}
|