@proagentstore/cli 0.4.115 → 0.4.117

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/bootstrap.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  newestPayload,
9
9
  olderThan,
10
10
  payloadAt
11
- } from "./chunk-RGNU4W6B.js";
11
+ } from "./chunk-LQ57ADUG.js";
12
12
 
13
13
  // src/bootstrap.ts
14
14
  import { existsSync } from "fs";
@@ -7,9 +7,11 @@
7
7
  * getting a raise. `engine-auth.ts` READS this env; this builds it.
8
8
  *
9
9
  * 1. {@link mergeEnv} — the platform's overlay over the machine's, where empty means REMOVE.
10
- * 2. {@link engineSpawnEnv} — that, plus the `gh` guard ahead of the real binary on `PATH`.
10
+ * 2. {@link engineSpawnEnv} — that, plus the `gh` guard ahead of the real binary on `PATH`, and the
11
+ * `git` stash guard (#1021) ahead of both.
11
12
  */
12
13
  import { ghGuardEnv } from "./gh-guard.js";
14
+ import { gitGuardEnv } from "./git-guard.js";
13
15
  /**
14
16
  * Merge the platform's resolved engine env over the machine's, where an EMPTY value means
15
17
  * REMOVE rather than "set to empty".
@@ -41,6 +43,7 @@ export function mergeEnv(base, overlay) {
41
43
  * `ghGuardEnv` returns its input untouched whenever the guard cannot be installed, so this is safe
42
44
  * in front of every spawn: the failure mode is the machine's own `gh`, never a missing one.
43
45
  */
44
- export function engineSpawnEnv(overlay, ghScope, ghGuardRoot) {
45
- return ghGuardEnv(mergeEnv(process.env, overlay), ghScope, ghGuardRoot);
46
+ export function engineSpawnEnv(overlay, ghScope, ghGuardRoot, gitGuardRoot) {
47
+ // The git guard needs no scope: no session may restore or discard the stash by position (#1021).
48
+ return gitGuardEnv(ghGuardEnv(mergeEnv(process.env, overlay), ghScope, ghGuardRoot), gitGuardRoot);
46
49
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The Engine may not restore or discard the repo-global stash by POSITION (#1021).
3
+ *
4
+ * ── The incident
5
+ *
6
+ * Setting up a fail-before comparison, an Engine composed `SRC="a.ts b.ts"; git stash push -- $SRC;
7
+ * …; git stash pop`. zsh passed `$SRC` as ONE pathspec, so the save failed and created nothing — and
8
+ * the chained `pop` restored the top of the stack anyway: someone else's retained recovery stash,
9
+ * applied into the active checkout as merge conflicts. The stash is repo-global; `pop`, `drop` and
10
+ * `stash@{n}` name a position in everyone's stack, not the thing this command saved.
11
+ *
12
+ * The Pilot's `no_stash_restore` refusal (`workers/api/src/lib/coding-scope-constraints.ts`) only
13
+ * screens what the PILOT sends. The incident command was composed by the Engine itself, which
14
+ * runs with `--dangerously-skip-permissions`, so the only enforcement point it cannot talk its way
15
+ * past is the `git` it executes. This is that point.
16
+ *
17
+ * ── What the shim does
18
+ *
19
+ * A POSIX `sh` script named `git`, first on every Engine's `PATH` (all engines, every session —
20
+ * unlike the `gh` guard it needs no scope). For `git [global options] stash <action> …`:
21
+ *
22
+ * pop, drop, clear refused — each consumes or discards by position, and there is no form of
23
+ * them that names an exact object.
24
+ * apply, branch allowed only with an exact object id (40 or 64 hex) — what `git stash
25
+ * create` printed — so a restore is tied to an object this command made.
26
+ *
27
+ * Refusal exits {@link GIT_REFUSED_EXIT} BEFORE git runs, with {@link GIT_REFUSED_MARKER} on stderr.
28
+ * Everything else — including `git stash push`, `list`, `show`, `create` — is `exec`'d to the real
29
+ * git with its own exit status. Its tests run THIS text against real temporary repositories.
30
+ *
31
+ * ── What it does not stop, stated rather than hidden
32
+ *
33
+ * {@link GIT_GUARD_GAPS}: a PATH shim is bypassed by an absolute `/usr/bin/git` or a rewritten PATH,
34
+ * and a git alias that expands to `stash pop` is resolved inside git, past the shim.
35
+ */
36
+ import { createHash } from "node:crypto";
37
+ import { accessSync, constants, existsSync, mkdirSync, writeFileSync } from "node:fs";
38
+ import { homedir } from "node:os";
39
+ import { join } from "node:path";
40
+ /** Distinct from git's own 1/128/129, so a refusal is recognisable from its status alone. */
41
+ export const GIT_REFUSED_EXIT = 3;
42
+ export const GIT_REFUSED_MARKER = "pags: refused";
43
+ export const GIT_GUARD_GAPS = [
44
+ "A PATH shim is bypassable: invoking git by absolute path (/usr/bin/git), or rewriting PATH, skips it.",
45
+ "A git alias that expands to `stash pop`/`drop`/`clear` is expanded inside git, past the shim.",
46
+ ];
47
+ export function gitGuardRoot() {
48
+ return join(homedir(), ".config", "proagentstore", "git-guard");
49
+ }
50
+ /** The first executable `git` on `path` that is not one of our own shims. */
51
+ export function findRealGit(path, guardRoot) {
52
+ for (const dir of (path ?? "").split(":")) {
53
+ if (!dir || dir.startsWith(guardRoot))
54
+ continue;
55
+ const candidate = join(dir, "git");
56
+ try {
57
+ accessSync(candidate, constants.X_OK);
58
+ return candidate;
59
+ }
60
+ catch {
61
+ /* not here — keep looking */
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ const sq = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
67
+ export function renderGitShim(realGit) {
68
+ return `#!/bin/sh
69
+ # ProAgentStore git stash guard (#1021) — GENERATED. Source of truth:
70
+ # packages/browser-runner/src/coding/git-guard.ts. Editing this file lets an agent restore or discard
71
+ # someone else's stash; that is a bypass, not a configuration.
72
+ REAL=${sq(realGit)}
73
+
74
+ state=global; skip=0; sub=""; action=""; ref1=""; ref2=""
75
+ for a in "$@"; do
76
+ if [ "$skip" = 1 ]; then skip=0; continue; fi
77
+ if [ "$state" = global ]; then
78
+ case "$a" in
79
+ -C|-c|--git-dir|--work-tree|--namespace|--config-env|--super-prefix) skip=1 ;;
80
+ -*) : ;;
81
+ *) sub="$a"; state=args ;;
82
+ esac
83
+ continue
84
+ fi
85
+ [ "$sub" = stash ] || break
86
+ case "$a" in
87
+ -*) : ;;
88
+ *) if [ -z "$action" ]; then action="$a"; elif [ -z "$ref1" ]; then ref1="$a"; elif [ -z "$ref2" ]; then ref2="$a"; fi ;;
89
+ esac
90
+ done
91
+
92
+ refuse() {
93
+ echo "${GIT_REFUSED_MARKER}: \\\`git stash $action\\\` $1 The stash is shared by everyone working in this repository, so a position in it is not what you saved." >&2
94
+ echo "Restore only an exact object you just created: S=\\$(git stash create) && [ -n \\"\\$S\\" ] && … && git stash apply \\"\\$S\\" — or compare in an isolated \\\`git worktree add --detach\\\` checkout, which never touches the stash." >&2
95
+ exit ${GIT_REFUSED_EXIT}
96
+ }
97
+
98
+ exact_object() {
99
+ case "$1" in ""|*[!0-9a-f]*) return 1 ;; esac
100
+ [ \${#1} -eq 40 ] || [ \${#1} -eq 64 ]
101
+ }
102
+
103
+ if [ "$sub" = stash ]; then
104
+ case "$action" in
105
+ pop) refuse "restores and DROPS the top of the stack, whoever saved it." ;;
106
+ drop) refuse "discards a stack entry by position." ;;
107
+ clear) refuse "discards every entry, including other people's recovery work." ;;
108
+ apply) exact_object "$ref1" || refuse "needs an exact object id, not a stack position (got '\${ref1:-top of stack}')." ;;
109
+ branch) exact_object "$ref2" || refuse "needs an exact object id, not a stack position (got '\${ref2:-top of stack}')." ;;
110
+ esac
111
+ fi
112
+
113
+ exec "$REAL" "$@"
114
+ `;
115
+ }
116
+ const installed = new Map();
117
+ /** Install (idempotently) the shim and return its directory, or null when it cannot be. */
118
+ export function installGitGuard(env, root = gitGuardRoot()) {
119
+ const realGit = findRealGit(env.PATH, root);
120
+ if (!realGit)
121
+ return null;
122
+ const script = renderGitShim(realGit);
123
+ const key = createHash("sha256").update(`${root} ${script}`).digest("hex").slice(0, 16);
124
+ const cached = installed.get(key);
125
+ if (cached)
126
+ return cached;
127
+ const dir = join(root, key);
128
+ try {
129
+ mkdirSync(dir, { recursive: true });
130
+ const shim = join(dir, "git");
131
+ if (!existsSync(shim))
132
+ writeFileSync(shim, script, { mode: 0o755 });
133
+ installed.set(key, dir);
134
+ return dir;
135
+ }
136
+ catch {
137
+ // The gh guard's rule: a runner that cannot write its own config dir still runs the Engine,
138
+ // with the machine's git — never with a `git` that does not exist.
139
+ return null;
140
+ }
141
+ }
142
+ /** `env` with the guard first on PATH; untouched when it could not be installed. */
143
+ export function gitGuardEnv(env, root) {
144
+ const dir = installGitGuard(env, root);
145
+ return dir ? { ...env, PATH: `${dir}:${env.PATH ?? ""}` } : env;
146
+ }
@@ -2,7 +2,7 @@
2
2
  import { execFile } from "child_process";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from "fs";
4
4
  import { homedir } from "os";
5
- import { join } from "path";
5
+ import { join, win32 } from "path";
6
6
  import { promisify } from "util";
7
7
  var run = promisify(execFile);
8
8
  var CLI_PACKAGE = "@proagentstore/cli";
@@ -47,9 +47,32 @@ function cachedPayloads(root = payloadRoot()) {
47
47
  function newestPayload(payloads) {
48
48
  return payloads.reduce((best, p) => !best || olderThan(best.version, p.version) ? p : best, null);
49
49
  }
50
+ var CMD_META = /([()\][%!^"`<>&|;, *?])/g;
51
+ function quoteCmdArg(arg) {
52
+ const quoted = `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
53
+ return quoted.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
54
+ }
55
+ var envVar = (env, name) => env[Object.keys(env).find((k) => k.toUpperCase() === name) ?? name];
56
+ function npmInvocation(args, deps = {}) {
57
+ if ((deps.platform ?? process.platform) !== "win32") return { file: "npm", args };
58
+ const env = deps.env ?? process.env;
59
+ const exists = deps.exists ?? existsSync;
60
+ const execPath = deps.execPath ?? process.execPath;
61
+ const cli = (dir) => win32.join(dir, "node_modules", "npm", "bin", "npm-cli.js");
62
+ const onPath = (envVar(env, "PATH") ?? "").split(";").filter((dir) => dir && exists(win32.join(dir, "npm.cmd")));
63
+ for (const dir of [...onPath, win32.dirname(execPath)]) {
64
+ if (exists(cli(dir))) return { file: execPath, args: [cli(dir), ...args] };
65
+ }
66
+ const line = ["npm", ...args.map(quoteCmdArg)].join(" ");
67
+ return { file: envVar(env, "COMSPEC") || "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
68
+ }
69
+ function runNpm(args, options) {
70
+ const npm = npmInvocation(args);
71
+ return run(npm.file, npm.args, { ...options, ...npm.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {} });
72
+ }
50
73
  async function latestPublishedVersion(timeoutMs = 3e4, signal) {
51
74
  try {
52
- const { stdout } = await run("npm", ["view", CLI_PACKAGE, "version"], { timeout: timeoutMs, signal });
75
+ const { stdout } = await runNpm(["view", CLI_PACKAGE, "version"], { timeout: timeoutMs, signal });
53
76
  return stdout.trim() || null;
54
77
  } catch {
55
78
  return null;
@@ -57,7 +80,7 @@ async function latestPublishedVersion(timeoutMs = 3e4, signal) {
57
80
  }
58
81
  async function npmInstallInto(dir, version) {
59
82
  try {
60
- await run("npm", ["install", "--prefix", dir, "--omit=dev", "--no-audit", "--no-fund", "--no-save", `${CLI_PACKAGE}@${version}`], { timeout: 5 * 6e4 });
83
+ await runNpm(["install", "--prefix", dir, "--omit=dev", "--no-audit", "--no-fund", "--no-save", `${CLI_PACKAGE}@${version}`], { timeout: 5 * 6e4 });
61
84
  } catch (e) {
62
85
  const stderr = String(e.stderr ?? "").trim();
63
86
  throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
@@ -96,6 +119,7 @@ export {
96
119
  payloadAt,
97
120
  cachedPayloads,
98
121
  newestPayload,
122
+ runNpm,
99
123
  latestPublishedVersion,
100
124
  installPayload
101
125
  };
package/dist/index.js CHANGED
@@ -5,8 +5,9 @@ import {
5
5
  installPayload,
6
6
  isVersion,
7
7
  latestPublishedVersion,
8
- olderThan
9
- } from "./chunk-RGNU4W6B.js";
8
+ olderThan,
9
+ runNpm
10
+ } from "./chunk-LQ57ADUG.js";
10
11
 
11
12
  // src/index.ts
12
13
  import { createRequire as createRequire3 } from "module";
@@ -1256,9 +1257,7 @@ function sampleResources(read = os, now = Date.now(), extra = {}) {
1256
1257
  }
1257
1258
 
1258
1259
  // src/commands/runner/self-update.ts
1259
- import { execFile, spawn as spawn3 } from "child_process";
1260
- import { promisify } from "util";
1261
- var run = promisify(execFile);
1260
+ import { spawn as spawn3 } from "child_process";
1262
1261
  var RUNNER_UPDATE_PATH = "/pags/runner/update";
1263
1262
  var RUNNER_RESTART_PATH = "/pags/runner/restart";
1264
1263
  var RUNNER_RESTART_EXIT_CODE = 75;
@@ -1306,7 +1305,7 @@ function restartUpArgs(opts) {
1306
1305
  }
1307
1306
  function planRunnerUpdate(f) {
1308
1307
  if (f.fromSource) return { action: "refused", current: f.current, reason: "This runner runs from a source checkout \u2014 update it with `git pull` there, not npm." };
1309
- if (!f.latest) return { action: "refused", current: f.current, reason: `npm could not be asked for the latest ${CLI_PACKAGE} from this machine.` };
1308
+ if (!f.latest) return { action: "refused", current: f.current, reason: `npm could not be asked for the latest ${CLI_PACKAGE} from this machine.`, retryable: true };
1310
1309
  if (!olderThan(f.current, f.latest)) return { action: "up-to-date", current: f.current };
1311
1310
  if (!f.restarter) {
1312
1311
  return {
@@ -1346,7 +1345,7 @@ async function installVersion(version2) {
1346
1345
  return;
1347
1346
  }
1348
1347
  try {
1349
- await run("npm", ["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 });
1348
+ await runNpm(["i", "-g", `${CLI_PACKAGE}@${version2}`], { timeout: 5 * 6e4 });
1350
1349
  } catch (e) {
1351
1350
  const stderr = String(e.stderr ?? "").trim();
1352
1351
  throw new Error((stderr || (e instanceof Error ? e.message : String(e))).slice(-400));
@@ -1984,7 +1983,8 @@ var AutomaticUpdateController = class {
1984
1983
  return;
1985
1984
  }
1986
1985
  if (plan.action === "refused") {
1987
- this.deps.status("unsupported", { reason: plan.reason });
1986
+ if (plan.retryable) this.failures++;
1987
+ this.deps.status(plan.retryable ? "failure" : "unsupported", { reason: plan.reason });
1988
1988
  this.schedule();
1989
1989
  return;
1990
1990
  }
@@ -2073,16 +2073,35 @@ function isWorkShape(work) {
2073
2073
  const w2 = work;
2074
2074
  return count(w2.codingTurns) && count(w2.localRuns) && Array.isArray(w2.detail) && w2.detail.every((item) => typeof item === "string");
2075
2075
  }
2076
+ function sessionStateOf(session) {
2077
+ if (!session || typeof session !== "object") return "unknown";
2078
+ const s = session;
2079
+ if (s.alive === false) return "stopped";
2080
+ const state = typeof s.runState === "string" && s.runState ? s.runState : null;
2081
+ if (s.alive !== true || !state) return "unknown";
2082
+ return state === "idle" ? "idle_retained" : "active_turn";
2083
+ }
2084
+ function sessionStateCounts(sessions) {
2085
+ const counts = { activeTurns: 0, idleRetained: 0, stopped: 0, unknown: 0 };
2086
+ for (const session of sessions) {
2087
+ const state = sessionStateOf(session);
2088
+ if (state === "active_turn") counts.activeTurns++;
2089
+ else if (state === "idle_retained") counts.idleRetained++;
2090
+ else if (state === "stopped") counts.stopped++;
2091
+ else counts.unknown++;
2092
+ }
2093
+ return counts;
2094
+ }
2076
2095
  function observedRunnerWork(observation) {
2077
2096
  const sessions = observation.sessions;
2078
2097
  const work = observation.work;
2079
2098
  if (!Array.isArray(sessions) || !isWorkShape(work)) return ["runner-work-observation-unavailable"];
2080
2099
  const { codingTurns, localRuns, detail } = work;
2081
2100
  const activeCoding = sessions.flatMap((session) => {
2082
- if (session?.alive === false) return [];
2101
+ const kind = sessionStateOf(session);
2102
+ if (kind === "stopped" || kind === "idle_retained") return [];
2083
2103
  const id = typeof session?.sessionId === "string" && session.sessionId ? session.sessionId : "unidentified-session";
2084
2104
  const state = typeof session?.runState === "string" && session.runState ? session.runState : "unknown";
2085
- if (session?.alive === true && state === "idle") return [];
2086
2105
  return [`coding session ${id} (${state})`];
2087
2106
  });
2088
2107
  const unmatchedCodingTurns = Math.max(0, codingTurns - activeCoding.length);
@@ -2100,7 +2119,14 @@ function readWorkLook(sessions, health, sampledAt) {
2100
2119
  return {
2101
2120
  busy,
2102
2121
  observations: [
2103
- { source: "sessions", success: sessionsOk, sampledAt, ...sessions.status === "fulfilled" && !sessionsOk ? { malformed: true } : {} },
2122
+ {
2123
+ source: "sessions",
2124
+ success: sessionsOk,
2125
+ sampledAt,
2126
+ ...sessions.status === "fulfilled" && !sessionsOk ? { malformed: true } : {},
2127
+ // What the readable list held, by lifecycle state — counts only (#1020).
2128
+ ...sessions.status === "fulfilled" && sessionsOk ? { sessionStates: sessionStateCounts(sessions.value.sessions) } : {}
2129
+ },
2104
2130
  { source: "health", success: healthOk, sampledAt, ...health.status === "fulfilled" && !healthOk ? { malformed: true } : {} }
2105
2131
  ]
2106
2132
  };
@@ -2193,7 +2219,7 @@ function createInstallBridge(deps) {
2193
2219
  release(drained);
2194
2220
  throw new BoundExpired(`${what} did not answer within ${(deps.bound?.ms ?? 3e4) / 1e3}s; nothing restarted, and it will be retried.`);
2195
2221
  };
2196
- const run2 = async (plan, automatic = false, lease) => {
2222
+ const run = async (plan, automatic = false, lease) => {
2197
2223
  if (inFlight || lease && !deps.leaseActive(lease)) return false;
2198
2224
  inFlight = true;
2199
2225
  if (automatic) {
@@ -2240,7 +2266,7 @@ function createInstallBridge(deps) {
2240
2266
  deps.detachAll();
2241
2267
  return true;
2242
2268
  };
2243
- return { run: run2, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2269
+ return { run, inFlight: () => inFlight, installedPendingRestart: () => installedPendingRestart };
2244
2270
  }
2245
2271
 
2246
2272
  // src/commands/runner/relay.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.115",
3
+ "version": "0.4.117",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",