@signalridge/pi-subagents 1.10.3 → 1.11.1

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/src/settings.ts CHANGED
@@ -12,6 +12,7 @@ import { isManagedAgentTier, MAX_AGENT_TIER_PROFILES } from "@signalridge/pi-sub
12
12
  // edit index.ts in the same change.
13
13
  import { setDefaultToolTimeoutMs } from "./agent-runner.js";
14
14
  import { NO_FALLBACK } from "./agent-types.js";
15
+ import { isProjectResource } from "./project-trust.js";
15
16
  import type { JoinMode, ThinkingLevel } from "./types.js";
16
17
 
17
18
  /** How a `@handle message` mention is dispatched. See `agentMentions`. */
@@ -722,11 +723,13 @@ function readSettingsFile(path: string): ReadSettings {
722
723
  * Load merged settings: global provides defaults, project overrides. Agent-tier
723
724
  * profiles are complete replacement units: a project entry never inherits one
724
725
  * field from the global entry, and an invalid project entry blocks that global
725
- * entry instead of silently reviving it.
726
+ * entry instead of silently reviving it. Runtime callers must pass captured trust;
727
+ * the default preserves standalone helper compatibility.
726
728
  */
727
- export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
728
- const global = readSettingsFile(globalPath());
729
- const project = readSettingsFile(projectPath(cwd));
729
+ export function loadSettings(cwd: string = process.cwd(), projectTrusted = true): SubagentsSettings {
730
+ const empty = (): ReadSettings => ({ settings: {}, agentTiers: emptyAgentTiersSource() });
731
+ const global = projectTrusted || !isProjectResource(globalPath(), cwd) ? readSettingsFile(globalPath()) : empty();
732
+ const project = projectTrusted ? readSettingsFile(projectPath(cwd)) : empty();
730
733
  const { agentTiers: _globalAgentTiers, ...globalSettings } = global.settings;
731
734
  const { agentTiers: _projectAgentTiers, ...projectSettings } = project.settings;
732
735
  const agentTiers = mergeAgentTierSources(global.agentTiers, project.agentTiers);
@@ -30,37 +30,48 @@ export interface PreloadedSkill {
30
30
  content: string;
31
31
  }
32
32
 
33
- export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
34
- return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
33
+ export interface PreloadSkillsOptions {
34
+ /** Denied/unknown project trust excludes both project roots; user skills remain available. */
35
+ projectTrusted: boolean;
36
+ /** Preserve denied source provenance even through aliases of user skill roots. */
37
+ deniedResource?: (path: string) => boolean;
35
38
  }
36
39
 
37
- function loadSkillContent(name: string, cwd: string): string {
40
+ export function preloadSkills(skillNames: string[], cwd: string, options: PreloadSkillsOptions): PreloadedSkill[] {
41
+ return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd, options) }));
42
+ }
43
+
44
+ function loadSkillContent(name: string, cwd: string, options: PreloadSkillsOptions): string {
38
45
  if (isUnsafeName(name)) {
39
46
  return `(Skill "${name}" skipped: name contains path traversal characters)`;
40
47
  }
41
48
  const roots = [
42
- join(cwd, ".pi", "skills"), // project — Pi standard
43
- join(cwd, ".agents", "skills"), // project — Agent Skills spec
49
+ ...(options.projectTrusted === true ? [
50
+ join(cwd, ".pi", "skills"), // project — Pi standard
51
+ join(cwd, ".agents", "skills"), // project — Agent Skills spec
52
+ ] : []),
44
53
  join(getAgentDir(), "skills"), // user — Pi standard
45
54
  join(homedir(), ".agents", "skills"), // user — Agent Skills spec
46
55
  join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
47
56
  ];
48
57
  for (const root of roots) {
49
- const content = findInRoot(root, name);
58
+ if (options.deniedResource?.(root)) continue;
59
+ const content = findInRoot(root, name, options.deniedResource);
50
60
  if (content !== undefined) return content;
51
61
  }
52
62
  return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
53
63
  }
54
64
 
55
- function findInRoot(root: string, name: string): string | undefined {
65
+ function findInRoot(root: string, name: string, deniedResource?: (path: string) => boolean): string | undefined {
56
66
  if (isSymlink(root)) return undefined; // reject symlinked roots entirely
57
- const flat = safeReadFile(join(root, `${name}.md`))?.trim();
67
+ const flatPath = join(root, `${name}.md`);
68
+ const flat = deniedResource?.(flatPath) ? undefined : safeReadFile(flatPath)?.trim();
58
69
  if (flat !== undefined) return flat;
59
- return findSkillDirectory(root, name);
70
+ return findSkillDirectory(root, name, deniedResource);
60
71
  }
61
72
 
62
73
  /** BFS under `root` for a directory named `name` containing `SKILL.md`. Pi-conforming filters. */
63
- function findSkillDirectory(root: string, name: string): string | undefined {
74
+ function findSkillDirectory(root: string, name: string, deniedResource?: (path: string) => boolean): string | undefined {
64
75
  if (!existsSync(root)) return undefined;
65
76
  const queue: string[] = [root];
66
77
 
@@ -85,6 +96,7 @@ function findSkillDirectory(root: string, name: string): string | undefined {
85
96
  // Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
86
97
  const path = join(current, entry.name);
87
98
  const skillMd = join(path, "SKILL.md");
99
+ if (deniedResource?.(skillMd)) continue;
88
100
  const isSkillDir = existsSync(skillMd);
89
101
 
90
102
  if (isSkillDir) {
package/src/types.ts CHANGED
@@ -42,10 +42,12 @@ export interface AgentConfig {
42
42
  */
43
43
  color?: string;
44
44
  /**
45
- * Tools whose every call needs the user to agree first (`ask_tools:`). The
46
- * third answer between `tools:` and `disallowed_tools:`, for tools that are
47
- * usually fine and occasionally not. Approval comes from the human, never
48
- * from a model — see `ask-tools.ts`.
45
+ * Tools whose first call needs human approval for the child session
46
+ * (`ask_tools:`), including resumed turns of this in-memory child. A reopened
47
+ * child must ask again. The third answer between
48
+ * `tools:` and `disallowed_tools:` for tools that are usually fine and
49
+ * occasionally not. Approval comes from the human, never from a model —
50
+ * see `ask-tools.ts`.
49
51
  */
50
52
  askTools?: string[];
51
53
  /**