@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.
@@ -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
- throw new Error(`Refusing to clone into non-empty directory "${dir}" (no .git found) — move it aside or point at an empty path.`);
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 Error(`"${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.`);
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 Error(`Unknown coding action: ${action.kind}`);
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 Error(`No coding session: ${sessionId}`);
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 Error("A target must include a backend prefix like `tmux:main`, or pass `backend` separately.");
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 Error(`No tmux session "${t.id}".`);
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 Error("A `command` is required.");
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 Error(`No tmux session "${t.id}".`);
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 Error("Provide `text` and/or `keys` to send.");
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 Error(`No tmux session "${t.id}".`);
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 Error("iTerm2 generic key events are not supported yet; send text or run a command.");
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 Error("A `name` is required for tmux.");
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
- out.submitted = { url, at, gateId };
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
- event.url = url;
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
- const { detail: raw, ...rest } = ev;
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 Error(`Task is not waiting for approval: ${task.status}`);
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 Error("browser.open requires an http(s) url");
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 Error(`Unknown task type: ${task.type}`);
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 Error(`Task not found: ${id}`);
709
+ throw new RunnerInputError(`Task not found: ${id}`, 404);
710
710
  return task;
711
711
  }
712
712
  async getBrowserContext() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.142",
3
+ "version": "0.4.144",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",