agent-skillboard 0.2.12 → 0.2.14

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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.14 — 2026-07-03
6
+
7
+ ### Fixed
8
+
9
+ - Sudo ownership restoration now only chowns managed guidance paths inside the
10
+ resolved invoking user's home, avoiding accidental ownership changes for
11
+ explicit agent roots outside that home.
12
+
13
+ ## 0.2.13 — 2026-07-03
14
+
15
+ ### Fixed
16
+
17
+ - Sudo-driven setup now restores ownership of managed agent guidance files and
18
+ directories under the invoking user's home to `SUDO_UID:SUDO_GID`, avoiding
19
+ root-owned `SKILL.md` files after `sudo npm install -g agent-skillboard`.
20
+
5
21
  ## 0.2.12 — 2026-07-03
6
22
 
7
23
  ### Changed
package/README.md CHANGED
@@ -96,8 +96,9 @@ npm install -g agent-skillboard
96
96
  If your system npm requires elevated permissions, `sudo npm install -g
97
97
  agent-skillboard` is also supported. In that flow, install-time setup resolves
98
98
  `SUDO_USER` and writes the user-level guidance skill under the invoking user's
99
- agent homes, while the `skillboard` binary still lands in the global prefix used
100
- by that npm command.
99
+ agent homes. Managed guidance files written under the user's home are restored
100
+ to the invoking user's ownership, while the `skillboard` binary still lands in
101
+ the global prefix used by that npm command.
101
102
 
102
103
  The install-time setup writes a user-level `skillboard` guidance skill under
103
104
  detected agent homes. For Codex, detection includes `CODEX_HOME/skills`,
package/docs/install.md CHANGED
@@ -38,7 +38,9 @@ sudo npm install -g agent-skillboard
38
38
  Under sudo, the postinstall setup resolves `SUDO_USER` and targets that user's
39
39
  agent homes instead of writing guidance under `/root`. The executable prefix is
40
40
  still decided by the npm command you run, so use the same npm/Node environment
41
- you expect to provide `skillboard` on `PATH`.
41
+ you expect to provide `skillboard` on `PATH`. Managed guidance files and
42
+ directories written under the user's home are restored to the invoking user's
43
+ `SUDO_UID:SUDO_GID` ownership.
42
44
 
43
45
  The install-time setup writes a user-level SkillBoard guidance skill under
44
46
  detected agent homes such as `CODEX_HOME`, `AGENTS_HOME`, `CLAUDE_HOME`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Let AI agents pick and use allowed skills in each workflow.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { access, chown, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createInterface } from "node:readline";
5
5
  import { promisify } from "node:util";
@@ -49,7 +49,9 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
49
49
  if (options.get("dir") !== undefined) {
50
50
  throw new Error("skillboard setup is agent-layer setup and does not accept --dir");
51
51
  }
52
- const targets = await agentSetupTargets(options, runtime);
52
+ const env = runtime.env ?? process.env;
53
+ const home = await resolveSetupHome(env, runtime);
54
+ const targets = await agentSetupTargets(options, runtime, home);
53
55
  if (targets.length === 0) {
54
56
  stdout.write("No supported agent user skill roots were detected.\n");
55
57
  stdout.write(`Create a supported agent home or pass --agent ${supportedAgentNames().join(",")} to choose targets.\n`);
@@ -66,7 +68,7 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
66
68
  return 1;
67
69
  }
68
70
  }
69
- const result = await installAgentIntegration(targets);
71
+ const result = await installAgentIntegration(targets, setupOwnership(env, runtime, home));
70
72
  stdout.write("SkillBoard agent integration installed.\n");
71
73
  writeList(stdout, "Created", result.created);
72
74
  writeList(stdout, "Updated", result.updated);
@@ -219,9 +221,9 @@ function defaultRuntime() {
219
221
  };
220
222
  }
221
223
 
222
- async function agentSetupTargets(options, runtime) {
224
+ async function agentSetupTargets(options, runtime, setupHome) {
223
225
  const env = runtime.env ?? process.env;
224
- const home = await resolveSetupHome(env, runtime);
226
+ const home = setupHome ?? await resolveSetupHome(env, runtime);
225
227
  const requested = readCsv(options.get("agent"));
226
228
  const supported = new Set(supportedAgentNames());
227
229
  const names = requested.length === 0 ? supportedAgentNames() : requested;
@@ -252,6 +254,31 @@ async function resolveSetupHome(env, runtime) {
252
254
  return env.HOME ?? env.USERPROFILE;
253
255
  }
254
256
 
257
+ function setupOwnership(env, runtime, home) {
258
+ if (process.platform === "win32" || !shouldUseSudoUserHome(env)) {
259
+ return null;
260
+ }
261
+ const uid = parseNonNegativeInteger(env.SUDO_UID);
262
+ const gid = parseNonNegativeInteger(env.SUDO_GID);
263
+ if (uid === null || gid === null) {
264
+ return null;
265
+ }
266
+ return {
267
+ uid,
268
+ gid,
269
+ home: resolve(home),
270
+ chown: runtime.chown ?? chown
271
+ };
272
+ }
273
+
274
+ function parseNonNegativeInteger(value) {
275
+ if (value === undefined || !/^\d+$/.test(value)) {
276
+ return null;
277
+ }
278
+ const parsed = Number(value);
279
+ return Number.isSafeInteger(parsed) ? parsed : null;
280
+ }
281
+
255
282
  function shouldUseSudoUserHome(env) {
256
283
  const sudoUser = nonEmpty(env.SUDO_USER);
257
284
  return process.platform !== "win32"
@@ -308,7 +335,7 @@ function nonEmpty(value) {
308
335
  return value === undefined || value.trim() === "" ? null : value;
309
336
  }
310
337
 
311
- async function installAgentIntegration(targets) {
338
+ async function installAgentIntegration(targets, ownership = null) {
312
339
  const created = [];
313
340
  const updated = [];
314
341
  const unchanged = [];
@@ -331,11 +358,45 @@ async function installAgentIntegration(targets) {
331
358
  }
332
359
  await mkdir(dirname(target.skillPath), { recursive: true });
333
360
  await writeFile(target.skillPath, content, "utf8");
361
+ await applyOwnership(target.skillPath, ownership);
334
362
  (existing === null ? created : updated).push(`${target.agent}:${target.skillPath}`);
335
363
  }
336
364
  return { created, updated, unchanged, preserved };
337
365
  }
338
366
 
367
+ async function applyOwnership(path, ownership) {
368
+ if (ownership === null) {
369
+ return;
370
+ }
371
+ for (const ownedPath of ownershipPaths(path, ownership.home)) {
372
+ await ownership.chown(ownedPath, ownership.uid, ownership.gid);
373
+ }
374
+ }
375
+
376
+ function ownershipPaths(path, home) {
377
+ const resolvedHome = resolve(home);
378
+ const resolvedPath = resolve(path);
379
+ if (!isInside(resolvedPath, resolvedHome)) {
380
+ return [];
381
+ }
382
+ const directories = [];
383
+ let current = dirname(resolvedPath);
384
+ while (current !== resolvedHome && isInside(current, resolvedHome)) {
385
+ directories.push(current);
386
+ const parent = dirname(current);
387
+ if (parent === current) {
388
+ break;
389
+ }
390
+ current = parent;
391
+ }
392
+ return [...directories.reverse(), resolvedPath];
393
+ }
394
+
395
+ function isInside(path, parent) {
396
+ const relativePath = relative(parent, path);
397
+ return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
398
+ }
399
+
339
400
  function agentIntegrationSkill() {
340
401
  return `---
341
402
  name: skillboard