@yail259/overnight 0.1.0 → 0.2.0

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.
@@ -0,0 +1,162 @@
1
+ import { appendFileSync } from "fs";
2
+ import { resolve, relative, isAbsolute } from "path";
3
+ import { type SecurityConfig, DEFAULT_DENY_PATTERNS } from "./types.js";
4
+
5
+ // Simple glob pattern matching (supports * and **)
6
+ function matchesPattern(filePath: string, pattern: string): boolean {
7
+ // Normalize path
8
+ const normalizedPath = filePath.replace(/\\/g, "/");
9
+
10
+ // Convert glob to regex
11
+ let regex = pattern
12
+ .replace(/\./g, "\\.") // Escape dots
13
+ .replace(/\*\*/g, "{{GLOBSTAR}}") // Placeholder for **
14
+ .replace(/\*/g, "[^/]*") // * matches anything except /
15
+ .replace(/{{GLOBSTAR}}/g, ".*"); // ** matches anything including /
16
+
17
+ // Match anywhere in path if pattern doesn't start with /
18
+ if (!pattern.startsWith("/")) {
19
+ regex = `(^|/)${regex}`;
20
+ }
21
+
22
+ return new RegExp(regex + "$").test(normalizedPath);
23
+ }
24
+
25
+ function isPathWithinSandbox(filePath: string, sandboxDir: string): boolean {
26
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
27
+ const absoluteSandbox = isAbsolute(sandboxDir) ? sandboxDir : resolve(process.cwd(), sandboxDir);
28
+
29
+ const relativePath = relative(absoluteSandbox, absolutePath);
30
+
31
+ // If relative path starts with .. or is absolute, it's outside sandbox
32
+ return !relativePath.startsWith("..") && !isAbsolute(relativePath);
33
+ }
34
+
35
+ function isPathDenied(filePath: string, denyPatterns: string[]): string | null {
36
+ for (const pattern of denyPatterns) {
37
+ if (matchesPattern(filePath, pattern)) {
38
+ return pattern;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export function createSecurityHooks(config: SecurityConfig) {
45
+ const sandboxDir = config.sandbox_dir;
46
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
47
+ const auditLog = config.audit_log;
48
+
49
+ // PreToolUse hook for path validation
50
+ const preToolUseHook = async (
51
+ input: Record<string, unknown>,
52
+ _toolUseId: string | null,
53
+ _context: { signal?: AbortSignal }
54
+ ) => {
55
+ const hookEventName = input.hook_event_name as string;
56
+ if (hookEventName !== "PreToolUse") return {};
57
+
58
+ const toolName = input.tool_name as string;
59
+ const toolInput = input.tool_input as Record<string, unknown>;
60
+
61
+ // Extract file path based on tool
62
+ let filePath: string | undefined;
63
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
64
+ filePath = toolInput.file_path as string;
65
+ } else if (toolName === "Glob" || toolName === "Grep") {
66
+ filePath = toolInput.path as string;
67
+ } else if (toolName === "Bash") {
68
+ // For Bash, we can't easily validate paths, but we can log
69
+ const command = toolInput.command as string;
70
+ if (auditLog) {
71
+ const timestamp = new Date().toISOString();
72
+ appendFileSync(auditLog, `${timestamp} [BASH] ${command}\n`);
73
+ }
74
+ return {};
75
+ }
76
+
77
+ if (!filePath) return {};
78
+
79
+ // Check sandbox
80
+ if (sandboxDir && !isPathWithinSandbox(filePath, sandboxDir)) {
81
+ return {
82
+ hookSpecificOutput: {
83
+ hookEventName,
84
+ permissionDecision: "deny",
85
+ permissionDecisionReason: `Path "${filePath}" is outside sandbox directory "${sandboxDir}"`,
86
+ },
87
+ };
88
+ }
89
+
90
+ // Check deny patterns
91
+ const matchedPattern = isPathDenied(filePath, denyPatterns);
92
+ if (matchedPattern) {
93
+ return {
94
+ hookSpecificOutput: {
95
+ hookEventName,
96
+ permissionDecision: "deny",
97
+ permissionDecisionReason: `Path "${filePath}" matches deny pattern "${matchedPattern}"`,
98
+ },
99
+ };
100
+ }
101
+
102
+ return {};
103
+ };
104
+
105
+ // PostToolUse hook for audit logging
106
+ const postToolUseHook = async (
107
+ input: Record<string, unknown>,
108
+ _toolUseId: string | null,
109
+ _context: { signal?: AbortSignal }
110
+ ) => {
111
+ if (!auditLog) return {};
112
+
113
+ const hookEventName = input.hook_event_name as string;
114
+ if (hookEventName !== "PostToolUse") return {};
115
+
116
+ const toolName = input.tool_name as string;
117
+ const toolInput = input.tool_input as Record<string, unknown>;
118
+ const timestamp = new Date().toISOString();
119
+
120
+ let logEntry = `${timestamp} [${toolName}]`;
121
+
122
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit") {
123
+ logEntry += ` ${toolInput.file_path}`;
124
+ } else if (toolName === "Glob") {
125
+ logEntry += ` pattern=${toolInput.pattern} path=${toolInput.path ?? "."}`;
126
+ } else if (toolName === "Grep") {
127
+ logEntry += ` pattern=${toolInput.pattern}`;
128
+ }
129
+
130
+ appendFileSync(auditLog, logEntry + "\n");
131
+ return {};
132
+ };
133
+
134
+ return {
135
+ PreToolUse: [
136
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [preToolUseHook] },
137
+ ],
138
+ PostToolUse: [
139
+ { matcher: "Read|Write|Edit|Glob|Grep|Bash", hooks: [postToolUseHook] },
140
+ ],
141
+ };
142
+ }
143
+
144
+ export function validateSecurityConfig(config: SecurityConfig): void {
145
+ if (config.sandbox_dir) {
146
+ const resolved = isAbsolute(config.sandbox_dir)
147
+ ? config.sandbox_dir
148
+ : resolve(process.cwd(), config.sandbox_dir);
149
+ console.log(` Sandbox: ${resolved}`);
150
+ }
151
+
152
+ const denyPatterns = config.deny_patterns ?? DEFAULT_DENY_PATTERNS;
153
+ console.log(` Deny patterns: ${denyPatterns.length} patterns`);
154
+
155
+ if (config.max_turns) {
156
+ console.log(` Max turns: ${config.max_turns}`);
157
+ }
158
+
159
+ if (config.audit_log) {
160
+ console.log(` Audit log: ${config.audit_log}`);
161
+ }
162
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,13 @@
1
+ export interface SecurityConfig {
2
+ sandbox_dir?: string; // All paths must be under this directory
3
+ deny_patterns?: string[]; // Block files matching these glob patterns
4
+ max_turns?: number; // Max agent iterations (prevents runaway)
5
+ audit_log?: string; // Path to audit log file
6
+ }
7
+
1
8
  export interface JobConfig {
9
+ id?: string; // Stable task identifier for dependency references
10
+ depends_on?: string[]; // IDs of tasks that must complete before this one
2
11
  prompt: string;
3
12
  working_dir?: string;
4
13
  timeout_seconds?: number;
@@ -8,6 +17,7 @@ export interface JobConfig {
8
17
  allowed_tools?: string[];
9
18
  retry_count?: number;
10
19
  retry_delay?: number;
20
+ security?: SecurityConfig;
11
21
  }
12
22
 
13
23
  export interface JobResult {
@@ -20,11 +30,17 @@ export interface JobResult {
20
30
  retries: number;
21
31
  }
22
32
 
33
+ export interface InProgressTask {
34
+ hash: string;
35
+ prompt: string;
36
+ sessionId?: string; // SDK session ID for resumption
37
+ startedAt: string;
38
+ }
39
+
23
40
  export interface RunState {
24
- completed_indices: number[];
25
- results: JobResult[];
41
+ completed: Record<string, JobResult>; // keyed by task hash
42
+ inProgress?: InProgressTask; // currently running task
26
43
  timestamp: string;
27
- total_jobs: number;
28
44
  }
29
45
 
30
46
  export interface TasksFile {
@@ -34,6 +50,7 @@ export interface TasksFile {
34
50
  verify?: boolean;
35
51
  verify_prompt?: string;
36
52
  allowed_tools?: string[];
53
+ security?: SecurityConfig;
37
54
  };
38
55
  tasks: (string | JobConfig)[];
39
56
  }
@@ -43,6 +60,22 @@ export const DEFAULT_TIMEOUT = 300;
43
60
  export const DEFAULT_STALL_TIMEOUT = 120;
44
61
  export const DEFAULT_RETRY_COUNT = 3;
45
62
  export const DEFAULT_RETRY_DELAY = 5;
46
- export const DEFAULT_VERIFY_PROMPT = "Verify this is complete and correct. If there are issues, list them.";
63
+ export const DEFAULT_VERIFY_PROMPT = "Review what you just implemented. Check for correctness, completeness, and compile errors. Fix any issues you find.";
47
64
  export const DEFAULT_STATE_FILE = ".overnight-state.json";
48
65
  export const DEFAULT_NTFY_TOPIC = "overnight";
66
+ export const DEFAULT_MAX_TURNS = 100;
67
+ export const DEFAULT_DENY_PATTERNS = [
68
+ "**/.env",
69
+ "**/.env.*",
70
+ "**/.git/config",
71
+ "**/credentials*",
72
+ "**/*.key",
73
+ "**/*.pem",
74
+ "**/*.p12",
75
+ "**/id_rsa*",
76
+ "**/id_ed25519*",
77
+ "**/.ssh/*",
78
+ "**/.aws/*",
79
+ "**/.npmrc",
80
+ "**/.netrc",
81
+ ];