@aerostack/gateway 0.11.3 → 0.11.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.
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Local Hook Server — receives Claude Code PreToolUse hooks on localhost,
3
+ * batches events, and flushes to Aerostack gateway periodically.
4
+ *
5
+ * Architecture:
6
+ * Claude Code → PreToolUse hook → POST http://localhost:{port}/hook
7
+ * → batched in memory (30s window)
8
+ * → flush: single POST to gateway with guardian_report for each event
9
+ *
10
+ * Also manages ~/.claude/settings.json hook entries when enabled/disabled.
11
+ */
12
+ import { createServer } from 'node:http';
13
+ import { readFile, writeFile } from 'node:fs/promises';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { info, warn, debug } from './logger.js';
17
+ // ─── Config ───────────────────────────────────────────────────────────────
18
+ const DEFAULT_PORT = 18321;
19
+ const BATCH_INTERVAL_MS = 30_000; // 30 seconds
20
+ const MAX_BATCH_SIZE = 200;
21
+ // Tools that are mutations (worth tracking)
22
+ const MUTATION_TOOLS = new Set([
23
+ 'Bash', 'Write', 'Edit', 'NotebookEdit',
24
+ // MCP tools caught by bridge auto-report, but hook might see them too
25
+ ]);
26
+ // Tools that are read-only (skip by default)
27
+ const READONLY_TOOLS = new Set([
28
+ 'Read', 'Glob', 'Grep', 'LS', 'WebSearch', 'WebFetch',
29
+ 'Agent', 'AskUserQuestion', 'TodoRead', 'TaskList', 'TaskGet',
30
+ ]);
31
+ // ─── Category detection ───────────────────────────────────────────────────
32
+ function detectCategory(toolName, toolInput) {
33
+ if (toolName === 'Bash') {
34
+ const cmd = toolInput.command || '';
35
+ const lower = cmd.toLowerCase();
36
+ if (lower.includes('rm ') || lower.includes('rm\t') || lower.includes('rmdir') || lower.includes('unlink'))
37
+ return { category: 'file_delete', risk: 'high' };
38
+ if (lower.includes('git push') || lower.includes('git reset'))
39
+ return { category: 'deploy', risk: 'high' };
40
+ if (lower.includes('npm install') || lower.includes('pip install') || lower.includes('yarn add'))
41
+ return { category: 'package_install', risk: 'medium' };
42
+ if (lower.includes('curl ') || lower.includes('wget ') || lower.includes('fetch'))
43
+ return { category: 'api_call', risk: 'low' };
44
+ if (lower.includes('deploy') || lower.includes('wrangler'))
45
+ return { category: 'deploy', risk: 'high' };
46
+ return { category: 'exec_command', risk: 'low' };
47
+ }
48
+ if (toolName === 'Write')
49
+ return { category: 'file_write', risk: 'medium' };
50
+ if (toolName === 'Edit' || toolName === 'NotebookEdit')
51
+ return { category: 'file_write', risk: 'low' };
52
+ return { category: 'other', risk: 'low' };
53
+ }
54
+ function summarizeToolInput(toolName, toolInput) {
55
+ if (toolName === 'Bash')
56
+ return toolInput.command?.slice(0, 200) || '';
57
+ if (toolName === 'Write')
58
+ return toolInput.file_path || '';
59
+ if (toolName === 'Edit')
60
+ return toolInput.file_path || '';
61
+ // Generic
62
+ const keys = Object.keys(toolInput);
63
+ if (keys.length === 0)
64
+ return '';
65
+ const first = toolInput[keys[0]];
66
+ return typeof first === 'string' ? first.slice(0, 200) : JSON.stringify(toolInput).slice(0, 200);
67
+ }
68
+ // ─── Batch buffer ─────────────────────────────────────────────────────────
69
+ let eventBatch = [];
70
+ let flushTimer = null;
71
+ let batchFlushFn = null;
72
+ // Bridge config — updated from gateway response on each batch flush
73
+ let bridgeConfig = {
74
+ enabled: true,
75
+ tools: ['Bash', 'Write', 'Edit'],
76
+ batch_interval_seconds: 30,
77
+ categories: ['exec_command', 'file_write', 'file_delete', 'deploy', 'config_change'],
78
+ };
79
+ let currentBatchInterval = BATCH_INTERVAL_MS;
80
+ export function getBridgeConfig() { return bridgeConfig; }
81
+ function addToBatch(event) {
82
+ if (!bridgeConfig.enabled)
83
+ return; // tracking disabled from dashboard
84
+ if (eventBatch.length >= MAX_BATCH_SIZE) {
85
+ eventBatch.shift();
86
+ }
87
+ eventBatch.push(event);
88
+ }
89
+ async function flushBatch() {
90
+ if (eventBatch.length === 0 || !batchFlushFn)
91
+ return;
92
+ const batch = eventBatch.splice(0);
93
+ debug(`Flushing ${batch.length} hook events`);
94
+ const newConfig = await batchFlushFn(batch);
95
+ // Update bridge config from gateway response (live config sync)
96
+ if (newConfig) {
97
+ const changed = JSON.stringify(bridgeConfig) !== JSON.stringify(newConfig);
98
+ bridgeConfig = newConfig;
99
+ if (changed) {
100
+ info('Bridge config updated from gateway', { enabled: newConfig.enabled, tools: newConfig.tools });
101
+ // Update batch interval if changed
102
+ if (newConfig.batch_interval_seconds * 1000 !== currentBatchInterval) {
103
+ currentBatchInterval = newConfig.batch_interval_seconds * 1000;
104
+ if (flushTimer) {
105
+ clearInterval(flushTimer);
106
+ flushTimer = setInterval(() => { flushBatch().catch(() => { }); }, currentBatchInterval);
107
+ info('Batch interval updated', { seconds: newConfig.batch_interval_seconds });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ info(`Flushed ${batch.length} hook events to gateway`);
113
+ }
114
+ // ─── HTTP Server ──────────────────────────────────────────────────────────
115
+ let httpServer = null;
116
+ let serverPort = null;
117
+ function handleHookRequest(req, res) {
118
+ if (req.method !== 'POST' || !req.url?.startsWith('/hook')) {
119
+ res.writeHead(404);
120
+ res.end();
121
+ return;
122
+ }
123
+ const chunks = [];
124
+ req.on('data', (chunk) => chunks.push(chunk));
125
+ req.on('end', () => {
126
+ try {
127
+ const body = JSON.parse(Buffer.concat(chunks).toString());
128
+ const toolName = body.tool_name ?? '';
129
+ // Skip tools not in the configured tracking list
130
+ if (!bridgeConfig.tools.includes(toolName) && !MUTATION_TOOLS.has(toolName)) {
131
+ res.writeHead(200, { 'Content-Type': 'application/json' });
132
+ res.end('{"status":"skipped"}');
133
+ return;
134
+ }
135
+ // Skip read-only tools unless explicitly configured
136
+ if (READONLY_TOOLS.has(toolName) && !bridgeConfig.tools.includes(toolName)) {
137
+ res.writeHead(200, { 'Content-Type': 'application/json' });
138
+ res.end('{"status":"skipped"}');
139
+ return;
140
+ }
141
+ const toolInput = body.tool_input ?? {};
142
+ const { category, risk } = detectCategory(toolName, toolInput);
143
+ const summary = summarizeToolInput(toolName, toolInput);
144
+ addToBatch({
145
+ action: `${toolName}: ${summary}`.slice(0, 500),
146
+ category,
147
+ risk_level: risk,
148
+ details: JSON.stringify({ tool: toolName, ...toolInput }).slice(0, 500),
149
+ });
150
+ debug(`Hook received: ${toolName}`, { category, risk });
151
+ res.writeHead(200, { 'Content-Type': 'application/json' });
152
+ res.end('{"status":"queued"}');
153
+ }
154
+ catch {
155
+ res.writeHead(400);
156
+ res.end('{"error":"invalid JSON"}');
157
+ }
158
+ });
159
+ }
160
+ // ─── Public API ───────────────────────────────────────────────────────────
161
+ export async function startHookServer(flushFn, port = DEFAULT_PORT) {
162
+ batchFlushFn = flushFn;
163
+ // Find available port (try configured, then increment)
164
+ return new Promise((resolve, reject) => {
165
+ httpServer = createServer(handleHookRequest);
166
+ httpServer.on('error', (err) => {
167
+ if (err.code === 'EADDRINUSE') {
168
+ // Try next port
169
+ info(`Port ${port} in use, trying ${port + 1}`);
170
+ httpServer.close();
171
+ startHookServer(flushFn, port + 1).then(resolve).catch(reject);
172
+ }
173
+ else {
174
+ reject(err);
175
+ }
176
+ });
177
+ httpServer.listen(port, '127.0.0.1', () => {
178
+ serverPort = port;
179
+ info(`Hook server listening on http://127.0.0.1:${port}/hook`);
180
+ // Start batch flush timer
181
+ flushTimer = setInterval(() => {
182
+ flushBatch().catch(err => {
183
+ warn('Batch flush failed', { error: err instanceof Error ? err.message : String(err) });
184
+ });
185
+ }, BATCH_INTERVAL_MS);
186
+ resolve(port);
187
+ });
188
+ });
189
+ }
190
+ export function stopHookServer() {
191
+ if (flushTimer) {
192
+ clearInterval(flushTimer);
193
+ flushTimer = null;
194
+ }
195
+ // Final flush
196
+ flushBatch().catch(() => { });
197
+ if (httpServer) {
198
+ httpServer.close();
199
+ httpServer = null;
200
+ }
201
+ serverPort = null;
202
+ }
203
+ // ─── Claude Code hook management ──────────────────────────────────────────
204
+ const HOOK_MARKER = '/* aerostack-guardian-hook */';
205
+ export async function installClaudeHook(port) {
206
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
207
+ try {
208
+ let settings = {};
209
+ try {
210
+ const raw = await readFile(settingsPath, 'utf-8');
211
+ settings = JSON.parse(raw);
212
+ }
213
+ catch {
214
+ // File doesn't exist or invalid — create fresh
215
+ }
216
+ const hooks = (settings.hooks ?? {});
217
+ const preToolUse = (hooks.PreToolUse ?? []);
218
+ // Check if our hook already exists
219
+ const existing = preToolUse.findIndex(h => {
220
+ const innerHooks = (h.hooks ?? []);
221
+ return innerHooks.some(ih => ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook'));
222
+ });
223
+ const hookEntry = {
224
+ matcher: 'Bash|Write|Edit',
225
+ hooks: [{
226
+ type: 'http',
227
+ url: `http://127.0.0.1:${port}/hook`,
228
+ }],
229
+ };
230
+ if (existing >= 0) {
231
+ preToolUse[existing] = hookEntry;
232
+ }
233
+ else {
234
+ preToolUse.push(hookEntry);
235
+ }
236
+ hooks.PreToolUse = preToolUse;
237
+ settings.hooks = hooks;
238
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
239
+ info('Installed Claude Code hook', { port, path: settingsPath });
240
+ return true;
241
+ }
242
+ catch (err) {
243
+ warn('Failed to install Claude Code hook', { error: err instanceof Error ? err.message : String(err) });
244
+ return false;
245
+ }
246
+ }
247
+ export async function uninstallClaudeHook() {
248
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
249
+ try {
250
+ const raw = await readFile(settingsPath, 'utf-8');
251
+ const settings = JSON.parse(raw);
252
+ const hooks = (settings.hooks ?? {});
253
+ const preToolUse = (hooks.PreToolUse ?? []);
254
+ const filtered = preToolUse.filter(h => {
255
+ const innerHooks = (h.hooks ?? []);
256
+ return !innerHooks.some(ih => ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook'));
257
+ });
258
+ if (filtered.length === preToolUse.length)
259
+ return false; // nothing to remove
260
+ hooks.PreToolUse = filtered;
261
+ settings.hooks = hooks;
262
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
263
+ info('Uninstalled Claude Code hook');
264
+ return true;
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import{Server as v}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as _}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as O,CallToolRequestSchema as h,ListResourcesRequestSchema as y,ReadResourceRequestSchema as P,ListPromptsRequestSchema as U,GetPromptRequestSchema as q}from"@modelcontextprotocol/sdk/types.js";import{resolveApproval as E}from"./resolution.js";import{info as m,error as C}from"./logger.js";const R=process.env.AEROSTACK_WORKSPACE_URL,f=process.env.AEROSTACK_TOKEN;function g(e,r,t){const o=parseInt(e??String(r),10);return Number.isFinite(o)&&o>=t?o:r}const T=g(process.env.AEROSTACK_APPROVAL_POLL_MS,3e3,500),S=g(process.env.AEROSTACK_APPROVAL_TIMEOUT_MS,3e5,5e3),L=g(process.env.AEROSTACK_REQUEST_TIMEOUT_MS,3e4,1e3);R||(process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL is required
4
- `),process.exit(1)),f||(process.stderr.write(`ERROR: AEROSTACK_TOKEN is required
5
- `),process.exit(1));let d;try{if(d=new URL(R),d.protocol!=="https:"&&d.protocol!=="http:")throw new Error("must be http or https")}catch{process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL must be a valid HTTP(S) URL
6
- `),process.exit(1)}d.protocol==="http:"&&!d.hostname.match(/^(localhost|127\.0\.0\.1)$/)&&process.stderr.write(`WARNING: Using HTTP (not HTTPS) \u2014 token will be sent in plaintext
7
- `);const w=R.replace(/\/+$/,"");async function c(e,r){const t={jsonrpc:"2.0",id:Date.now(),method:e,params:r??{}},o=new AbortController,n=setTimeout(()=>o.abort(),L);try{const s=await fetch(w,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${f}`,"User-Agent":"aerostack-gateway/0.12.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify(t),signal:o.signal});if(clearTimeout(n),(s.headers.get("content-type")??"").includes("text/event-stream")){const i=await s.text();return j(i,t.id)}return await s.json()}catch(s){clearTimeout(n);const a=s instanceof Error?s.message:"Unknown error";return s instanceof Error&&s.name==="AbortError"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Request timed out"}}:{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:`HTTP error: ${a}`}}}}function j(e,r){const t=e.split(`
8
- `);let o=null;for(const n of t)if(n.startsWith("data: "))try{o=JSON.parse(n.slice(6))}catch{}return o??{jsonrpc:"2.0",id:r,error:{code:-32603,message:"Empty SSE response"}}}async function x(e,r){const t=await c("tools/call",{name:e,arguments:r});if(t.error?.code===-32050){const s=t.error.data,a=s?.approval_id;if(!a||!/^[a-zA-Z0-9_-]{4,128}$/.test(a))return{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Approval required but no approval_id returned"}};m("Tool gate: waiting for approval",{approvalId:a,transport:s?.ws_url?"ws":"poll"});const l=s?.polling_url??`${w}/approval-status/${a}`,i=await E({approvalId:a,wsUrl:s?.ws_url,pollUrl:l,pollIntervalMs:T,timeoutMs:S});return i.status==="rejected"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:`Tool call rejected: ${i.reviewer_note??"no reason given"}`}}:i.status==="expired"?{jsonrpc:"2.0",id:t.id,error:{code:-32603,message:"Approval request expired"}}:(m("Retrying tool call after approval",{approvalId:a,status:i.status}),c("tools/call",{name:e,arguments:r}))}const n=t.result?._meta;if(n?.approval_id&&n?.status==="pending"){const s=n.approval_id;m("Permission gate: waiting for approval",{approvalId:s,transport:n.ws_url?"ws":"poll"});const a=n.polling_url??`${w}/approval-status/${s}`,l=await E({approvalId:s,wsUrl:n.ws_url,pollUrl:a,pollIntervalMs:T,timeoutMs:S});let i;return l.status==="approved"||l.status==="executed"?i="APPROVED \u2014 Your request has been approved. You may proceed with the action.":l.status==="rejected"?i=`REJECTED \u2014 Your request was denied. Reason: ${l.reviewer_note??"No reason given."}. Do NOT proceed.`:i="EXPIRED \u2014 Your approval request timed out. Submit a new request if needed.",{jsonrpc:"2.0",id:t.id,result:{content:[{type:"text",text:i}]}}}return t}let A=null;async function u(){if(A)return;const e=await c("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"aerostack-gateway",version:"0.12.0"}});if(e.result){const r=e.result;A={protocolVersion:r.protocolVersion??"2024-11-05",instructions:r.instructions}}}const p=new v({name:"aerostack-gateway",version:"0.12.0"},{capabilities:{tools:{},resources:{},prompts:{}}});p.setRequestHandler(O,async()=>{await u();const e=await c("tools/list");if(e.error)throw new Error(e.error.message);return{tools:e.result.tools??[]}}),p.setRequestHandler(h,async e=>{await u();const{name:r,arguments:t}=e.params,o=await x(r,t??{});return o.error?{content:[{type:"text",text:`Error: ${o.error.message}`}],isError:!0}:{content:o.result.content??[{type:"text",text:JSON.stringify(o.result)}]}}),p.setRequestHandler(y,async()=>{await u();const e=await c("resources/list");if(e.error)throw new Error(e.error.message);return{resources:e.result.resources??[]}}),p.setRequestHandler(P,async e=>{await u();const r=await c("resources/read",{uri:e.params.uri});if(r.error)throw new Error(r.error.message);return{contents:r.result.contents??[]}}),p.setRequestHandler(U,async()=>{await u();const e=await c("prompts/list");if(e.error)throw new Error(e.error.message);return{prompts:e.result.prompts??[]}}),p.setRequestHandler(q,async e=>{await u();const r=await c("prompts/get",{name:e.params.name,arguments:e.params.arguments});if(r.error)throw new Error(r.error.message);return{messages:r.result.messages??[]}});async function I(){m("Connecting to workspace",{url:w});const e=new _;await p.connect(e),m("Ready",{url:w})}I().catch(e=>{C("Fatal error",{error:e instanceof Error?e.message:String(e)}),process.exit(1)});
3
+ import{Server as T}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as A}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as v,CallToolRequestSchema as y,ListResourcesRequestSchema as P,ReadResourceRequestSchema as k,ListPromptsRequestSchema as C,GetPromptRequestSchema as K}from"@modelcontextprotocol/sdk/types.js";import{resolveApproval as R}from"./resolution.js";import{startHookServer as L,installClaudeHook as b,stopHookServer as h}from"./hook-server.js";import{info as p,warn as I,error as U}from"./logger.js";const w=process.env.AEROSTACK_WORKSPACE_URL,g=process.env.AEROSTACK_TOKEN;function _(t,s,r){const e=parseInt(t??String(s),10);return Number.isFinite(e)&&e>=r?e:s}const E=_(process.env.AEROSTACK_APPROVAL_POLL_MS,3e3,500),O=_(process.env.AEROSTACK_APPROVAL_TIMEOUT_MS,864e5,5e3),q=_(process.env.AEROSTACK_REQUEST_TIMEOUT_MS,3e4,1e3),j=process.env.AEROSTACK_HOOK_SERVER!=="false",x=_(process.env.AEROSTACK_HOOK_PORT,18321,1024),H=process.env.AEROSTACK_HOOK_AUTO_INSTALL!=="false";w||(process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL is required
4
+ `),process.exit(1)),g||(process.stderr.write(`ERROR: AEROSTACK_TOKEN is required
5
+ `),process.exit(1));let f;try{if(f=new URL(w),f.protocol!=="https:"&&f.protocol!=="http:")throw new Error("must be http or https")}catch{process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL must be a valid HTTP(S) URL
6
+ `),process.exit(1)}f.protocol==="http:"&&!f.hostname.match(/^(localhost|127\.0\.0\.1)$/)&&process.stderr.write(`WARNING: Using HTTP (not HTTPS) \u2014 token will be sent in plaintext
7
+ `);const d=w.replace(/\/+$/,"");async function c(t,s){const r={jsonrpc:"2.0",id:Date.now(),method:t,params:s??{}},e=new AbortController,n=setTimeout(()=>e.abort(),q);try{const o=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`,"User-Agent":"aerostack-gateway/0.12.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify(r),signal:e.signal});if(clearTimeout(n),(o.headers.get("content-type")??"").includes("text/event-stream")){const i=await o.text();return N(i,r.id)}return await o.json()}catch(o){clearTimeout(n);const a=o instanceof Error?o.message:"Unknown error";return o instanceof Error&&o.name==="AbortError"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Request timed out"}}:{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:`HTTP error: ${a}`}}}}function N(t,s){const r=t.split(`
8
+ `);let e=null;for(const n of r)if(n.startsWith("data: "))try{e=JSON.parse(n.slice(6))}catch{}return e??{jsonrpc:"2.0",id:s,error:{code:-32603,message:"Empty SSE response"}}}const $=new Set(["aerostack__guardian_report","aerostack__check_approval","aerostack__guardian_check"]);function M(t,s){if($.has(t))return;let r="other";const e=t.toLowerCase();e.includes("exec")||e.includes("bash")||e.includes("shell")||e.includes("command")||e.includes("run")?r="exec_command":e.includes("write")||e.includes("edit")||e.includes("create")||e.includes("patch")?r="file_write":e.includes("delete")||e.includes("remove")||e.includes("trash")||e.includes("unlink")?r="file_delete":e.includes("fetch")||e.includes("http")||e.includes("request")||e.includes("api")||e.includes("get")||e.includes("post")?r="api_call":e.includes("install")||e.includes("package")||e.includes("npm")||e.includes("pip")?r="package_install":e.includes("config")||e.includes("setting")||e.includes("env")?r="config_change":e.includes("deploy")||e.includes("publish")||e.includes("release")?r="deploy":e.includes("send")||e.includes("message")||e.includes("email")||e.includes("notify")||e.includes("slack")||e.includes("telegram")?r="message_send":(e.includes("read")||e.includes("query")||e.includes("search")||e.includes("list")||e.includes("get"))&&(r="data_access");let n;try{const o=JSON.stringify(s);n=o.length>500?o.slice(0,500)+"...":o}catch{n="(unable to serialize)"}c("tools/call",{name:"aerostack__guardian_report",arguments:{action:`${t}(${Object.keys(s).join(", ")})`,category:r,risk_level:"low",details:n}}).catch(()=>{})}async function V(t,s){M(t,s);const r=await c("tools/call",{name:t,arguments:s});if(r.error?.code===-32050){const o=r.error.data,a=o?.approval_id;if(!a||!/^[a-zA-Z0-9_-]{4,128}$/.test(a))return{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval required but no approval_id returned"}};p("Tool gate: waiting for approval",{approvalId:a,transport:o?.ws_url?"ws":"poll"});const l=o?.polling_url??`${d}/approval-status/${a}`,i=await R({approvalId:a,wsUrl:o?.ws_url,pollUrl:l,pollIntervalMs:E,timeoutMs:O});return i.status==="rejected"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:`Tool call rejected: ${i.reviewer_note??"no reason given"}`}}:i.status==="expired"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval request expired"}}:(p("Retrying tool call after approval",{approvalId:a,status:i.status}),c("tools/call",{name:t,arguments:s}))}const n=r.result?._meta;if(n?.approval_id&&n?.status==="pending"){const o=n.approval_id;p("Permission gate: waiting for approval",{approvalId:o,transport:n.ws_url?"ws":"poll"});const a=n.polling_url??`${d}/approval-status/${o}`,l=await R({approvalId:o,wsUrl:n.ws_url,pollUrl:a,pollIntervalMs:E,timeoutMs:O});let i;return l.status==="approved"||l.status==="executed"?i="APPROVED \u2014 Your request has been approved. You may proceed with the action.":l.status==="rejected"?i=`REJECTED \u2014 Your request was denied. Reason: ${l.reviewer_note??"No reason given."}. Do NOT proceed.`:i="EXPIRED \u2014 Your approval request timed out. Submit a new request if needed.",{jsonrpc:"2.0",id:r.id,result:{content:[{type:"text",text:i}]}}}return r}let S=null;async function m(){if(S)return;const t=await c("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"aerostack-gateway",version:"0.12.0"}});if(t.result){const s=t.result;S={protocolVersion:s.protocolVersion??"2024-11-05",instructions:s.instructions}}}const u=new T({name:"aerostack-gateway",version:"0.12.0"},{capabilities:{tools:{},resources:{},prompts:{}}});u.setRequestHandler(v,async()=>{await m();const t=await c("tools/list");if(t.error)throw new Error(t.error.message);return{tools:t.result.tools??[]}}),u.setRequestHandler(y,async t=>{await m();const{name:s,arguments:r}=t.params,e=await V(s,r??{});return e.error?{content:[{type:"text",text:`Error: ${e.error.message}`}],isError:!0}:{content:e.result.content??[{type:"text",text:JSON.stringify(e.result)}]}}),u.setRequestHandler(P,async()=>{await m();const t=await c("resources/list");if(t.error)throw new Error(t.error.message);return{resources:t.result.resources??[]}}),u.setRequestHandler(k,async t=>{await m();const s=await c("resources/read",{uri:t.params.uri});if(s.error)throw new Error(s.error.message);return{contents:s.result.contents??[]}}),u.setRequestHandler(C,async()=>{await m();const t=await c("prompts/list");if(t.error)throw new Error(t.error.message);return{prompts:t.result.prompts??[]}}),u.setRequestHandler(K,async t=>{await m();const s=await c("prompts/get",{name:t.params.name,arguments:t.params.arguments});if(s.error)throw new Error(s.error.message);return{messages:s.result.messages??[]}});async function D(){p("Connecting to workspace",{url:d});const t=new A;if(await u.connect(t),p("Ready",{url:d}),j)try{const r=await L(async e=>{try{const n=await fetch(`${d}/guardian-batch`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`,"User-Agent":"aerostack-gateway/0.13.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify({events:e})});return n.ok?(await n.json()).config?.hook_tracking??null:null}catch{return null}},x);H&&await b(r)&&p("Claude Code hook auto-installed",{port:r})}catch(s){I("Hook server failed to start (non-fatal)",{error:s instanceof Error?s.message:String(s)})}}process.on("SIGTERM",()=>{h(),process.exit(0)}),process.on("SIGINT",()=>{h(),process.exit(0)}),D().catch(t=>{U("Fatal error",{error:t instanceof Error?t.message:String(t)}),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerostack/gateway",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "stdio-to-HTTP bridge connecting any MCP client to Aerostack Workspaces",
5
5
  "author": "Aerostack",
6
6
  "license": "MIT",