pi-task-tracker 0.2.4 → 0.2.6
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 +1 -1
- package/index.ts +34 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Optional `taskTracker` key in `~/.pi/agent/settings.json` (all default to `true`
|
|
|
76
76
|
- `readme` — maintain the `README.md` project-structure section
|
|
77
77
|
- `autoCommit` — per-task commits; these are the `/rollback` checkpoints, so disabling this also disables rollback targets
|
|
78
78
|
- `guard` — master switch for the built-in permission layer (default `true`). Set `false` to defer permission decisions entirely to another extension (e.g. `pi-permission-system`): no external-write checks, no git-ask prompts, no cd sandbox.
|
|
79
|
-
- `externalWrite` — policy for tool calls that would create/modify files OUTSIDE the session directory: `"block"` (default, hard deny — external changes cannot be tracked or rolled back), `"ask"` (per-attempt confirmation), `"off"` (no enforcement; `pi-permission-system` rules still apply). Reads stay free. Copying files FROM outside INTO the session is allowed (`cp` checks only its destination); moving/deleting external paths is blocked. Everything INSIDE the session directory passes by default — the only sandbox-internal intervention is the `cd` disable. Git history protection is **inherited**: history-rewriting git (rebase/reset/amend/force-push/clean/...) prompts for confirmation right here, so `pi-permission-system` can be uninstalled without losing that layer; routine git passes untouched, an external `git init`/`clone` is allowed with a heads-up that the new repo is not tracked. Shell commands are analyzed statically (redirects, `rm/mv/cp/touch/...`, `cd` tracking, PowerShell equivalents); git protection is inherited from `pi-permission-system` — routine git passes untouched and history-rewriting git still prompts there; only redirect targets and `-C` destinations on git segments are checked. **`cd` is free inside the session directory; a `cd` that would move the working directory outside it is blocked** (that is the one operation that changes pi's effective scope). No cd-tracking is performed — resolution baseline stays the session root, so a deep in-session `cd` followed by enough `../` levels to escape may over-block, which is the fail-safe direction.
|
|
79
|
+
- `externalWrite` — policy for tool calls that would create/modify files OUTSIDE the session directory: `"block"` (default, hard deny — external changes cannot be tracked or rolled back), `"ask"` (per-attempt confirmation), `"off"` (no enforcement; `pi-permission-system` rules still apply). Reads stay free. Copying files FROM outside INTO the session is allowed (`cp` checks only its destination); moving/deleting external paths is blocked. Everything INSIDE the session directory passes by default — the only sandbox-internal intervention is the `cd` disable. Git history protection is **inherited**: history-rewriting git (rebase/reset/amend/force-push/clean/...) prompts for confirmation right here, so `pi-permission-system` can be uninstalled without losing that layer; routine git passes untouched, an external `git init`/`clone` is allowed with a heads-up that the new repo is not tracked. Git-Bash/MSYS paths (`/c/Users/...`, `/cygdrive/c/...`) are normalized before evaluation, so drive-absolute forms do not false-positive as external; `/tmp` and other non-drive names stay external (they resolve outside the session on Windows). Shell commands are analyzed statically (redirects, `rm/mv/cp/touch/...`, `cd` tracking, PowerShell equivalents); git protection is inherited from `pi-permission-system` — routine git passes untouched and history-rewriting git still prompts there; only redirect targets and `-C` destinations on git segments are checked. **`cd` is free inside the session directory; a `cd` that would move the working directory outside it is blocked** (that is the one operation that changes pi's effective scope). No cd-tracking is performed — resolution baseline stays the session root, so a deep in-session `cd` followed by enough `../` levels to escape may over-block, which is the fail-safe direction.
|
|
80
80
|
|
|
81
81
|
## Requirements
|
|
82
82
|
|
package/index.ts
CHANGED
|
@@ -142,10 +142,22 @@ const GUARD_PS_WRITE_CMDS = new Set([
|
|
|
142
142
|
"copy-item", "move-item", "clear-content", "ni", "ri", "mi", "ac", "sc", "sp",
|
|
143
143
|
]);
|
|
144
144
|
|
|
145
|
+
// Normalize a Git-Bash/MSYS style path before resolution. On Windows,
|
|
146
|
+
// path.resolve("/c/Users/x") yields the drive-relative "C:\c\Users\x", which
|
|
147
|
+
// which breaks the external check. "/c/Users/x" and "/cygdrive/c/Users/x"
|
|
148
|
+
// become "C:\Users\x"-style drive paths. Names longer than one letter (/tmp,
|
|
149
|
+
// /usr, ...) are NOT drive mappings and stay untouched (outside the session
|
|
150
|
+
// on Windows anyway).
|
|
151
|
+
function normalizeShellPath(t: string): string {
|
|
152
|
+
const m = /^\/(?:cygdrive\/)?([a-zA-Z])\/(.*)$/.exec(t);
|
|
153
|
+
if (!m) return t;
|
|
154
|
+
const BS = String.fromCharCode(92);
|
|
155
|
+
return m[1].toUpperCase() + ":" + BS + m[2].split("/").join(BS);
|
|
156
|
+
}
|
|
145
157
|
// Resolve a shell token to an absolute path; unresolvable tokens (env vars,
|
|
146
158
|
// command substitution) are treated as external - conservative by design.
|
|
147
159
|
function guardResolve(sessionCwd: string, token: string): { abs: string; external: boolean } {
|
|
148
|
-
let t = token.replace(/^["']|["']$/g, "").trim();
|
|
160
|
+
let t = normalizeShellPath(token.replace(/^["']|["']$/g, "").trim());
|
|
149
161
|
if (!t) return { abs: sessionCwd, external: false };
|
|
150
162
|
if (t.startsWith("~")) return { abs: t, external: true };
|
|
151
163
|
if (/[\$`]/.test(t)) return { abs: t, external: true };
|
|
@@ -244,7 +256,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
|
|
|
244
256
|
// interpreter: block only if the command TEXT names an external path
|
|
245
257
|
for (const m of seg.matchAll(/[A-Za-z]:[\/]\S*|\/(?:[\w.-]+\/)+[\w.-]+/g)) {
|
|
246
258
|
if (guardIsDevNull(m[0])) continue;
|
|
247
|
-
|
|
259
|
+
const np = normalizeShellPath(m[0]);
|
|
260
|
+
if (isExternalAbs(np, sessionCwd)) { out.block = np; break; }
|
|
248
261
|
}
|
|
249
262
|
continue;
|
|
250
263
|
}
|
|
@@ -254,7 +267,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
|
|
|
254
267
|
pathArgs.push(tokens[k]);
|
|
255
268
|
}
|
|
256
269
|
const checkList = isCopy ? pathArgs.slice(-1) : pathArgs;
|
|
257
|
-
for (const
|
|
270
|
+
for (const raw of checkList) {
|
|
271
|
+
const t = normalizeShellPath(raw);
|
|
258
272
|
if (/^[A-Za-z]:[\\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
|
|
259
273
|
if (guardIsDevNull(t)) continue;
|
|
260
274
|
if (isExternalAbs(t, sessionCwd)) out.block = t;
|
|
@@ -276,7 +290,7 @@ const GUARD_ASK_PROMPT = "pi-task-tracker external-write guard";
|
|
|
276
290
|
async function guardExternalWriteHit(
|
|
277
291
|
event: { toolName: string; input?: unknown },
|
|
278
292
|
sessionCwd: string,
|
|
279
|
-
mode: "block" | "ask",
|
|
293
|
+
mode: "block" | "ask" | "warn",
|
|
280
294
|
ctx?: { hasUI?: boolean; ui?: { confirm: (t: string, m: string) => Promise<boolean>; notify: (m: string, kind?: string) => void } }
|
|
281
295
|
): Promise<string | null> {
|
|
282
296
|
if (!sessionCwd) return null;
|
|
@@ -299,6 +313,12 @@ async function guardExternalWriteHit(
|
|
|
299
313
|
}
|
|
300
314
|
if (hit.block) {
|
|
301
315
|
const why = "External changes cannot be tracked or rolled back by pi-task-tracker.";
|
|
316
|
+
if (mode === "warn") {
|
|
317
|
+
if (ctx?.hasUI && ctx.ui) {
|
|
318
|
+
ctx.ui.notify(`pi-task-tracker: command writes to '${hit.block}' outside the session directory. ${why}`, "warning");
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
302
322
|
if (mode === "ask") {
|
|
303
323
|
if (!ctx?.hasUI || !ctx.ui) return `pi-task-tracker scope guard: '${hit.block}' is outside the session directory and no UI is available. ${why}`;
|
|
304
324
|
const ok = await ctx.ui.confirm(
|
|
@@ -321,6 +341,12 @@ async function guardExternalWriteHit(
|
|
|
321
341
|
if (!isWriteish && (GUARD_READ_TOOLS.has(toolName) || coveredByGuardHeuristics(toolName))) return null;
|
|
322
342
|
if (!isExternalAbs2(sessionCwd, p)) return null;
|
|
323
343
|
const why = "External changes cannot be tracked or rolled back by pi-task-tracker.";
|
|
344
|
+
if (mode === "warn") {
|
|
345
|
+
if (ctx?.hasUI && ctx.ui) {
|
|
346
|
+
ctx.ui.notify(`pi-task-tracker: '${toolName}' targets '${p}' outside the session directory. ${why}`, "warning");
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
324
350
|
if (mode === "ask") {
|
|
325
351
|
if (!ctx?.hasUI || !ctx.ui) return `pi-task-tracker scope guard: '${p}' is outside the session directory and no UI is available to approve external writes. ${why}`;
|
|
326
352
|
const ok = await ctx.ui.confirm(
|
|
@@ -334,6 +360,7 @@ async function guardExternalWriteHit(
|
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
function isExternalAbs2(base: string, p: string): boolean {
|
|
363
|
+
p = normalizeShellPath(p);
|
|
337
364
|
const abs = path.isAbsolute(p) ? p : path.resolve(base, p);
|
|
338
365
|
const rel = path.relative(base, abs);
|
|
339
366
|
return rel.startsWith("..") || path.isAbsolute(rel);
|
|
@@ -584,8 +611,8 @@ interface TrackerConfig {
|
|
|
584
611
|
/** master switch for the built-in permission layer (external-write guard,
|
|
585
612
|
* git-ask inheritance, cd disable). false = defer entirely to other extensions */
|
|
586
613
|
guard: boolean;
|
|
587
|
-
/** Writes outside the session directory: "
|
|
588
|
-
externalWrite: "block" | "ask" | "off";
|
|
614
|
+
/** Writes outside the session directory: "ask" (default) | "warn" (allow + yellow notice) | "block" | "off" */
|
|
615
|
+
externalWrite: "block" | "ask" | "warn" | "off";
|
|
589
616
|
}
|
|
590
617
|
|
|
591
618
|
function readConfig(): TrackerConfig {
|
|
@@ -600,7 +627,7 @@ function readConfig(): TrackerConfig {
|
|
|
600
627
|
readme: t.readme !== false,
|
|
601
628
|
autoCommit: t.autoCommit !== false,
|
|
602
629
|
guard: t.guard !== false,
|
|
603
|
-
externalWrite: t.externalWrite === "block" || t.externalWrite === "off" ? t.externalWrite : "ask",
|
|
630
|
+
externalWrite: t.externalWrite === "block" || t.externalWrite === "off" || t.externalWrite === "warn" ? t.externalWrite : "ask",
|
|
604
631
|
};
|
|
605
632
|
} catch {
|
|
606
633
|
return fallback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-task-tracker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Task workflow tracking for the pi coding agent: TODO/README maintenance, per-task git auto-commits as checkpoints, and an interactive /rollback. Hashline-aware, nested-repo aware, subagent-artifact-safe.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|