overarch-linux-arm64 0.8.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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/bin/overarch.d.ts +2 -0
- package/dist/bin/overarch.js +178 -0
- package/dist/src/agent/context.d.ts +14 -0
- package/dist/src/agent/context.js +200 -0
- package/dist/src/agent/context_compressor.d.ts +21 -0
- package/dist/src/agent/context_compressor.js +90 -0
- package/dist/src/agent/manifest.d.ts +22 -0
- package/dist/src/agent/manifest.js +131 -0
- package/dist/src/agent/planner.d.ts +38 -0
- package/dist/src/agent/planner.js +118 -0
- package/dist/src/agent/runtime.d.ts +59 -0
- package/dist/src/agent/runtime.js +400 -0
- package/dist/src/cli/approval.d.ts +13 -0
- package/dist/src/cli/approval.js +71 -0
- package/dist/src/cli/args.d.ts +15 -0
- package/dist/src/cli/args.js +47 -0
- package/dist/src/cli/commands/audit.d.ts +10 -0
- package/dist/src/cli/commands/audit.js +79 -0
- package/dist/src/cli/commands/companion.d.ts +1 -0
- package/dist/src/cli/commands/companion.js +143 -0
- package/dist/src/cli/commands/config.d.ts +1 -0
- package/dist/src/cli/commands/config.js +135 -0
- package/dist/src/cli/commands/doctor.d.ts +1 -0
- package/dist/src/cli/commands/doctor.js +217 -0
- package/dist/src/cli/commands/init.d.ts +1 -0
- package/dist/src/cli/commands/init.js +134 -0
- package/dist/src/cli/commands/memory.d.ts +2 -0
- package/dist/src/cli/commands/memory.js +98 -0
- package/dist/src/cli/commands/onboarding.d.ts +3 -0
- package/dist/src/cli/commands/onboarding.js +223 -0
- package/dist/src/cli/commands/sessions.d.ts +1 -0
- package/dist/src/cli/commands/sessions.js +22 -0
- package/dist/src/cli/commands/status.d.ts +1 -0
- package/dist/src/cli/commands/status.js +37 -0
- package/dist/src/cli/commands/update.d.ts +9 -0
- package/dist/src/cli/commands/update.js +105 -0
- package/dist/src/cli/repl.d.ts +3 -0
- package/dist/src/cli/repl.js +821 -0
- package/dist/src/cli/ui.d.ts +53 -0
- package/dist/src/cli/ui.js +215 -0
- package/dist/src/companion/petdex_adapter.d.ts +31 -0
- package/dist/src/companion/petdex_adapter.js +171 -0
- package/dist/src/companion/petdex_bridge.d.ts +65 -0
- package/dist/src/companion/petdex_bridge.js +356 -0
- package/dist/src/companion/types.d.ts +57 -0
- package/dist/src/companion/types.js +11 -0
- package/dist/src/config/index.d.ts +24 -0
- package/dist/src/config/index.js +237 -0
- package/dist/src/config/types.d.ts +58 -0
- package/dist/src/config/types.js +7 -0
- package/dist/src/core/diagnostics.d.ts +48 -0
- package/dist/src/core/diagnostics.js +115 -0
- package/dist/src/core/index.d.ts +23 -0
- package/dist/src/core/index.js +23 -0
- package/dist/src/core/mission.d.ts +55 -0
- package/dist/src/core/mission.js +173 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/learning/memory_manager.d.ts +26 -0
- package/dist/src/learning/memory_manager.js +211 -0
- package/dist/src/learning/types.d.ts +54 -0
- package/dist/src/learning/types.js +1 -0
- package/dist/src/mcp/registry.d.ts +17 -0
- package/dist/src/mcp/registry.js +97 -0
- package/dist/src/mcp/types.d.ts +13 -0
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/providers/anthropic.d.ts +10 -0
- package/dist/src/providers/anthropic.js +95 -0
- package/dist/src/providers/base.d.ts +15 -0
- package/dist/src/providers/base.js +108 -0
- package/dist/src/providers/factory.d.ts +28 -0
- package/dist/src/providers/factory.js +142 -0
- package/dist/src/providers/gemini.d.ts +10 -0
- package/dist/src/providers/gemini.js +97 -0
- package/dist/src/providers/openai.d.ts +9 -0
- package/dist/src/providers/openai.js +85 -0
- package/dist/src/providers/openrouter.d.ts +5 -0
- package/dist/src/providers/openrouter.js +14 -0
- package/dist/src/providers/registry.d.ts +19 -0
- package/dist/src/providers/registry.js +209 -0
- package/dist/src/providers/types.d.ts +59 -0
- package/dist/src/providers/types.js +1 -0
- package/dist/src/providers/validator.d.ts +24 -0
- package/dist/src/providers/validator.js +161 -0
- package/dist/src/security/audit.d.ts +56 -0
- package/dist/src/security/audit.js +154 -0
- package/dist/src/security/guardrails.d.ts +9 -0
- package/dist/src/security/guardrails.js +32 -0
- package/dist/src/security/permissions.d.ts +12 -0
- package/dist/src/security/permissions.js +68 -0
- package/dist/src/security/policy.d.ts +220 -0
- package/dist/src/security/policy.js +720 -0
- package/dist/src/session/manager.d.ts +22 -0
- package/dist/src/session/manager.js +172 -0
- package/dist/src/session/types.d.ts +36 -0
- package/dist/src/session/types.js +1 -0
- package/dist/src/tools/checkpoint.d.ts +26 -0
- package/dist/src/tools/checkpoint.js +160 -0
- package/dist/src/tools/diagnostics.d.ts +2 -0
- package/dist/src/tools/diagnostics.js +125 -0
- package/dist/src/tools/diff.d.ts +14 -0
- package/dist/src/tools/diff.js +124 -0
- package/dist/src/tools/filesystem.d.ts +2 -0
- package/dist/src/tools/filesystem.js +224 -0
- package/dist/src/tools/git.d.ts +2 -0
- package/dist/src/tools/git.js +138 -0
- package/dist/src/tools/markdown.d.ts +33 -0
- package/dist/src/tools/markdown.js +190 -0
- package/dist/src/tools/project.d.ts +2 -0
- package/dist/src/tools/project.js +63 -0
- package/dist/src/tools/registry.d.ts +14 -0
- package/dist/src/tools/registry.js +230 -0
- package/dist/src/tools/search.d.ts +2 -0
- package/dist/src/tools/search.js +136 -0
- package/dist/src/tools/shell.d.ts +2 -0
- package/dist/src/tools/shell.js +74 -0
- package/dist/src/tools/symbols.d.ts +30 -0
- package/dist/src/tools/symbols.js +155 -0
- package/dist/src/tools/types.d.ts +28 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/web_search.d.ts +14 -0
- package/dist/src/tools/web_search.js +154 -0
- package/install.sh +29 -0
- package/overarch +15 -0
- package/package.json +28 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Message } from './types.js';
|
|
2
|
+
export interface ValidatedMessagePayload {
|
|
3
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
4
|
+
content: string | null;
|
|
5
|
+
tool_call_id?: string;
|
|
6
|
+
tool_calls?: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
type: 'function';
|
|
9
|
+
function: {
|
|
10
|
+
name: string;
|
|
11
|
+
arguments: string;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Pre-flight message validator and healer for LLM APIs (especially OpenAI / OpenRouter).
|
|
17
|
+
* Guarantees that every assistant tool call is paired with a corresponding tool result message,
|
|
18
|
+
* and fixes orphaned calls or results that would cause provider 400 Bad Request errors.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateAndHealMessages(messages: Message[]): Message[];
|
|
21
|
+
/**
|
|
22
|
+
* Serializes Message array into strict OpenAI / OpenRouter Chat Completion format.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatForOpenAI(messages: Message[]): ValidatedMessagePayload[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight message validator and healer for LLM APIs (especially OpenAI / OpenRouter).
|
|
3
|
+
* Guarantees that every assistant tool call is paired with a corresponding tool result message,
|
|
4
|
+
* and fixes orphaned calls or results that would cause provider 400 Bad Request errors.
|
|
5
|
+
*/
|
|
6
|
+
export function validateAndHealMessages(messages) {
|
|
7
|
+
if (!messages || messages.length === 0) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const healed = [];
|
|
11
|
+
const pendingToolCalls = new Map(); // id -> toolName
|
|
12
|
+
for (let i = 0; i < messages.length; i++) {
|
|
13
|
+
const rawMsg = messages[i];
|
|
14
|
+
const role = rawMsg.role;
|
|
15
|
+
const content = rawMsg.content || '';
|
|
16
|
+
const toolCallId = rawMsg.toolCallId || rawMsg.tool_call_id;
|
|
17
|
+
const toolCalls = rawMsg.toolCalls || rawMsg.tool_calls;
|
|
18
|
+
if (role === 'assistant') {
|
|
19
|
+
// If we had pending tool calls from a previous assistant message that were never answered,
|
|
20
|
+
// heal them by inserting synthesized tool result messages before continuing
|
|
21
|
+
if (pendingToolCalls.size > 0) {
|
|
22
|
+
for (const [orphanId, orphanName] of pendingToolCalls.entries()) {
|
|
23
|
+
healed.push({
|
|
24
|
+
role: 'tool',
|
|
25
|
+
toolCallId: orphanId,
|
|
26
|
+
content: JSON.stringify({ success: false, error: `Tool call '${orphanName}' (${orphanId}) had no returned result.` })
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
pendingToolCalls.clear();
|
|
30
|
+
}
|
|
31
|
+
if (toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
32
|
+
const normalizedToolCalls = toolCalls.map((tc, idx) => {
|
|
33
|
+
const id = tc.id || `call_${Date.now()}_${idx}`;
|
|
34
|
+
const name = tc.name || tc.function?.name || 'unknown_tool';
|
|
35
|
+
let args = tc.arguments;
|
|
36
|
+
if (typeof args === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
args = JSON.parse(args);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
args = {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
pendingToolCalls.set(id, name);
|
|
45
|
+
return { id, name, arguments: args || {} };
|
|
46
|
+
});
|
|
47
|
+
healed.push({
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
content,
|
|
50
|
+
toolCalls: normalizedToolCalls
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
healed.push({
|
|
55
|
+
role: 'assistant',
|
|
56
|
+
content
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (role === 'tool') {
|
|
61
|
+
const validId = toolCallId || `call_unpaired_${Date.now()}`;
|
|
62
|
+
if (pendingToolCalls.has(validId)) {
|
|
63
|
+
pendingToolCalls.delete(validId);
|
|
64
|
+
healed.push({
|
|
65
|
+
role: 'tool',
|
|
66
|
+
toolCallId: validId,
|
|
67
|
+
content
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (pendingToolCalls.size > 0) {
|
|
71
|
+
// Match first available pending tool call if ID mismatched due to corruption
|
|
72
|
+
const firstPendingId = pendingToolCalls.keys().next().value;
|
|
73
|
+
if (firstPendingId) {
|
|
74
|
+
pendingToolCalls.delete(firstPendingId);
|
|
75
|
+
healed.push({
|
|
76
|
+
role: 'tool',
|
|
77
|
+
toolCallId: firstPendingId,
|
|
78
|
+
content
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Orphaned tool response with no preceding tool call.
|
|
84
|
+
// Convert to a system/user observation so providers don't reject it with missing tool_call_id
|
|
85
|
+
healed.push({
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: `[Previous Tool Observation]: ${content}`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// System or User message.
|
|
93
|
+
// If there are pending tool calls, heal them first!
|
|
94
|
+
if (pendingToolCalls.size > 0) {
|
|
95
|
+
for (const [orphanId, orphanName] of pendingToolCalls.entries()) {
|
|
96
|
+
healed.push({
|
|
97
|
+
role: 'tool',
|
|
98
|
+
toolCallId: orphanId,
|
|
99
|
+
content: JSON.stringify({ success: false, error: `Tool call '${orphanName}' (${orphanId}) was interrupted.` })
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
pendingToolCalls.clear();
|
|
103
|
+
}
|
|
104
|
+
healed.push({
|
|
105
|
+
role: role,
|
|
106
|
+
content
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Trailing check: if the very last message was an assistant tool call, synthesize results
|
|
111
|
+
if (pendingToolCalls.size > 0) {
|
|
112
|
+
for (const [orphanId, orphanName] of pendingToolCalls.entries()) {
|
|
113
|
+
healed.push({
|
|
114
|
+
role: 'tool',
|
|
115
|
+
toolCallId: orphanId,
|
|
116
|
+
content: JSON.stringify({ success: false, error: `Tool call '${orphanName}' (${orphanId}) awaiting completion.` })
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
pendingToolCalls.clear();
|
|
120
|
+
}
|
|
121
|
+
return healed;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Serializes Message array into strict OpenAI / OpenRouter Chat Completion format.
|
|
125
|
+
*/
|
|
126
|
+
export function formatForOpenAI(messages) {
|
|
127
|
+
const healed = validateAndHealMessages(messages);
|
|
128
|
+
return healed.map((m) => {
|
|
129
|
+
if (m.role === 'tool') {
|
|
130
|
+
return {
|
|
131
|
+
role: 'tool',
|
|
132
|
+
tool_call_id: m.toolCallId || 'call_fallback',
|
|
133
|
+
content: m.content
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (m.role === 'assistant') {
|
|
137
|
+
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
138
|
+
return {
|
|
139
|
+
role: 'assistant',
|
|
140
|
+
content: m.content || null,
|
|
141
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
142
|
+
id: tc.id,
|
|
143
|
+
type: 'function',
|
|
144
|
+
function: {
|
|
145
|
+
name: tc.name,
|
|
146
|
+
arguments: JSON.stringify(tc.arguments || {})
|
|
147
|
+
}
|
|
148
|
+
}))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
role: 'assistant',
|
|
153
|
+
content: m.content || ''
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
role: m.role,
|
|
158
|
+
content: m.content || ''
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Capability, RiskLevel } from './policy.js';
|
|
2
|
+
export interface AuditRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
action: string;
|
|
6
|
+
capability: Capability;
|
|
7
|
+
riskLevel: RiskLevel;
|
|
8
|
+
decision: 'ALLOWED' | 'DENIED' | 'APPROVED' | 'ESCALATED' | 'STOPPED';
|
|
9
|
+
outcome: 'SUCCESS' | 'FAILED' | 'BLOCKED' | 'PENDING';
|
|
10
|
+
reason?: string;
|
|
11
|
+
params?: Record<string, any>;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface AuditFilterOptions {
|
|
15
|
+
limit?: number;
|
|
16
|
+
riskLevel?: RiskLevel;
|
|
17
|
+
decision?: string;
|
|
18
|
+
action?: string;
|
|
19
|
+
capability?: Capability;
|
|
20
|
+
since?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class AuditLogger {
|
|
23
|
+
private logPath;
|
|
24
|
+
private static instance;
|
|
25
|
+
constructor(customPath?: string);
|
|
26
|
+
static getInstance(customPath?: string): AuditLogger;
|
|
27
|
+
getLogPath(): string;
|
|
28
|
+
setLogPath(newPath: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Append an entry to the append-only JSONL audit log.
|
|
31
|
+
*/
|
|
32
|
+
log(entry: {
|
|
33
|
+
action: string;
|
|
34
|
+
capability: Capability;
|
|
35
|
+
riskLevel: RiskLevel;
|
|
36
|
+
decision: 'ALLOWED' | 'DENIED' | 'APPROVED' | 'ESCALATED' | 'STOPPED';
|
|
37
|
+
outcome: 'SUCCESS' | 'FAILED' | 'BLOCKED' | 'PENDING';
|
|
38
|
+
reason?: string;
|
|
39
|
+
params?: Record<string, any>;
|
|
40
|
+
durationMs?: number;
|
|
41
|
+
}): AuditRecord;
|
|
42
|
+
/**
|
|
43
|
+
* Read and filter entries from the audit log.
|
|
44
|
+
*/
|
|
45
|
+
readEntries(options?: AuditFilterOptions): AuditRecord[];
|
|
46
|
+
/**
|
|
47
|
+
* Export audit log to a target file in JSON, JSONL, or CSV format.
|
|
48
|
+
*/
|
|
49
|
+
exportEntries(destinationPath: string, format?: 'json' | 'jsonl' | 'csv'): string;
|
|
50
|
+
/**
|
|
51
|
+
* Clear audit log (primarily for testing).
|
|
52
|
+
*/
|
|
53
|
+
clear(): void;
|
|
54
|
+
private sanitizeParams;
|
|
55
|
+
}
|
|
56
|
+
export declare const auditLogger: AuditLogger;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
export class AuditLogger {
|
|
6
|
+
logPath;
|
|
7
|
+
static instance = null;
|
|
8
|
+
constructor(customPath) {
|
|
9
|
+
if (customPath) {
|
|
10
|
+
this.logPath = customPath;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const workspaceDir = path.join(process.cwd(), '.overarch');
|
|
14
|
+
if (fs.existsSync(workspaceDir)) {
|
|
15
|
+
this.logPath = path.join(workspaceDir, 'audit.jsonl');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const globalDir = path.join(os.homedir(), '.overarch');
|
|
19
|
+
if (!fs.existsSync(globalDir)) {
|
|
20
|
+
try {
|
|
21
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
this.logPath = path.join(globalDir, 'audit.jsonl');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
static getInstance(customPath) {
|
|
30
|
+
if (!AuditLogger.instance || customPath) {
|
|
31
|
+
AuditLogger.instance = new AuditLogger(customPath);
|
|
32
|
+
}
|
|
33
|
+
return AuditLogger.instance;
|
|
34
|
+
}
|
|
35
|
+
getLogPath() {
|
|
36
|
+
return this.logPath;
|
|
37
|
+
}
|
|
38
|
+
setLogPath(newPath) {
|
|
39
|
+
this.logPath = newPath;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Append an entry to the append-only JSONL audit log.
|
|
43
|
+
*/
|
|
44
|
+
log(entry) {
|
|
45
|
+
const record = {
|
|
46
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
action: entry.action,
|
|
49
|
+
capability: entry.capability,
|
|
50
|
+
riskLevel: entry.riskLevel,
|
|
51
|
+
decision: entry.decision,
|
|
52
|
+
outcome: entry.outcome,
|
|
53
|
+
reason: entry.reason,
|
|
54
|
+
params: entry.params ? this.sanitizeParams(entry.params) : undefined,
|
|
55
|
+
durationMs: entry.durationMs,
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const dir = path.dirname(this.logPath);
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
fs.appendFileSync(this.logPath, JSON.stringify(record) + '\n', 'utf8');
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Append-only logging fails closed without crashing runtime
|
|
66
|
+
console.error(`[Overarch Audit] Failed to append audit log: ${err}`);
|
|
67
|
+
}
|
|
68
|
+
return record;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read and filter entries from the audit log.
|
|
72
|
+
*/
|
|
73
|
+
readEntries(options = {}) {
|
|
74
|
+
if (!fs.existsSync(this.logPath)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const content = fs.readFileSync(this.logPath, 'utf8');
|
|
79
|
+
const lines = content.split('\n').filter(line => line.trim().length > 0);
|
|
80
|
+
const records = [];
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
try {
|
|
83
|
+
const rec = JSON.parse(line);
|
|
84
|
+
if (options.riskLevel && rec.riskLevel !== options.riskLevel)
|
|
85
|
+
continue;
|
|
86
|
+
if (options.decision && rec.decision.toLowerCase() !== options.decision.toLowerCase())
|
|
87
|
+
continue;
|
|
88
|
+
if (options.action && !rec.action.toLowerCase().includes(options.action.toLowerCase()))
|
|
89
|
+
continue;
|
|
90
|
+
if (options.capability && rec.capability !== options.capability)
|
|
91
|
+
continue;
|
|
92
|
+
if (options.since && new Date(rec.timestamp) < new Date(options.since))
|
|
93
|
+
continue;
|
|
94
|
+
records.push(rec);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
if (options.limit && options.limit > 0) {
|
|
99
|
+
return records.slice(-options.limit);
|
|
100
|
+
}
|
|
101
|
+
return records;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Export audit log to a target file in JSON, JSONL, or CSV format.
|
|
109
|
+
*/
|
|
110
|
+
exportEntries(destinationPath, format = 'jsonl') {
|
|
111
|
+
const entries = this.readEntries();
|
|
112
|
+
const destDir = path.dirname(destinationPath);
|
|
113
|
+
if (!fs.existsSync(destDir)) {
|
|
114
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
if (format === 'json') {
|
|
117
|
+
fs.writeFileSync(destinationPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
118
|
+
}
|
|
119
|
+
else if (format === 'csv') {
|
|
120
|
+
const header = 'id,timestamp,action,capability,riskLevel,decision,outcome,reason\n';
|
|
121
|
+
const rows = entries.map(e => `"${e.id}","${e.timestamp}","${e.action}","${e.capability}","${e.riskLevel}","${e.decision}","${e.outcome}","${(e.reason || '').replace(/"/g, '""')}"`).join('\n');
|
|
122
|
+
fs.writeFileSync(destinationPath, header + rows + '\n', 'utf8');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const content = entries.map(e => JSON.stringify(e)).join('\n') + (entries.length ? '\n' : '');
|
|
126
|
+
fs.writeFileSync(destinationPath, content, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
return destinationPath;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Clear audit log (primarily for testing).
|
|
132
|
+
*/
|
|
133
|
+
clear() {
|
|
134
|
+
if (fs.existsSync(this.logPath)) {
|
|
135
|
+
fs.writeFileSync(this.logPath, '', 'utf8');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
sanitizeParams(params) {
|
|
139
|
+
const sanitized = {};
|
|
140
|
+
for (const [key, val] of Object.entries(params)) {
|
|
141
|
+
if (/key|secret|token|password|auth/i.test(key)) {
|
|
142
|
+
sanitized[key] = '[REDACTED]';
|
|
143
|
+
}
|
|
144
|
+
else if (typeof val === 'string' && val.length > 200) {
|
|
145
|
+
sanitized[key] = val.slice(0, 197) + '...';
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
sanitized[key] = val;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return sanitized;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export const auditLogger = AuditLogger.getInstance();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function validatePath(filepath: string, basePath: string): boolean;
|
|
2
|
+
export declare const DANGEROUS_COMMANDS: string[];
|
|
3
|
+
export declare function isCommandDangerous(command: string): {
|
|
4
|
+
dangerous: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function abort(): void;
|
|
8
|
+
export declare function isAborted(): boolean;
|
|
9
|
+
export declare function resetAbort(): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
export function validatePath(filepath, basePath) {
|
|
3
|
+
const resolvedPath = path.resolve(basePath, filepath);
|
|
4
|
+
const resolvedBase = path.resolve(basePath);
|
|
5
|
+
return resolvedPath.startsWith(resolvedBase);
|
|
6
|
+
}
|
|
7
|
+
export const DANGEROUS_COMMANDS = [
|
|
8
|
+
'rm -rf /',
|
|
9
|
+
'format',
|
|
10
|
+
':(){:|:&};:',
|
|
11
|
+
'dd if=',
|
|
12
|
+
'mkfs',
|
|
13
|
+
'> /dev/sda'
|
|
14
|
+
];
|
|
15
|
+
export function isCommandDangerous(command) {
|
|
16
|
+
for (const pattern of DANGEROUS_COMMANDS) {
|
|
17
|
+
if (command.includes(pattern)) {
|
|
18
|
+
return { dangerous: true, reason: `Matches dangerous pattern: ${pattern}` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { dangerous: false };
|
|
22
|
+
}
|
|
23
|
+
let aborted = false;
|
|
24
|
+
export function abort() {
|
|
25
|
+
aborted = true;
|
|
26
|
+
}
|
|
27
|
+
export function isAborted() {
|
|
28
|
+
return aborted;
|
|
29
|
+
}
|
|
30
|
+
export function resetAbort() {
|
|
31
|
+
aborted = false;
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PermissionTier, PermissionConfig } from '../config/types.js';
|
|
2
|
+
import { ToolDefinition } from '../tools/types.js';
|
|
3
|
+
import * as readline from 'node:readline/promises';
|
|
4
|
+
export declare function classifyAction(tool: ToolDefinition): PermissionTier;
|
|
5
|
+
export declare function checkPermission(tool: ToolDefinition, params: Record<string, unknown>, config: PermissionConfig, options?: {
|
|
6
|
+
autoApprove?: boolean;
|
|
7
|
+
rl?: readline.Interface;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
allowed: boolean;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function formatPermissionPrompt(tool: ToolDefinition, params: Record<string, unknown>): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PermissionTier } from '../config/types.js';
|
|
2
|
+
import * as readline from 'node:readline/promises';
|
|
3
|
+
import { stdin, stdout } from 'node:process';
|
|
4
|
+
export function classifyAction(tool) {
|
|
5
|
+
return tool.tier;
|
|
6
|
+
}
|
|
7
|
+
export async function checkPermission(tool, params, config, options) {
|
|
8
|
+
const tier = classifyAction(tool);
|
|
9
|
+
// Check deny list
|
|
10
|
+
if (config.deny?.includes(tool.name)) {
|
|
11
|
+
return { allowed: false, reason: `Tool '${tool.name}' is in the deny list` };
|
|
12
|
+
}
|
|
13
|
+
if (tier === PermissionTier.ReadOnly) {
|
|
14
|
+
return { allowed: true };
|
|
15
|
+
}
|
|
16
|
+
if (tier === PermissionTier.SafeMutation) {
|
|
17
|
+
return { allowed: true };
|
|
18
|
+
}
|
|
19
|
+
if (tier === PermissionTier.StateMutating) {
|
|
20
|
+
if (options?.autoApprove)
|
|
21
|
+
return { allowed: true };
|
|
22
|
+
if (config.autoApprove?.includes(PermissionTier.StateMutating))
|
|
23
|
+
return { allowed: true };
|
|
24
|
+
const prompt = formatPermissionPrompt(tool, params);
|
|
25
|
+
let answer = '';
|
|
26
|
+
if (options?.rl) {
|
|
27
|
+
answer = await options.rl.question(`\n${prompt}\nAllow? [Y/n] `);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
31
|
+
answer = await rl.question(`\n${prompt}\nAllow? [Y/n] `);
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
35
|
+
return { allowed: false, reason: 'User denied permission' };
|
|
36
|
+
}
|
|
37
|
+
return { allowed: true };
|
|
38
|
+
}
|
|
39
|
+
if (tier === PermissionTier.Destructive) {
|
|
40
|
+
if (options?.autoApprove) {
|
|
41
|
+
// Even with autoApprove, destructive actions need explicit confirmation
|
|
42
|
+
// unless the config specifically auto-approves destructive
|
|
43
|
+
if (!config.autoApprove?.includes(PermissionTier.Destructive)) {
|
|
44
|
+
const prompt = formatPermissionPrompt(tool, params);
|
|
45
|
+
let answer = '';
|
|
46
|
+
if (options?.rl) {
|
|
47
|
+
answer = await options.rl.question(`\n⚠ DESTRUCTIVE ACTION\n${prompt}\nType 'yes' to confirm: `);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
51
|
+
answer = await rl.question(`\n⚠ DESTRUCTIVE ACTION\n${prompt}\nType 'yes' to confirm: `);
|
|
52
|
+
rl.close();
|
|
53
|
+
}
|
|
54
|
+
if (answer.trim().toLowerCase() !== 'yes') {
|
|
55
|
+
return { allowed: false, reason: 'User denied destructive action' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
return { allowed: false, reason: 'Unknown permission tier' };
|
|
62
|
+
}
|
|
63
|
+
export function formatPermissionPrompt(tool, params) {
|
|
64
|
+
const paramStr = Object.entries(params)
|
|
65
|
+
.map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
66
|
+
.join('\n');
|
|
67
|
+
return `Tool: ${tool.name}\n${paramStr}`;
|
|
68
|
+
}
|