@tduyng/pi-guard 0.0.1 → 0.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tduyng/pi-guard",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Four guards: keep parallel pi sessions off each other's files, redact secrets out of read results, keep a stray rejected promise from killing the session, and stop git from hanging on an editor or skipping hooks.",
5
5
  "keywords": [
6
6
  "pi",
package/src/cloak.ts CHANGED
@@ -111,7 +111,6 @@ function readConfigFile(path: string): CloakConfig {
111
111
  export function mergeCloakConfig(base: CloakConfig, project: CloakConfig): CloakConfig {
112
112
  return {
113
113
  ...base,
114
- ...project,
115
114
  patterns: [
116
115
  ...(Array.isArray(base.patterns) ? base.patterns : []),
117
116
  ...(Array.isArray(project.patterns) ? project.patterns : [])
@@ -2,8 +2,6 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
2
2
  import { Validator } from 'ata-validator'
3
3
  import { t } from 'ata-validator/t'
4
4
 
5
- const NO_VERIFY_RE = /--no-verify\b|\bgit\b[^;\n]*\bcommit\b[^;\n]*(?:^|\s)-[A-Za-z]*n[A-Za-z]*(?:\s|=|$)/
6
-
7
5
  const SENSITIVE_COMMAND_RE =
8
6
  /(?:^|[\s"'=:~]|\/)\.env(?:\.[A-Za-z0-9_-]+)?(?=$|[\s"';&|),\]} ])|(?:^|[\s"'=:~]|\/)(?:[^\s"';&|]*\/)?(?:id_rsa|id_dsa|id_ecdsa|id_ed25519)(?=$|[\s"';&|),\]} ])|(?:^|[\s"'=:~]|\/)[^\s"';&|]*\.(?:pem|key|p12|pfx)(?=$|[\s"';&|),\]} ])/i
9
7
 
@@ -24,6 +22,41 @@ const isCommandInput = new Validator(CommandInput).isValidObject
24
22
  const isThenRunCommand = new Validator(t.object({ command: t.string() })).isValidObject
25
23
  const isThenRunText = new Validator(t.string()).isValidObject
26
24
 
25
+ function words(segment: string): string[] {
26
+ const found = segment.match(/"(?:[^"\\]|\\.)*"|'[^']*'|[^\s]+/g) ?? []
27
+ return found.map((word) => word.replace(/^(["'])(.*)\1$/, '$2'))
28
+ }
29
+
30
+ function blocksHooks(command: string): boolean {
31
+ if (!/\bgit\b/.test(command)) return false
32
+ for (const segment of command.split(/[;&|\n]+/)) {
33
+ const tokens = words(segment)
34
+ const git = tokens.findIndex((token) => token === 'git' || token.endsWith('/git'))
35
+ if (git === -1) continue
36
+ const args = tokens.slice(git + 1)
37
+ const commit = args.findIndex((token) => token === 'commit')
38
+ if (commit === -1) continue
39
+ if (args.includes('--no-verify')) return true
40
+ if (args.some((token, index) => token === '-c' && (args[index + 1]?.startsWith('core.hooksPath=') ?? false)))
41
+ return true
42
+ const after = args.slice(commit + 1)
43
+ const message = after.findIndex(
44
+ (token) => token === '-m' || token === '--message' || token.startsWith('--message=')
45
+ )
46
+ const flags = message === -1 ? after : after.slice(0, message)
47
+ if (flags.some((token) => /^-[A-Za-z]*n[A-Za-z]*$/.test(token))) return true
48
+ }
49
+ return false
50
+ }
51
+
52
+ function commands(input: { command?: string; then_run?: string | { command: string } }): string[] {
53
+ const found: string[] = []
54
+ if (input.command !== undefined) found.push(input.command)
55
+ if (isThenRunCommand(input.then_run)) found.push(input.then_run.command)
56
+ else if (isThenRunText(input.then_run)) found.push(input.then_run)
57
+ return found
58
+ }
59
+
27
60
  export default function (pi: ExtensionAPI) {
28
61
  process.env['GIT_EDITOR'] = 'true'
29
62
  process.env['GIT_SEQUENCE_EDITOR'] = 'true'
@@ -31,20 +64,10 @@ export default function (pi: ExtensionAPI) {
31
64
 
32
65
  pi.on('tool_call', (event) => {
33
66
  if (!isCommandInput(event.input)) return
34
- const input = event.input
35
- let command = input.command
36
- if (command === undefined && input.then_run !== undefined) {
37
- if (isThenRunCommand(input.then_run)) command = input.then_run.command
38
- else if (isThenRunText(input.then_run)) command = input.then_run
39
- else return
40
- }
41
- if (!command) return
42
-
43
- if (/\bgit\b/.test(command) && NO_VERIFY_RE.test(command)) {
44
- return { block: true, reason: BLOCK_REASON }
45
- }
46
- if (SENSITIVE_COMMAND_RE.test(command)) {
47
- return { block: true, reason: SENSITIVE_REASON }
67
+ for (const command of commands(event.input)) {
68
+ if (command.trim() === '') continue
69
+ if (blocksHooks(command)) return { block: true, reason: BLOCK_REASON }
70
+ if (SENSITIVE_COMMAND_RE.test(command)) return { block: true, reason: SENSITIVE_REASON }
48
71
  }
49
72
  })
50
73
  }
package/src/reserve.ts CHANGED
@@ -119,12 +119,14 @@ function stripQuotes(token: string): string {
119
119
  */
120
120
  function writtenPaths(command: string): string[] {
121
121
  const redirects = [...command.matchAll(REDIRECT)].map((match) => stripQuotes(match[1] ?? ''))
122
- if (redirects.length > 0) return redirects
123
- if (!SHELL_MUTATIONS.test(command)) return []
124
- return command
122
+ const mutates = SHELL_MUTATIONS.test(command)
123
+ if (redirects.length > 0 && !mutates) return redirects
124
+ if (!mutates) return []
125
+ const tokens = command
125
126
  .split(/[\s;&|<>()]+/)
126
127
  .map(stripQuotes)
127
128
  .filter((token) => token !== '' && !token.startsWith('-'))
129
+ return [...new Set([...redirects, ...tokens])]
128
130
  }
129
131
 
130
132
  export function commandConflicts(cwd: string, session: string, command: string): Conflict[] {