@danypops/pi-packed 0.28.2 → 0.28.4

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.
@@ -7,6 +7,15 @@ export interface SmokeRegistrations {
7
7
  providers: string[];
8
8
  events: string[];
9
9
  renderers: string[];
10
+ /**
11
+ * Names, within tools/commands/shortcuts/flags respectively, whose registration call carried an
12
+ * explicit `shared: true` marker -- see @danypops/vehicle-client-pi's own markSharedRegistration.
13
+ * doctor.ts only suppresses a name's conflict once EVERY one of its claimants marked it shared.
14
+ */
15
+ sharedTools: string[];
16
+ sharedCommands: string[];
17
+ sharedShortcuts: string[];
18
+ sharedFlags: string[];
10
19
  }
11
20
  export interface ExtensionSmokeResult {
12
21
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AAmJD,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
1
+ {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AA2DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AA0LD,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.28.2",
3
+ "version": "0.28.4",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,13 +40,13 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@danypops/pi-tui-harness": "^0.0.1",
43
- "@danypops/vehicle-client-pi": "^0.43.1",
43
+ "@danypops/vehicle-client-pi": "^0.44.0",
44
44
  "@danypops/vehicle-conformance": "^0.4.0",
45
45
  "@types/semver": "^7.7.1",
46
46
  "follow-redirects": "^1.16.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "@danypops/vehicle-client-pi": "^0.43.1",
49
+ "@danypops/vehicle-client-pi": "^0.44.0",
50
50
  "@earendil-works/pi-coding-agent": "*",
51
51
  "@earendil-works/pi-tui": "*",
52
52
  "typebox": "*"
@@ -31,6 +31,18 @@ const CONFLICT_KIND: Record<RegistrationKind, "tool" | "command" | "shortcut" |
31
31
  shortcuts: "shortcut",
32
32
  flags: "flag",
33
33
  };
34
+ /**
35
+ * Maps each RegistrationKind to the smoke result's own parallel "which of these names were marked
36
+ * shared" field (see smoke.ts's own SmokeRegistrations doc comment). A name is only exempted from
37
+ * conflict reporting once EVERY one of its claimants marked it shared -- see runDoctor's own
38
+ * allSharedByName tracking below.
39
+ */
40
+ const SHARED_REGISTRATION_KIND: Record<RegistrationKind, "sharedTools" | "sharedCommands" | "sharedShortcuts" | "sharedFlags"> = {
41
+ tools: "sharedTools",
42
+ commands: "sharedCommands",
43
+ shortcuts: "sharedShortcuts",
44
+ flags: "sharedFlags",
45
+ };
34
46
 
35
47
  const MAX_EXTENSIONS_SCANNED = 50;
36
48
 
@@ -63,6 +75,11 @@ export async function runDoctor(piHome: string, projectRoot?: string, options: S
63
75
  const bounded = jobs.slice(0, MAX_EXTENSIONS_SCANNED);
64
76
 
65
77
  const claims = new Map<RegistrationKind, Map<string, DoctorClaim[]>>(REGISTRATION_KINDS.map((kind) => [kind, new Map()]));
78
+ // True for a (kind, name) pair only while EVERY claimant seen so far marked that exact
79
+ // registration shared: true -- a single unmarked claimant permanently flips it false, since an
80
+ // unmarked registration is a genuinely ambiguous case, not the coordinated-by-design pattern
81
+ // shared:true exists to exempt.
82
+ const allSharedByName = new Map<RegistrationKind, Map<string, boolean>>(REGISTRATION_KINDS.map((kind) => [kind, new Map()]));
66
83
  const extensions: DoctorExtensionResult[] = [];
67
84
  let anyNotOk = false;
68
85
 
@@ -82,14 +99,20 @@ export async function runDoctor(piHome: string, projectRoot?: string, options: S
82
99
  const claim: DoctorClaim = { name: job.group.name, source: job.group.source, scope: job.group.scope, extension: job.extension };
83
100
  for (const kind of REGISTRATION_KINDS) {
84
101
  const byName = claims.get(kind)!;
85
- for (const name of result.registrations[kind]) byName.set(name, [...(byName.get(name) ?? []), claim]);
102
+ const sharedFlags = allSharedByName.get(kind)!;
103
+ const sharedNamesHere = new Set(result.registrations[SHARED_REGISTRATION_KIND[kind]]);
104
+ for (const name of result.registrations[kind]) {
105
+ byName.set(name, [...(byName.get(name) ?? []), claim]);
106
+ sharedFlags.set(name, (sharedFlags.get(name) ?? true) && sharedNamesHere.has(name));
107
+ }
86
108
  }
87
109
  }
88
110
 
89
111
  const conflicts: DoctorConflict[] = [];
90
112
  for (const kind of REGISTRATION_KINDS) {
113
+ const sharedFlags = allSharedByName.get(kind)!;
91
114
  for (const [name, claimants] of claims.get(kind)!) {
92
- if (claimants.length > 1) conflicts.push({ kind: CONFLICT_KIND[kind], name, claimants });
115
+ if (claimants.length > 1 && !sharedFlags.get(name)) conflicts.push({ kind: CONFLICT_KIND[kind], name, claimants });
93
116
  }
94
117
  }
95
118
  conflicts.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
@@ -8,6 +8,15 @@ interface Registrations {
8
8
  providers: string[];
9
9
  events: string[];
10
10
  renderers: string[];
11
+ /**
12
+ * Names, within tools/commands/shortcuts/flags respectively, whose registration call carried an
13
+ * explicit `shared: true` marker -- see @danypops/vehicle-client-pi's own markSharedRegistration.
14
+ * doctor.ts only suppresses a name's conflict once EVERY one of its claimants marked it shared.
15
+ */
16
+ sharedTools: string[];
17
+ sharedCommands: string[];
18
+ sharedShortcuts: string[];
19
+ sharedFlags: string[];
11
20
  }
12
21
 
13
22
  const MAX_REGISTRATIONS_TOTAL = 100;
@@ -43,6 +52,10 @@ const registrations: Registrations = {
43
52
  providers: [],
44
53
  events: [],
45
54
  renderers: [],
55
+ sharedTools: [],
56
+ sharedCommands: [],
57
+ sharedShortcuts: [],
58
+ sharedFlags: [],
46
59
  };
47
60
 
48
61
  function capture(kind: keyof Registrations, value: unknown): void {
@@ -51,19 +64,33 @@ function capture(kind: keyof Registrations, value: unknown): void {
51
64
  registrations[kind].push(name.slice(0, MAX_NAME_LENGTH));
52
65
  }
53
66
 
67
+ const SHARED_KIND: Partial<Record<keyof Registrations, keyof Registrations>> = {
68
+ tools: "sharedTools",
69
+ commands: "sharedCommands",
70
+ shortcuts: "sharedShortcuts",
71
+ flags: "sharedFlags",
72
+ };
73
+
74
+ /** Captures the plain registration as usual, plus a parallel sharedX entry when the caller's own definition/options object carried an explicit `shared: true` marker. */
75
+ function captureWithSharedMarker(kind: keyof Registrations, value: unknown, sharedMarker: unknown): void {
76
+ capture(kind, value);
77
+ const sharedKind = SHARED_KIND[kind];
78
+ if (sharedKind && sharedMarker === true) capture(sharedKind, value);
79
+ }
80
+
54
81
  const api = new Proxy<Record<string, unknown>>(
55
82
  {},
56
83
  {
57
84
  get(_target, property) {
58
85
  switch (property) {
59
86
  case "registerTool":
60
- return (definition: { name?: unknown }) => capture("tools", definition?.name);
87
+ return (definition: { name?: unknown; shared?: unknown }) => captureWithSharedMarker("tools", definition?.name, definition?.shared);
61
88
  case "registerCommand":
62
- return (name: unknown) => capture("commands", name);
89
+ return (name: unknown, options?: { shared?: unknown }) => captureWithSharedMarker("commands", name, options?.shared);
63
90
  case "registerShortcut":
64
- return (name: unknown) => capture("shortcuts", name);
91
+ return (name: unknown, options?: { shared?: unknown }) => captureWithSharedMarker("shortcuts", name, options?.shared);
65
92
  case "registerFlag":
66
- return (name: unknown) => capture("flags", name);
93
+ return (name: unknown, options?: { shared?: unknown }) => captureWithSharedMarker("flags", name, options?.shared);
67
94
  case "registerProvider":
68
95
  return (name: unknown) => capture("providers", name);
69
96
  case "registerMessageRenderer":
@@ -1,4 +1,4 @@
1
- import { existsSync, realpathSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -13,6 +13,15 @@ export interface SmokeRegistrations {
13
13
  providers: string[];
14
14
  events: string[];
15
15
  renderers: string[];
16
+ /**
17
+ * Names, within tools/commands/shortcuts/flags respectively, whose registration call carried an
18
+ * explicit `shared: true` marker -- see @danypops/vehicle-client-pi's own markSharedRegistration.
19
+ * doctor.ts only suppresses a name's conflict once EVERY one of its claimants marked it shared.
20
+ */
21
+ sharedTools: string[];
22
+ sharedCommands: string[];
23
+ sharedShortcuts: string[];
24
+ sharedFlags: string[];
16
25
  }
17
26
 
18
27
  export interface ExtensionSmokeResult {
@@ -43,6 +52,10 @@ const EMPTY_REGISTRATIONS: SmokeRegistrations = {
43
52
  providers: [],
44
53
  events: [],
45
54
  renderers: [],
55
+ sharedTools: [],
56
+ sharedCommands: [],
57
+ sharedShortcuts: [],
58
+ sharedFlags: [],
46
59
  };
47
60
 
48
61
  function bounded(value: number | undefined, fallback: number, maximum: number): number {
@@ -105,6 +118,41 @@ export function resolveDependencyModulesDir(fromDir: string, name: string): stri
105
118
  }
106
119
  }
107
120
 
121
+ const MAX_PROCESS_SCAN_ENTRIES = 100_000;
122
+
123
+ /**
124
+ * RLIMIT_NPROC (prlimit's own --nproc) is a per-real-UID, system-wide limit -- NOT scoped to
125
+ * this sandbox's own process tree. A small absolute constant breaks the instant the invoking
126
+ * user's REAL total process count already exceeds it (routine on a heavily-used workstation
127
+ * with several long-running agent sessions): any further thread/fork Bun's own runtime needs
128
+ * internally (confirmed live: its native TS transpiler spawns one) immediately fails, surfacing
129
+ * as an unrecoverable SIGABRT (exit 134) with no output at all, not a catchable error. Correct
130
+ * enforcement of "at most N NEW processes the sandboxed extension can spawn" requires an offset
131
+ * from the CURRENT real count, not an absolute cap. Returns 0 (never throws) if /proc can't be
132
+ * read at all -- the caller's own maxProcesses then applies as an absolute floor, same as before
133
+ * this existed.
134
+ */
135
+ function currentProcessCountForUid(uid: number): number {
136
+ let entries: string[];
137
+ try {
138
+ entries = readdirSync("/proc");
139
+ } catch {
140
+ return 0;
141
+ }
142
+ let count = 0;
143
+ for (const entry of entries.slice(0, MAX_PROCESS_SCAN_ENTRIES)) {
144
+ if (!/^\d+$/.test(entry)) continue;
145
+ try {
146
+ const status = readFileSync(`/proc/${entry}/status`, "utf8");
147
+ const match = status.match(/^Uid:\s+(\d+)/m);
148
+ if (match && Number(match[1]) === uid) count++;
149
+ } catch {
150
+ // The process exited mid-scan, or is otherwise unreadable -- not this sandbox's concern.
151
+ }
152
+ }
153
+ return count;
154
+ }
155
+
108
156
  function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses: number): string[] | undefined {
109
157
  if (process.platform !== "linux" || !existsSync("/usr/bin/bwrap") || !existsSync("/usr/bin/prlimit")) return undefined;
110
158
  const serviceRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
@@ -185,7 +233,7 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
185
233
  packageRoot,
186
234
  "--",
187
235
  "/usr/bin/prlimit",
188
- `--nproc=${maxProcesses}`,
236
+ `--nproc=${(typeof process.getuid === "function" ? currentProcessCountForUid(process.getuid()) : 0) + maxProcesses}`,
189
237
  "--nofile=64",
190
238
  "--fsize=16777216",
191
239
  "--as=8589934592",
@@ -247,6 +295,10 @@ function normalizeRegistrations(value: unknown): SmokeRegistrations {
247
295
  providers: names("providers"),
248
296
  events: names("events"),
249
297
  renderers: names("renderers"),
298
+ sharedTools: names("sharedTools"),
299
+ sharedCommands: names("sharedCommands"),
300
+ sharedShortcuts: names("sharedShortcuts"),
301
+ sharedFlags: names("sharedFlags"),
250
302
  };
251
303
  }
252
304
 
@@ -132,6 +132,57 @@ describeIfSandboxed("runDoctor", () => {
132
132
  expect(report.conflicts).toEqual([]);
133
133
  });
134
134
 
135
+ it("never flags a conflict when every claimant self-declared the shared name via shared: true -- e.g. registerSharedSecretsCommand's own /secrets command, or Vehicle Shell's tools_list/tools_man/tools_type, both of which every claimant's own smoke scan sees as 'the first (and only) one' since each runs in its own isolated sandbox", async () => {
136
+ const home = piHome(["npm:pi-a", "npm:pi-b"]);
137
+ installNpmPackage(
138
+ home,
139
+ "pi-a",
140
+ { extensions: ["extension/index.ts"] },
141
+ {
142
+ "extension/index.ts": 'export default function (pi: any) { pi.registerCommand("secrets", { handler() {}, shared: true }); }',
143
+ },
144
+ );
145
+ installNpmPackage(
146
+ home,
147
+ "pi-b",
148
+ { extensions: ["extension/index.ts"] },
149
+ {
150
+ "extension/index.ts": 'export default function (pi: any) { pi.registerCommand("secrets", { handler() {}, shared: true }); }',
151
+ },
152
+ );
153
+
154
+ const report = await runDoctor(home);
155
+
156
+ expect(report.ok).toBe(true);
157
+ expect(report.conflicts).toEqual([]);
158
+ });
159
+
160
+ it("still flags a conflict when only SOME claimants self-declared the name shared -- an unmarked claimant is a genuinely ambiguous case, not the coordinated-by-design pattern shared: true exists to exempt", async () => {
161
+ const home = piHome(["npm:pi-a", "npm:pi-b"]);
162
+ installNpmPackage(
163
+ home,
164
+ "pi-a",
165
+ { extensions: ["extension/index.ts"] },
166
+ {
167
+ "extension/index.ts": 'export default function (pi: any) { pi.registerCommand("secrets", { handler() {}, shared: true }); }',
168
+ },
169
+ );
170
+ installNpmPackage(
171
+ home,
172
+ "pi-b",
173
+ { extensions: ["extension/index.ts"] },
174
+ {
175
+ "extension/index.ts": 'export default function (pi: any) { pi.registerCommand("secrets", { handler() {} }); }',
176
+ },
177
+ );
178
+
179
+ const report = await runDoctor(home);
180
+
181
+ expect(report.ok).toBe(false);
182
+ expect(report.conflicts).toHaveLength(1);
183
+ expect(report.conflicts[0]).toMatchObject({ kind: "command", name: "secrets" });
184
+ });
185
+
135
186
  it("surfaces a genuinely crashing extension as a non-ok result without treating it as a conflict", async () => {
136
187
  const home = piHome(["npm:pi-broken"]);
137
188
  installNpmPackage(
@@ -112,6 +112,10 @@ describeIfSandboxed("isolated extension smoke runner", () => {
112
112
  commands: ["hello"],
113
113
  shortcuts: ["ctrl+h"],
114
114
  flags: ["hello"],
115
+ sharedTools: [],
116
+ sharedCommands: [],
117
+ sharedShortcuts: [],
118
+ sharedFlags: [],
115
119
  providers: ["local"],
116
120
  events: ["session_start"],
117
121
  renderers: [],
@@ -213,6 +217,24 @@ describeIfSandboxed("isolated extension smoke runner", () => {
213
217
  expect(result.registrations.commands).toEqual(["object"]);
214
218
  });
215
219
 
220
+ it("never breaks under a real per-UID process count that already exceeds the sandbox's own maxProcesses budget -- confirmed live: prlimit's --nproc is a per-real-UID, system-wide limit, not scoped to this sandbox's own process tree, so a small absolute cap unconditionally breaks the instant the invoking user's REAL total process count exceeds it (routine on a busy multi-session workstation) -- any further thread Bun's own runtime needs internally (its native TS transpiler spawns one) immediately fails, aborting with SIGABRT and zero output, not a catchable error", async () => {
221
+ const dummyProcessCount = 40;
222
+ const dummies = Array.from({ length: dummyProcessCount }, () => Bun.spawn(["/usr/bin/sleep", "20"], { stdin: "ignore", stdout: "ignore", stderr: "ignore" }));
223
+ try {
224
+ const fixture = extension(`export default function (pi: any) {
225
+ pi.registerCommand("hello", { handler() {} });
226
+ }`);
227
+
228
+ const result = await runExtensionSmoke(fixture.root, fixture.path, { maxProcesses: 5 });
229
+
230
+ expect(result.status).toBe("ok");
231
+ expect(result.registrations.commands).toEqual(["hello"]);
232
+ } finally {
233
+ for (const dummy of dummies) dummy.kill("SIGKILL");
234
+ await Promise.all(dummies.map((dummy) => dummy.exited));
235
+ }
236
+ });
237
+
216
238
  it("keeps default package checks static and adds smoke results only when requested", async () => {
217
239
  const fixture = extension(
218
240
  'import { writeFileSync } from "node:fs"; export default function (pi: any) { writeFileSync("executed", "yes"); pi.registerCommand("x", { handler() {} }); }',