rterm-backend 3.7.7 → 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 +181 -55
  2. package/package.json +1 -1
package/bin/gybackend.cjs CHANGED
@@ -296501,6 +296501,8 @@ var PSRPTransport = class {
296501
296501
  /** pypsrp WSMan.session_id — one uuid for the life of the transport. */
296502
296502
  sessionId = `uuid:${(0, import_node_crypto3.randomUUID)().toUpperCase()}`;
296503
296503
  http;
296504
+ /** Persistent runspace (v3.8.0): reuse one Microsoft.PowerShell shell across commands. */
296505
+ pool = null;
296504
296506
  constructor(opts) {
296505
296507
  this.http = new WinHttpAuth({
296506
296508
  host: opts.host,
@@ -296524,6 +296526,7 @@ var PSRPTransport = class {
296524
296526
  const wsmv = "http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd";
296525
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>`;
296526
296528
  }
296529
+ /** Test hook: subclasses may override. */
296527
296530
  async post(action, body, extraHeaders) {
296528
296531
  const envelope = this.envelope(action, body, extraHeaders);
296529
296532
  const res = await this.http.post(envelope);
@@ -296537,6 +296540,57 @@ var PSRPTransport = class {
296537
296540
  * Receive loop (PIPELINE_OUTPUT / ERROR_RECORD / PIPELINE_STATE) → Delete.
296538
296541
  * The script travels inside the PSRP message body — no command-line length limit.
296539
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
+ }
296540
296594
  async runScript(script, opts) {
296541
296595
  const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
296542
296596
  const rpid = (0, import_node_crypto3.randomUUID)().toUpperCase();
@@ -296569,63 +296623,76 @@ var PSRPTransport = class {
296569
296623
  this.assertOk(commanded, "Command");
296570
296624
  const commandId = firstText2(commanded.body, "CommandId");
296571
296625
  if (!commandId) throw new Error("PSRP Command succeeded but no CommandId returned.");
296572
- let stdout = "";
296573
- let stderr = "";
296574
- let hadErrors = false;
296575
- let exitCode = 0;
296576
- let pipelineDone = false;
296577
- for (; ; ) {
296578
- if (opts?.signal?.aborted) throw new Error("AbortError");
296579
- if (Date.now() > deadline) {
296580
- throw new Error(`PSRP command timed out after ${opts?.timeoutMs ?? 12e4}ms`);
296581
- }
296582
- const receiveBody = `<rsp:Receive><rsp:DesiredStream CommandId="${commandId}">stdout</rsp:DesiredStream></rsp:Receive>`;
296583
- const receiveHeaders = `<wsman:OptionSet s:mustUnderstand="true"><wsman:Option Name="WSMAN_CMDSHELL_OPTION_KEEPALIVE">True</wsman:Option></wsman:OptionSet>` + this.shellHeaders(shellId);
296584
- const received = await this.post(
296585
- "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive",
296586
- receiveBody,
296587
- receiveHeaders
296588
- );
296589
- this.assertOk(received, "Receive");
296590
- const streamRe = /<\w*:?Stream\b[^>]*\bName="(?:stdout|stderr|pr)"[^>]*>([\s\S]*?)<\/\w*:?Stream>/gi;
296591
- let m2;
296592
- while ((m2 = streamRe.exec(received.body)) !== null) {
296593
- const b64 = m2[1].replace(/\s+/g, "");
296594
- if (!b64) continue;
296595
- for (const raw of unfragmentMessages(import_node_buffer2.Buffer.from(b64, "base64"))) {
296596
- if (raw.length < 40) continue;
296597
- const messageType = raw.readUInt32LE(4);
296598
- const payload = raw.subarray(40).toString("utf8").replace(/^\uFEFF/, "");
296599
- if (messageType === MSG.PIPELINE_OUTPUT) {
296600
- const sRe = /<S[^>]*>([\s\S]*?)<\/S>/gi;
296601
- let sm;
296602
- while ((sm = sRe.exec(payload)) !== null) stdout += sm[1];
296603
- } else if (messageType === MSG.ERROR_RECORD) {
296604
- hadErrors = true;
296605
- const mRe = payload.match(/<S N="Message">([\s\S]*?)<\/S>/i);
296606
- stderr += (mRe ? mRe[1] : payload) + "\n";
296607
- } else if (messageType === MSG.PIPELINE_STATE) {
296608
- pipelineDone = true;
296609
- const stateMatch = payload.match(/<I32 N="State">(\d+)<\/I32>/i);
296610
- if (stateMatch) {
296611
- if (parseInt(stateMatch[1], 10) === 5) hadErrors = true;
296612
- }
296613
- const ec = payload.match(/N="ExitCode"[^>]*>(-?\d+)</i);
296614
- if (ec) exitCode = parseInt(ec[1], 10);
296615
- }
296616
- }
296617
- }
296618
- const wsExit = received.body.match(/<\w*:?ExitCode>\s*(-?\d+)\s*<\/\w*:?ExitCode>/i);
296619
- if (wsExit) exitCode = parseInt(wsExit[1], 10);
296620
- if (pipelineDone) break;
296621
- if (/CommandState\/Done/i.test(received.body)) break;
296622
- }
296623
- if (exitCode !== 0) hadErrors = true;
296624
- return { stdout, stderr, exitCode, hadErrors };
296626
+ return await this.receivePipeline(shellId, commandId, deadline, opts);
296625
296627
  } finally {
296626
296628
  await this.deleteShell(shellId);
296627
296629
  }
296628
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
+ }
296629
296696
  /**
296630
296697
  * Drain Receive until RUNSPACEPOOL_STATE reports Opened (state=2).
296631
296698
  * Must run after Create and before Command.
@@ -296830,7 +296897,7 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
296830
296897
  \x1B[36m\u276F ${command}\x1B[0m\r
296831
296898
  `);
296832
296899
  const ps = this.translateCmdToPowerShell(command, instance.cwd);
296833
- const result = await transport.runScript(ps, {
296900
+ const result = await transport.runScriptOnPool(ps, {
296834
296901
  timeoutMs: options?.timeoutMs ?? DEFAULT_WINRM_TIMEOUT_MS,
296835
296902
  signal: options?.signal
296836
296903
  });
@@ -296951,6 +297018,9 @@ Run commands with exec_command / run_fleet_command. Interactive TUI apps are not
296951
297018
  void instance.winrm.deleteShell(instance.persistentShellId);
296952
297019
  instance.persistentShellId = void 0;
296953
297020
  }
297021
+ if (instance.psrp) {
297022
+ void instance.psrp.closePool();
297023
+ }
296954
297024
  instance.exitCallback?.(0);
296955
297025
  }
296956
297026
  onData(ptyId, callback) {
@@ -345290,6 +345360,28 @@ var MessagesZodState = external_exports.object({ messages: withLangGraph(externa
345290
345360
  // ../../node_modules/@langchain/langgraph/dist/index.js
345291
345361
  initializeAsyncLocalStorageSingleton();
345292
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
+
345293
345385
  // ../../packages/backend/src/services/agentRunLedger.ts
345294
345386
  var import_node_fs3 = __toESM(require("node:fs"), 1);
345295
345387
  var import_node_path5 = __toESM(require("node:path"), 1);
@@ -366967,7 +367059,7 @@ function reconcileToolCalls(toolCalls) {
366967
367059
  var TOOL_RESULT_MAX_CHARS = 32768;
366968
367060
  function stringifyToolResult(result) {
366969
367061
  const raw = typeof result === "string" ? result : JSON.stringify(result);
366970
- return clipTextMiddle(raw, TOOL_RESULT_MAX_CHARS);
367062
+ return redactText(clipTextMiddle(raw, TOOL_RESULT_MAX_CHARS));
366971
367063
  }
366972
367064
  function clipTextMiddle(input, maxChars) {
366973
367065
  if (maxChars <= 0) return "";
@@ -373029,6 +373121,26 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
373029
373121
 
373030
373122
  // ../../packages/backend/src/services/Gateway/WebSocketGatewayAdapter.ts
373031
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
373032
373144
  var import_meta7 = {};
373033
373145
  var nodeRequire = (0, import_node_module4.createRequire)(
373034
373146
  typeof __filename !== "undefined" ? __filename : import_meta7.url
@@ -373133,6 +373245,7 @@ var WebSocketGatewayAdapter = class {
373133
373245
  server = null;
373134
373246
  transportIdBySocket = /* @__PURE__ */ new Map();
373135
373247
  isSameMachineBySocket = /* @__PURE__ */ new WeakMap();
373248
+ scopesBySocket = /* @__PURE__ */ new WeakMap();
373136
373249
  serverFactory;
373137
373250
  logger;
373138
373251
  start() {
@@ -373230,6 +373343,12 @@ var WebSocketGatewayAdapter = class {
373230
373343
  this.isLoopbackAddress(String(remote))
373231
373344
  );
373232
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
+ }
373233
373352
  state.authorized = true;
373234
373353
  this.logger.info(
373235
373354
  `[WebSocketGatewayAdapter] Client connected: ${remote} (${transport.id})`
@@ -373527,6 +373646,13 @@ var WebSocketGatewayAdapter = class {
373527
373646
  );
373528
373647
  }
373529
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
+ }
373530
373656
  const params = request.params ?? {};
373531
373657
  if (request.method.startsWith("observability:")) {
373532
373658
  const bridge = this.options.observabilityBridge;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rterm-backend",
3
- "version": "3.7.7",
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",