killeros 2.0.20 → 2.0.21

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.
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { StringDecoder } from "node:string_decoder";
5
+ import { containsLikelySecret } from "./secret-detector.ts";
5
6
 
6
7
  export const INIT_READ_TOOL = "killeros_init_read";
7
8
  export const INIT_LIST_TOOL = "killeros_init_list";
@@ -19,7 +20,7 @@ const EXCLUDED_GUIDANCE = new Set([
19
20
  ".cursorrules", "agents.md", "agents.local.md", "claude.md", "claude.local.md", "copilot-instructions.md", "gemini.md", "memory.md", "skill.md",
20
21
  ]);
21
22
  const ROOT_EVIDENCE = [
22
- "README.md", "README.rst", "README.txt", "CONTRIBUTING.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "compose.yaml", "compose.yml", "config.yaml", "config.yml", "tsconfig.json", "vite.config.ts", "vite.config.js", "eslint.config.js", "eslint.config.mjs",
23
+ "README.md", "README.rst", "README.txt", "CONTRIBUTING.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "compose.yaml", "compose.yml", "tsconfig.json", "vite.config.ts", "vite.config.js", "eslint.config.js", "eslint.config.mjs",
23
24
  ] as const;
24
25
  const NESTED_EVIDENCE = new Set(["package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod"]);
25
26
 
@@ -43,6 +44,10 @@ function sensitiveEvidencePath(relativePath: string): boolean {
43
44
  const name = path.posix.basename(normalized);
44
45
  return /^\.env(?:\.|$)/u.test(name)
45
46
  || [".npmrc", ".pypirc", ".netrc", "id_rsa", "id_ed25519", "credentials.json"].includes(name)
47
+ || /(?:^|\/)\.aws\/credentials$/u.test(normalized)
48
+ || /(?:^|\/)\.docker\/config\.json$/u.test(normalized)
49
+ || /(?:^|\/)\.kube\/config$/u.test(normalized)
50
+ || /(?:credential|secret|token)/u.test(name)
46
51
  || /^service-account.*\.json$/u.test(name)
47
52
  || /\.(?:pem|key|p12|pfx|jks|keystore)$/u.test(name);
48
53
  }
@@ -200,6 +205,13 @@ async function validateAndRead(projectRoot: string, absolutePath: string, limit:
200
205
  }
201
206
  }
202
207
 
