@proagentstore/cli 0.4.31 → 0.4.32

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.
@@ -2,6 +2,7 @@
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { startRunnerServer } from "./server.js";
5
+ import { randomUUID } from "node:crypto";
5
6
  // Resilience: a stray error in any runtime must NOT take the whole runner down —
6
7
  // that drops the tunnel and forces the user to restart `pags up` (and lose their
7
8
  // session). Log it and keep serving. Per-component handlers catch most things;
@@ -29,7 +30,11 @@ function configFromArgs() {
29
30
  host: arg("--host", process.env.PAGS_RUNNER_HOST || "127.0.0.1") || "127.0.0.1",
30
31
  port: Number(arg("--port", process.env.PAGS_RUNNER_PORT || "49171")),
31
32
  dataDir,
32
- token: arg("--token", process.env.PAGS_RUNNER_TOKEN),
33
+ // Never start unauthenticated (#245). This surface drives a coding CLI with permissions
34
+ // skipped, and `authorize` now fails closed — so a missing token would make the runner
35
+ // answer nothing rather than answer everyone. Generate one and print it, mirroring what
36
+ // `pags runner connect` has always done.
37
+ token: arg("--token", process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`,
33
38
  instanceId: arg("--instance-id", process.env.PAGS_INSTANCE_ID),
34
39
  headless: flag("--headless") || process.env.PAGS_RUNNER_HEADLESS === "1",
35
40
  };
@@ -391,13 +391,35 @@ async function route(runner, req, res) {
391
391
  }
392
392
  return json(res, 404, { error: "Not found" });
393
393
  }
394
+ /**
395
+ * Authorize a request to the local runner (#245).
396
+ *
397
+ * This surface drives a coding CLI that `pags up` launches with
398
+ * `--dangerously-skip-permissions` / `--sandbox danger-full-access`, so "who may POST here" is
399
+ * the whole security boundary. Two things were the wrong way round:
400
+ *
401
+ * 1. **No token used to mean ALLOW.** `pags up` always generates one, so that path was safe —
402
+ * but `pags-browser-runner` run directly (its own --help documents this) passes
403
+ * `token: undefined`, and served the entire surface unauthenticated. Now it fails CLOSED;
404
+ * the standalone entrypoint generates a token instead of starting open.
405
+ *
406
+ * 2. **A browser could reach it.** Binding to loopback is not isolation: a page the user is
407
+ * visiting cannot READ a cross-origin response, but it can still SEND the POST, and the
408
+ * server sets no CORS headers and did no Origin check. The token already made that
409
+ * unguessable — but nothing legitimate that calls this runner is a browser (the cloud
410
+ * dispatches over the relay; the CLI calls it directly), and neither sends `Origin`. So the
411
+ * presence of that header is by itself proof the caller is a web page, and is refused before
412
+ * the token is even considered. Also closes DNS-rebinding, which loopback does not.
413
+ */
394
414
  function authorize(req, config) {
415
+ if (req.headers.origin)
416
+ return false;
395
417
  const token = config.token;
396
418
  if (config.instanceId && req.headers["x-pags-instance-id"] !== config.instanceId) {
397
419
  return false;
398
420
  }
399
421
  if (!token)
400
- return true;
422
+ return false;
401
423
  const auth = req.headers.authorization || "";
402
424
  const headerToken = req.headers["x-pags-runner-token"];
403
425
  return auth === `Bearer ${token}` || headerToken === token;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",