opencode-workflow-guard 1.8.4 → 1.8.5
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 +1 -1
- package/src/lib/project-memory.ts +2 -2
- package/src/lib/shell.ts +16 -1
- package/src/policies/shell-safety.ts +23 -5
package/package.json
CHANGED
|
@@ -88,8 +88,8 @@ export function openProjectMemory(projectId: string, directory = getProjectMemor
|
|
|
88
88
|
writeFileSync(activeMarker, "", { mode: 0o600 });
|
|
89
89
|
db = new Database(path);
|
|
90
90
|
db.exec(`
|
|
91
|
-
PRAGMA journal_mode = WAL;
|
|
92
91
|
PRAGMA busy_timeout = 2000;
|
|
92
|
+
PRAGMA journal_mode = WAL;
|
|
93
93
|
CREATE TABLE IF NOT EXISTS memories (
|
|
94
94
|
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL,
|
|
95
95
|
source TEXT NOT NULL, created_at INTEGER NOT NULL, session_id TEXT, commit_sha TEXT,
|
|
@@ -122,7 +122,7 @@ export function openProjectMemory(projectId: string, directory = getProjectMemor
|
|
|
122
122
|
function acquireDatabaseLock(directory: string): () => void {
|
|
123
123
|
const lock = new Database(join(directory, ".coordination"));
|
|
124
124
|
try {
|
|
125
|
-
lock.exec("PRAGMA busy_timeout =
|
|
125
|
+
lock.exec("PRAGMA busy_timeout = 10000; BEGIN IMMEDIATE");
|
|
126
126
|
} catch (error) {
|
|
127
127
|
lock.close();
|
|
128
128
|
throw error;
|
package/src/lib/shell.ts
CHANGED
|
@@ -242,7 +242,22 @@ function unwrapShellWordsImpl(command: string): { words: string[]; changesCwd: b
|
|
|
242
242
|
continue;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
if (word === "command"
|
|
245
|
+
if (word === "command") {
|
|
246
|
+
i++;
|
|
247
|
+
while (i < words.length && words[i] === "-p") i++;
|
|
248
|
+
if (words[i] === "--") i++;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (word === "exec") {
|
|
252
|
+
i++;
|
|
253
|
+
while (i < words.length && words[i]!.startsWith("-")) {
|
|
254
|
+
const option = words[i++]!;
|
|
255
|
+
if (option === "--") break;
|
|
256
|
+
if (option === "-a" && i < words.length) i++;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (word === "nohup") {
|
|
246
261
|
i++;
|
|
247
262
|
continue;
|
|
248
263
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { splitShellSegments, unwrapShellWords } from "../lib/shell.ts";
|
|
2
4
|
|
|
3
5
|
// AI agents frequently run dangerous package manager flags (e.g. npm audit fix --force,
|
|
4
6
|
// npm install -g, pip install --force-reinstall, agent-driven publish) that introduce
|
|
@@ -48,11 +50,6 @@ const INTERACTIVE_COMMAND_PATTERNS: Array<{ regex: RegExp; name: string; advice:
|
|
|
48
50
|
name: "interactive text editor",
|
|
49
51
|
advice: "Use the edit, write, or apply_patch tools instead of interactive terminal editors.",
|
|
50
52
|
},
|
|
51
|
-
{
|
|
52
|
-
regex: /\b(?:less|more|most)\b/i,
|
|
53
|
-
name: "terminal pager",
|
|
54
|
-
advice: "Use cat, head, or grep with non-interactive pipes instead of interactive pagers.",
|
|
55
|
-
},
|
|
56
53
|
{
|
|
57
54
|
regex: /\bsudo\b/i,
|
|
58
55
|
name: "sudo with password prompt",
|
|
@@ -86,6 +83,7 @@ const INTERACTIVE_COMMAND_PATTERNS: Array<{ regex: RegExp; name: string; advice:
|
|
|
86
83
|
];
|
|
87
84
|
|
|
88
85
|
const MONITOR_ADVICE = "Use ps aux, uptime, or batch flags (e.g. top -b -n 1) instead of interactive monitors.";
|
|
86
|
+
const PAGER_ADVICE = "Use cat, head, or grep with non-interactive pipes instead of interactive pagers.";
|
|
89
87
|
// Process monitoring tokens are matched on whitespace/segment boundaries so that
|
|
90
88
|
// an unrelated word such as `desktop`, a hyphenated filename like `top-level-dir`,
|
|
91
89
|
// or a quoted argument like `echo "top"` is not mistaken for the `top` command.
|
|
@@ -105,9 +103,29 @@ function checkProcessMonitorCommand(command: string): { isInteractive: boolean;
|
|
|
105
103
|
return { isInteractive: false };
|
|
106
104
|
}
|
|
107
105
|
|
|
106
|
+
function containsPagerCommand(command: string, depth = 0): boolean {
|
|
107
|
+
if (depth >= 16) return false;
|
|
108
|
+
for (const segment of splitShellSegments(command)) {
|
|
109
|
+
const words = unwrapShellWords(segment);
|
|
110
|
+
while (words[0] === "--") words.shift();
|
|
111
|
+
const executable = basename(words[0] ?? "");
|
|
112
|
+
if (/^(?:less|more|most)$/i.test(executable)) return true;
|
|
113
|
+
if (executable === "eval" && words.length > 1 && containsPagerCommand(words.slice(1).join(" "), depth + 1)) return true;
|
|
114
|
+
if (executable === "busybox" && /^(?:less|more)$/i.test(words[1] ?? "")) return true;
|
|
115
|
+
if (/^(?:ba|z|da|k)?sh$/i.test(executable)) {
|
|
116
|
+
const commandFlag = words.findIndex((word, index) => index > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
|
|
117
|
+
if (commandFlag >= 0 && words[commandFlag + 1] && containsPagerCommand(words[commandFlag + 1]!, depth + 1)) return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
export function checkInteractiveTtyCommand(command: string): { isInteractive: boolean; name?: string; advice?: string } {
|
|
109
124
|
const monitor = checkProcessMonitorCommand(command);
|
|
110
125
|
if (monitor.isInteractive) return monitor;
|
|
126
|
+
if (containsPagerCommand(command)) {
|
|
127
|
+
return { isInteractive: true, name: "terminal pager", advice: PAGER_ADVICE };
|
|
128
|
+
}
|
|
111
129
|
for (const { regex, name, advice } of INTERACTIVE_COMMAND_PATTERNS) {
|
|
112
130
|
if (regex.test(command)) {
|
|
113
131
|
return { isInteractive: true, name, advice };
|