@aerostack/gateway 0.13.2 → 0.14.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,134 @@
1
+ /**
2
+ * ApprovalStore — In-memory store of pending and resolved approval requests.
3
+ *
4
+ * Decouples approval resolution from the blocking tool call. The bridge stores
5
+ * approvals locally and delivers results when the LLM next interacts.
6
+ *
7
+ * - Max 500 entries (oldest evicted on overflow)
8
+ * - Auto-eviction after 2 hours
9
+ * - Each entry can hold a cancel() handle to close its background WebSocket
10
+ */
11
+ const MAX_ENTRIES = 500;
12
+ const EVICTION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
13
+ const CLEANUP_INTERVAL_MS = 60_000; // check every 60s
14
+ export class ApprovalStore {
15
+ store = new Map();
16
+ cancelHandles = new Map();
17
+ cleanupTimer = null;
18
+ constructor() {
19
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
20
+ }
21
+ /** Store or update an approval entry. */
22
+ set(approvalId, data) {
23
+ // Evict oldest if at capacity
24
+ if (!this.store.has(approvalId) && this.store.size >= MAX_ENTRIES) {
25
+ const oldest = this.store.keys().next().value;
26
+ if (oldest)
27
+ this.evict(oldest);
28
+ }
29
+ this.store.set(approvalId, {
30
+ ...data,
31
+ delivered: data.delivered ?? false,
32
+ });
33
+ }
34
+ /** Get a single approval by ID. */
35
+ get(approvalId) {
36
+ return this.store.get(approvalId);
37
+ }
38
+ /** Get all resolved approvals that have NOT been delivered to the LLM yet. */
39
+ getResolved() {
40
+ const resolved = [];
41
+ for (const entry of this.store.values()) {
42
+ if (!entry.delivered && entry.status !== 'pending') {
43
+ resolved.push(entry);
44
+ }
45
+ }
46
+ return resolved;
47
+ }
48
+ /** Mark an approval as delivered (LLM received the result). */
49
+ markDelivered(approvalId) {
50
+ const entry = this.store.get(approvalId);
51
+ if (entry) {
52
+ entry.delivered = true;
53
+ }
54
+ }
55
+ /** Get all pending approvals (still waiting for resolution). */
56
+ getPending() {
57
+ const pending = [];
58
+ for (const entry of this.store.values()) {
59
+ if (entry.status === 'pending') {
60
+ pending.push(entry);
61
+ }
62
+ }
63
+ return pending;
64
+ }
65
+ /** Register a cancel function for a background resolver. */
66
+ setCancelHandle(approvalId, cancel) {
67
+ this.cancelHandles.set(approvalId, cancel);
68
+ }
69
+ /** Update status when approval resolves (called by background resolver). */
70
+ resolve(approvalId, status, reviewerNote, result) {
71
+ const entry = this.store.get(approvalId);
72
+ if (!entry)
73
+ return;
74
+ entry.status = status;
75
+ entry.resolvedAt = Date.now();
76
+ entry.reviewerNote = reviewerNote;
77
+ if (result !== undefined)
78
+ entry.result = result;
79
+ }
80
+ /** Number of entries in the store. */
81
+ get size() {
82
+ return this.store.size;
83
+ }
84
+ /** Get all approval IDs (for shutdown reporting). */
85
+ allIds() {
86
+ return Array.from(this.store.keys());
87
+ }
88
+ /** Get undelivered approval IDs (for shutdown reporting). */
89
+ getUndeliveredIds() {
90
+ const ids = [];
91
+ for (const [id, entry] of this.store) {
92
+ if (!entry.delivered && entry.status !== 'pending') {
93
+ ids.push(id);
94
+ }
95
+ }
96
+ return ids;
97
+ }
98
+ /** Clean up old entries and their background resolvers. */
99
+ cleanup() {
100
+ const now = Date.now();
101
+ for (const [id, entry] of this.store) {
102
+ if (now - entry.createdAt > EVICTION_TTL_MS) {
103
+ this.evict(id);
104
+ }
105
+ }
106
+ }
107
+ /** Evict a single entry, cancelling its background resolver if any. */
108
+ evict(approvalId) {
109
+ const cancel = this.cancelHandles.get(approvalId);
110
+ if (cancel) {
111
+ try {
112
+ cancel();
113
+ }
114
+ catch { /* ignore */ }
115
+ this.cancelHandles.delete(approvalId);
116
+ }
117
+ this.store.delete(approvalId);
118
+ }
119
+ /** Stop cleanup timer and cancel all background resolvers. */
120
+ destroy() {
121
+ if (this.cleanupTimer) {
122
+ clearInterval(this.cleanupTimer);
123
+ this.cleanupTimer = null;
124
+ }
125
+ for (const cancel of this.cancelHandles.values()) {
126
+ try {
127
+ cancel();
128
+ }
129
+ catch { /* ignore */ }
130
+ }
131
+ this.cancelHandles.clear();
132
+ this.store.clear();
133
+ }
134
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
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)});
3
+ import{Server as $}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as L}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as N,CallToolRequestSchema as b,ListResourcesRequestSchema as q,ReadResourceRequestSchema as K,ListPromptsRequestSchema as j,GetPromptRequestSchema as x}from"@modelcontextprotocol/sdk/types.js";import{resolveApproval as O,startBackgroundResolver as y}from"./resolution.js";import{ApprovalStore as D}from"./approval-store.js";import{startHookServer as H,installClaudeHook as M,stopHookServer as V}from"./hook-server.js";import{info as p,warn as z,error as B}from"./logger.js";const T=process.env.AEROSTACK_WORKSPACE_URL,f=process.env.AEROSTACK_TOKEN;function v(t,s,r){const e=parseInt(t??String(s),10);return Number.isFinite(e)&&e>=r?e:s}const m=v(process.env.AEROSTACK_APPROVAL_POLL_MS,3e3,500),k=v(process.env.AEROSTACK_APPROVAL_TIMEOUT_MS,864e5,5e3),G=v(process.env.AEROSTACK_REQUEST_TIMEOUT_MS,3e4,1e3),w=process.env.AEROSTACK_APPROVAL_MODE==="async"?"async":"blocking",J=process.env.AEROSTACK_HOOK_SERVER!=="false",W=v(process.env.AEROSTACK_HOOK_PORT,18321,1024),Y=process.env.AEROSTACK_HOOK_AUTO_INSTALL!=="false";T||(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 h;try{if(h=new URL(T),h.protocol!=="https:"&&h.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)}h.protocol==="http:"&&!h.hostname.match(/^(localhost|127\.0\.0\.1)$/)&&process.stderr.write(`WARNING: Using HTTP (not HTTPS) \u2014 token will be sent in plaintext
7
+ `);const c=T.replace(/\/+$/,""),S=crypto.randomUUID(),X=process.env.AEROSTACK_AGENT_TYPE||"unknown",l=new D;async function d(t,s){const r={jsonrpc:"2.0",id:Date.now(),method:t,params:s??{}},e=new AbortController,a=setTimeout(()=>e.abort(),G);try{const o=await fetch(c,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${f}`,"User-Agent":"aerostack-gateway/0.14.0","X-Agent-Id":"aerostack-gateway","X-Bridge-Id":S,"X-Agent-Type":X},body:JSON.stringify(r),signal:e.signal});if(clearTimeout(a),(o.headers.get("content-type")??"").includes("text/event-stream")){const i=await o.text();return F(i,r.id)}return await o.json()}catch(o){clearTimeout(a);const n=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: ${n}`}}}}function F(t,s){const r=t.split(`
8
+ `);let e=null;for(const a of r)if(a.startsWith("data: "))try{e=JSON.parse(a.slice(6))}catch{}return e??{jsonrpc:"2.0",id:s,error:{code:-32603,message:"Empty SSE response"}}}const Q=new Set(["aerostack__guardian_report","aerostack__check_approval","aerostack__guardian_check"]);function Z(t,s){if(Q.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 a;try{const o=JSON.stringify(s);a=o.length>500?o.slice(0,500)+"...":o}catch{a="(unable to serialize)"}d("tools/call",{name:"aerostack__guardian_report",arguments:{action:`${t}(${Object.keys(s).join(", ")})`,category:r,risk_level:"low",details:a}}).catch(()=>{})}function P(t,s){return fetch(`${c}/approval-delivery-status`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${f}`,"X-Bridge-Id":S},body:JSON.stringify({approvals:t.map(r=>({id:r,delivery_status:s,delivery_channel:"bridge_check_approval"}))})}).then(()=>{}).catch(()=>{})}const ee=/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,63}$/;function I(t){return t&&ee.test(t)?t:"aerostack__check_approval"}async function te(t,s){if(Z(t,s),w==="async"&&t==="aerostack__check_approval"){const o=s.approval_id;if(o){const n=l.get(o);if(n&&n.status!=="pending")return l.markDelivered(o),P([o],"delivered"),{jsonrpc:"2.0",id:Date.now(),result:{content:[{type:"text",text:JSON.stringify({approval_id:o,status:n.status,reviewer_note:n.reviewerNote??null})}]}}}}const r=await d("tools/call",{name:t,arguments:s});if(r.error?.code===-32050){const o=r.error.data,n=o?.approval_id;if(!n||!/^[a-zA-Z0-9_-]{4,128}$/.test(n))return{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval required but no approval_id returned"}};if(w==="async"){p("Tool gate (async): returning pending to LLM",{approvalId:n}),l.set(n,{approvalId:n,toolName:t,toolArgs:s,status:"pending",createdAt:Date.now()});const R=o?.polling_url??`${c}/approval-status/${n}`,A=y({approvalId:n,wsUrl:o?.ws_url,pollUrl:R,pollIntervalMs:m},l);l.setCancelHandle(n,A.cancel);const E=I(o?.check_tool);return{jsonrpc:"2.0",id:r.id,result:{content:[{type:"text",text:`APPROVAL REQUIRED \u2014 This action needs human approval.
9
+ Approval ID: ${n}
10
+ Status: pending
11
+
12
+ The workspace owner has been notified. To check approval status, call:
13
+ ${E}({ "approval_id": "${n}" })
14
+
15
+ When status is "executed", retry the original tool call to get the result.
16
+ Do NOT proceed with the action until approved.`}]}}}p("Tool gate: waiting for approval",{approvalId:n,transport:o?.ws_url?"ws":"poll"});const u=o?.polling_url??`${c}/approval-status/${n}`,i=await O({approvalId:n,wsUrl:o?.ws_url,pollUrl:u,pollIntervalMs:m,timeoutMs:k});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==="changes_requested"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:`Changes requested: ${i.reviewer_note??"no details given"}. Revise and resubmit.`}}:i.status==="expired"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval request expired"}}:(p("Retrying tool call after approval",{approvalId:n,status:i.status}),d("tools/call",{name:t,arguments:s}))}const a=r.result?._meta;if(a?.approval_id&&a?.status==="pending"){const o=a.approval_id;if(!/^[a-zA-Z0-9_-]{4,128}$/.test(o))return r;if(w==="async"){p("Permission gate (async): returning pending to LLM",{approvalId:o}),l.set(o,{approvalId:o,toolName:t,toolArgs:s,status:"pending",createdAt:Date.now()});const R=a.polling_url??`${c}/approval-status/${o}`,A=y({approvalId:o,wsUrl:a.ws_url,pollUrl:R,pollIntervalMs:m},l);l.setCancelHandle(o,A.cancel);const E=I(a.check_tool);return{jsonrpc:"2.0",id:r.id,result:{content:[{type:"text",text:`PERMISSION PENDING \u2014 Your request requires human approval.
17
+ Approval ID: ${o}
18
+ Status: pending
19
+
20
+ Call ${E}({ "approval_id": "${o}" }) to check status.
21
+ You MUST NOT proceed with this action until approved.`}]}}}p("Permission gate: waiting for approval",{approvalId:o,transport:a.ws_url?"ws":"poll"});const n=a.polling_url??`${c}/approval-status/${o}`,u=await O({approvalId:o,wsUrl:a.ws_url,pollUrl:n,pollIntervalMs:m,timeoutMs:k});let i;return u.status==="approved"||u.status==="executed"?i="APPROVED \u2014 Your request has been approved. You may proceed with the action.":u.status==="rejected"?i=`REJECTED \u2014 Your request was denied. Reason: ${u.reviewer_note??"No reason given."}. Do NOT proceed.`:u.status==="changes_requested"?i=`CHANGES REQUESTED \u2014 ${u.reviewer_note??"No details given."}. Revise and resubmit your request.`: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 C=null;async function g(){if(C)return;const t=await d("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"aerostack-gateway",version:"0.14.0"}});if(t.result){const s=t.result;C={protocolVersion:s.protocolVersion??"2024-11-05",instructions:s.instructions}}}const _=new $({name:"aerostack-gateway",version:"0.14.0"},{capabilities:{tools:{},resources:{},prompts:{}}});_.setRequestHandler(N,async()=>{await g();const t=await d("tools/list");if(t.error)throw new Error(t.error.message);return{tools:t.result.tools??[]}}),_.setRequestHandler(b,async t=>{await g();const{name:s,arguments:r}=t.params,e=await te(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)}]}}),_.setRequestHandler(q,async()=>{await g();const t=await d("resources/list");if(t.error)throw new Error(t.error.message);return{resources:t.result.resources??[]}}),_.setRequestHandler(K,async t=>{await g();const s=await d("resources/read",{uri:t.params.uri});if(s.error)throw new Error(s.error.message);return{contents:s.result.contents??[]}}),_.setRequestHandler(j,async()=>{await g();const t=await d("prompts/list");if(t.error)throw new Error(t.error.message);return{prompts:t.result.prompts??[]}}),_.setRequestHandler(x,async t=>{await g();const s=await d("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 re(){try{const t=await fetch(`${c}/undelivered-approvals`,{headers:{Authorization:`Bearer ${f}`,"X-Bridge-Id":S}});if(!t.ok)return;const s=await t.json();if(!s.approvals?.length)return;for(const r of s.approvals)l.set(r.id,{approvalId:r.id,toolName:r.tool_name,toolArgs:{},status:r.status,reviewerNote:r.reviewer_note??void 0,resolvedAt:r.resolved_at??void 0,createdAt:r.resolved_at??Date.now()});p(`Loaded ${s.approvals.length} undelivered approvals from server`)}catch{}}async function se(){p("Connecting to workspace",{url:c});const t=new L;if(await _.connect(t),p("Ready",{url:c}),w==="async"&&re().catch(()=>{}),J)try{const r=await H(async e=>{try{const a=await fetch(`${c}/guardian-batch`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${f}`,"User-Agent":"aerostack-gateway/0.14.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify({events:e})});return a.ok?(await a.json()).config?.hook_tracking??null:null}catch{return null}},W);Y&&await M(r)&&p("Claude Code hook auto-installed",{port:r})}catch(s){z("Hook server failed to start (non-fatal)",{error:s instanceof Error?s.message:String(s)})}}async function U(){const t=l.getUndeliveredIds();t.length>0&&await P(t,"agent_disconnected"),l.destroy(),V(),process.exit(0)}process.on("SIGTERM",()=>{U()}),process.on("SIGINT",()=>{U()}),se().catch(t=>{B("Fatal error",{error:t instanceof Error?t.message:String(t)}),process.exit(1)});
@@ -1 +1 @@
1
- import{info as u,warn as g,debug as b}from"./logger.js";async function j(e){const{approvalId:r,wsUrl:n,pollUrl:o,pollIntervalMs:l,timeoutMs:k}=e,d=Date.now()+k;if(n)try{return await A(r,n,o,d)}catch(c){const f=c instanceof Error?c.message:"Unknown WS error";g("WebSocket resolution failed, falling back to polling",{approvalId:r,error:f})}return O(r,o,l,d)}async function A(e,r,n,o){const l=await E();return new Promise((k,d)=>{let c=!1,f=null,m=null;const y=()=>{c=!0,f&&clearInterval(f),m&&clearTimeout(m)},w=t=>{c||(y(),k(t))},p=t=>{c||(y(),d(t))};b("Connecting WebSocket",{approvalId:e,wsUrl:r});const a=new l(r);a.onopen=()=>{u("WebSocket connected, waiting for resolution",{approvalId:e})},a.onmessage=t=>{try{const i=typeof t.data=="string"?JSON.parse(t.data):JSON.parse(String(t.data)),s=i?.status;if(b("WebSocket message received",{approvalId:e,status:s}),s==="executed"||s==="approved"||s==="rejected"||s==="expired"){u("Approval resolved via WebSocket",{approvalId:e,status:s}),w({status:s,reviewer_note:i?.reviewer_note});try{a.close(1e3)}catch{}}}catch{g("Failed to parse WebSocket message",{approvalId:e})}},a.onerror=t=>{const i=t instanceof Error?t.message:"WebSocket error";b("WebSocket error",{approvalId:e,error:i}),p(new Error(`WebSocket error: ${i}`))},a.onclose=t=>{c||(b("WebSocket closed without resolution",{approvalId:e,code:t.code}),p(new Error(`WebSocket closed unexpectedly (code ${t.code})`)))},typeof a.on=="function"&&a.on("unexpected-response",async(t,i)=>{try{const s=[];for await(const x of i)s.push(x);const _=Buffer.concat(s).toString(),v=JSON.parse(_),S=v?.status;if(S&&S!=="pending"){u("Approval already resolved (WS endpoint returned JSON)",{approvalId:e,status:S}),w({status:S,reviewer_note:v?.reviewer_note});return}}catch{}p(new Error("WebSocket upgrade rejected by server"))}),f=setInterval(async()=>{if(!c)try{const t=await h(n);if(t){u("Approval resolved via safety-net poll",{approvalId:e,status:t.status}),w(t);try{a.close(1e3)}catch{}}}catch{}},3e4);const W=o-Date.now();if(W<=0){p(new Error("Approval timeout"));return}m=setTimeout(()=>{if(!c){g("Approval timed out",{approvalId:e}),w({status:"expired"});try{a.close(1e3)}catch{}}},W)})}async function O(e,r,n,o){for(u("Polling for approval resolution",{approvalId:e,intervalMs:n});Date.now()<o;){await P(n);try{const l=await h(r);if(l)return u("Approval resolved via polling",{approvalId:e,status:l.status}),l}catch{}}return g("Approval polling timed out",{approvalId:e}),{status:"expired"}}async function h(e){const r=await fetch(e,{headers:{"User-Agent":"aerostack-gateway/0.12.0"}});if(!r.ok)return null;const n=await r.json(),o=n.status;return o==="executed"||o==="approved"?{status:o,reviewer_note:n.reviewer_note}:o==="rejected"?{status:"rejected",reviewer_note:n.reviewer_note}:o==="expired"?{status:"expired"}:null}async function E(){if(typeof globalThis.WebSocket<"u")return globalThis.WebSocket;try{return(await import("ws")).default}catch{throw new Error('WebSocket not available. Install the "ws" package: npm install ws')}}function P(e){return new Promise(r=>setTimeout(r,e))}export{j as resolveApproval};
1
+ import{info as v,warn as b,debug as y}from"./logger.js";function J(e,n){const{approvalId:r}=e;let o=!1,a=null,g=null,w=null;const c=()=>{if(o=!0,g&&(clearInterval(g),g=null),w&&(clearTimeout(w),w=null),a){try{a.close(1e3)}catch{}a=null}},p=S=>{o||(n.resolve(r,S.status,S.reviewer_note),v("Background resolver: approval resolved",{approvalId:r,status:S.status}),c())};return(async()=>{if(e.wsUrl)try{const u=await x(),d=new u(e.wsUrl);a=d,d.onmessage=s=>{try{const h=typeof s.data=="string"?JSON.parse(s.data):JSON.parse(String(s.data)),f=h?.status;(f==="executed"||f==="approved"||f==="rejected"||f==="expired"||f==="changes_requested")&&p({status:f,reviewer_note:h?.reviewer_note})}catch{}},d.onerror=()=>{y("Background resolver WS error",{approvalId:r})},d.onclose=()=>{a=null},typeof d.on=="function"&&d.on("unexpected-response",async(s,h)=>{try{const f=[];for await(const l of h)f.push(l);const t=Buffer.concat(f).toString(),i=JSON.parse(t);i?.status&&i.status!=="pending"&&p({status:i.status,reviewer_note:i?.reviewer_note})}catch{}})}catch{y("Background resolver WS connect failed, using polling only",{approvalId:r})}g=setInterval(async()=>{if(!o)try{const u=await _(e.pollUrl);u&&p(u)}catch{}},3e4);try{const u=await _(e.pollUrl);if(u){p(u);return}}catch{}const m=e.timeoutMs??2*60*60*1e3;w=setTimeout(()=>{o||(n.resolve(r,"expired"),c())},m)})().catch(()=>{}),{cancel:c}}async function N(e){const{approvalId:n,wsUrl:r,pollUrl:o,pollIntervalMs:a,timeoutMs:g}=e,w=Date.now()+g;if(r)try{return await L(n,r,o,w)}catch(c){const p=c instanceof Error?c.message:"Unknown WS error";b("WebSocket resolution failed, falling back to polling",{approvalId:n,error:p})}return A(n,o,a,w)}async function L(e,n,r,o){const a=await x();return new Promise((g,w)=>{let c=!1,p=null,S=null;const m=()=>{c=!0,p&&clearInterval(p),S&&clearTimeout(S)},u=t=>{c||(m(),g(t))},d=t=>{c||(m(),w(t))};y("Connecting WebSocket",{approvalId:e,wsUrl:n});const s=new a(n);s.onopen=()=>{v("WebSocket connected, waiting for resolution",{approvalId:e})},s.onmessage=t=>{try{const i=typeof t.data=="string"?JSON.parse(t.data):JSON.parse(String(t.data)),l=i?.status;if(y("WebSocket message received",{approvalId:e,status:l}),l==="executed"||l==="approved"||l==="rejected"||l==="expired"||l==="changes_requested"){v("Approval resolved via WebSocket",{approvalId:e,status:l}),u({status:l,reviewer_note:i?.reviewer_note});try{s.close(1e3)}catch{}}}catch{b("Failed to parse WebSocket message",{approvalId:e})}},s.onerror=t=>{const i=t instanceof Error?t.message:"WebSocket error";y("WebSocket error",{approvalId:e,error:i}),d(new Error(`WebSocket error: ${i}`))},s.onclose=t=>{c||(y("WebSocket closed without resolution",{approvalId:e,code:t.code}),d(new Error(`WebSocket closed unexpectedly (code ${t.code})`)))},typeof s.on=="function"&&s.on("unexpected-response",async(t,i)=>{try{const l=[];for await(const T of i)l.push(T);const O=Buffer.concat(l).toString(),W=JSON.parse(O),k=W?.status;if(k&&k!=="pending"){v("Approval already resolved (WS endpoint returned JSON)",{approvalId:e,status:k}),u({status:k,reviewer_note:W?.reviewer_note});return}}catch{}d(new Error("WebSocket upgrade rejected by server"))}),p=setInterval(async()=>{if(!c)try{const t=await _(r);if(t){v("Approval resolved via safety-net poll",{approvalId:e,status:t.status}),u(t);try{s.close(1e3)}catch{}}}catch{}},3e4);const f=o-Date.now();if(f<=0){d(new Error("Approval timeout"));return}S=setTimeout(()=>{if(!c){b("Approval timed out",{approvalId:e}),u({status:"expired"});try{s.close(1e3)}catch{}}},f)})}async function A(e,n,r,o){for(v("Polling for approval resolution",{approvalId:e,intervalMs:r});Date.now()<o;){await P(r);try{const a=await _(n);if(a)return v("Approval resolved via polling",{approvalId:e,status:a.status}),a}catch{}}return b("Approval polling timed out",{approvalId:e}),{status:"expired"}}async function _(e){const n=await fetch(e,{headers:{"User-Agent":"aerostack-gateway/0.14.0"}});if(!n.ok)return null;const r=await n.json(),o=r.status;return o==="executed"||o==="approved"?{status:o,reviewer_note:r.reviewer_note}:o==="rejected"?{status:"rejected",reviewer_note:r.reviewer_note}:o==="changes_requested"?{status:"changes_requested",reviewer_note:r.reviewer_note}:o==="expired"?{status:"expired"}:null}async function x(){if(typeof globalThis.WebSocket<"u")return globalThis.WebSocket;try{return(await import("ws")).default}catch{throw new Error('WebSocket not available. Install the "ws" package: npm install ws')}}function P(e){return new Promise(n=>setTimeout(n,e))}export{N as resolveApproval,J as startBackgroundResolver};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerostack/gateway",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "stdio-to-HTTP bridge connecting any MCP client to Aerostack Workspaces",
5
5
  "author": "Aerostack",
6
6
  "license": "MIT",