@astrasyncai/verification-gateway 3.6.0 → 3.7.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/dist/adapters/express.js +1 -1
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +1 -1
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.js +1 -1
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +1 -1
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.js +1 -1
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +1 -1
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.js +1 -1
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +1 -1
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +4791 -0
- package/dist/bin/astrasync-guard.js +5990 -0
- package/dist/bin/astrasync.js +6840 -330
- package/dist/browser/background.js +1 -1
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +1 -1
- package/dist/browser/background.mjs.map +1 -1
- package/dist/claude-code/claude-code-adapter.d.mts +96 -0
- package/dist/claude-code/claude-code-adapter.d.ts +96 -0
- package/dist/claude-code/claude-code-adapter.js +277 -0
- package/dist/claude-code/claude-code-adapter.js.map +1 -0
- package/dist/claude-code/claude-code-adapter.mjs +249 -0
- package/dist/claude-code/claude-code-adapter.mjs.map +1 -0
- package/dist/cursor/extension.js +1 -1
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +1 -1
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/gateway/gateway.js +1 -1
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +1 -1
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// src/adapter-interface/interface.ts
|
|
2
|
+
var ADAPTER_INTERFACE_VERSION = 1;
|
|
3
|
+
|
|
4
|
+
// src/adapter-interface/purpose-mapping.ts
|
|
5
|
+
var TOOL_PURPOSE_MAP = {
|
|
6
|
+
// Shell
|
|
7
|
+
shell_exec: "shell.exec",
|
|
8
|
+
run_command: "shell.exec",
|
|
9
|
+
execute: "shell.exec",
|
|
10
|
+
terminal_exec: "shell.exec",
|
|
11
|
+
run_terminal_command: "shell.exec",
|
|
12
|
+
// File read
|
|
13
|
+
file_read: "file.read",
|
|
14
|
+
read_file: "file.read",
|
|
15
|
+
// File write
|
|
16
|
+
file_write: "file.write",
|
|
17
|
+
write_file: "file.write",
|
|
18
|
+
create_file: "file.write",
|
|
19
|
+
edit_file: "file.write",
|
|
20
|
+
// File delete
|
|
21
|
+
file_delete: "file.delete",
|
|
22
|
+
delete_file: "file.delete",
|
|
23
|
+
// Network
|
|
24
|
+
http_request: "network.request",
|
|
25
|
+
fetch: "network.request",
|
|
26
|
+
web_request: "network.request",
|
|
27
|
+
// Email
|
|
28
|
+
send_email: "email.send",
|
|
29
|
+
read_email: "email.read",
|
|
30
|
+
// Calendar
|
|
31
|
+
create_event: "calendar.create",
|
|
32
|
+
// Database
|
|
33
|
+
query_database: "database.query",
|
|
34
|
+
write_database: "database.write",
|
|
35
|
+
// Payment
|
|
36
|
+
payment_execute: "payment.execute"
|
|
37
|
+
};
|
|
38
|
+
function mapToolToPurpose(toolName) {
|
|
39
|
+
return TOOL_PURPOSE_MAP[toolName] || `tool.${toolName}`;
|
|
40
|
+
}
|
|
41
|
+
function registerToolMappings(mappings) {
|
|
42
|
+
for (const [tool, purpose] of Object.entries(mappings)) {
|
|
43
|
+
if (!(tool in TOOL_PURPOSE_MAP)) {
|
|
44
|
+
TOOL_PURPOSE_MAP[tool] = purpose;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function extractTarget(toolName, args) {
|
|
49
|
+
const purpose = mapToolToPurpose(toolName);
|
|
50
|
+
if (purpose.startsWith("shell.")) {
|
|
51
|
+
return String(args.command || args.cmd || args.script || "");
|
|
52
|
+
}
|
|
53
|
+
if (purpose.startsWith("file.")) {
|
|
54
|
+
return String(args.path || args.file || args.filename || args.file_path || "");
|
|
55
|
+
}
|
|
56
|
+
if (purpose.startsWith("network.")) {
|
|
57
|
+
return String(args.url || args.endpoint || args.uri || "");
|
|
58
|
+
}
|
|
59
|
+
if (purpose.startsWith("email.")) {
|
|
60
|
+
return String(args.to || args.recipient || args.address || "");
|
|
61
|
+
}
|
|
62
|
+
if (purpose.startsWith("database.")) {
|
|
63
|
+
return String(args.query || args.table || "");
|
|
64
|
+
}
|
|
65
|
+
if (purpose.startsWith("payment.")) {
|
|
66
|
+
return String(args.description || args.merchant || args.amount || "");
|
|
67
|
+
}
|
|
68
|
+
if (args.command) return String(args.command);
|
|
69
|
+
if (args.path) return String(args.path);
|
|
70
|
+
if (args.url) return String(args.url);
|
|
71
|
+
for (const val of Object.values(args)) {
|
|
72
|
+
if (typeof val === "string" && val.length > 0) return val;
|
|
73
|
+
}
|
|
74
|
+
return toolName;
|
|
75
|
+
}
|
|
76
|
+
function extractNetworkDomains(target) {
|
|
77
|
+
try {
|
|
78
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
79
|
+
const url = new URL(target);
|
|
80
|
+
return [url.hostname];
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/claude-code/tool-mapping.ts
|
|
88
|
+
var CLAUDE_CODE_TOOL_MAP = {
|
|
89
|
+
Bash: "shell.exec",
|
|
90
|
+
Write: "file.write",
|
|
91
|
+
Edit: "file.write",
|
|
92
|
+
MultiEdit: "file.write",
|
|
93
|
+
NotebookEdit: "file.write",
|
|
94
|
+
Read: "file.read",
|
|
95
|
+
Glob: "file.read",
|
|
96
|
+
Grep: "file.read",
|
|
97
|
+
WebFetch: "network.request",
|
|
98
|
+
WebSearch: "network.request",
|
|
99
|
+
Task: "sub_agent.spawn",
|
|
100
|
+
Agent: "sub_agent.spawn"
|
|
101
|
+
};
|
|
102
|
+
var registered = false;
|
|
103
|
+
function registerClaudeCodeMappings() {
|
|
104
|
+
if (registered) return;
|
|
105
|
+
registerToolMappings(CLAUDE_CODE_TOOL_MAP);
|
|
106
|
+
registered = true;
|
|
107
|
+
}
|
|
108
|
+
function mapMcpTool(toolName) {
|
|
109
|
+
const match = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
return `${match[1]}.${match[2]}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/claude-code/claude-code-adapter.ts
|
|
115
|
+
var DEFAULT_SKIP_TOOLS = [
|
|
116
|
+
"TodoWrite",
|
|
117
|
+
"TaskCreate",
|
|
118
|
+
"TaskUpdate",
|
|
119
|
+
"TaskList",
|
|
120
|
+
"TaskGet",
|
|
121
|
+
"ExitPlanMode",
|
|
122
|
+
"EnterPlanMode",
|
|
123
|
+
"AskUserQuestion",
|
|
124
|
+
"BashOutput",
|
|
125
|
+
"KillShell",
|
|
126
|
+
"TaskStop",
|
|
127
|
+
"TaskOutput",
|
|
128
|
+
"ListMcpResourcesTool"
|
|
129
|
+
];
|
|
130
|
+
var ClaudeCodeAdapter = class {
|
|
131
|
+
constructor(options) {
|
|
132
|
+
this.interfaceVersion = ADAPTER_INTERFACE_VERSION;
|
|
133
|
+
this._isRunning = false;
|
|
134
|
+
this.skipTools = new Set(options?.skipTools ?? DEFAULT_SKIP_TOOLS);
|
|
135
|
+
this.strictUnmapped = options?.strictUnmapped ?? false;
|
|
136
|
+
this.governMcp = options?.governMcp ?? true;
|
|
137
|
+
registerClaudeCodeMappings();
|
|
138
|
+
}
|
|
139
|
+
get isRunning() {
|
|
140
|
+
return this._isRunning;
|
|
141
|
+
}
|
|
142
|
+
async initialize(config) {
|
|
143
|
+
this.gateway = config.gateway;
|
|
144
|
+
if (Array.isArray(config.adapterOptions.skipTools)) {
|
|
145
|
+
this.skipTools = new Set(config.adapterOptions.skipTools);
|
|
146
|
+
}
|
|
147
|
+
if (typeof config.adapterOptions.strictUnmapped === "boolean") {
|
|
148
|
+
this.strictUnmapped = config.adapterOptions.strictUnmapped;
|
|
149
|
+
}
|
|
150
|
+
this._isRunning = true;
|
|
151
|
+
}
|
|
152
|
+
async shutdown() {
|
|
153
|
+
this._isRunning = false;
|
|
154
|
+
}
|
|
155
|
+
async interceptAction(action) {
|
|
156
|
+
const raw = action.raw;
|
|
157
|
+
if (!raw || typeof raw.tool_name !== "string" || raw.tool_name.length === 0) {
|
|
158
|
+
return { intercepted: false, skipReason: "No tool_name in payload" };
|
|
159
|
+
}
|
|
160
|
+
if (this.skipTools.has(raw.tool_name)) {
|
|
161
|
+
return { intercepted: false, skipReason: `Tool ${raw.tool_name} is not governed` };
|
|
162
|
+
}
|
|
163
|
+
const isMcp = mapMcpTool(raw.tool_name) !== null;
|
|
164
|
+
if (isMcp && !this.governMcp) {
|
|
165
|
+
return { intercepted: false, skipReason: `MCP tools not governed (governMcp off)` };
|
|
166
|
+
}
|
|
167
|
+
const isMapped = raw.tool_name in CLAUDE_CODE_TOOL_MAP || isMcp;
|
|
168
|
+
if (!isMapped && !this.strictUnmapped) {
|
|
169
|
+
return {
|
|
170
|
+
intercepted: false,
|
|
171
|
+
skipReason: `Unmapped tool ${raw.tool_name} (strictUnmapped off)`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const context = this.extractContext(action);
|
|
175
|
+
return { intercepted: true, context };
|
|
176
|
+
}
|
|
177
|
+
extractContext(action) {
|
|
178
|
+
const raw = action.raw;
|
|
179
|
+
const toolName = raw.tool_name;
|
|
180
|
+
const args = raw.tool_input ?? {};
|
|
181
|
+
const mcpPurpose = mapMcpTool(toolName);
|
|
182
|
+
const purpose = mcpPurpose ?? mapToolToPurpose(toolName);
|
|
183
|
+
const target = extractTarget(toolName, args);
|
|
184
|
+
const networkAccess = extractNetworkDomains(target);
|
|
185
|
+
return {
|
|
186
|
+
purpose,
|
|
187
|
+
action: toolName,
|
|
188
|
+
target,
|
|
189
|
+
...networkAccess && { networkAccess },
|
|
190
|
+
metadata: {
|
|
191
|
+
...raw.session_id && { sessionId: raw.session_id },
|
|
192
|
+
...raw.cwd && { cwd: raw.cwd },
|
|
193
|
+
platform: "claude-code"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Translate a gateway decision into hook stdout JSON.
|
|
199
|
+
* Returns null on ALLOW — the hook stays silent and Claude Code's own
|
|
200
|
+
* permission system remains in charge (strictly-tightening invariant).
|
|
201
|
+
*/
|
|
202
|
+
decisionToHookOutput(decision) {
|
|
203
|
+
switch (decision.recommendation) {
|
|
204
|
+
case "DENY":
|
|
205
|
+
return {
|
|
206
|
+
hookSpecificOutput: {
|
|
207
|
+
hookEventName: "PreToolUse",
|
|
208
|
+
permissionDecision: "deny",
|
|
209
|
+
permissionDecisionReason: `AstraSync Local Guard: ${decision.reason}`
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
case "MANUAL_REVIEW":
|
|
213
|
+
return {
|
|
214
|
+
hookSpecificOutput: {
|
|
215
|
+
hookEventName: "PreToolUse",
|
|
216
|
+
permissionDecision: "ask",
|
|
217
|
+
permissionDecisionReason: `AstraSync Local Guard requires approval: ${decision.reason}`
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
case "ALLOW":
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Convenience for the hook entry: full payload → hook output (or null).
|
|
226
|
+
*/
|
|
227
|
+
async evaluatePayload(payload) {
|
|
228
|
+
const action = {
|
|
229
|
+
raw: payload,
|
|
230
|
+
platform: "claude-code",
|
|
231
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
232
|
+
};
|
|
233
|
+
const interception = await this.interceptAction(action);
|
|
234
|
+
if (!interception.intercepted || !interception.context) return null;
|
|
235
|
+
const decision = await this.gateway.evaluate(interception.context);
|
|
236
|
+
return this.decisionToHookOutput(decision);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Enforcement happens via the stdout JSON the hook prints (see
|
|
240
|
+
* decisionToHookOutput); there is no in-process side to enforce on.
|
|
241
|
+
*/
|
|
242
|
+
async enforceDecision(_decision) {
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
export {
|
|
246
|
+
ClaudeCodeAdapter,
|
|
247
|
+
DEFAULT_SKIP_TOOLS
|
|
248
|
+
};
|
|
249
|
+
//# sourceMappingURL=claude-code-adapter.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapter-interface/interface.ts","../../src/adapter-interface/purpose-mapping.ts","../../src/claude-code/tool-mapping.ts","../../src/claude-code/claude-code-adapter.ts"],"sourcesContent":["/**\n * PlatformAdapter Interface\n *\n * The contract that Layer 4 platform adapters implement.\n * Agent-side interception: governs what agents are allowed to do before they do it.\n *\n * Each adapter is 200-500 lines of platform-specific code.\n * All verification logic lives in the gateway — adapters just translate\n * between the platform's world and AstraSync's world.\n */\n\nimport type { AstraSyncGateway } from '../gateway/gateway';\nimport type { PDLSSContext, VerificationDecision, AgentAction, InterceptResult } from '../gateway/types';\n\nexport interface AdapterConfig {\n /** The AstraSyncGateway instance (handles mode routing) */\n gateway: AstraSyncGateway;\n /** Platform-specific configuration */\n adapterOptions: Record<string, unknown>;\n}\n\nexport interface PlatformAdapter {\n /**\n * Interface version for compatibility checking.\n * Current version: 1.\n */\n readonly interfaceVersion: number;\n\n /**\n * Platform-specific initialization.\n * Load config, register hooks, establish connections.\n */\n initialize(config: AdapterConfig): Promise<void>;\n\n /**\n * Graceful shutdown.\n * Drain in-flight verifications, deregister hooks, close connections.\n */\n shutdown(): Promise<void>;\n\n /**\n * Intercept an agent action before execution.\n *\n * How this works depends on the platform:\n * - CLI adapter: proxy server captures outbound request\n * - Browser adapter: content script intercepts DOM interaction\n * - Express: middleware captures inbound request\n */\n interceptAction(action: AgentAction): Promise<InterceptResult>;\n\n /**\n * Extract PDLSS-compatible context from a platform-specific action.\n * Maps platform-native action format to the universal PDLSSContext.\n */\n extractContext(action: AgentAction): PDLSSContext;\n\n /**\n * Enforce the verification decision in a platform-specific way.\n *\n * How this works depends on the platform:\n * - CLI: block command / allow command / prompt for approval\n * - Browser: block navigation / show confirmation dialog\n * - Express: return 403 / pass through / inject headers\n */\n enforceDecision(decision: VerificationDecision): Promise<void>;\n}\n\n/**\n * Current adapter interface version.\n */\nexport const ADAPTER_INTERFACE_VERSION = 1;\n\n/**\n * Check if an adapter is compatible with the current interface version.\n */\nexport function isCompatibleAdapter(adapter: PlatformAdapter): boolean {\n return adapter.interfaceVersion === ADAPTER_INTERFACE_VERSION;\n}\n","/**\n * Shared purpose mapping utilities for Layer 4 platform adapters.\n *\n * Maps platform-native action names to PDLSS purpose categories.\n * Used by OpenClaw CLI, Cursor, browser, and future adapters.\n */\n\n// -----------------------------------------------------------------------\n// Tool → Purpose mapping\n// -----------------------------------------------------------------------\n\n/** Standard tool name → PDLSS purpose mapping used by all adapters */\nconst TOOL_PURPOSE_MAP: Record<string, string> = {\n // Shell\n shell_exec: 'shell.exec',\n run_command: 'shell.exec',\n execute: 'shell.exec',\n terminal_exec: 'shell.exec',\n run_terminal_command: 'shell.exec',\n\n // File read\n file_read: 'file.read',\n read_file: 'file.read',\n\n // File write\n file_write: 'file.write',\n write_file: 'file.write',\n create_file: 'file.write',\n edit_file: 'file.write',\n\n // File delete\n file_delete: 'file.delete',\n delete_file: 'file.delete',\n\n // Network\n http_request: 'network.request',\n fetch: 'network.request',\n web_request: 'network.request',\n\n // Email\n send_email: 'email.send',\n read_email: 'email.read',\n\n // Calendar\n create_event: 'calendar.create',\n\n // Database\n query_database: 'database.query',\n write_database: 'database.write',\n\n // Payment\n payment_execute: 'payment.execute',\n};\n\n/**\n * Map a tool/action name to a PDLSS purpose category.\n * Returns `tool.<name>` for unmapped tools (denied by default).\n */\nexport function mapToolToPurpose(toolName: string): string {\n return TOOL_PURPOSE_MAP[toolName] || `tool.${toolName}`;\n}\n\n/**\n * Register additional tool → purpose mappings (e.g. from a platform adapter).\n * Does not overwrite existing mappings.\n */\nexport function registerToolMappings(mappings: Record<string, string>): void {\n for (const [tool, purpose] of Object.entries(mappings)) {\n if (!(tool in TOOL_PURPOSE_MAP)) {\n TOOL_PURPOSE_MAP[tool] = purpose;\n }\n }\n}\n\n// -----------------------------------------------------------------------\n// Target extraction\n// -----------------------------------------------------------------------\n\n/**\n * Extract the meaningful target string from tool arguments.\n * Uses the purpose category to determine which argument field is relevant.\n */\nexport function extractTarget(toolName: string, args: Record<string, unknown>): string {\n const purpose = mapToolToPurpose(toolName);\n\n if (purpose.startsWith('shell.')) {\n return String(args.command || args.cmd || args.script || '');\n }\n\n if (purpose.startsWith('file.')) {\n return String(args.path || args.file || args.filename || args.file_path || '');\n }\n\n if (purpose.startsWith('network.')) {\n return String(args.url || args.endpoint || args.uri || '');\n }\n\n if (purpose.startsWith('email.')) {\n return String(args.to || args.recipient || args.address || '');\n }\n\n if (purpose.startsWith('database.')) {\n return String(args.query || args.table || '');\n }\n\n if (purpose.startsWith('payment.')) {\n return String(args.description || args.merchant || args.amount || '');\n }\n\n // Fallback: try common field names\n if (args.command) return String(args.command);\n if (args.path) return String(args.path);\n if (args.url) return String(args.url);\n\n // Default: use first non-empty string argument or tool name\n for (const val of Object.values(args)) {\n if (typeof val === 'string' && val.length > 0) return val;\n }\n return toolName;\n}\n\n// -----------------------------------------------------------------------\n// Network domain extraction\n// -----------------------------------------------------------------------\n\n/**\n * Extract network domains from a URL target.\n * Returns undefined if the target is not a URL.\n */\nexport function extractNetworkDomains(target: string): string[] | undefined {\n try {\n if (target.startsWith('http://') || target.startsWith('https://')) {\n const url = new URL(target);\n return [url.hostname];\n }\n } catch {\n // Not a URL\n }\n return undefined;\n}\n","/**\n * @astrasyncai/adapter-claude-code — tool → purpose mapping\n *\n * Maps Claude Code's built-in tool names (as delivered in PreToolUse hook\n * payloads) to PDLSS purpose categories, and parses MCP tool names\n * (mcp__{server}__{tool}) into app-connector style purposes.\n */\n\nimport { registerToolMappings } from '../adapter-interface/purpose-mapping';\n\n/** Claude Code built-in tool name → PDLSS purpose */\nexport const CLAUDE_CODE_TOOL_MAP: Record<string, string> = {\n Bash: 'shell.exec',\n Write: 'file.write',\n Edit: 'file.write',\n MultiEdit: 'file.write',\n NotebookEdit: 'file.write',\n Read: 'file.read',\n Glob: 'file.read',\n Grep: 'file.read',\n WebFetch: 'network.request',\n WebSearch: 'network.request',\n Task: 'sub_agent.spawn',\n Agent: 'sub_agent.spawn',\n};\n\nlet registered = false;\n\n/**\n * Register the Claude Code mappings into the shared adapter purpose map.\n * Idempotent; safe to call from both the adapter and the hook entry.\n */\nexport function registerClaudeCodeMappings(): void {\n if (registered) return;\n registerToolMappings(CLAUDE_CODE_TOOL_MAP);\n registered = true;\n}\n\n/**\n * Parse an MCP tool name (mcp__{server}__{tool}) into an app-connector\n * style purpose `{server}.{tool}`. Returns null for non-MCP tool names.\n */\nexport function mapMcpTool(toolName: string): string | null {\n const match = /^mcp__(.+?)__(.+)$/.exec(toolName);\n if (!match) return null;\n return `${match[1]}.${match[2]}`;\n}\n","/**\n * @astrasyncai/adapter-claude-code\n *\n * Layer 4 adapter for Claude Code (Anthropic's CLI coding agent).\n * Intercepts tool calls via Claude Code's native PreToolUse hook:\n * the hook binary receives {tool_name, tool_input, ...} on stdin and\n * answers with a permissionDecision on stdout.\n *\n * Decision mapping is strictly tightening:\n * - DENY → permissionDecision \"deny\"\n * - MANUAL_REVIEW → permissionDecision \"ask\" (Claude Code's own permission\n * prompt is the human-in-the-loop)\n * - ALLOW → no output at all — the guard NEVER emits \"allow\",\n * because \"allow\" would bypass Claude Code's native\n * permission system rather than defer to it.\n *\n * ~250 lines — thin, disposable, platform-specific.\n */\n\nimport type { PlatformAdapter, AdapterConfig } from '../adapter-interface/interface';\nimport type {\n PDLSSContext,\n VerificationDecision,\n AgentAction,\n InterceptResult,\n} from '../gateway/types';\nimport type { AstraSyncGateway } from '../gateway/gateway';\nimport { ADAPTER_INTERFACE_VERSION } from '../adapter-interface/interface';\nimport {\n mapToolToPurpose,\n extractTarget,\n extractNetworkDomains,\n} from '../adapter-interface/purpose-mapping';\nimport { registerClaudeCodeMappings, mapMcpTool, CLAUDE_CODE_TOOL_MAP } from './tool-mapping';\n\n// -----------------------------------------------------------------------\n// Claude Code hook payload / output shapes\n// (contract: https://code.claude.com/docs/en/hooks.md)\n// -----------------------------------------------------------------------\n\n/** The PreToolUse payload Claude Code writes to the hook's stdin. */\nexport interface ClaudeCodePreToolUsePayload {\n hook_event_name?: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n session_id?: string;\n cwd?: string;\n transcript_path?: string;\n permission_mode?: string;\n}\n\n/** The JSON the hook prints to stdout when it wants to tighten a decision. */\nexport interface ClaudeHookOutput {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse';\n permissionDecision: 'deny' | 'ask';\n permissionDecisionReason: string;\n };\n}\n\n// -----------------------------------------------------------------------\n// Configuration\n// -----------------------------------------------------------------------\n\n/**\n * Tools that are never governed: session bookkeeping and UI-only tools\n * with no side effects outside the conversation.\n */\nexport const DEFAULT_SKIP_TOOLS = [\n 'TodoWrite',\n 'TaskCreate',\n 'TaskUpdate',\n 'TaskList',\n 'TaskGet',\n 'ExitPlanMode',\n 'EnterPlanMode',\n 'AskUserQuestion',\n 'BashOutput',\n 'KillShell',\n 'TaskStop',\n 'TaskOutput',\n 'ListMcpResourcesTool',\n];\n\nexport interface ClaudeCodeAdapterOptions {\n /** Tool names to never intercept (replaces DEFAULT_SKIP_TOOLS if given) */\n skipTools?: string[];\n /**\n * When true, unmapped non-MCP tools are still intercepted (purpose\n * `tool.<name>`, denied by default policy). When false (default),\n * unmapped tools are skipped — Claude Code's own permission system\n * remains their only gate.\n */\n strictUnmapped?: boolean;\n /**\n * Govern MCP tools (mcp__{server}__{tool} → purpose {server}.{tool}).\n * Defaults to true. Note the evaluator denies purposes missing from the\n * policy, so with governMcp on, every MCP server in use needs purpose\n * rules; installers may default this off until the policy lists them.\n */\n governMcp?: boolean;\n}\n\n// -----------------------------------------------------------------------\n// Adapter implementation\n// -----------------------------------------------------------------------\n\nexport class ClaudeCodeAdapter implements PlatformAdapter {\n readonly interfaceVersion = ADAPTER_INTERFACE_VERSION;\n\n private gateway!: AstraSyncGateway;\n private skipTools: Set<string>;\n private strictUnmapped: boolean;\n private governMcp: boolean;\n private _isRunning = false;\n\n constructor(options?: ClaudeCodeAdapterOptions) {\n this.skipTools = new Set(options?.skipTools ?? DEFAULT_SKIP_TOOLS);\n this.strictUnmapped = options?.strictUnmapped ?? false;\n this.governMcp = options?.governMcp ?? true;\n registerClaudeCodeMappings();\n }\n\n get isRunning(): boolean {\n return this._isRunning;\n }\n\n async initialize(config: AdapterConfig): Promise<void> {\n this.gateway = config.gateway as AstraSyncGateway;\n\n if (Array.isArray(config.adapterOptions.skipTools)) {\n this.skipTools = new Set(config.adapterOptions.skipTools as string[]);\n }\n if (typeof config.adapterOptions.strictUnmapped === 'boolean') {\n this.strictUnmapped = config.adapterOptions.strictUnmapped;\n }\n\n this._isRunning = true;\n }\n\n async shutdown(): Promise<void> {\n this._isRunning = false;\n }\n\n async interceptAction(action: AgentAction): Promise<InterceptResult> {\n const raw = action.raw as Partial<ClaudeCodePreToolUsePayload> | null;\n\n if (!raw || typeof raw.tool_name !== 'string' || raw.tool_name.length === 0) {\n return { intercepted: false, skipReason: 'No tool_name in payload' };\n }\n\n if (this.skipTools.has(raw.tool_name)) {\n return { intercepted: false, skipReason: `Tool ${raw.tool_name} is not governed` };\n }\n\n const isMcp = mapMcpTool(raw.tool_name) !== null;\n if (isMcp && !this.governMcp) {\n return { intercepted: false, skipReason: `MCP tools not governed (governMcp off)` };\n }\n\n const isMapped = raw.tool_name in CLAUDE_CODE_TOOL_MAP || isMcp;\n if (!isMapped && !this.strictUnmapped) {\n return {\n intercepted: false,\n skipReason: `Unmapped tool ${raw.tool_name} (strictUnmapped off)`,\n };\n }\n\n const context = this.extractContext(action);\n return { intercepted: true, context };\n }\n\n extractContext(action: AgentAction): PDLSSContext {\n const raw = action.raw as ClaudeCodePreToolUsePayload;\n const toolName = raw.tool_name;\n const args = (raw.tool_input ?? {}) as Record<string, unknown>;\n\n const mcpPurpose = mapMcpTool(toolName);\n const purpose = mcpPurpose ?? mapToolToPurpose(toolName);\n const target = extractTarget(toolName, args);\n const networkAccess = extractNetworkDomains(target);\n\n return {\n purpose,\n action: toolName,\n target,\n ...(networkAccess && { networkAccess }),\n metadata: {\n ...(raw.session_id && { sessionId: raw.session_id }),\n ...(raw.cwd && { cwd: raw.cwd }),\n platform: 'claude-code',\n },\n };\n }\n\n /**\n * Translate a gateway decision into hook stdout JSON.\n * Returns null on ALLOW — the hook stays silent and Claude Code's own\n * permission system remains in charge (strictly-tightening invariant).\n */\n decisionToHookOutput(decision: VerificationDecision): ClaudeHookOutput | null {\n switch (decision.recommendation) {\n case 'DENY':\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'deny',\n permissionDecisionReason: `AstraSync Local Guard: ${decision.reason}`,\n },\n };\n case 'MANUAL_REVIEW':\n return {\n hookSpecificOutput: {\n hookEventName: 'PreToolUse',\n permissionDecision: 'ask',\n permissionDecisionReason: `AstraSync Local Guard requires approval: ${decision.reason}`,\n },\n };\n case 'ALLOW':\n return null;\n }\n }\n\n /**\n * Convenience for the hook entry: full payload → hook output (or null).\n */\n async evaluatePayload(payload: ClaudeCodePreToolUsePayload): Promise<ClaudeHookOutput | null> {\n const action: AgentAction = {\n raw: payload,\n platform: 'claude-code',\n timestamp: new Date(),\n };\n\n const interception = await this.interceptAction(action);\n if (!interception.intercepted || !interception.context) return null;\n\n const decision = await this.gateway.evaluate(interception.context);\n return this.decisionToHookOutput(decision);\n }\n\n /**\n * Enforcement happens via the stdout JSON the hook prints (see\n * decisionToHookOutput); there is no in-process side to enforce on.\n */\n async enforceDecision(_decision: VerificationDecision): Promise<void> {\n // Contract no-op: Claude Code enforces based on the hook's stdout.\n }\n}\n"],"mappings":"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","names":[]}
|