@proagentstore/cli 0.4.153 → 0.4.155
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 +41 -1
- package/dist/browser-runner/coding/engine-env.js +21 -2
- package/dist/browser-runner/coding/engine-spawn.js +20 -1
- package/dist/browser-runner/coding/grok-continuity.js +53 -0
- package/dist/browser-runner/coding/headless-state.js +1 -1
- package/dist/browser-runner/coding/headless.js +27 -1
- package/dist/browser-runner/index.js +4 -0
- package/dist/index.js +69 -47
- package/package.json +1 -1
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { unwrapLauncher } from "./launcher-wrapper.js";
|
|
3
|
-
import { statSync } from "node:fs";
|
|
3
|
+
import { realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { handlerFor } from "./handlers.js";
|
|
7
7
|
import { parseCommand } from "./headless.js";
|
|
8
8
|
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
9
|
+
function safeRealpath(p) {
|
|
10
|
+
try {
|
|
11
|
+
return realpathSync(p);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function defaultVersionText(launch, env) {
|
|
18
|
+
// The file the check FOUND, not the bare name: off Windows `command` is the name as given, which spawnSync would
|
|
19
|
+
// resolve against this process's PATH rather than the one being checked — and report another binary's version.
|
|
20
|
+
const command = launch.via === "direct" && launch.binaryPath ? launch.binaryPath : launch.command;
|
|
21
|
+
const r = spawnSync(command, launch.args, { encoding: "utf8", env, timeout: 5_000, windowsHide: true, windowsVerbatimArguments: launch.windowsVerbatimArguments });
|
|
22
|
+
return r.error || r.status !== 0 ? null : `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
/** `codex-cli 0.146.0`, `2.1.3 (Claude Code)`, `grok 1.0.62 (1474f9cbdd91)` → the version. Null when there is none. */
|
|
25
|
+
export function parseEngineVersion(text) {
|
|
26
|
+
return text?.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/)?.[0] ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** Whose `grok` answered: xAI's names itself; the community npm one prints a bare version from a node_modules script. */
|
|
29
|
+
export function grokVendor(text, binaryPath) {
|
|
30
|
+
if (text && /^\s*grok \d+\.\d+/m.test(text))
|
|
31
|
+
return "xai";
|
|
32
|
+
if (/[\\/]node_modules[\\/]|@vibe-kit/.test(binaryPath ?? ""))
|
|
33
|
+
return "community";
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
9
36
|
function defaultExists(p) {
|
|
10
37
|
try {
|
|
11
38
|
statSync(p);
|
|
@@ -52,6 +79,14 @@ export function detectEngineLogin(engine, deps = {}) {
|
|
|
52
79
|
}
|
|
53
80
|
return { login: "missing", where: file };
|
|
54
81
|
}
|
|
82
|
+
if (engine === "grok") {
|
|
83
|
+
// xAI's Grok CLI keeps its subscription login (grok.com / accounts.x.ai) in auth.json under GROK_HOME, default
|
|
84
|
+
// ~/.grok. An external auth provider or OIDC setup signs in another way, so absence is not proof there.
|
|
85
|
+
if (env.GROK_AUTH_PROVIDER_COMMAND?.trim() || env.GROK_OIDC_ISSUER?.trim() || env.GROK_DEPLOYMENT_KEY?.trim())
|
|
86
|
+
return { login: "unknown" };
|
|
87
|
+
const file = join(env.GROK_HOME?.trim() || join(home, ".grok"), "auth.json");
|
|
88
|
+
return { login: exists(file) ? "found" : "missing", where: file };
|
|
89
|
+
}
|
|
55
90
|
if (engine === "gemini") {
|
|
56
91
|
// Google sign-in lands here; Gemini also accepts other auth (gcloud, Vertex), so absence is
|
|
57
92
|
// not proof of "signed out".
|
|
@@ -73,6 +108,9 @@ export function checkEngine(input, deps = {}) {
|
|
|
73
108
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
74
109
|
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
75
110
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
111
|
+
// Grok's version is what tells xAI's CLI from a same-named npm package, so it is always read for Grok (a native
|
|
112
|
+
// binary, milliseconds); the others only when asked (coding_engine_get), because each is a Node startup.
|
|
113
|
+
const versionText = binaryPath && (engine === "grok" || input.withVersion) ? (deps.versionText ?? defaultVersionText)(resolveEngineSpawn(bin, ["--version"], deps), deps.env ?? process.env) : null;
|
|
76
114
|
return {
|
|
77
115
|
checked: true,
|
|
78
116
|
engine,
|
|
@@ -81,5 +119,7 @@ export function checkEngine(input, deps = {}) {
|
|
|
81
119
|
...(binaryPath ? { binaryPath, binaryVia: via } : {}),
|
|
82
120
|
login: login.login,
|
|
83
121
|
...(login.where ? { loginWhere: login.where } : {}),
|
|
122
|
+
...(binaryPath && (engine === "grok" || input.withVersion) ? { version: parseEngineVersion(versionText) } : {}),
|
|
123
|
+
...(engine === "grok" && binaryPath ? { vendor: grokVendor(versionText, (deps.realpath ?? safeRealpath)(binaryPath)) } : {}),
|
|
84
124
|
};
|
|
85
125
|
}
|
|
@@ -6,12 +6,29 @@
|
|
|
6
6
|
* `headless.ts` sat one line under its size pin — so the concern gets a file rather than the pin
|
|
7
7
|
* getting a raise. `engine-auth.ts` READS this env; this builds it.
|
|
8
8
|
*
|
|
9
|
-
* 1. {@link mergeEnv} — the platform's overlay over the machine's, where empty means REMOVE
|
|
9
|
+
* 1. {@link mergeEnv} — the platform's overlay over the machine's, where empty means REMOVE, and
|
|
10
|
+
* never the platform's own credentials ({@link PLATFORM_CREDENTIAL_ENV}, #1163).
|
|
10
11
|
* 2. {@link engineSpawnEnv} — that, plus the `gh` guard ahead of the real binary on `PATH`, and the
|
|
11
12
|
* `git` stash guard (#1021) ahead of both.
|
|
12
13
|
*/
|
|
13
14
|
import { ghGuardEnv } from "./gh-guard.js";
|
|
14
15
|
import { gitGuardEnv } from "./git-guard.js";
|
|
16
|
+
/**
|
|
17
|
+
* The platform's own credentials, which no engine may inherit (#1163).
|
|
18
|
+
*
|
|
19
|
+
* An engine runs with its permission prompts off, on repositories and issues that carry untrusted
|
|
20
|
+
* text, so a prompt injection ("run `curl -d $PAGS_TOKEN …`") gets whatever its environment holds.
|
|
21
|
+
* `PAGS_TOKEN` is the owner's 30-day ACCOUNT session — every agent, and through relay-token minting
|
|
22
|
+
* every machine. `PAGS_RUNNER_TOKEN` is this runtime's own bearer and `PAGS_RUNNER_CONTROL_NONCE` its
|
|
23
|
+
* shutdown credential. None of them is the engine's to use.
|
|
24
|
+
*/
|
|
25
|
+
export const PLATFORM_CREDENTIAL_ENV = ["PAGS_TOKEN", "PAGS_RUNNER_TOKEN", "PAGS_RUNNER_CONTROL_NONCE"];
|
|
26
|
+
/** `env` without {@link PLATFORM_CREDENTIAL_ENV}, in place — what the runtime does to its own env at start. */
|
|
27
|
+
export function dropPlatformCredentials(env) {
|
|
28
|
+
for (const name of PLATFORM_CREDENTIAL_ENV)
|
|
29
|
+
delete env[name];
|
|
30
|
+
return env;
|
|
31
|
+
}
|
|
15
32
|
/**
|
|
16
33
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
17
34
|
* REMOVE rather than "set to empty".
|
|
@@ -30,7 +47,9 @@ export function mergeEnv(base, overlay) {
|
|
|
30
47
|
else
|
|
31
48
|
out[k] = v;
|
|
32
49
|
}
|
|
33
|
-
|
|
50
|
+
// Every engine's env is built here — coding, local-browser, local-artifact — so this is the one place
|
|
51
|
+
// the platform's credentials are kept out of all of them, whatever the machine or an overlay holds (#1163).
|
|
52
|
+
return dropPlatformCredentials(out);
|
|
34
53
|
}
|
|
35
54
|
/**
|
|
36
55
|
* The env a turn is spawned with: the merged env, with the `gh` write guard on `PATH` when the
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { posix, win32 } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* xAI's Grok CLI for a bare `grok`, wherever PATH points (#1156). Its installer (x.ai/cli/install.sh, install.ps1) always
|
|
6
|
+
* puts it in `~/.grok/bin` (`GROK_BIN_DIR` overrides), and adds that to PATH only in the SHELL's profile — so a
|
|
7
|
+
* `pags up` started by launchd, systemd, a Scheduled Task or an older terminal does not see it. And an npm package of
|
|
8
|
+
* the same name, the community `@vibe-kit/grok-cli` (API-key billed, different flags), can sit earlier on PATH: on the
|
|
9
|
+
* Mac mini it did, in nvm's bin. The preset means xAI's CLI, so its own location wins when it is there. An explicit path
|
|
10
|
+
* in a preset is the owner's choice and is never rewritten.
|
|
11
|
+
*/
|
|
12
|
+
export function officialEngineBinary(bin, deps = {}) {
|
|
13
|
+
if (bin !== "grok")
|
|
14
|
+
return null;
|
|
15
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const dir = envVar(env, "GROK_BIN_DIR", windows)?.trim() || (windows ? win32 : posix).join(deps.home ?? homedir(), ".grok", "bin");
|
|
18
|
+
const file = (windows ? win32 : posix).join(dir, windows ? "grok.exe" : "grok");
|
|
19
|
+
return (deps.isExecutable ?? defaultIsExecutable)(file) ? file : null;
|
|
20
|
+
}
|
|
3
21
|
function defaultIsExecutable(p) {
|
|
4
22
|
try {
|
|
5
23
|
if (!statSync(p).isFile())
|
|
@@ -67,7 +85,8 @@ export function quoteCmdArg(arg) {
|
|
|
67
85
|
a = `"${a}"`;
|
|
68
86
|
return a.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
|
|
69
87
|
}
|
|
70
|
-
export function resolveEngineSpawn(
|
|
88
|
+
export function resolveEngineSpawn(requested, args, deps = {}) {
|
|
89
|
+
const bin = officialEngineBinary(requested, deps) ?? requested;
|
|
71
90
|
const windows = (deps.platform ?? process.platform) === "win32";
|
|
72
91
|
if (!windows)
|
|
73
92
|
return { command: bin, args, binaryPath: resolveBinary(bin, deps), via: "direct" };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok keeps ITS OWN conversation across one-shot turns (#1156) — by an id the runner chooses.
|
|
3
|
+
*
|
|
4
|
+
* Every Grok turn is a new `grok … -p "<turn>"` process, so without this each turn started with no memory of the last
|
|
5
|
+
* one but the platform's replay. `-c/--continue` was the CLI's only offer when the preset was written, and it picks the
|
|
6
|
+
* MOST RECENT session in the directory — another `grok` run in the same checkout (the owner's own) would answer instead
|
|
7
|
+
* (`coding-engines.ts`, "NO PRESET RESUMES"). xAI's Grok CLI now takes an explicit id, so continuity is exact:
|
|
8
|
+
*
|
|
9
|
+
* first turn `-s <uuid>` a NEW conversation under an id the runner made up (1.x: must be a UUID, must not exist);
|
|
10
|
+
* later turns `-r <uuid>` resume exactly that one (errors if it does not exist — never "the most recent").
|
|
11
|
+
*
|
|
12
|
+
* Checked against `grok --help` of xAI's Grok CLI 1.0.62 (2026-10-18). 0.2.x also accepts both: there `-s` creates-or-
|
|
13
|
+
* resumes and `-r` errors on an unknown id. NOT yet exercised end to end — the CLI refuses every turn until it is signed
|
|
14
|
+
* in, and it was not on the machine this was written on — so the runner treats it as unproven: a turn whose flags are
|
|
15
|
+
* rejected is retried once without them and the session runs without continuity, and the platform's replay is still
|
|
16
|
+
* sent with every turn (`holdsConversationThisTurn` stays false for Grok) until a signed-in run proves it.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
const UUID_RE = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
|
20
|
+
/** Only a UUID ever reaches argv — anything else read back from the state file is discarded. */
|
|
21
|
+
export function isGrokConversationId(value) {
|
|
22
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
23
|
+
}
|
|
24
|
+
export function newGrokConversation(makeId = randomUUID) {
|
|
25
|
+
return { id: makeId(), started: false };
|
|
26
|
+
}
|
|
27
|
+
/** The turn's argv: the continuity flag first (any order parses), then the preset, then the turn as `-p`'s value. */
|
|
28
|
+
export function grokTurnArgs(userArgs, turnText, conversation) {
|
|
29
|
+
if (!conversation)
|
|
30
|
+
return [...userArgs, turnText];
|
|
31
|
+
return [conversation.started ? "-r" : "-s", conversation.id, ...userArgs, turnText];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The CLI refused the continuity flags themselves — an argument error, printed before it signs in or runs anything (the
|
|
35
|
+
* argument parser runs first). Narrow on purpose: retrying any other failure could run a real instruction twice.
|
|
36
|
+
*/
|
|
37
|
+
// Only a line that BEGINS with the CLI's own `error:` (clap's argument errors), and only about these flags or the id —
|
|
38
|
+
// a tool's output in a failed turn that merely mentions a session must never cause a same-turn retry.
|
|
39
|
+
const REJECTED_RE = /^\s*error:\s(?:.*unexpected argument '?-(?:s|r)\b|.*(?:--session-id|--resume)\b|.*must be a valid uuid|.*session\b.*\b(?:not found|already exists|does not exist))/i;
|
|
40
|
+
export function grokRejectedContinuity(lines) {
|
|
41
|
+
return lines.some((l) => REJECTED_RE.test(l));
|
|
42
|
+
}
|
|
43
|
+
/** What a finished turn means for the conversation. Pure. */
|
|
44
|
+
export function afterGrokTurn(conversation, outcome) {
|
|
45
|
+
// The flags themselves were refused before anything ran: run without continuity from now on.
|
|
46
|
+
if (outcome.code && outcome.rejected)
|
|
47
|
+
return { next: null, persist: null, disable: true };
|
|
48
|
+
// Done: the CLI holds the conversation now. Remember it, so the next turn — and the next runner start — resume it.
|
|
49
|
+
if (outcome.code === 0)
|
|
50
|
+
return { next: { ...conversation, started: true }, persist: conversation.id, disable: false };
|
|
51
|
+
// Any other failure: the conversation may be half-made or unknown. Forget it, so the next turn starts a clean one.
|
|
52
|
+
return { next: null, persist: null, disable: false };
|
|
53
|
+
}
|
|
@@ -5,7 +5,7 @@ export function isCodexThreadId(value) {
|
|
|
5
5
|
return typeof value === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value);
|
|
6
6
|
}
|
|
7
7
|
function stateKey(id, engine) {
|
|
8
|
-
return engine === "
|
|
8
|
+
return engine === "claude" ? id : `${engine}:${id}`;
|
|
9
9
|
}
|
|
10
10
|
function loadFile(path) {
|
|
11
11
|
if (!path || !existsSync(path))
|
|
@@ -14,6 +14,7 @@ import { handlerFor } from "./handlers.js";
|
|
|
14
14
|
import { resolveEngineAuth } from "./engine-auth.js";
|
|
15
15
|
import { buildCodexResumeArgs, engineAdapterFor, engineInvocationModeFromAdapter, engineInvocationWarning, genericRawEngineAdapter } from "./engine-adapter.js";
|
|
16
16
|
import { isCodexThreadId, readCodexState, readState, writeState } from "./headless-state.js";
|
|
17
|
+
import { afterGrokTurn, grokRejectedContinuity, grokTurnArgs, isGrokConversationId, newGrokConversation } from "./grok-continuity.js";
|
|
17
18
|
export { buildClaudeArgs } from "./engine-adapter.js";
|
|
18
19
|
export { defaultStatePath } from "./headless-state.js";
|
|
19
20
|
/**
|
|
@@ -70,6 +71,10 @@ export class HeadlessSession {
|
|
|
70
71
|
claudeSessionId = null;
|
|
71
72
|
/** Codex's thread id from `thread.started` — used only by the explicit-ID resume path (#848). */
|
|
72
73
|
codexThreadId = null;
|
|
74
|
+
/** Grok's own conversation across one-shot turns (#1156, grok-continuity.ts); null = the next turn starts one. */
|
|
75
|
+
grokConversation = null;
|
|
76
|
+
/** Off once this machine's CLI refused the continuity flags: the session then runs as before, turn by turn. */
|
|
77
|
+
grokContinuity = true;
|
|
73
78
|
/**
|
|
74
79
|
* The cloud's context brief, until the first turn spends it (ADR 0005, #693). Null once
|
|
75
80
|
* delivered, and null from the start when the engine resumed its own conversation.
|
|
@@ -270,6 +275,8 @@ export class HeadlessSession {
|
|
|
270
275
|
config.clientType === "codex" && this.mode === "stream-json" && !config.fresh
|
|
271
276
|
? readCodexState(config.statePath, config.id) ?? (config.resumeFrom ? readCodexState(config.statePath, config.resumeFrom) : null)
|
|
272
277
|
: null;
|
|
278
|
+
const storedGrok = config.clientType === "grok" && !config.fresh ? (readState(config.statePath, config.id, "grok") ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom, "grok") : null)) : null;
|
|
279
|
+
this.grokConversation = isGrokConversationId(storedGrok) ? { id: storedGrok, started: true } : null;
|
|
273
280
|
// AFTER both lines above, because `resumedConversation` reads them: the brief is the fallback,
|
|
274
281
|
// so an engine that found its own conversation drops it unread rather than being handed a
|
|
275
282
|
// summary of the conversation it is already in.
|
|
@@ -590,9 +597,15 @@ export class HeadlessSession {
|
|
|
590
597
|
this.structuredOutputRejected = false;
|
|
591
598
|
const resumedCodexThreadId = this.codexResumeThreadId;
|
|
592
599
|
const inputId = this.turnInputId;
|
|
600
|
+
// #1156: Grok carries its conversation by an id this runner chose — `-s` to make it, `-r` to resume it.
|
|
601
|
+
if (this.config.clientType === "grok" && this.grokContinuity && !this.grokConversation)
|
|
602
|
+
this.grokConversation = newGrokConversation();
|
|
603
|
+
const grokConversation = this.config.clientType === "grok" && this.grokContinuity ? this.grokConversation : null;
|
|
593
604
|
const turnArgs = resumedCodexThreadId
|
|
594
605
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
595
|
-
:
|
|
606
|
+
: grokConversation
|
|
607
|
+
? grokTurnArgs(this.cmdArgs, text, grokConversation)
|
|
608
|
+
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
596
609
|
const env = this.spawnEnv;
|
|
597
610
|
const target = spawnTarget(this.cmdPrefix, this.cmdBin, turnArgs);
|
|
598
611
|
const launch = resolveEngineSpawn(target.command, target.args, { env });
|
|
@@ -687,6 +700,19 @@ export class HeadlessSession {
|
|
|
687
700
|
this.runOneShot(text);
|
|
688
701
|
return;
|
|
689
702
|
}
|
|
703
|
+
if (grokConversation) {
|
|
704
|
+
const after = afterGrokTurn(grokConversation, { code, rejected: grokRejectedContinuity(this.turnTail) });
|
|
705
|
+
this.grokConversation = after.next;
|
|
706
|
+
writeState(this.config.statePath, this.config.id, after.persist, "grok");
|
|
707
|
+
// The flags were refused before anything ran (an argument error): the same turn again, without them.
|
|
708
|
+
if (after.disable) {
|
|
709
|
+
this.grokContinuity = false;
|
|
710
|
+
this.proc = null;
|
|
711
|
+
this.push("[grok CLI rejected its session flags; retrying this turn without conversation continuity]");
|
|
712
|
+
this.runOneShot(text);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
690
716
|
// A rejected/expired explicit ID is not retried as the same turn: the CLI may have started
|
|
691
717
|
// work before failing. Drop only that known-bad local key so the NEXT turn is safely fresh.
|
|
692
718
|
if (code && resumedCodexThreadId && this.codexThreadId === resumedCodexThreadId) {
|
|
@@ -5,6 +5,7 @@ import { CrashLog, crashReport, describeError, installCrashHandlers, runtimeCras
|
|
|
5
5
|
import { startRunnerServer } from "./server.js";
|
|
6
6
|
import { reapOnStartup } from "./reaper.js";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { dropPlatformCredentials } from "./coding/engine-env.js";
|
|
8
9
|
function arg(name, fallback) {
|
|
9
10
|
const index = process.argv.indexOf(name);
|
|
10
11
|
if (index === -1)
|
|
@@ -58,6 +59,9 @@ Endpoints:
|
|
|
58
59
|
process.exit(0);
|
|
59
60
|
}
|
|
60
61
|
const config = configFromArgs();
|
|
62
|
+
// Read, so out of the environment (#1163): nothing this runtime spawns — engines, git, gh, a browser —
|
|
63
|
+
// inherits the account session or this runtime's own bearer and shutdown credential.
|
|
64
|
+
dropPlatformCredentials(process.env);
|
|
61
65
|
// Resilience: a stray error in any runtime must NOT take the whole runner down —
|
|
62
66
|
// that drops the tunnel and forces the user to restart `pags up` (and lose their
|
|
63
67
|
// session). Keep serving. Per-component handlers catch most things; this is the
|
package/dist/index.js
CHANGED
|
@@ -875,24 +875,43 @@ async function defaultAsk(question) {
|
|
|
875
875
|
|
|
876
876
|
// src/commands/runner/http.ts
|
|
877
877
|
import { hostname as hostname3 } from "os";
|
|
878
|
+
|
|
879
|
+
// src/commands/runner/session-token.ts
|
|
880
|
+
var held = null;
|
|
878
881
|
function clean(value) {
|
|
882
|
+
return (value ?? "").trim();
|
|
883
|
+
}
|
|
884
|
+
function takeSessionTokenFromEnv(env = process.env) {
|
|
885
|
+
const token = clean(env.PAGS_TOKEN);
|
|
886
|
+
if (token) held = token;
|
|
887
|
+
delete env.PAGS_TOKEN;
|
|
888
|
+
}
|
|
889
|
+
function holdSessionToken(token) {
|
|
890
|
+
held = clean(token) || held;
|
|
891
|
+
}
|
|
892
|
+
function sessionToken(explicit) {
|
|
893
|
+
return clean(explicit) || clean(held) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/commands/runner/http.ts
|
|
897
|
+
function clean2(value) {
|
|
879
898
|
const trimmed = value?.trim();
|
|
880
899
|
return trimmed || void 0;
|
|
881
900
|
}
|
|
882
901
|
function runnerBaseUrl(url) {
|
|
883
|
-
return (
|
|
902
|
+
return (clean2(url) || clean2(process.env.PAGS_RUNNER_URL) || "http://127.0.0.1:49171").replace(/\/$/, "");
|
|
884
903
|
}
|
|
885
904
|
function pagsApiBase(url) {
|
|
886
|
-
return (
|
|
905
|
+
return (clean2(url) || clean2(process.env.PAGS_API_BASE) || "https://api.proagentstore.online").replace(/\/$/, "");
|
|
887
906
|
}
|
|
888
907
|
function pagsHeaders(token) {
|
|
889
|
-
const resolved =
|
|
908
|
+
const resolved = sessionToken(token);
|
|
890
909
|
return resolved ? { Authorization: `Bearer ${resolved}` } : {};
|
|
891
910
|
}
|
|
892
911
|
function runnerRequestHeaders(opts) {
|
|
893
|
-
const resolved =
|
|
912
|
+
const resolved = clean2(opts.token) || clean2(process.env.PAGS_RUNNER_TOKEN);
|
|
894
913
|
const headers = resolved ? { Authorization: `Bearer ${resolved}` } : {};
|
|
895
|
-
const instanceId =
|
|
914
|
+
const instanceId = clean2(opts.instanceId) || clean2(process.env.PAGS_INSTANCE_ID);
|
|
896
915
|
if (instanceId) headers["X-PAGS-Instance-Id"] = instanceId;
|
|
897
916
|
return headers;
|
|
898
917
|
}
|
|
@@ -903,11 +922,11 @@ function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
|
903
922
|
const node = hostname3();
|
|
904
923
|
const machine = loadMachineIdentity(node);
|
|
905
924
|
return {
|
|
906
|
-
endpointUrl:
|
|
907
|
-
token:
|
|
925
|
+
endpointUrl: clean2(opts.endpointUrl) || opts.endpointUrl,
|
|
926
|
+
token: clean2(opts.runnerToken) || clean2(opts.token) || clean2(process.env.PAGS_RUNNER_TOKEN),
|
|
908
927
|
placement: opts.placement === "managed" ? "managed" : "local",
|
|
909
928
|
capabilities,
|
|
910
|
-
runnerVersion:
|
|
929
|
+
runnerVersion: clean2(opts.runnerVersion) || "",
|
|
911
930
|
runnerNode: node,
|
|
912
931
|
machineId: machine.id,
|
|
913
932
|
machineNames: machine.names
|
|
@@ -1284,11 +1303,11 @@ async function findFreePort2(start) {
|
|
|
1284
1303
|
}
|
|
1285
1304
|
function buildRunnerArgs(opts) {
|
|
1286
1305
|
const args = [];
|
|
1287
|
-
if (
|
|
1288
|
-
if (
|
|
1289
|
-
if (
|
|
1290
|
-
if (
|
|
1291
|
-
if (
|
|
1306
|
+
if (clean2(opts.host)) args.push("--host", clean2(opts.host));
|
|
1307
|
+
if (clean2(opts.port)) args.push("--port", clean2(opts.port));
|
|
1308
|
+
if (clean2(opts.dataDir)) args.push("--data-dir", clean2(opts.dataDir));
|
|
1309
|
+
if (clean2(opts.token)) args.push("--token", clean2(opts.token));
|
|
1310
|
+
if (clean2(opts.instanceId)) args.push("--instance-id", clean2(opts.instanceId));
|
|
1292
1311
|
if (opts.headless) args.push("--headless");
|
|
1293
1312
|
return args;
|
|
1294
1313
|
}
|
|
@@ -1545,9 +1564,9 @@ function partitionByPin(instances, thisNode, alsoKnownAs = []) {
|
|
|
1545
1564
|
for (const inst of instances) (isEligible(inst, thisNode, alsoKnownAs) ? here : elsewhere).push(inst);
|
|
1546
1565
|
return { here, elsewhere };
|
|
1547
1566
|
}
|
|
1548
|
-
function pinnedAway(
|
|
1567
|
+
function pinnedAway(held2, instances, thisNode, alsoKnownAs = []) {
|
|
1549
1568
|
const pins = new Map(instances.map((i) => [i.id, i.config?.runnerNode]));
|
|
1550
|
-
return [...
|
|
1569
|
+
return [...held2].filter((id) => {
|
|
1551
1570
|
const pin = pins.get(id);
|
|
1552
1571
|
return !!pin && pin !== thisNode && !alsoKnownAs.includes(pin);
|
|
1553
1572
|
});
|
|
@@ -2567,7 +2586,7 @@ var MEMBERSHIP_SYNC_PATH = "/pags/membership/sync";
|
|
|
2567
2586
|
var CLI_CONTROL_PATHS = /* @__PURE__ */ new Set([MEMBERSHIP_SYNC_PATH, RUNNER_UPDATE_PATH, RUNNER_RESTART_PATH]);
|
|
2568
2587
|
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
|
|
2569
2588
|
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
2570
|
-
const pagsToken = () =>
|
|
2589
|
+
const pagsToken = () => sessionToken(opts.pagsToken);
|
|
2571
2590
|
if (!pagsToken()) throw new Error("PAGS token required for WebSocket relay");
|
|
2572
2591
|
const runnerNode = hostname5();
|
|
2573
2592
|
const machine = loadMachineIdentity(runnerNode);
|
|
@@ -3154,18 +3173,18 @@ function clip(text, max) {
|
|
|
3154
3173
|
return `${text.substring(0, max)}
|
|
3155
3174
|
[cut: showing the first ${max} of ${text.length} characters]`;
|
|
3156
3175
|
}
|
|
3157
|
-
var
|
|
3176
|
+
var clean3 = (value, max) => clip(maskSecrets(String(value ?? "")), max);
|
|
3158
3177
|
var shortId = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : void 0;
|
|
3159
3178
|
function describeError(error) {
|
|
3160
3179
|
if (error instanceof Error) {
|
|
3161
3180
|
const cause = error.cause;
|
|
3162
3181
|
return {
|
|
3163
|
-
message:
|
|
3164
|
-
stack: error.stack ?
|
|
3165
|
-
cause: cause === void 0 ? void 0 :
|
|
3182
|
+
message: clean3(`${error.name}: ${error.message}`, MAX_MESSAGE),
|
|
3183
|
+
stack: error.stack ? clean3(error.stack, MAX_STACK) : void 0,
|
|
3184
|
+
cause: cause === void 0 ? void 0 : clean3(cause instanceof Error ? `${cause.name}: ${cause.message}` : cause, MAX_MESSAGE)
|
|
3166
3185
|
};
|
|
3167
3186
|
}
|
|
3168
|
-
return { message:
|
|
3187
|
+
return { message: clean3(error, MAX_MESSAGE) };
|
|
3169
3188
|
}
|
|
3170
3189
|
function crashReport(input) {
|
|
3171
3190
|
return {
|
|
@@ -3173,9 +3192,9 @@ function crashReport(input) {
|
|
|
3173
3192
|
at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3174
3193
|
source: input.source,
|
|
3175
3194
|
kind: input.kind,
|
|
3176
|
-
message:
|
|
3177
|
-
...input.stack ? { stack:
|
|
3178
|
-
...input.cause ? { cause:
|
|
3195
|
+
message: clean3(input.message, MAX_MESSAGE),
|
|
3196
|
+
...input.stack ? { stack: clean3(input.stack, MAX_STACK) } : {},
|
|
3197
|
+
...input.cause ? { cause: clean3(input.cause, MAX_MESSAGE) } : {},
|
|
3179
3198
|
...input.exitCode !== void 0 ? { exitCode: input.exitCode } : {},
|
|
3180
3199
|
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
3181
3200
|
...shortId(input.build) ? { build: input.build } : {},
|
|
@@ -3183,8 +3202,8 @@ function crashReport(input) {
|
|
|
3183
3202
|
...shortId(input.instanceId) ? { instanceId: input.instanceId } : {},
|
|
3184
3203
|
...shortId(input.sessionId) ? { sessionId: input.sessionId } : {},
|
|
3185
3204
|
...shortId(input.taskId) ? { taskId: input.taskId } : {},
|
|
3186
|
-
...input.route ? { route:
|
|
3187
|
-
...input.tail?.length ? { tail: input.tail.slice(-20).map((line) =>
|
|
3205
|
+
...input.route ? { route: clean3(input.route, 200) } : {},
|
|
3206
|
+
...input.tail?.length ? { tail: input.tail.slice(-20).map((line) => clean3(line, 300)) } : {}
|
|
3188
3207
|
};
|
|
3189
3208
|
}
|
|
3190
3209
|
function writeJsonAtomic(path, data, mode = 384) {
|
|
@@ -3458,10 +3477,11 @@ function createRunnerCommand() {
|
|
|
3458
3477
|
await startRunnerForeground(opts);
|
|
3459
3478
|
});
|
|
3460
3479
|
command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").option("--watch-instances", "Attach newly eligible agents while running, without a restart").action(async (instanceIds, opts) => {
|
|
3461
|
-
|
|
3462
|
-
const
|
|
3463
|
-
const
|
|
3464
|
-
const
|
|
3480
|
+
takeSessionTokenFromEnv();
|
|
3481
|
+
const runnerToken = clean2(opts.token) || clean2(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID4()}`;
|
|
3482
|
+
const host = clean2(opts.host) || "127.0.0.1";
|
|
3483
|
+
const port = clean2(opts.port) || String(await findFreePort2(49171));
|
|
3484
|
+
const lockAccount = clean2(process.env.PAGS_LOCK_ACCOUNT) || "";
|
|
3465
3485
|
let lockHeld = null;
|
|
3466
3486
|
if (lockAccount) {
|
|
3467
3487
|
const taken = await acquireLock({ account: lockAccount, node: hostname6(), machineId: loadMachineIdentity().id || null, version: CLI_VERSION, port: Number(port), runnerToken });
|
|
@@ -3505,7 +3525,7 @@ function createRunnerCommand() {
|
|
|
3505
3525
|
});
|
|
3506
3526
|
const deliver = async () => {
|
|
3507
3527
|
if (!crashes) return;
|
|
3508
|
-
const token =
|
|
3528
|
+
const token = sessionToken(opts.pagsToken);
|
|
3509
3529
|
if (!token) return;
|
|
3510
3530
|
const result = await deliverCrashReports({ apiBase: pagsApiBase(opts.apiBase), token, account: crashAccount, paths: crashes });
|
|
3511
3531
|
if (result.delivered) writeLine(`Delivered ${result.delivered} crash report(s) to ProAgentStore.`);
|
|
@@ -3543,12 +3563,12 @@ function createRunnerCommand() {
|
|
|
3543
3563
|
await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force), Boolean(opts.watchInstances));
|
|
3544
3564
|
void deliver().catch(() => void 0);
|
|
3545
3565
|
setInterval(() => void deliver().catch(() => void 0), 5 * 6e4).unref();
|
|
3546
|
-
if (!
|
|
3566
|
+
if (!clean2(opts.pagsToken)) {
|
|
3547
3567
|
startSessionRefresh({
|
|
3548
3568
|
apiBase: pagsApiBase(opts.apiBase),
|
|
3549
|
-
token: () =>
|
|
3569
|
+
token: () => sessionToken() || null,
|
|
3550
3570
|
onRefreshed: (token, previous) => {
|
|
3551
|
-
|
|
3571
|
+
holdSessionToken(token);
|
|
3552
3572
|
const stored = loadSession();
|
|
3553
3573
|
if (stored?.token === previous) saveSession({ ...stored, token, expiresAt: sessionExpiresAt(token) ?? stored.expiresAt });
|
|
3554
3574
|
},
|
|
@@ -3738,8 +3758,8 @@ async function waitForBackgroundHolder(childPid, read, alive, opts = {}) {
|
|
|
3738
3758
|
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
3739
3759
|
while (Date.now() < deadline) {
|
|
3740
3760
|
if (!alive(childPid)) return "exited";
|
|
3741
|
-
const
|
|
3742
|
-
if (
|
|
3761
|
+
const held2 = await read();
|
|
3762
|
+
if (held2.lock?.pid === childPid && held2.verdict === "live") return "live";
|
|
3743
3763
|
await sleep(opts.intervalMs ?? 500);
|
|
3744
3764
|
}
|
|
3745
3765
|
return alive(childPid) ? "timeout" : "exited";
|
|
@@ -3747,8 +3767,8 @@ async function waitForBackgroundHolder(childPid, read, alive, opts = {}) {
|
|
|
3747
3767
|
async function waitUntilTakeable(read, opts = {}) {
|
|
3748
3768
|
const deadline = Date.now() + (opts.timeoutMs ?? 15e3);
|
|
3749
3769
|
for (; ; ) {
|
|
3750
|
-
const
|
|
3751
|
-
if (!
|
|
3770
|
+
const held2 = await read();
|
|
3771
|
+
if (!held2.lock || isTakeable(held2.verdict)) return true;
|
|
3752
3772
|
if (Date.now() >= deadline) return false;
|
|
3753
3773
|
await sleep(opts.intervalMs ?? 500);
|
|
3754
3774
|
}
|
|
@@ -3785,8 +3805,8 @@ function describeStatus(lock, verdict, bg, now = Date.now()) {
|
|
|
3785
3805
|
}
|
|
3786
3806
|
var statusCommand = new Command9("status").description("Is a runner running on this machine for your account \u2014 and how was it started").action(async () => {
|
|
3787
3807
|
const session = requireSession();
|
|
3788
|
-
const
|
|
3789
|
-
const { running, lines } = describeStatus(
|
|
3808
|
+
const held2 = await inspectHolder(lockPath(session.user.login));
|
|
3809
|
+
const { running, lines } = describeStatus(held2.lock, held2.lock ? held2.verdict : null, readBackgroundState(backgroundStatePath(session.user.login)));
|
|
3790
3810
|
writeLine("");
|
|
3791
3811
|
for (const line of lines) writeLine(` ${line}`);
|
|
3792
3812
|
writeLine("");
|
|
@@ -4039,13 +4059,13 @@ async function startInBackground(start, deps = processDeps()) {
|
|
|
4039
4059
|
const upArgs = restartUpArgs(start.opts);
|
|
4040
4060
|
const env = {
|
|
4041
4061
|
...deps.env,
|
|
4042
|
-
PAGS_TOKEN: start.session.token,
|
|
4043
4062
|
PAGS_LOCK_ACCOUNT: account,
|
|
4044
4063
|
[BACKGROUND_ENV]: "1",
|
|
4045
4064
|
[BACKGROUND_LOG_ENV]: logPath,
|
|
4046
4065
|
[RESTART_COMMAND_ENV]: backgroundRestartCommand(deps.execPath, start.runnerArgs[0], upArgs, deps.platform)
|
|
4047
4066
|
};
|
|
4048
4067
|
delete env.PAGS_UP_SUPERVISED;
|
|
4068
|
+
delete env.PAGS_TOKEN;
|
|
4049
4069
|
deps.step("Starting the runner in the background\u2026", "wait");
|
|
4050
4070
|
const pid = spawnDetached(deps.execPath, start.runnerArgs, env, logPath, deps.spawn);
|
|
4051
4071
|
writeBackgroundState(backgroundStatePath(account, deps.env), {
|
|
@@ -4427,17 +4447,17 @@ var upCommand = new Command10("up").description("Start the browser runner for al
|
|
|
4427
4447
|
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
4428
4448
|
const lockFile = lockPath(session.user.login);
|
|
4429
4449
|
if (unattended || opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile), { timeoutMs: 6e4 });
|
|
4430
|
-
const
|
|
4431
|
-
if (
|
|
4432
|
-
logUpdateStep(`start: the lock is held by pid ${
|
|
4450
|
+
const held2 = await inspectHolder(lockFile);
|
|
4451
|
+
if (held2.lock && !isTakeable(held2.verdict)) {
|
|
4452
|
+
logUpdateStep(`start: the lock is held by pid ${held2.lock.pid} (${held2.verdict})${opts.replace ? " \u2014 replacing it" : " \u2014 exiting"}`);
|
|
4433
4453
|
if (!opts.replace) {
|
|
4434
4454
|
clearScreen();
|
|
4435
4455
|
printLogo(CLI_VERSION2);
|
|
4436
|
-
for (const line of holderMessage(
|
|
4456
|
+
for (const line of holderMessage(held2.lock, held2.verdict)) writeLine(` ${line}`);
|
|
4437
4457
|
writeLine("");
|
|
4438
4458
|
process.exit(1);
|
|
4439
4459
|
}
|
|
4440
|
-
const replaced = await replaceHolder(
|
|
4460
|
+
const replaced = await replaceHolder(held2.lock, { now: opts.now === true });
|
|
4441
4461
|
if (!replaced.ok) {
|
|
4442
4462
|
clearScreen();
|
|
4443
4463
|
printLogo(CLI_VERSION2);
|
|
@@ -4461,7 +4481,9 @@ var upCommand = new Command10("up").description("Start the browser runner for al
|
|
|
4461
4481
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4462
4482
|
// The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
|
|
4463
4483
|
// contender must find and what `--replace` must be able to stop.
|
|
4464
|
-
|
|
4484
|
+
// No PAGS_TOKEN (#1163): the child reads session.json itself, so the account session is in no
|
|
4485
|
+
// environment its runtime and engines could inherit.
|
|
4486
|
+
env: { ...process.env, PAGS_TOKEN: void 0, [SUPERVISED_ENV]: SUPERVISOR_RESTARTS, PAGS_LOCK_ACCOUNT: session.user.login }
|
|
4465
4487
|
});
|
|
4466
4488
|
const logs = [];
|
|
4467
4489
|
let childRsid = "";
|