@mrclrchtr/supi-consulting 11.0.1 → 11.2.0

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/config.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { loadSupiConfig } from "@mrclrchtr/supi-core/config";
1
+ import { constants } from "node:fs";
2
+ import { access, realpath, stat } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { loadSupiConfig, loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
2
5
 
3
6
  /** The persisted SuPi configuration section owned by this package. */
4
7
  export const CONSULTING_CONFIG_SECTION = "consulting";
@@ -7,20 +10,73 @@ export const CONSULTING_CONFIG_SECTION = "consulting";
7
10
  export interface ConsultingConfig extends Record<string, unknown> {
8
11
  /** Enable agent availability discovery and the consulting_run tool. */
9
12
  agentToolEnabled: boolean;
13
+ /** Absolute directories available for inspection with either Workspace Access selection. */
14
+ additionalReadOnlyDirectories: readonly string[];
10
15
  }
11
16
 
12
17
  /** Default Consulting configuration. */
13
18
  export const CONSULTING_DEFAULTS: ConsultingConfig = Object.freeze({
14
19
  agentToolEnabled: true,
20
+ additionalReadOnlyDirectories: Object.freeze([]),
15
21
  });
16
22
 
17
- /** Load only the new consulting configuration section for a workspace. */
18
- export function loadConsultingConfig(cwd: string, homeDir?: string): ConsultingConfig {
19
- const raw = loadSupiConfig(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, { homeDir });
23
+ /** Load trusted scoped configuration; untrusted projects cannot grant external directory access. */
24
+ export function loadConsultingConfig(
25
+ cwd: string,
26
+ homeDir?: string,
27
+ projectTrusted = true,
28
+ ): ConsultingConfig {
29
+ const raw = projectTrusted
30
+ ? loadSupiConfig(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, { homeDir })
31
+ : loadSupiConfigForScope(CONSULTING_CONFIG_SECTION, cwd, CONSULTING_DEFAULTS, {
32
+ scope: "global",
33
+ homeDir,
34
+ });
20
35
  return {
21
36
  agentToolEnabled:
22
37
  typeof raw.agentToolEnabled === "boolean"
23
38
  ? raw.agentToolEnabled
24
39
  : CONSULTING_DEFAULTS.agentToolEnabled,
40
+ additionalReadOnlyDirectories: validateAdditionalReadOnlyDirectories(
41
+ raw.additionalReadOnlyDirectories,
42
+ ),
25
43
  };
26
44
  }
45
+
46
+ /** Validate directory-list syntax before persistence or execution; paths are literal, not globs. */
47
+ export function validateAdditionalReadOnlyDirectories(value: unknown): string[] {
48
+ if (
49
+ !Array.isArray(value) ||
50
+ value.some((path) => typeof path !== "string" || !isAbsolute(path) || path.includes("\0"))
51
+ ) {
52
+ throw new Error(
53
+ "consulting.additionalReadOnlyDirectories must be a list of absolute directory paths.",
54
+ );
55
+ }
56
+ return [...new Set(value)];
57
+ }
58
+
59
+ /** Resolve current scoped grants for each run, following directory aliases and failing before process work. */
60
+ export async function resolveAdditionalReadOnlyDirectories(
61
+ cwd: string,
62
+ homeDir?: string,
63
+ projectTrusted = true,
64
+ ): Promise<string[]> {
65
+ const config = loadConsultingConfig(cwd, homeDir, projectTrusted);
66
+ const directories = await Promise.all(
67
+ config.additionalReadOnlyDirectories.map(async (path) => {
68
+ try {
69
+ const canonical = await realpath(path);
70
+ if (!(await stat(canonical)).isDirectory()) throw new Error("Not a directory.");
71
+ await access(canonical, constants.R_OK | constants.X_OK);
72
+ return canonical;
73
+ } catch (error) {
74
+ throw new Error(
75
+ `Consulting additional read-only directory is missing, inaccessible, or not a directory: ${path}`,
76
+ { cause: error },
77
+ );
78
+ }
79
+ }),
80
+ );
81
+ return [...new Set(directories)];
82
+ }
package/src/runtime.ts CHANGED
@@ -17,6 +17,7 @@ type FooterState = "idle" | "checking" | "ready";
17
17
  /** Context needed to report availability and update the footer. */
18
18
  export type ConsultingRefreshContext = {
19
19
  ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
20
+ isProjectTrusted?: ExtensionContext["isProjectTrusted"];
20
21
  };
21
22
 
22
23
  /** Session runtime for immutable availability and Conversation Handle state. */
@@ -85,7 +86,7 @@ export class ConsultingRuntime {
85
86
  if (context) this.#statusUi = context.ui;
86
87
  this.#stopSpinner();
87
88
  this.#setFooterState("idle");
88
- const config = loadConsultingConfig(cwd, this.#homeDir);
89
+ const config = loadConsultingConfig(cwd, this.#homeDir, context?.isProjectTrusted?.() ?? true);
89
90
  if (!config.agentToolEnabled) {
90
91
  this.#deactivateTool();
91
92
  return;
@@ -141,6 +142,7 @@ export class ConsultingRuntime {
141
142
  adapter: this.adapter,
142
143
  availability,
143
144
  handles: this.handles,
145
+ homeDir: this.#homeDir,
144
146
  });
145
147
  this.#toolRegistered = true;
146
148
  }
package/src/settings.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
3
- import { CONSULTING_CONFIG_SECTION, CONSULTING_DEFAULTS } from "./config.ts";
3
+ import {
4
+ CONSULTING_CONFIG_SECTION,
5
+ CONSULTING_DEFAULTS,
6
+ validateAdditionalReadOnlyDirectories,
7
+ } from "./config.ts";
4
8
  import type { ConsultingRuntime } from "./runtime.ts";
5
9
 
6
10
  /** Register the Consulting setting with an awaited availability refresh. */
@@ -21,6 +25,13 @@ export function registerConsultingSettings(
21
25
  label: "Consultation tool",
22
26
  description: "Enable consulting_run and its availability checks.",
23
27
  },
28
+ {
29
+ kind: "stringList" as const,
30
+ key: "additionalReadOnlyDirectories",
31
+ label: "Additional read-only directories",
32
+ description:
33
+ "Comma-separated absolute directories for every Consultation, including without Workspace Access. No shell expansion or globs; broad paths can expose sensitive files.",
34
+ },
24
35
  ],
25
36
  ...(homeDir ? { homeDir } : {}),
26
37
  });
@@ -28,9 +39,17 @@ export function registerConsultingSettings(
28
39
  registerSettings(pi, {
29
40
  ...fixedSettings,
30
41
  apply: async (request) => {
42
+ if (request.fieldKey === "additionalReadOnlyDirectories" && request.action.kind === "set") {
43
+ validateAdditionalReadOnlyDirectories(
44
+ request.action.value
45
+ .split(",")
46
+ .map((path) => path.trim())
47
+ .filter(Boolean),
48
+ );
49
+ }
31
50
  const result = await fixedSettings.apply(request);
32
51
  if (request.fieldKey === "agentToolEnabled") {
33
- await runtime.refresh(request.cwd, request.ctx ? { ui: request.ctx.ui } : undefined);
52
+ await runtime.refresh(request.cwd, request.ctx);
34
53
  }
35
54
  return result;
36
55
  },
@@ -5,6 +5,7 @@ import type {
5
5
  ExtensionContext,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import type { ConsultingAgentAdapter, ConsultingAgentAvailability } from "../../agents/types.ts";
8
+ import { resolveAdditionalReadOnlyDirectories } from "../../config.ts";
8
9
  import {
9
10
  appendHandleCreated,
10
11
  appendHandleRetired,
@@ -19,6 +20,8 @@ export interface ConsultingRunDependencies {
19
20
  adapter: ConsultingAgentAdapter;
20
21
  availability: ConsultingAgentAvailability;
21
22
  handles: ConversationHandleStore;
23
+ /** Override the user home for scoped configuration loading. */
24
+ homeDir?: string;
22
25
  }
23
26
 
24
27
  type ConsultingExecute = NonNullable<Parameters<ExtensionAPI["registerTool"]>[0]["execute"]>;
@@ -84,6 +87,11 @@ async function runNew(options: NewRunOptions): Promise<ReturnType<typeof buildCo
84
87
  model: input.new.model,
85
88
  canonicalWorkingDirectory: cwd,
86
89
  workspaceAccess,
90
+ additionalReadOnlyDirectories: await resolveAdditionalReadOnlyDirectories(
91
+ ctx.cwd,
92
+ dependencies.homeDir,
93
+ ctx.isProjectTrusted(),
94
+ ),
87
95
  signal,
88
96
  onProgress: progress,
89
97
  onWarning: (warning) => {
@@ -136,6 +144,11 @@ async function runContinue(
136
144
  model: record.model,
137
145
  canonicalWorkingDirectory: record.canonicalWorkingDirectory,
138
146
  workspaceAccess: record.workspaceAccess,
147
+ additionalReadOnlyDirectories: await resolveAdditionalReadOnlyDirectories(
148
+ ctx.cwd,
149
+ dependencies.homeDir,
150
+ ctx.isProjectTrusted(),
151
+ ),
139
152
  continuation: record.continuation,
140
153
  signal,
141
154
  onProgress: progress,
@@ -1,3 +1,3 @@
1
1
  /** Selection guidance for the dynamically activated consulting_run tool. */
2
2
  export const toolDescription =
3
- "Consult a supported external agent for focused web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts that do not need external analysis. Expose the current workspace only when the Consultation needs repository evidence.";
3
+ "Consult an external agent for web research, design advice, workspace analysis, or an independent second opinion. Use direct tools for repository facts. Expose the current workspace only when the Consultation needs repository evidence. Configured extra read-only directories apply to either workspace selection.";
@@ -12,6 +12,8 @@ export interface RegisterConsultingRunOptions {
12
12
  adapter: ConsultingAgentAdapter;
13
13
  availability: ConsultingAgentAvailability;
14
14
  handles: ConversationHandleStore;
15
+ /** Override the user home for scoped configuration loading. */
16
+ homeDir?: string;
15
17
  }
16
18
 
17
19
  /** Register consulting_run from one immutable availability snapshot. */
@@ -31,6 +33,7 @@ export function registerConsultingRunTool(options: RegisterConsultingRunOptions)
31
33
  adapter: options.adapter,
32
34
  availability: options.availability,
33
35
  handles: options.handles,
36
+ homeDir: options.homeDir,
34
37
  }),
35
38
  });
36
39
  }