@proagentstore/cli 0.4.142 → 0.4.144
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/repo.js +4 -2
- package/dist/browser-runner/coding/runtime.js +3 -2
- package/dist/browser-runner/coding/terminal.js +9 -8
- package/dist/browser-runner/local-apply/contract.js +6 -4
- package/dist/browser-runner/local-browser/contract.js +21 -2
- package/dist/browser-runner/local-browser/runtime.js +5 -3
- package/dist/browser-runner/runner.js +4 -4
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { RunnerInputError } from "../errors.js";
|
|
4
5
|
/**
|
|
5
6
|
* Inspect a workdir: does it exist, does it hold anything, is it a checkout. Never throws —
|
|
6
7
|
* every failure is a `false`, because this function exists to REPORT a broken path and one that
|
|
@@ -175,10 +176,11 @@ export function ensureRepo(dir, opts = {}) {
|
|
|
175
176
|
// The owner's own folder (a monorepo subfolder, a plain project) — run in it untouched.
|
|
176
177
|
if (opts.ownFolder)
|
|
177
178
|
return dir;
|
|
178
|
-
|
|
179
|
+
// A refusal about the owner's folder, answered as one (#1116) — never a runner crash.
|
|
180
|
+
throw new RunnerInputError(`Refusing to clone into non-empty directory "${dir}" (no .git found) — move it aside or point at an empty path.`, 409);
|
|
179
181
|
}
|
|
180
182
|
if (opts.ownFolder && checkWorkdir(dir).insideWorkTree) {
|
|
181
|
-
throw new
|
|
183
|
+
throw new RunnerInputError(`"${dir}" is an empty folder inside another git checkout — not cloning a second repository into it. Point this repo at the checkout itself, or remove the folder.`, 409);
|
|
182
184
|
}
|
|
183
185
|
rmSync(dir, { recursive: true, force: true });
|
|
184
186
|
}
|
|
@@ -314,7 +314,7 @@ export class CodingRuntime {
|
|
|
314
314
|
session.interrupt();
|
|
315
315
|
break;
|
|
316
316
|
default:
|
|
317
|
-
throw new
|
|
317
|
+
throw new RunnerInputError(`Unknown coding action: ${action.kind}`);
|
|
318
318
|
}
|
|
319
319
|
return this.snapshot(sessionId);
|
|
320
320
|
}
|
|
@@ -472,8 +472,9 @@ export class CodingRuntime {
|
|
|
472
472
|
}
|
|
473
473
|
require(sessionId) {
|
|
474
474
|
const session = this.sessions.get(sessionId);
|
|
475
|
+
// A caller naming a session this runner does not hold is a lookup, not a crash (#1116): 404, with the same sentence.
|
|
475
476
|
if (!session)
|
|
476
|
-
throw new
|
|
477
|
+
throw new RunnerInputError(`No coding session: ${sessionId}`, 404);
|
|
477
478
|
return session;
|
|
478
479
|
}
|
|
479
480
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { RunnerInputError } from "../errors.js";
|
|
4
5
|
import { capturePane, createSession, killSession, listSessionsDetailed, runCommand as tmuxRunCommand, sendKey as tmuxSendKey, sendText as tmuxSendText, sessionExists, tmuxExec } from "./tmux.js";
|
|
5
6
|
function shellPath() {
|
|
6
7
|
return process.env.SHELL || "/bin/zsh";
|
|
@@ -14,7 +15,7 @@ export function splitTerminalTarget(raw, fallback) {
|
|
|
14
15
|
if (m)
|
|
15
16
|
return { backend: m[1].toLowerCase(), id: m[2].trim() };
|
|
16
17
|
if (!fallback)
|
|
17
|
-
throw new
|
|
18
|
+
throw new RunnerInputError("A target must include a backend prefix like `tmux:main`, or pass `backend` separately.");
|
|
18
19
|
return { backend: fallback, id: value };
|
|
19
20
|
}
|
|
20
21
|
export function listTerminalTargets(backend = "all") {
|
|
@@ -43,7 +44,7 @@ export function captureTerminalTarget(target, opts = {}) {
|
|
|
43
44
|
switch (t.backend) {
|
|
44
45
|
case "tmux":
|
|
45
46
|
if (!sessionExists(t.id))
|
|
46
|
-
throw new
|
|
47
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
47
48
|
return capturePane(t.id, Math.min(Math.max(Number(opts.lines) || 200, 1), 2000));
|
|
48
49
|
case "kitty":
|
|
49
50
|
return kittyExec(["@", "get-text", "--match", `id:${t.id}`, "--extent", "all"], 5000);
|
|
@@ -227,11 +228,11 @@ function kittyForegroundCommand(id) {
|
|
|
227
228
|
export function runTerminalCommand(target, command, backend) {
|
|
228
229
|
const t = splitTerminalTarget(target, backend);
|
|
229
230
|
if (!command.trim())
|
|
230
|
-
throw new
|
|
231
|
+
throw new RunnerInputError("A `command` is required.");
|
|
231
232
|
switch (t.backend) {
|
|
232
233
|
case "tmux":
|
|
233
234
|
if (!sessionExists(t.id))
|
|
234
|
-
throw new
|
|
235
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
235
236
|
tmuxRunCommand(t.id, command);
|
|
236
237
|
return capturePane(t.id, 200);
|
|
237
238
|
case "kitty":
|
|
@@ -246,11 +247,11 @@ export function runTerminalCommand(target, command, backend) {
|
|
|
246
247
|
export function sendTerminalKeys(target, opts) {
|
|
247
248
|
const t = splitTerminalTarget(target, opts.backend);
|
|
248
249
|
if (opts.text == null && (!opts.keys || opts.keys.length === 0))
|
|
249
|
-
throw new
|
|
250
|
+
throw new RunnerInputError("Provide `text` and/or `keys` to send.");
|
|
250
251
|
switch (t.backend) {
|
|
251
252
|
case "tmux":
|
|
252
253
|
if (!sessionExists(t.id))
|
|
253
|
-
throw new
|
|
254
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
254
255
|
if (opts.text != null)
|
|
255
256
|
tmuxSendText(t.id, opts.text);
|
|
256
257
|
for (const key of opts.keys ?? [])
|
|
@@ -264,7 +265,7 @@ export function sendTerminalKeys(target, opts) {
|
|
|
264
265
|
return captureTerminalTarget(t.id, { backend: "kitty" });
|
|
265
266
|
case "iterm2":
|
|
266
267
|
if ((opts.keys ?? []).length > 0)
|
|
267
|
-
throw new
|
|
268
|
+
throw new RunnerInputError("iTerm2 generic key events are not supported yet; send text or run a command.");
|
|
268
269
|
itermSessionScript(t.id, `tell theSession to write text ${appleString(opts.text ?? "")}`);
|
|
269
270
|
return captureTerminalTarget(t.id, { backend: "iterm2" });
|
|
270
271
|
}
|
|
@@ -275,7 +276,7 @@ export function createTerminalTarget(opts) {
|
|
|
275
276
|
case "tmux": {
|
|
276
277
|
const name = String(opts.name || "").trim();
|
|
277
278
|
if (!name)
|
|
278
|
-
throw new
|
|
279
|
+
throw new RunnerInputError("A `name` is required for tmux.");
|
|
279
280
|
if (sessionExists(name))
|
|
280
281
|
return { backend: "tmux", id: name, name, existed: true, workDir };
|
|
281
282
|
createSession(name, workDir, opts.command);
|
|
@@ -262,9 +262,11 @@ export function parseLocalApplyResult(raw) {
|
|
|
262
262
|
const gateId = str(s.gateId, 100);
|
|
263
263
|
if (mode !== "auto_submit")
|
|
264
264
|
return { error: "a submitted outcome requires a run in auto_submit mode" };
|
|
265
|
-
if (!url || !isHttpUrl(url) || !at || Number.isNaN(Date.parse(at)) || !gateId)
|
|
265
|
+
if (!url || !isHttpUrl(url) || !traceUrl(url) || !at || Number.isNaN(Date.parse(at)) || !gateId)
|
|
266
266
|
return { error: "a submitted outcome needs the confirmed url, its time and the gateId" };
|
|
267
|
-
|
|
267
|
+
// A page URL kept as a fact keeps origin and path only, as a trace's does (#1069): a confirmation
|
|
268
|
+
// page's query can carry session or application tokens, and the receipt is the page, not its query.
|
|
269
|
+
out.submitted = { url: traceUrl(url), at, gateId };
|
|
268
270
|
out.submitAttempted = true;
|
|
269
271
|
}
|
|
270
272
|
// #975 — accepted ONLY as the closed vocabulary it declares. A runner that sends prose here gets
|
|
@@ -296,10 +298,10 @@ export function parseLocalApplyResult(raw) {
|
|
|
296
298
|
const reason = oneOf(LOCAL_APPLY_UNAVAILABLE_REASONS, evidence.reason);
|
|
297
299
|
const url = str(evidence.url, 2000);
|
|
298
300
|
const observedAt = str(evidence.observedAt, 40);
|
|
299
|
-
if (!reason || !url || !isHttpUrl(url) || !observedAt || Number.isNaN(Date.parse(observedAt)) || evidence.source !== "page_notice") {
|
|
301
|
+
if (!reason || !url || !isHttpUrl(url) || !traceUrl(url) || !observedAt || Number.isNaN(Date.parse(observedAt)) || evidence.source !== "page_notice") {
|
|
300
302
|
return { error: "a job_unavailable result needs runner-verified unavailable evidence (reason, url, observedAt, source)" };
|
|
301
303
|
}
|
|
302
|
-
out.unavailable = { reason, url, observedAt, source: "page_notice" };
|
|
304
|
+
out.unavailable = { reason, url: traceUrl(url), observedAt, source: "page_notice" };
|
|
303
305
|
}
|
|
304
306
|
else if (o.unavailable !== undefined)
|
|
305
307
|
return { error: "unavailable evidence is only valid for a job_unavailable result" };
|
|
@@ -122,6 +122,20 @@ export function redactDetail(detail, depth = 0, secrets = []) {
|
|
|
122
122
|
const str = (v, max) => (typeof v === "string" && v.trim() ? v.trim().slice(0, max) : null);
|
|
123
123
|
const isHttpUrl = (v) => /^https?:\/\/[^\s/]+/i.test(v);
|
|
124
124
|
const oneOf = (list, v) => (list.includes(v) ? v : null);
|
|
125
|
+
/**
|
|
126
|
+
* A page URL as a trace may keep it (#1069): origin and path — no query, no fragment, no credentials
|
|
127
|
+
* (`origin` never carries a `user:pass@`). The same rule as the local-apply contract's `traceUrl`;
|
|
128
|
+
* restated here because each vendored contract imports nothing. Empty when it is not an http(s) URL.
|
|
129
|
+
*/
|
|
130
|
+
export function traceUrl(raw) {
|
|
131
|
+
try {
|
|
132
|
+
const u = new URL(raw);
|
|
133
|
+
return u.protocol === "http:" || u.protocol === "https:" ? `${u.origin}${u.pathname}` : "";
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
125
139
|
/** A validated event, or null. */
|
|
126
140
|
export function parseLocalBrowserEvent(raw) {
|
|
127
141
|
if (!raw || typeof raw !== "object")
|
|
@@ -132,9 +146,14 @@ export function parseLocalBrowserEvent(raw) {
|
|
|
132
146
|
if (!type || !at || Number.isNaN(Date.parse(at)))
|
|
133
147
|
return null;
|
|
134
148
|
const event = { type, at };
|
|
149
|
+
// Origin and path only (#1069): a research trace keeps where the browser went, never a page's query
|
|
150
|
+
// or fragment — a sign-in page's OAuth state/nonce/PKCE values or a session id ride there.
|
|
135
151
|
const url = str(o.url, 2000);
|
|
136
|
-
if (url && isHttpUrl(url))
|
|
137
|
-
|
|
152
|
+
if (url && isHttpUrl(url)) {
|
|
153
|
+
const kept = traceUrl(url);
|
|
154
|
+
if (kept)
|
|
155
|
+
event.url = kept;
|
|
156
|
+
}
|
|
138
157
|
const domain = str(o.domain, 253);
|
|
139
158
|
if (domain)
|
|
140
159
|
event.domain = domain.toLowerCase();
|
|
@@ -27,7 +27,7 @@ import { RunnerInputError } from "../errors.js";
|
|
|
27
27
|
import { BrowserBridge, domainWithin } from "./bridge.js";
|
|
28
28
|
import { SCOUT_SHOT_CAP, ShotRecorder } from "./replay.js";
|
|
29
29
|
import { BRIDGE_ENV } from "./bridge-stdio.js";
|
|
30
|
-
import { LOCAL_BROWSER_AUTH_MODES, LOCAL_BROWSER_ENGINES, LOCAL_BROWSER_TASK_TYPE, redactDetail, redactFinding, redactSourceFailure, redactText, secretEnvValues, } from "./contract.js";
|
|
30
|
+
import { LOCAL_BROWSER_AUTH_MODES, LOCAL_BROWSER_ENGINES, LOCAL_BROWSER_TASK_TYPE, redactDetail, redactFinding, redactSourceFailure, redactText, secretEnvValues, traceUrl, } from "./contract.js";
|
|
31
31
|
import { bridgeUnused, buildEngineSpec, finalText, missingLogin, observedEngineAuth, researchPrompt, signInHelp } from "./engine.js";
|
|
32
32
|
/**
|
|
33
33
|
* Why the run could not be launched, in words the owner can act on (#944).
|
|
@@ -303,9 +303,11 @@ export class LocalBrowserRuntime {
|
|
|
303
303
|
emit(run, ev) {
|
|
304
304
|
if (run.events.length >= MAX_EVENTS)
|
|
305
305
|
return;
|
|
306
|
-
|
|
306
|
+
// Origin and path only, before anything leaves the machine (#1069) — the cloud reduces it again.
|
|
307
|
+
const { detail: raw, url: rawUrl, ...rest } = ev;
|
|
308
|
+
const url = rawUrl ? traceUrl(rawUrl) : "";
|
|
307
309
|
const detail = redactDetail(raw, 0, run.secrets);
|
|
308
|
-
run.events.push({ ...rest, ...(detail && Object.keys(detail).length ? { detail } : {}), at: new Date(this.now()).toISOString(), seq: ++run.seq });
|
|
310
|
+
run.events.push({ ...rest, ...(url ? { url } : {}), ...(detail && Object.keys(detail).length ? { detail } : {}), at: new Date(this.now()).toISOString(), seq: ++run.seq });
|
|
309
311
|
}
|
|
310
312
|
pause(run, reason, detail) {
|
|
311
313
|
if (run.state === "ended" || run.cancelled)
|
|
@@ -242,7 +242,7 @@ export class LocalRunner {
|
|
|
242
242
|
async approveTask(id) {
|
|
243
243
|
const task = this.requireTask(id);
|
|
244
244
|
if (task.status !== "needs_approval") {
|
|
245
|
-
throw new
|
|
245
|
+
throw new RunnerInputError(`Task is not waiting for approval: ${task.status}`, 409);
|
|
246
246
|
}
|
|
247
247
|
task.status = "queued";
|
|
248
248
|
task.updatedAt = new Date().toISOString();
|
|
@@ -662,7 +662,7 @@ export class LocalRunner {
|
|
|
662
662
|
if (task.type === "browser.open") {
|
|
663
663
|
const url = String(task.input.url || "");
|
|
664
664
|
if (!url || !/^https?:\/\//.test(url)) {
|
|
665
|
-
throw new
|
|
665
|
+
throw new RunnerInputError("browser.open requires an http(s) url");
|
|
666
666
|
}
|
|
667
667
|
const context = await this.getBrowserContext();
|
|
668
668
|
const page = context.pages()[0] || (await context.newPage());
|
|
@@ -701,12 +701,12 @@ export class LocalRunner {
|
|
|
701
701
|
this.addTaskEvent(task, "browser.goto.completed", "Browser page loaded", output);
|
|
702
702
|
return output;
|
|
703
703
|
}
|
|
704
|
-
throw new
|
|
704
|
+
throw new RunnerInputError(`Unknown task type: ${task.type}`);
|
|
705
705
|
}
|
|
706
706
|
requireTask(id) {
|
|
707
707
|
const task = this.store.getTask(id);
|
|
708
708
|
if (!task)
|
|
709
|
-
throw new
|
|
709
|
+
throw new RunnerInputError(`Task not found: ${id}`, 404);
|
|
710
710
|
return task;
|
|
711
711
|
}
|
|
712
712
|
async getBrowserContext() {
|