rterm-backend 3.7.6 → 3.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.
Files changed (2) hide show
  1. package/bin/gybackend.cjs +347 -142
  2. package/package.json +1 -1
package/bin/gybackend.cjs CHANGED
@@ -276397,9 +276397,12 @@ var init_compoundingStore = __esm({
276397
276397
  init_betterSqlite3Runtime();
276398
276398
  init_historyStoragePaths();
276399
276399
  COMPOUNDING_DB_FILE = "gyshell-compounding.sqlite";
276400
- CompoundingStore = class {
276400
+ CompoundingStore = class _CompoundingStore {
276401
276401
  filePath;
276402
276402
  db;
276403
+ /** Avoid sync SQLite on every agent turn (desktop freeze). Invalidated on writes. */
276404
+ promptCache = null;
276405
+ static PROMPT_CACHE_MS = 2e3;
276403
276406
  constructor(options) {
276404
276407
  this.filePath = options?.filePath || import_node_path28.default.join(resolveHistoryStorageDir(), COMPOUNDING_DB_FILE);
276405
276408
  import_node_fs12.default.mkdirSync(import_node_path28.default.dirname(this.filePath), { recursive: true });
@@ -276453,6 +276456,7 @@ var init_compoundingStore = __esm({
276453
276456
  recordLessons(lessons, runId) {
276454
276457
  const now = Date.now();
276455
276458
  const out = [];
276459
+ this.promptCache = null;
276456
276460
  try {
276457
276461
  const sel = this.db.prepare("SELECT * FROM lessons WHERE fingerprint = ?");
276458
276462
  const ins = this.db.prepare(
@@ -276506,6 +276510,7 @@ var init_compoundingStore = __esm({
276506
276510
  }
276507
276511
  }
276508
276512
  upsertEstateFact(input) {
276513
+ this.promptCache = null;
276509
276514
  try {
276510
276515
  const now = Date.now();
276511
276516
  this.db.prepare(
@@ -276552,6 +276557,7 @@ var init_compoundingStore = __esm({
276552
276557
  }
276553
276558
  }
276554
276559
  upsertGoal(input) {
276560
+ this.promptCache = null;
276555
276561
  try {
276556
276562
  this.db.prepare(
276557
276563
  `INSERT INTO goals (id, session_id, text, status, blocked_by, next_probe, updated_at)
@@ -276589,6 +276595,7 @@ var init_compoundingStore = __esm({
276589
276595
  }
276590
276596
  }
276591
276597
  recordProbe(sessionId, tag, hypothesis, command, ok) {
276598
+ this.promptCache = null;
276592
276599
  try {
276593
276600
  this.db.prepare(
276594
276601
  `INSERT INTO probes (session_id, tag, hypothesis, command, ok, at)
@@ -276620,6 +276627,11 @@ var init_compoundingStore = __esm({
276620
276627
  */
276621
276628
  promptBlock(maxChars = 4e3) {
276622
276629
  try {
276630
+ const now = Date.now();
276631
+ if (this.promptCache && now - this.promptCache.at < _CompoundingStore.PROMPT_CACHE_MS) {
276632
+ const cached2 = this.promptCache.text;
276633
+ return cached2.length > maxChars ? cached2.slice(0, maxChars) + "\n\u2026" : cached2;
276634
+ }
276623
276635
  const lessons = this.listLessons(20);
276624
276636
  const facts = this.listEstateFacts();
276625
276637
  const lines = ["# Compounding knowledge (auto)"];
@@ -276641,6 +276653,7 @@ var init_compoundingStore = __esm({
276641
276653
  }
276642
276654
  }
276643
276655
  const text = lines.join("\n");
276656
+ this.promptCache = { at: now, text };
276644
276657
  return text.length > maxChars ? text.slice(0, maxChars) + "\n\u2026" : text;
276645
276658
  } catch {
276646
276659
  return "";
@@ -296488,6 +296501,8 @@ var PSRPTransport = class {
296488
296501
  /** pypsrp WSMan.session_id — one uuid for the life of the transport. */
296489
296502
  sessionId = `uuid:${(0, import_node_crypto3.randomUUID)().toUpperCase()}`;
296490
296503
  http;
296504
+ /** Persistent runspace (v3.8.0): reuse one Microsoft.PowerShell shell across commands. */
296505
+ pool = null;
296491
296506
  constructor(opts) {
296492
296507
  this.http = new WinHttpAuth({
296493
296508
  host: opts.host,
@@ -296511,6 +296526,7 @@ var PSRPTransport = class {
296511
296526
  const wsmv = "http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd";
296512
296527
  return `<s:Envelope xmlns:s="${NS2.s}" xmlns:wsa="${NS2.a}" xmlns:wsman="${NS2.w}" xmlns:wsmv="${wsmv}" xmlns:rsp="${NS2.rsp}" xml:lang="en-US"><s:Header><wsa:Action s:mustUnderstand="true">${action}</wsa:Action><wsmv:DataLocale s:mustUnderstand="false" xml:lang="en-US" /><wsman:Locale s:mustUnderstand="false" xml:lang="en-US" /><wsman:MaxEnvelopeSize s:mustUnderstand="true">512000</wsman:MaxEnvelopeSize><wsa:MessageID>${mid}</wsa:MessageID><wsman:OperationTimeout>PT60.000S</wsman:OperationTimeout><wsa:ReplyTo><wsa:Address s:mustUnderstand="true">${NS2.a}/role/anonymous</wsa:Address></wsa:ReplyTo><wsman:ResourceURI s:mustUnderstand="true">${PSRP_SHELL_URI}</wsman:ResourceURI><wsmv:SessionId s:mustUnderstand="false">${this.sessionId}</wsmv:SessionId><wsa:To>${to}</wsa:To>${extraHeaders}</s:Header><s:Body>${body}</s:Body></s:Envelope>`;
296513
296528
  }
296529
+ /** Test hook: subclasses may override. */
296514
296530
  async post(action, body, extraHeaders) {
296515
296531
  const envelope = this.envelope(action, body, extraHeaders);
296516
296532
  const res = await this.http.post(envelope);
@@ -296524,6 +296540,57 @@ var PSRPTransport = class {
296524
296540
  * Receive loop (PIPELINE_OUTPUT / ERROR_RECORD / PIPELINE_STATE) → Delete.
296525
296541
  * The script travels inside the PSRP message body — no command-line length limit.
296526
296542
  */
296543
+ /** Open (or return) a persistent runspace pool. */
296544
+ async ensurePool(opts) {
296545
+ if (this.pool) return { shellId: this.pool.shellId, rpid: this.pool.rpid };
296546
+ const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
296547
+ const rpid = (0, import_node_crypto3.randomUUID)().toUpperCase();
296548
+ const creation = fragmentMessages([
296549
+ psrpMessage(MSG.SESSION_CAPABILITY, rpid, "00000000-0000-0000-0000-000000000000", sessionCapabilityXml()),
296550
+ psrpMessage(MSG.INIT_RUNSPACEPOOL, rpid, "00000000-0000-0000-0000-000000000000", initRunspacePoolXml())
296551
+ ], 1);
296552
+ const createBody = `<rsp:Shell ShellId="${rpid}"><rsp:InputStreams>stdin pr</rsp:InputStreams><rsp:OutputStreams>stdout</rsp:OutputStreams><creationXml xmlns="http://schemas.microsoft.com/powershell">` + creation.blob.toString("base64") + `</creationXml></rsp:Shell>`;
296553
+ const createHeaders = `<wsman:OptionSet s:mustUnderstand="true"><wsman:Option MustComply="true" Name="protocolversion">2.3</wsman:Option></wsman:OptionSet>`;
296554
+ const created = await this.post(
296555
+ "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create",
296556
+ createBody,
296557
+ createHeaders
296558
+ );
296559
+ this.assertOk(created, "Create");
296560
+ const shellId = extractShellId2(created.body);
296561
+ if (!shellId) throw new Error("PSRP Create succeeded but no ShellId returned.");
296562
+ await this.waitForRunspaceOpened(shellId, deadline, opts?.signal);
296563
+ this.pool = { shellId, rpid, nextObjectId: creation.nextObjectId };
296564
+ return { shellId, rpid };
296565
+ }
296566
+ async closePool() {
296567
+ const p = this.pool;
296568
+ this.pool = null;
296569
+ if (p) await this.deleteShell(p.shellId);
296570
+ }
296571
+ /**
296572
+ * Run a script on the persistent pool (opens one if needed). Does NOT delete
296573
+ * the shell — call closePool() when the tab dies.
296574
+ */
296575
+ async runScriptOnPool(script, opts) {
296576
+ const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
296577
+ const pool = await this.ensurePool(opts);
296578
+ const pipelineId = (0, import_node_crypto3.randomUUID)().toUpperCase();
296579
+ const createPipelineMsg = psrpMessage(MSG.CREATE_PIPELINE, pool.rpid, pipelineId, createPipelineXml(script));
296580
+ const frag = fragmentMessages([createPipelineMsg], this.pool.nextObjectId);
296581
+ this.pool.nextObjectId = frag.nextObjectId;
296582
+ const commandBody = `<rsp:CommandLine CommandId="${pipelineId}"><rsp:Command></rsp:Command><rsp:Arguments>${frag.blob.toString("base64")}</rsp:Arguments></rsp:CommandLine>`;
296583
+ const commandHeaders = `<wsman:OptionSet s:mustUnderstand="true"><wsman:Option Name="WINRS_SKIP_CMD_SHELL">False</wsman:Option></wsman:OptionSet>` + this.shellHeaders(pool.shellId);
296584
+ const commanded = await this.post(
296585
+ "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command",
296586
+ commandBody,
296587
+ commandHeaders
296588
+ );
296589
+ this.assertOk(commanded, "Command");
296590
+ const commandId = firstText2(commanded.body, "CommandId");
296591
+ if (!commandId) throw new Error("PSRP Command succeeded but no CommandId returned.");
296592
+ return this.receivePipeline(pool.shellId, commandId, deadline, opts);
296593
+ }
296527
296594
  async runScript(script, opts) {
296528
296595
  const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
296529
296596
  const rpid = (0, import_node_crypto3.randomUUID)().toUpperCase();
@@ -296556,63 +296623,76 @@ var PSRPTransport = class {
296556
296623
  this.assertOk(commanded, "Command");
296557
296624
  const commandId = firstText2(commanded.body, "CommandId");
296558
296625
  if (!commandId) throw new Error("PSRP Command succeeded but no CommandId returned.");
296559
- let stdout = "";
296560
- let stderr = "";
296561
- let hadErrors = false;
296562
- let exitCode = 0;
296563
- let pipelineDone = false;
296564
- for (; ; ) {
296565
- if (opts?.signal?.aborted) throw new Error("AbortError");
296566
- if (Date.now() > deadline) {
296567
- throw new Error(`PSRP command timed out after ${opts?.timeoutMs ?? 12e4}ms`);
296568
- }
296569
- const receiveBody = `<rsp:Receive><rsp:DesiredStream CommandId="${commandId}">stdout</rsp:DesiredStream></rsp:Receive>`;
296570
- const receiveHeaders = `<wsman:OptionSet s:mustUnderstand="true"><wsman:Option Name="WSMAN_CMDSHELL_OPTION_KEEPALIVE">True</wsman:Option></wsman:OptionSet>` + this.shellHeaders(shellId);
296571
- const received = await this.post(
296572
- "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive",
296573
- receiveBody,
296574
- receiveHeaders
296575
- );
296576
- this.assertOk(received, "Receive");
296577
- const streamRe = /<\w*:?Stream\b[^>]*\bName="(?:stdout|stderr|pr)"[^>]*>([\s\S]*?)<\/\w*:?Stream>/gi;
296578
- let m2;
296579
- while ((m2 = streamRe.exec(received.body)) !== null) {
296580
- const b64 = m2[1].replace(/\s+/g, "");
296581
- if (!b64) continue;
296582
- for (const raw of unfragmentMessages(import_node_buffer2.Buffer.from(b64, "base64"))) {
296583
- if (raw.length < 40) continue;
296584
- const messageType = raw.readUInt32LE(4);
296585
- const payload = raw.subarray(40).toString("utf8").replace(/^\uFEFF/, "");
296586
- if (messageType === MSG.PIPELINE_OUTPUT) {
296587
- const sRe = /<S[^>]*>([\s\S]*?)<\/S>/gi;
296588
- let sm;
296589
- while ((sm = sRe.exec(payload)) !== null) stdout += sm[1];
296590
- } else if (messageType === MSG.ERROR_RECORD) {
296591
- hadErrors = true;
296592
- const mRe = payload.match(/<S N="Message">([\s\S]*?)<\/S>/i);
296593
- stderr += (mRe ? mRe[1] : payload) + "\n";
296594
- } else if (messageType === MSG.PIPELINE_STATE) {
296595
- pipelineDone = true;
296596
- const stateMatch = payload.match(/<I32 N="State">(\d+)<\/I32>/i);
296597
- if (stateMatch) {
296598
- if (parseInt(stateMatch[1], 10) === 5) hadErrors = true;
296599
- }
296600
- const ec = payload.match(/N="ExitCode"[^>]*>(-?\d+)</i);
296601
- if (ec) exitCode = parseInt(ec[1], 10);
296602
- }
296603
- }
296604
- }
296605
- const wsExit = received.body.match(/<\w*:?ExitCode>\s*(-?\d+)\s*<\/\w*:?ExitCode>/i);
296606
- if (wsExit) exitCode = parseInt(wsExit[1], 10);
296607
- if (pipelineDone) break;
296608
- if (/CommandState\/Done/i.test(received.body)) break;
296609
- }
296610
- if (exitCode !== 0) hadErrors = true;
296611
- return { stdout, stderr, exitCode, hadErrors };
296626
+ return await this.receivePipeline(shellId, commandId, deadline, opts);
296612
296627
  } finally {
296613
296628
  await this.deleteShell(shellId);
296614
296629
  }
296615
296630
  }
296631
+ async receivePipeline(shellId, commandId, deadline, opts) {
296632
+ let stdout = "";
296633
+ let stderr = "";
296634
+ let hadErrors = false;
296635
+ let exitCode = 0;
296636
+ let pipelineDone = false;
296637
+ for (; ; ) {
296638
+ if (opts?.signal?.aborted) throw new Error("AbortError");
296639
+ if (Date.now() > deadline) {
296640
+ throw new Error(`PSRP command timed out after ${opts?.timeoutMs ?? 12e4}ms`);
296641
+ }
296642
+ const receiveBody = `<rsp:Receive><rsp:DesiredStream CommandId="${commandId}">stdout</rsp:DesiredStream></rsp:Receive>`;
296643
+ const receiveHeaders = `<wsman:OptionSet s:mustUnderstand="true"><wsman:Option Name="WSMAN_CMDSHELL_OPTION_KEEPALIVE">True</wsman:Option></wsman:OptionSet>` + this.shellHeaders(shellId);
296644
+ const received = await this.post(
296645
+ "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive",
296646
+ receiveBody,
296647
+ receiveHeaders
296648
+ );
296649
+ this.assertOk(received, "Receive");
296650
+ const streamRe = /<\w*:?Stream\b[^>]*\bName="(?:stdout|stderr|pr)"[^>]*>([\s\S]*?)<\/\w*:?Stream>/gi;
296651
+ let m2;
296652
+ while ((m2 = streamRe.exec(received.body)) !== null) {
296653
+ const b64 = m2[1].replace(/\s+/g, "");
296654
+ if (!b64) continue;
296655
+ for (const raw of unfragmentMessages(import_node_buffer2.Buffer.from(b64, "base64"))) {
296656
+ if (raw.length < 40) continue;
296657
+ const messageType = raw.readUInt32LE(4);
296658
+ const payload = raw.subarray(40).toString("utf8").replace(/^\uFEFF/, "");
296659
+ if (messageType === MSG.PIPELINE_OUTPUT) {
296660
+ const sRe = /<S[^>]*>([\s\S]*?)<\/S>/gi;
296661
+ let sm;
296662
+ let extracted = false;
296663
+ while ((sm = sRe.exec(payload)) !== null) {
296664
+ stdout += sm[1];
296665
+ extracted = true;
296666
+ }
296667
+ if (!extracted) {
296668
+ const nRe = /<(?:I32|I64|B|ToString)[^>]*>([\s\S]*?)<\/(?:I32|I64|B|ToString)>/i;
296669
+ const nm = payload.match(nRe);
296670
+ if (nm) stdout += nm[1];
296671
+ else stdout += payload.replace(/<[^>]+>/g, "").trim();
296672
+ }
296673
+ } else if (messageType === MSG.ERROR_RECORD) {
296674
+ hadErrors = true;
296675
+ const mRe = payload.match(/<S N="Message">([\s\S]*?)<\/S>/i);
296676
+ stderr += (mRe ? mRe[1] : payload) + "\n";
296677
+ } else if (messageType === MSG.PIPELINE_STATE) {
296678
+ pipelineDone = true;
296679
+ const stateMatch = payload.match(/<I32 N="State">(\d+)<\/I32>/i);
296680
+ if (stateMatch) {
296681
+ if (parseInt(stateMatch[1], 10) === 5) hadErrors = true;
296682
+ }
296683
+ const ec = payload.match(/N="ExitCode"[^>]*>(-?\d+)</i);
296684
+ if (ec) exitCode = parseInt(ec[1], 10);
296685
+ }
296686
+ }
296687
+ }
296688
+ const wsExit = received.body.match(/<\w*:?ExitCode>\s*(-?\d+)\s*<\/\w*:?ExitCode>/i);
296689
+ if (wsExit) exitCode = parseInt(wsExit[1], 10);
296690
+ if (pipelineDone) break;
296691
+ if (/CommandState\/Done/i.test(received.body)) break;
296692
+ }
296693
+ if (exitCode !== 0) hadErrors = true;
296694
+ return { stdout, stderr, exitCode, hadErrors };
296695
+ }
296616
296696
  /**
296617
296697
  * Drain Receive until RUNSPACEPOOL_STATE reports Opened (state=2).
296618
296698
  * Must run after Create and before Command.
@@ -296817,7 +296897,7 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
296817
296897
  \x1B[36m\u276F ${command}\x1B[0m\r
296818
296898
  `);
296819
296899
  const ps = this.translateCmdToPowerShell(command, instance.cwd);
296820
- const result = await transport.runScript(ps, {
296900
+ const result = await transport.runScriptOnPool(ps, {
296821
296901
  timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
296822
296902
  signal: options?.signal
296823
296903
  });
@@ -296938,6 +297018,9 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
296938
297018
  void instance.winrm.deleteShell(instance.persistentShellId);
296939
297019
  instance.persistentShellId = void 0;
296940
297020
  }
297021
+ if (instance.psrp) {
297022
+ void instance.psrp.closePool();
297023
+ }
296941
297024
  instance.exitCallback?.(0);
296942
297025
  }
296943
297026
  onData(ptyId, callback) {
@@ -345277,6 +345360,28 @@ var MessagesZodState = external_exports.object({ messages: withLangGraph(externa
345277
345360
  // ../../node_modules/@langchain/langgraph/dist/index.js
345278
345361
  initializeAsyncLocalStorageSingleton();
345279
345362
 
345363
+ // ../../packages/backend/src/services/redaction/secretRedact.ts
345364
+ var PATTERNS = [
345365
+ { re: /\b(gys_at_[A-Za-z0-9_-]{16,})\b/g, label: "RTERM_TOKEN" },
345366
+ { re: /\b(ghp_[A-Za-z0-9]{20,})\b/g, label: "GITHUB_PAT" },
345367
+ { re: /\b(sk-[A-Za-z0-9_-]{20,})\b/g, label: "API_KEY" },
345368
+ { re: /\b(AKIA[0-9A-Z]{16})\b/g, label: "AWS_KEY" },
345369
+ { re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, label: "PRIVATE_KEY" },
345370
+ { re: /(password|passwd|pwd|secret|token)\s*[:=]\s*["']?([^\s"']{6,})/gi, label: "CREDENTIAL" }
345371
+ ];
345372
+ function redactText(input, extraValues = []) {
345373
+ let out = String(input ?? "");
345374
+ for (const v of extraValues) {
345375
+ const t = String(v || "");
345376
+ if (t.length < 4) continue;
345377
+ out = out.split(t).join(`[REDACTED]`);
345378
+ }
345379
+ for (const p of PATTERNS) {
345380
+ out = out.replace(p.re, `[REDACTED:${p.label}]`);
345381
+ }
345382
+ return out;
345383
+ }
345384
+
345280
345385
  // ../../packages/backend/src/services/agentRunLedger.ts
345281
345386
  var import_node_fs3 = __toESM(require("node:fs"), 1);
345282
345387
  var import_node_path5 = __toESM(require("node:path"), 1);
@@ -366951,6 +367056,11 @@ function reconcileToolCalls(toolCalls) {
366951
367056
  }
366952
367057
  return result;
366953
367058
  }
367059
+ var TOOL_RESULT_MAX_CHARS = 32768;
367060
+ function stringifyToolResult(result) {
367061
+ const raw = typeof result === "string" ? result : JSON.stringify(result);
367062
+ return redactText(clipTextMiddle(raw, TOOL_RESULT_MAX_CHARS));
367063
+ }
366954
367064
  function clipTextMiddle(input, maxChars) {
366955
367065
  if (maxChars <= 0) return "";
366956
367066
  if (input.length <= maxChars) return input;
@@ -368873,7 +368983,7 @@ Actually, your intention might be different. Please re-read the description of t
368873
368983
  try {
368874
368984
  const pluginArgs = typeof toolCall.args === "string" ? JSON.parse(toolCall.args) : toolCall.args || {};
368875
368985
  const pluginResult = await pluginHandler(pluginArgs);
368876
- result = typeof pluginResult === "string" ? pluginResult : JSON.stringify(pluginResult);
368986
+ result = stringifyToolResult(pluginResult);
368877
368987
  } catch (err) {
368878
368988
  result = `Plugin tool "${toolCall.name}" error: ${err.message}`;
368879
368989
  }
@@ -369161,7 +369271,7 @@ Actually, your intention might be different. Please re-read the description of t
369161
369271
  const pluginHandler = this.pluginTools.get(name);
369162
369272
  if (pluginHandler) {
369163
369273
  const result = await pluginHandler(args);
369164
- return typeof result === "string" ? result : JSON.stringify(result);
369274
+ return stringifyToolResult(result);
369165
369275
  }
369166
369276
  return `Tool "${name}" is not supported in parallel execution mode.`;
369167
369277
  }
@@ -369242,7 +369352,7 @@ Actually, your intention might be different. Please re-read the description of t
369242
369352
  args,
369243
369353
  signal
369244
369354
  );
369245
- resultText = typeof result === "string" ? result : JSON.stringify(result, null, 2);
369355
+ resultText = stringifyToolResult(result);
369246
369356
  } catch (err) {
369247
369357
  if (this.helpers.isAbortError(err)) throw err;
369248
369358
  resultText = err instanceof Error ? err.message : String(err);
@@ -373011,6 +373121,26 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
373011
373121
 
373012
373122
  // ../../packages/backend/src/services/Gateway/WebSocketGatewayAdapter.ts
373013
373123
  var import_node_module4 = require("node:module");
373124
+
373125
+ // ../../packages/backend/src/services/security/tokenScopes.ts
373126
+ var SCOPE_FULL = "*";
373127
+ function methodAllowed(method, scopes) {
373128
+ if (!scopes || scopes.length === 0) return true;
373129
+ if (scopes.includes(SCOPE_FULL)) return true;
373130
+ const m2 = String(method || "");
373131
+ for (const s of scopes) {
373132
+ if (s === m2) return true;
373133
+ if (s.endsWith(":*")) {
373134
+ const prefix = s.slice(0, -1);
373135
+ if (m2.startsWith(prefix)) return true;
373136
+ if (prefix === "terminals:" && m2.startsWith("terminal:")) return true;
373137
+ }
373138
+ if (s.endsWith("*") && m2.startsWith(s.slice(0, -1))) return true;
373139
+ }
373140
+ return false;
373141
+ }
373142
+
373143
+ // ../../packages/backend/src/services/Gateway/WebSocketGatewayAdapter.ts
373014
373144
  var import_meta7 = {};
373015
373145
  var nodeRequire = (0, import_node_module4.createRequire)(
373016
373146
  typeof __filename !== "undefined" ? __filename : import_meta7.url
@@ -373021,6 +373151,22 @@ var WebSocketRpcError = class extends Error {
373021
373151
  this.code = code;
373022
373152
  }
373023
373153
  };
373154
+ function httpRouteMatches(pattern, pathname) {
373155
+ if (pattern === pathname) return true;
373156
+ if (pattern.endsWith("/*")) {
373157
+ const prefix = pattern.slice(0, -1);
373158
+ const base = pattern.slice(0, -2);
373159
+ return pathname === base || pathname.startsWith(prefix) || pathname.startsWith(base + "/");
373160
+ }
373161
+ const a = pattern.split("/").filter(Boolean);
373162
+ const b = pathname.split("/").filter(Boolean);
373163
+ if (a.length !== b.length) return false;
373164
+ for (let i = 0; i < a.length; i++) {
373165
+ if (a[i].startsWith(":")) continue;
373166
+ if (a[i] !== b[i]) return false;
373167
+ }
373168
+ return true;
373169
+ }
373024
373170
  function createDefaultWebSocketServerFactory(httpRoutes) {
373025
373171
  return ({ host, port }) => {
373026
373172
  if (!httpRoutes || httpRoutes.length === 0) {
@@ -373043,7 +373189,7 @@ function createDefaultWebSocketServerFactory(httpRoutes) {
373043
373189
  ).pathname;
373044
373190
  } catch {
373045
373191
  }
373046
- const route = routes.find((r) => r.path === pathname);
373192
+ const route = routes.find((r) => httpRouteMatches(r.path, pathname));
373047
373193
  if (!route) {
373048
373194
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
373049
373195
  res.end("not found");
@@ -373099,6 +373245,7 @@ var WebSocketGatewayAdapter = class {
373099
373245
  server = null;
373100
373246
  transportIdBySocket = /* @__PURE__ */ new Map();
373101
373247
  isSameMachineBySocket = /* @__PURE__ */ new WeakMap();
373248
+ scopesBySocket = /* @__PURE__ */ new WeakMap();
373102
373249
  serverFactory;
373103
373250
  logger;
373104
373251
  start() {
@@ -373196,6 +373343,12 @@ var WebSocketGatewayAdapter = class {
373196
373343
  this.isLoopbackAddress(String(remote))
373197
373344
  );
373198
373345
  this.gateway.registerTransport(transport);
373346
+ const token = this.extractAccessToken(request);
373347
+ const scopeFn = this.options.accessTokenAuth?.scopesForToken;
373348
+ if (token && scopeFn) {
373349
+ const scopes = await scopeFn(token);
373350
+ if (scopes && scopes.length) this.scopesBySocket.set(socket, scopes);
373351
+ }
373199
373352
  state.authorized = true;
373200
373353
  this.logger.info(
373201
373354
  `[WebSocketGatewayAdapter] Client connected: ${remote} (${transport.id})`
@@ -373493,6 +373646,13 @@ var WebSocketGatewayAdapter = class {
373493
373646
  );
373494
373647
  }
373495
373648
  async executeRequest(request, socket) {
373649
+ const scopes = this.scopesBySocket.get(socket);
373650
+ if (scopes && !methodAllowed(request.method, scopes)) {
373651
+ throw new WebSocketRpcError(
373652
+ "FORBIDDEN",
373653
+ `Token is not allowed to call ${request.method}`
373654
+ );
373655
+ }
373496
373656
  const params = request.params ?? {};
373497
373657
  if (request.method.startsWith("observability:")) {
373498
373658
  const bridge = this.options.observabilityBridge;
@@ -384954,8 +385114,9 @@ var IdleTimeoutService = class {
384954
385114
  // ../../packages/backend/src/services/Gateway/restApi.ts
384955
385115
  function matchRestRoute(routes, method, path33) {
384956
385116
  const normalized = path33.replace(/\/+$/, "") || "/";
385117
+ const verb = method.toUpperCase();
384957
385118
  for (const route of routes) {
384958
- if (route.method !== method.toUpperCase()) continue;
385119
+ if (route.method !== verb) continue;
384959
385120
  const patternParts = route.path.split("/").filter(Boolean);
384960
385121
  const pathParts = normalized.split("/").filter(Boolean);
384961
385122
  if (patternParts.length !== pathParts.length) continue;
@@ -384976,36 +385137,18 @@ function matchRestRoute(routes, method, path33) {
384976
385137
  }
384977
385138
  function defaultRestRoutes() {
384978
385139
  return [
385140
+ { method: "GET", path: "/api/v1/health", gatewayMethod: "gateway:ping", description: "Liveness check" },
385141
+ { method: "GET", path: "/api/v1/methods", gatewayMethod: "gateway:describe", description: "List all gateway RPC methods" },
385142
+ { method: "GET", path: "/api/v1/openapi.json", gatewayMethod: "__openapi", description: "OpenAPI 3 document for this REST overlay + RPC escape hatch" },
385143
+ { method: "GET", path: "/api/v1/terminals", gatewayMethod: "terminal:list", description: "List terminal tabs" },
385144
+ { method: "GET", path: "/api/v1/sessions", gatewayMethod: "session:list", description: "List chat sessions" },
384979
385145
  {
384980
- method: "GET",
384981
- path: "/api/v1/health",
384982
- gatewayMethod: "gateway:ping",
384983
- description: "Liveness check"
384984
- },
384985
- {
384986
- method: "GET",
384987
- path: "/api/v1/methods",
384988
- gatewayMethod: "gateway:describe",
384989
- description: "List all gateway methods (self-describing)"
384990
- },
384991
- {
384992
- method: "GET",
384993
- path: "/api/v1/terminals",
384994
- gatewayMethod: "terminal:list",
384995
- description: "List terminal tabs"
384996
- },
384997
- {
384998
- method: "GET",
385146
+ method: "POST",
384999
385147
  path: "/api/v1/sessions",
385000
- gatewayMethod: "session:list",
385001
- description: "List chat sessions"
385002
- },
385003
- {
385004
- method: "GET",
385005
- path: "/api/v1/skills",
385006
- gatewayMethod: "skills:getAll",
385007
- description: "List loaded skills"
385148
+ gatewayMethod: "gateway:createSession",
385149
+ description: "Create a chat/agent session"
385008
385150
  },
385151
+ { method: "GET", path: "/api/v1/skills", gatewayMethod: "skills:getAll", description: "List loaded skills" },
385009
385152
  {
385010
385153
  method: "GET",
385011
385154
  path: "/api/v1/observability/metrics",
@@ -385022,13 +385165,13 @@ function defaultRestRoutes() {
385022
385165
  method: "GET",
385023
385166
  path: "/api/v1/observability/apm",
385024
385167
  gatewayMethod: "observability:apmSummary",
385025
- description: "APM summary (LLM + app traces)"
385168
+ description: "APM summary"
385026
385169
  },
385027
385170
  {
385028
385171
  method: "GET",
385029
385172
  path: "/api/v1/history/search",
385030
385173
  gatewayMethod: "history:search",
385031
- description: "Cross-session history search (?q=...)",
385174
+ description: "Cross-session history search (?q=)",
385032
385175
  buildParams: (_p, body) => {
385033
385176
  const b = body ?? {};
385034
385177
  return { query: b.q ?? b.query ?? "" };
@@ -385048,17 +385191,18 @@ function defaultRestRoutes() {
385048
385191
  method: "GET",
385049
385192
  path: "/api/v1/terminals/:id/buffer",
385050
385193
  gatewayMethod: "terminal:getBufferDelta",
385051
- description: "Read a terminal tab output delta",
385194
+ description: "Read terminal output delta (?fromOffset=)",
385052
385195
  buildParams: (p, body) => {
385053
385196
  const b = body ?? {};
385054
- return { terminalId: p.id, fromOffset: b.fromOffset ?? 0 };
385197
+ const n2 = Number(b.fromOffset ?? 0);
385198
+ return { terminalId: p.id, fromOffset: Number.isFinite(n2) ? n2 : 0 };
385055
385199
  }
385056
385200
  },
385057
385201
  {
385058
385202
  method: "POST",
385059
385203
  path: "/api/v1/sessions/:id/chat",
385060
385204
  gatewayMethod: "agent:startTask",
385061
- description: "Send a message to the agent (blocking)",
385205
+ description: "Send a message (blocking until the run finishes \u2014 prefer chat-async)",
385062
385206
  buildParams: (p, body) => {
385063
385207
  const b = body ?? {};
385064
385208
  return { sessionId: p.id, userInput: b.message ?? b.userInput ?? "" };
@@ -385066,17 +385210,66 @@ function defaultRestRoutes() {
385066
385210
  },
385067
385211
  {
385068
385212
  method: "POST",
385069
- path: "/api/v1/rpc",
385070
- gatewayMethod: "",
385071
- description: "Escape hatch: dispatch any gateway method",
385072
- buildParams: (_p, body) => {
385213
+ path: "/api/v1/sessions/:id/chat-async",
385214
+ gatewayMethod: "agent:startTaskAsync",
385215
+ description: "Start an agent turn without waiting; subscribe to WS gateway:event for tokens",
385216
+ buildParams: (p, body) => {
385073
385217
  const b = body ?? {};
385074
- return { __rpcMethod: b.method ?? "", ...b.params ?? {} };
385218
+ return { sessionId: p.id, userInput: b.message ?? b.userInput ?? "" };
385075
385219
  }
385220
+ },
385221
+ {
385222
+ method: "POST",
385223
+ path: "/api/v1/rpc",
385224
+ gatewayMethod: "",
385225
+ description: "Escape hatch: any gateway method {method, params}"
385076
385226
  }
385077
385227
  ];
385078
385228
  }
385229
+ function buildOpenApiDocument() {
385230
+ const routes = defaultRestRoutes();
385231
+ const paths = {};
385232
+ for (const r of routes) {
385233
+ if (r.gatewayMethod === "__openapi") continue;
385234
+ const item = paths[r.path] || {};
385235
+ item[r.method.toLowerCase()] = {
385236
+ summary: r.description,
385237
+ operationId: `${r.method}_${r.path.replace(/[^a-zA-Z0-9]+/g, "_")}`,
385238
+ tags: ["rest"],
385239
+ ...r.method === "POST" ? {
385240
+ requestBody: {
385241
+ content: { "application/json": { schema: { type: "object" } } }
385242
+ }
385243
+ } : {},
385244
+ responses: { "200": { description: "OK" }, "401": { description: "Unauthorized" } }
385245
+ };
385246
+ paths[r.path] = item;
385247
+ }
385248
+ const rpcMethods = [...CORE_METHODS, DESCRIBE_METHOD].map((m2) => m2.name);
385249
+ return {
385250
+ openapi: "3.0.3",
385251
+ info: {
385252
+ title: "RTerm HTTP overlay",
385253
+ version: "1.0.0",
385254
+ description: `Thin REST on the same port as the WebSocket JSON-RPC gateway. Streaming agent/PTY traffic stays on ws://. POST /api/v1/rpc reaches every RPC method. Core RPC methods: ${rpcMethods.length}. Categories: ${METHOD_CATEGORIES.join(", ")}.`
385255
+ },
385256
+ paths,
385257
+ "x-gateway-rpc-methods": rpcMethods
385258
+ };
385259
+ }
385079
385260
  async function handleRestRequest(routes, dispatch, req) {
385261
+ const verb = req.method.toUpperCase();
385262
+ if (verb === "OPTIONS") {
385263
+ return {
385264
+ status: 204,
385265
+ body: "",
385266
+ headers: {
385267
+ "access-control-allow-origin": "*",
385268
+ "access-control-allow-methods": "GET, POST, OPTIONS",
385269
+ "access-control-allow-headers": "Authorization, Content-Type"
385270
+ }
385271
+ };
385272
+ }
385080
385273
  const match = matchRestRoute(routes, req.method, req.path);
385081
385274
  if (!match) {
385082
385275
  return {
@@ -385084,12 +385277,18 @@ async function handleRestRequest(routes, dispatch, req) {
385084
385277
  body: { error: "not_found", message: `No REST route for ${req.method} ${req.path}` }
385085
385278
  };
385086
385279
  }
385280
+ if (match.route.gatewayMethod === "__openapi") {
385281
+ return { status: 200, body: buildOpenApiDocument() };
385282
+ }
385087
385283
  let gatewayMethod = match.route.gatewayMethod;
385088
385284
  let params;
385089
385285
  if (gatewayMethod === "") {
385090
385286
  const raw = req.body ?? {};
385091
385287
  if (!raw.method) {
385092
- return { status: 400, body: { error: "bad_request", message: 'POST /api/v1/rpc needs {"method": "...", "params": {...}}' } };
385288
+ return {
385289
+ status: 400,
385290
+ body: { error: "bad_request", message: 'POST /api/v1/rpc needs {"method": "...", "params": {...}}' }
385291
+ };
385093
385292
  }
385094
385293
  gatewayMethod = raw.method;
385095
385294
  params = raw.params ?? {};
@@ -385105,6 +385304,52 @@ async function handleRestRequest(routes, dispatch, req) {
385105
385304
  return { status, body: { error: "gateway_error", message } };
385106
385305
  }
385107
385306
  }
385307
+ var JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
385308
+ function readJsonBody(req) {
385309
+ return new Promise((resolve2) => {
385310
+ let data = "";
385311
+ req.on?.("data", (d) => {
385312
+ data += String(d ?? "");
385313
+ if (data.length > 2e6) {
385314
+ resolve2({});
385315
+ }
385316
+ });
385317
+ req.on?.("end", () => {
385318
+ try {
385319
+ resolve2(data ? JSON.parse(data) : {});
385320
+ } catch {
385321
+ resolve2({});
385322
+ }
385323
+ });
385324
+ });
385325
+ }
385326
+ function makeRestCatchAllHandler(opts) {
385327
+ const routes = defaultRestRoutes();
385328
+ return async (req, res) => {
385329
+ const r = req;
385330
+ const s = res;
385331
+ try {
385332
+ if (!await opts.isAuthorized(r)) {
385333
+ s.writeHead?.(401, JSON_HEADERS);
385334
+ s.end?.(JSON.stringify({ error: "unauthorized" }));
385335
+ return;
385336
+ }
385337
+ const url2 = new URL(r.url ?? "/", "http://localhost");
385338
+ const body = r.method === "POST" || r.method === "PUT" ? await readJsonBody(r) : Object.fromEntries(url2.searchParams.entries());
385339
+ const result = await handleRestRequest(routes, opts.dispatch, {
385340
+ method: r.method ?? "GET",
385341
+ path: url2.pathname,
385342
+ body
385343
+ });
385344
+ s.writeHead?.(result.status, { ...JSON_HEADERS, ...result.headers ?? {} });
385345
+ if (result.body === "" || result.body === void 0) s.end?.();
385346
+ else s.end?.(typeof result.body === "string" ? result.body : JSON.stringify(result.body));
385347
+ } catch (e) {
385348
+ s.writeHead?.(500, JSON_HEADERS);
385349
+ s.end?.(JSON.stringify({ error: "internal", message: e instanceof Error ? e.message : String(e) }));
385350
+ }
385351
+ };
385352
+ }
385108
385353
 
385109
385354
  // ../../packages/backend/src/services/Gateway/gatewayRateLimit.ts
385110
385355
  var GatewayRateLimiter = class {
@@ -396172,51 +396417,8 @@ async function startGyBackend() {
396172
396417
  return await target.handleRequest(method, params);
396173
396418
  };
396174
396419
  const buildRestHttpRoutesSync = (opts) => {
396175
- const routes = defaultRestRoutes();
396176
- const staticRoutes = routes.filter((r) => !r.path.includes(":")).map((route) => ({
396177
- path: route.path,
396178
- handler: makeRestHandler(routes, opts)
396179
- }));
396180
- return staticRoutes;
396420
+ return [{ path: "/api/v1/*", handler: makeRestCatchAllHandler(opts) }];
396181
396421
  };
396182
- const makeRestHandler = (routes, opts) => {
396183
- return async (req, res) => {
396184
- const r = req;
396185
- const s = res;
396186
- try {
396187
- if (!await opts.isAuthorized(r)) {
396188
- s.writeHead?.(401, { "content-type": "application/json" });
396189
- s.end?.(JSON.stringify({ error: "unauthorized" }));
396190
- return;
396191
- }
396192
- const url2 = new URL(r.url ?? "/", "http://localhost");
396193
- const body = r.method === "POST" ? await readJsonBody(r) : Object.fromEntries(url2.searchParams.entries());
396194
- const result = await handleRestRequest(routes, opts.dispatch, {
396195
- method: r.method ?? "GET",
396196
- path: url2.pathname,
396197
- body
396198
- });
396199
- s.writeHead?.(result.status, { "content-type": "application/json" });
396200
- s.end?.(JSON.stringify(result.body));
396201
- } catch (e) {
396202
- s.writeHead?.(500, { "content-type": "application/json" });
396203
- s.end?.(JSON.stringify({ error: "internal", message: e instanceof Error ? e.message : String(e) }));
396204
- }
396205
- };
396206
- };
396207
- const readJsonBody = (req) => new Promise((resolve2) => {
396208
- let data = "";
396209
- req.on?.("data", (d) => {
396210
- data += String(d ?? "");
396211
- });
396212
- req.on?.("end", () => {
396213
- try {
396214
- resolve2(data ? JSON.parse(data) : {});
396215
- } catch {
396216
- resolve2({});
396217
- }
396218
- });
396219
- });
396220
396422
  const wsGatewayControlService = new WebSocketGatewayControlService({
396221
396423
  createAdapter: (host, port, ipFilter) => {
396222
396424
  const adapter = new WebSocketGatewayAdapter(gatewayService, {
@@ -396712,6 +396914,9 @@ async function startGyBackend() {
396712
396914
  console.log(
396713
396915
  `[gybackend] Live dashboard: http://${wsState.host}:${wsState.port}/dashboard`
396714
396916
  );
396917
+ console.log(
396918
+ `[gybackend] REST API: http://${wsState.host}:${wsState.port}/api/v1/health (OpenAPI /api/v1/openapi.json)`
396919
+ );
396715
396920
  } else {
396716
396921
  console.log("[gybackend] WebSocket RPC endpoint: disabled");
396717
396922
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rterm-backend",
3
- "version": "3.7.6",
3
+ "version": "3.8.0",
4
4
  "description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit.",
5
5
  "keywords": [
6
6
  "forward-deployed-engineer",