pi-task-tracker 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.ts +26 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -75,7 +75,8 @@ Optional `taskTracker` key in `~/.pi/agent/settings.json` (all default to `true`
75
75
  - `todo` — maintain `TODO.md`
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
- - `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. 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.
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`/`Set-Location` are disabled outright** (sandbox design: pi's bash tool runs each command in a fresh child process pinned to the session directory anyway, so a `cd` can never move the session — denying it keeps the analysis simple and the writable scope exactly the trackable scope).
79
80
 
80
81
  ## Requirements
81
82
 
package/index.ts CHANGED
@@ -144,15 +144,15 @@ const GUARD_PS_WRITE_CMDS = new Set([
144
144
 
145
145
  // Resolve a shell token to an absolute path; unresolvable tokens (env vars,
146
146
  // command substitution) are treated as external - conservative by design.
147
- function guardResolve(effCwd: string, sessionCwd: string, token: string): { abs: string; external: boolean } {
147
+ function guardResolve(sessionCwd: string, token: string): { abs: string; external: boolean } {
148
148
  let t = token.replace(/^["']|["']$/g, "").trim();
149
- if (!t) return { abs: effCwd, external: false };
149
+ if (!t) return { abs: sessionCwd, external: false };
150
150
  if (t.startsWith("~")) return { abs: t, external: true };
151
151
  if (/[\$`]/.test(t)) return { abs: t, external: true };
152
152
  if (/^[A-Za-z]:[\\/]/.test(t) || t.startsWith("/")) {
153
153
  return { abs: t, external: isExternalAbs(t, sessionCwd) };
154
154
  }
155
- const abs = path.resolve(effCwd, t);
155
+ const abs = path.resolve(sessionCwd, t);
156
156
  return { abs, external: isExternalAbs(abs, sessionCwd) };
157
157
  }
158
158
 
@@ -172,30 +172,31 @@ function guardIsDevNull(t: string): boolean {
172
172
  // write-command arguments (copy-like: destination only), interpreter payloads
173
173
  // mentioning external absolute paths, cd tracking. Git arguments are NOT
174
174
  // analyzed (inherited protection: only history-rewriting git asks).
175
+ // Analyze one shell command. cd/Set-Location are DISABLED outright (sandbox
176
+ // design: the process cwd is pinned to the session directory, which also
177
+ // removes any need for cd-tracking in this analysis). Returns
178
+ // { block, ask, notify } hits. External-write analysis is best effort:
179
+ // redirect targets, write-command arguments (copy-like: destination only),
180
+ // interpreter payloads mentioning external absolute paths. Git arguments are
181
+ // NOT analyzed (inherited protection: only history-rewriting git asks).
175
182
  function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: string; ask?: string; notify?: string } | null {
176
183
  const segments = cmd.split(/\n|&&|\|\||;|\|/).map((s) => s.trim()).filter(Boolean);
177
- let effCwd = sessionCwd;
178
184
  const out: { block?: string; ask?: string; notify?: string } = {};
179
185
  for (const seg of segments) {
180
- const cd = seg.match(/^cd\s+(.+)$/i) || seg.match(/^sl\s+(.+)$/i) || seg.match(/^set-location\s+(.+)$/i);
181
- if (cd) {
182
- const r = guardResolve(effCwd, sessionCwd, cd[1]);
183
- // switch to the real effective cwd even when external: only WRITE ops
184
- // are analyzed below, so `cd elsewhere && cat f` still passes while
185
- // `cd elsewhere && touch a.txt` is correctly judged external.
186
- effCwd = r.abs;
187
- continue;
186
+ if (/^(cd|sl|set-location)\s/i.test(seg)) {
187
+ out.block = "cd is disabled in this sandbox; run commands from the session directory";
188
+ return out;
188
189
  }
189
190
  // 2. redirects: > >> 2> 2>> &> &>> >| (not heredocs, not fd dups like 2>&1)
190
191
  for (const m of seg.matchAll(/(?:^|[^<>&=(])(?:\d?>{1,2}|&>>?|>\|)\s*([^\s;&|]+)/g)) {
191
192
  const tgt = m[1];
192
193
  if (/^&?\d+$/.test(tgt) || guardIsDevNull(tgt)) continue;
193
- const r = guardResolve(effCwd, sessionCwd, tgt);
194
+ const r = guardResolve(sessionCwd, tgt);
194
195
  if (r.external) out.block = r.abs;
195
196
  }
196
197
  const isGit = /^git(\s|$)/.test(seg);
197
198
  // 1. inherited git protection: history-rewriting git asks (matches
198
- // pi-permissions.jsonc bash patterns; prefix match like "git rebase*")
199
+ // the old pi-permissions.jsonc bash patterns; prefix match)
199
200
  if (isGit) {
200
201
  const low = seg.toLowerCase();
201
202
  if (GUARD_GIT_ASK.some((p) => low.startsWith(p))) {
@@ -203,7 +204,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
203
204
  continue;
204
205
  }
205
206
  if (/^git\s+(init|clone)/.test(low)) {
206
- const r = guardResolve(effCwd, sessionCwd, seg.split(/\s+/)[1] === "clone" ? (seg.match(/\s+(\S+)\s+\S+$/) || [])[1] || "." : ".");
207
+ const target = (seg.split(/\s+/).filter((x) => x && !x.startsWith("-")).pop()) || ".";
208
+ const r = guardResolve(sessionCwd, target);
207
209
  if (r.external) {
208
210
  out.notify = `git ${seg.startsWith("git clone") ? "clone" : "init"} outside the session directory (${r.abs}). pi-task-tracker does NOT track that directory; start a separate pi session there if you want it tracked.`;
209
211
  }
@@ -211,7 +213,7 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
211
213
  // external -C destination on any git command
212
214
  const c = seg.match(/\s-C\s+(\S+)/);
213
215
  if (c) {
214
- const r = guardResolve(effCwd, sessionCwd, c[1]);
216
+ const r = guardResolve(sessionCwd, c[1]);
215
217
  if (r.external) out.block = r.abs;
216
218
  }
217
219
  continue;
@@ -241,13 +243,13 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
241
243
  }
242
244
  const checkList = isCopy ? pathArgs.slice(-1) : pathArgs;
243
245
  for (const t of checkList) {
244
- if (/^[A-Za-z]:[\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
246
+ if (/^[A-Za-z]:[\\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
245
247
  if (guardIsDevNull(t)) continue;
246
248
  if (isExternalAbs(t, sessionCwd)) out.block = t;
247
249
  } else if (/[\$`~]/.test(t)) {
248
250
  out.block = t; // unresolvable -> conservative
249
251
  } else {
250
- const abs = path.resolve(effCwd, t);
252
+ const abs = path.resolve(sessionCwd, t);
251
253
  if (isExternalAbs(abs, sessionCwd)) out.block = t;
252
254
  }
253
255
  if (out.block) break;
@@ -567,12 +569,15 @@ interface TrackerConfig {
567
569
  todo: boolean;
568
570
  readme: boolean;
569
571
  autoCommit: boolean;
572
+ /** master switch for the built-in permission layer (external-write guard,
573
+ * git-ask inheritance, cd disable). false = defer entirely to other extensions */
574
+ guard: boolean;
570
575
  /** Writes outside the session directory: "block" (default) | "ask" | "off" */
571
576
  externalWrite: "block" | "ask" | "off";
572
577
  }
573
578
 
574
579
  function readConfig(): TrackerConfig {
575
- const fallback: TrackerConfig = { todo: true, readme: true, autoCommit: true, externalWrite: "block" };
580
+ const fallback: TrackerConfig = { todo: true, readme: true, autoCommit: true, externalWrite: "block", guard: true };
576
581
  try {
577
582
  const dir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
578
583
  const file = process.env.PI_TRACKER_SETTINGS || path.join(dir, "settings.json");
@@ -582,6 +587,7 @@ function readConfig(): TrackerConfig {
582
587
  todo: t.todo !== false,
583
588
  readme: t.readme !== false,
584
589
  autoCommit: t.autoCommit !== false,
590
+ guard: t.guard !== false,
585
591
  externalWrite: t.externalWrite === "ask" || t.externalWrite === "off" ? t.externalWrite : "block",
586
592
  };
587
593
  } catch {
@@ -669,7 +675,7 @@ export default function (pi: ExtensionAPI) {
669
675
  pi.on("tool_call", async (event, ctx) => {
670
676
  // External-write guard: keep the writable scope equal to the trackable
671
677
  // scope. See the "External-write guard" block above for the policy.
672
- if (config.externalWrite !== "off") {
678
+ if (config.guard && config.externalWrite !== "off") {
673
679
  const hit = await guardExternalWriteHit(event, cwd, config.externalWrite === "off" ? "block" : config.externalWrite, ctx);
674
680
  if (hit) {
675
681
  if (hit === "__ask__") return; // ask mode: user approved in guardExternalWriteHit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-task-tracker",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",