208
+ /** Reads evidence only when its bounded content does not resemble a credential. */
209
+ async function readEvidenceFile(projectRoot: string, absolutePath: string, limit: number): Promise<{ content: string; truncated: boolean }> {
210
+ const result = await validateAndRead(projectRoot, absolutePath, limit);
211
+ if (containsLikelySecret(result.content)) throw new Error("file is not available to /init");
212
+ return result;
213
+ }
214
+
203
215
  function normalizeRequestedPath(requestedPath: string): string {
204
216
  if (!requestedPath || requestedPath.trim() !== requestedPath || requestedPath.startsWith("~")
205
217
  || /^file:/iu.test(requestedPath) || path.isAbsolute(requestedPath)) {
@@ -237,7 +249,7 @@ export async function buildInitEvidence(projectRoot: string): Promise<InitEviden
237
249
  const relativePath = canonicalPaths.get(evidenceKey(requested));
238
250
  if (!relativePath || Buffer.byteLength(snapshot, "utf8") >= SNAPSHOT_LIMIT) continue;
239
251
  try {
240
- const result = await validateAndRead(projectRoot, path.join(projectRoot, relativePath), AUTOMATIC_FILE_LIMIT);
252
+ const result = await readEvidenceFile(projectRoot, path.join(projectRoot, relativePath), AUTOMATIC_FILE_LIMIT);
241
253
  const suffix = result.truncated ? "\n[truncated by /init]" : "";
242
254
  snapshot = appendWithinLimit(snapshot, `\n\n## ${relativePath}\n${result.content}${suffix}`, SNAPSHOT_LIMIT);
243
255
  } catch {
@@ -251,7 +263,7 @@ export async function readInitEvidence(index: InitEvidenceIndex, requestedPath:
251
263
  const normalized = normalizeRequestedPath(requestedPath);
252
264
  const relativePath = index.canonicalPaths.get(evidenceKey(normalized));
253
265
  if (!relativePath) throw new Error(`${requestedPath} is not available to /init`);
254
- const result = await validateAndRead(index.projectRoot, path.join(index.projectRoot, relativePath), READ_LIMIT);
266
+ const result = await readEvidenceFile(index.projectRoot, path.join(index.projectRoot, relativePath), READ_LIMIT);
255
267
  return result.truncated ? `${result.content}\n[truncated by /init at ${READ_LIMIT} bytes]` : result.content;
256
268
  }
257
269
 
@@ -1,8 +1,8 @@
1
- import { closeSync, openSync, readSync } from "node:fs";
1
+ import { closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { StringDecoder } from "node:string_decoder";
5
- import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import type { InitRuntime } from "./runtime.ts";
7
7
 
8
8
  const PERSONAL_INSTRUCTIONS_FILE = "AGENTS.local.md";
@@ -33,9 +33,59 @@ function readBoundedText(filePath: string, limit = PERSONAL_INSTRUCTIONS_LIMIT):
33
33
  }
34
34
  }
35
35
 
36
+ /** Checks path containment using host filesystem casing rules. */
37
+ function pathInside(root: string, candidate: string): boolean {
38
+ const normalizedRoot = process.platform === "win32" ? root.toLowerCase() : root;
39
+ const normalizedCandidate = process.platform === "win32" ? candidate.toLowerCase() : candidate;
40
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
41
+ return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
42
+ }
43
+
44
+ /** Reads one stable, unlinked import from inside Pi's agent directory. */
45
+ function readImportedText(filePath: string): string | undefined {
46
+ let descriptor: number | undefined;
47
+ try {
48
+ const allowedPath = path.resolve(getAgentDir());
49
+ const requestedPath = path.resolve(filePath);
50
+ if (!pathInside(allowedPath, requestedPath)) return undefined;
51
+
52
+ const allowedRealPath = realpathSync(allowedPath);
53
+ const requestedRealPath = realpathSync(requestedPath);
54
+ if (!pathInside(allowedRealPath, requestedRealPath)) return undefined;
55
+
56
+ let current = allowedPath;
57
+ for (const segment of path.relative(allowedPath, requestedPath).split(path.sep)) {
58
+ current = path.join(current, segment);
59
+ if (lstatSync(current).isSymbolicLink()) return undefined;
60
+ }
61
+
62
+ const pathStat = lstatSync(requestedPath);
63
+ if (!pathStat.isFile() || pathStat.isSymbolicLink() || pathStat.nlink !== 1) return undefined;
64
+ descriptor = openSync(requestedPath, "r");
65
+ const openedStat = fstatSync(descriptor);
66
+ if (!openedStat.isFile() || openedStat.nlink !== 1
67
+ || openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) return undefined;
68
+
69
+ const buffer = Buffer.alloc(PERSONAL_INSTRUCTIONS_LIMIT + 1);
70
+ const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
71
+ if (bytesRead > PERSONAL_INSTRUCTIONS_LIMIT) return undefined;
72
+ const content = new StringDecoder("utf8").write(buffer.subarray(0, bytesRead));
73
+ return content.trim() ? content : undefined;
74
+ } catch {
75
+ return undefined;
76
+ } finally {
77
+ if (descriptor !== undefined) {
78
+ try {
79
+ closeSync(descriptor);
80
+ } catch {
81
+ // Ignore cleanup failures after rejecting or reading an import.
82
+ }
83
+ }
84
+ }
85
+ }
86
+
36
87
  export function resolvePersonalInstructions(cwd: string): string | undefined {
37
- const localPath = path.join(cwd, PERSONAL_INSTRUCTIONS_FILE);
38
- const local = readBoundedText(localPath);
88
+ const local = readBoundedText(path.join(cwd, PERSONAL_INSTRUCTIONS_FILE));
39
89
  if (!local) return undefined;
40
90
 
41
91
  const importMatch = local.trim().match(/^@(.+)$/u);
@@ -45,7 +95,9 @@ export function resolvePersonalInstructions(cwd: string): string | undefined {
45
95
  const importedPath = requestedPath.startsWith("~/") || requestedPath.startsWith("~\\")
46
96
  ? path.join(os.homedir(), requestedPath.slice(2))
47
97
  : path.resolve(cwd, requestedPath);
48
- content = readBoundedText(importedPath) ?? local;
98
+ const imported = readImportedText(importedPath);
99
+ if (!imported) return undefined;
100
+ content = imported;
49
101
  }
50
102
  return `<personal_instructions>\n${content}\n</personal_instructions>`;
51
103
  }