pi-harness-delegate 0.6.0 → 0.6.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/README.md CHANGED
@@ -134,7 +134,7 @@ You are a senior engineer delegated by the pi coding agent.
134
134
 
135
135
  Custom templates are just files dropped in the above dirs — any registered name becomes a valid `mode`.
136
136
 
137
- **Project trust:** project-local templates (`.pi/delegate/templates/`) load only when pi itself considers the current project trusted (`ctx.isProjectTrusted()`, backed by pi's own trust store outside the project — the same trust that gates other project-scoped behavior). Trust it via pi's own trust prompt (shown the first time you open an untrusted directory) or your `defaultProjectTrust` setting; `/delegate status` reports whether the current project is trusted and, if not, that project-local templates are being skipped. There is **no way to grant trust from inside the project** — no `.pi/trusted` file, no environment variable. Earlier versions supported both (`.pi/trusted` containing `1`, or `PI_TRUSTED=1`/`PI_DELEGATE_TRUSTED=1` in the environment); both were removed as a security fix — a repo could commit `.pi/trusted` and declare itself trusted, letting a cloned hostile repo's templates silently override a builtin (e.g. widening `review` from `readonly` to `edit` and attaching a `verify:` command that runs host-side). If you relied on either, switch to pi's trust prompt or `defaultProjectTrust`.
137
+ **Project trust:** project-local templates (`.pi/delegate/templates/`) load only when pi itself considers the current project trusted (`ctx.isProjectTrusted()`, backed by pi's own trust store outside the project — the same trust that gates other project-scoped behavior). Trust it via pi's own trust prompt (shown the first time you open an untrusted directory) or your `defaultProjectTrust` setting; `/delegate status` reports whether the current project is trusted and, if not, that project-local templates are being skipped. There is **no way to grant trust from inside the project** — no `.pi/trusted` file, no environment variable. Earlier versions supported both (`.pi/trusted` containing `1`, or `PI_TRUSTED=1`/`PI_DELEGATE_TRUSTED=1` in the environment); both were removed as a security fix — a repo could commit `.pi/trusted` and declare itself trusted, letting a cloned hostile repo's templates silently override a builtin (e.g. widening `review` from `readonly` to `edit` and attaching a `verify:` command that runs host-side). If you relied on either, switch to pi's trust prompt or `defaultProjectTrust`. **On upgrade this is otherwise silent** — a project override shares its name with the builtin it replaces, so the run just uses the builtin and looks fine — so a delegation in an untrusted project that actually has `.pi/delegate/templates/` now prints a one-time warning saying they were skipped, and points out a leftover `.pi/trusted` file if one is present (it no longer does anything and can be deleted).
138
138
 
139
139
  ## How the main session consumes the output
140
140
 
@@ -86,7 +86,13 @@ import { NotifyBatcher } from './notify.ts';
86
86
  import { type FeedEntry, progressWindow } from './progress.ts';
87
87
  import { formatFanoutChip, multiProgressWindow, type RunRow } from './progress-multi.ts';
88
88
  import { runHarness } from './runner.ts';
89
- import { type DelegateTemplate, loadTemplates, resolveNativePermission } from './templates.ts';
89
+ import {
90
+ type DelegateTemplate,
91
+ describeSkippedProjectTemplates,
92
+ loadTemplates,
93
+ projectTemplatePresence,
94
+ resolveNativePermission,
95
+ } from './templates.ts';
90
96
  import { mapClaudeUsage } from './usage.ts';
91
97
 
92
98
  /** Render a possibly-unknown cost — `null` means the harness didn't report one, not a measured $0. */
@@ -188,6 +194,28 @@ function outputsDirFor(harness: string): string {
188
194
  * project-local delegate templates load — see the trust-tier comment on `loadTemplates`. Fails
189
195
  * closed (untrusted) if the host is old enough not to expose the method, or if it throws.
190
196
  */
197
+ /**
198
+ * Warn once per project when trusted-only content was silently skipped.
199
+ *
200
+ * Before 0.6.0 a committed `.pi/trusted` file (or `PI_TRUSTED=1`) granted trust; both were removed as
201
+ * a security fix. A user who relied on either loses their project-local templates on upgrade with no
202
+ * visible signal — an override shares its name with the builtin it replaces, so the run just uses the
203
+ * builtin and looks fine. `/delegate status` reports trust state, but nobody runs it unless something
204
+ * already looks wrong, so this fires on an actual delegation instead — and only when the project
205
+ * demonstrably has the content being skipped, so it never nags anyone unaffected.
206
+ */
207
+ const warnedUntrustedProjects = new Set<string>();
208
+
209
+ function warnIfProjectTemplatesSkipped(ctx: ExtensionContext, trusted: boolean): void {
210
+ if (trusted || warnedUntrustedProjects.has(ctx.cwd)) return;
211
+ const lines = describeSkippedProjectTemplates(projectTemplatePresence(ctx.cwd));
212
+ if (lines.length === 0) return;
213
+ warnedUntrustedProjects.add(ctx.cwd);
214
+ const msg = lines.join('\n');
215
+ if (ctx.hasUI) ctx.ui.notify?.(msg, 'warning');
216
+ else process.stderr.write(`${msg}\n`);
217
+ }
218
+
191
219
  function isProjectTrusted(ctx: ExtensionContext): boolean {
192
220
  try {
193
221
  return typeof ctx.isProjectTrusted === 'function' && ctx.isProjectTrusted() === true;
@@ -611,7 +639,9 @@ async function delegate(
611
639
  throw new Error(
612
640
  `unknown harness "${harnessName}". Available: ${HARNESS_NAMES.join(', ')} (aliases: ${Object.keys(ALIASES).join(', ')})`,
613
641
  );
614
- const templates = loadTemplates(ctx.cwd, harnessName, isProjectTrusted(ctx));
642
+ const projectTrusted = isProjectTrusted(ctx);
643
+ warnIfProjectTemplatesSkipped(ctx, projectTrusted);
644
+ const templates = loadTemplates(ctx.cwd, harnessName, projectTrusted);
615
645
  const mode = opts.mode || config.defaultMode;
616
646
  const template = templates.get(mode);
617
647
  if (!template)
@@ -201,3 +201,56 @@ export function resolveNativePermission(
201
201
  if (!nativePermission) return undefined;
202
202
  return effectivePermission === templatePermission ? nativePermission : undefined;
203
203
  }
204
+
205
+ /**
206
+ * What a project has on disk that only loads when the project is trusted.
207
+ *
208
+ * Used to warn a user whose project-local templates stopped loading after the 0.6.0 security fix
209
+ * (§19 of ROADMAP) — before it, a committed `.pi/trusted` file or `PI_TRUSTED=1` granted trust, and
210
+ * both were removed. The failure is otherwise invisible: an override shares its name with the
211
+ * builtin it replaces, so the run silently uses the builtin and produces plausible output.
212
+ *
213
+ * Pure filesystem inspection — no trust logic. The caller supplies the trust decision.
214
+ */
215
+ export function projectTemplatePresence(cwd: string): {
216
+ /** Template dirs that exist and would load if the project were trusted. */
217
+ dirs: string[];
218
+ /** A leftover `.pi/trusted` file — strong evidence the user relied on the removed mechanism. */
219
+ staleTrustFile: boolean;
220
+ } {
221
+ const candidates = [projectTemplatesDir(cwd), legacyProjectTemplatesDir(cwd)];
222
+ const dirs: string[] = [];
223
+ for (const dir of candidates) {
224
+ try {
225
+ if (readdirSync(dir).some(f => f.endsWith('.md'))) dirs.push(dir);
226
+ } catch {
227
+ // absent or unreadable — nothing to warn about
228
+ }
229
+ }
230
+ let staleTrustFile = false;
231
+ try {
232
+ staleTrustFile = existsSync(join(cwd, '.pi', 'trusted'));
233
+ } catch {
234
+ staleTrustFile = false;
235
+ }
236
+ return { dirs, staleTrustFile };
237
+ }
238
+
239
+ /** One-line notices for a project whose trusted-only content was skipped. Empty when nothing applies. */
240
+ export function describeSkippedProjectTemplates(presence: { dirs: string[]; staleTrustFile: boolean }): string[] {
241
+ if (presence.dirs.length === 0 && !presence.staleTrustFile) return [];
242
+ const out: string[] = [];
243
+ if (presence.dirs.length > 0) {
244
+ out.push(
245
+ `⚠ project-local templates were NOT loaded — this project is untrusted (${presence.dirs.join(', ')})`,
246
+ " trust it via pi's trust prompt or defaultProjectTrust; /delegate status shows trust state",
247
+ );
248
+ }
249
+ if (presence.staleTrustFile) {
250
+ out.push(
251
+ ' a leftover .pi/trusted file was found — it no longer grants trust (removed in 0.6.0 as a',
252
+ ' security fix, since a repo could use it to trust itself) and can be deleted',
253
+ );
254
+ }
255
+ return out;
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-delegate",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Delegate work to any harness (Claude Code, Muse, OpenCode, Amp) from the pi coding agent \u2014 code reviews, plans, implementation, security audits, docs, or your own custom templates.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.14",