@abstractframework/code 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,8 +40,14 @@ Gateway browser session and stores only app-scoped session cookies. The raw
40
40
  token is not persisted in browser settings. Vite development and the packaged
41
41
  server use the same session proxy; the browser keeps API requests same-origin.
42
42
 
43
- Choose an agent or registered workflow in the toolbar. Agent tasks use the
44
- composer; structured workflows use **Inputs**. Questions, tool approvals, and
43
+ Choose a workflow in the toolbar: **Gateway default** (the coding agent your
44
+ gateway's operator set, resolved by the gateway when the turn starts), a
45
+ published coding agent, or, with **Show all workflows**, any registered
46
+ workflow. The **Files** tab shows the conversation's workspace on the gateway
47
+ with previews, **Settings → Stream replies** shows replies as the model writes
48
+ them (on gateways that support it), and **About** lists the app's and the
49
+ gateway's versions. Agent tasks use the composer; structured workflows use
50
+ **Inputs**. Questions, tool approvals, and
45
51
  event waits appear in the conversation. Runs and history remain on the gateway
46
52
  when you close the browser. Unsent drafts and queued turns do not survive reload.
47
53
 
package/bin/server.js CHANGED
@@ -96,6 +96,34 @@ function isLoopbackPeer(req) {
96
96
  );
97
97
  }
98
98
 
99
+ /**
100
+ * The browser connection's real transport peer (req.socket.remoteAddress),
101
+ * IPv4-mapped IPv6 unwrapped, or "" when unknown. Headers are never read.
102
+ * Every request this server sends to the gateway on a browser's behalf
103
+ * carries it as `X-Forwarded-For`, OVERWRITING any client value (contract
104
+ * A-2; same rule as abstractuic's app-server). An unknown peer is refused.
105
+ */
106
+ export function socketPeerAddress(req) {
107
+ let address = String(req?.socket?.remoteAddress || "").trim().toLowerCase();
108
+ if (address.startsWith("::ffff:") && address.includes(".")) address = address.slice(7);
109
+ return address;
110
+ }
111
+
112
+ const UNKNOWN_PEER = { detail: "Cannot determine the client address of this connection" };
113
+ /** Marks every request this server sends to the gateway as coming through
114
+ * the Code app proxy (the gateway's same-machine fail-safe keys on it). A
115
+ * browser-supplied value is always dropped and replaced. */
116
+ const APP_PROXY_HEADER = "X-AbstractFramework-App-Proxy";
117
+ const APP_PROXY_NAME = "code";
118
+ const FORWARDING_HEADERS = new Set([
119
+ "x-abstractframework-app-proxy",
120
+ "x-forwarded-for",
121
+ "x-forwarded-host",
122
+ "x-forwarded-proto",
123
+ "x-real-ip",
124
+ "forwarded",
125
+ ]);
126
+
99
127
  function connectionConfigAllowed(req, config) {
100
128
  if (envBool(config.env, "ABSTRACTCODE_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG"))
101
129
  return true;
@@ -387,6 +415,11 @@ function cookieValueFromSetCookie(rawHeaders, name) {
387
415
  }
388
416
 
389
417
  async function handleConnectionApi(req, res, config) {
418
+ const peer = socketPeerAddress(req);
419
+ if (!peer) {
420
+ sendJson(res, 400, UNKNOWN_PEER);
421
+ return;
422
+ }
390
423
  if (req.method === "GET") {
391
424
  const session = browserSession(req, config);
392
425
  if (!session.sessionId) {
@@ -406,6 +439,8 @@ async function handleConnectionApi(req, res, config) {
406
439
  headers: {
407
440
  Accept: "application/json",
408
441
  "X-AbstractGateway-Session": session.sessionId,
442
+ "X-Forwarded-For": peer,
443
+ [APP_PROXY_HEADER]: APP_PROXY_NAME,
409
444
  },
410
445
  },
411
446
  undefined,
@@ -454,6 +489,8 @@ async function handleConnectionApi(req, res, config) {
454
489
  Accept: "application/json",
455
490
  "Content-Type": "application/json",
456
491
  "Content-Length": String(body.length),
492
+ "X-Forwarded-For": peer,
493
+ [APP_PROXY_HEADER]: APP_PROXY_NAME,
457
494
  },
458
495
  },
459
496
  body,
@@ -511,6 +548,8 @@ async function handleConnectionApi(req, res, config) {
511
548
  "Content-Length": String(body.length),
512
549
  "X-AbstractGateway-Session": session.sessionId,
513
550
  "X-AbstractGateway-CSRF": session.csrfToken,
551
+ "X-Forwarded-For": peer,
552
+ [APP_PROXY_HEADER]: APP_PROXY_NAME,
514
553
  },
515
554
  },
516
555
  body,
@@ -541,6 +580,11 @@ function proxyGatewayRequest(req, res, config) {
541
580
  sendJson(res, 401, { detail: "Gateway sign-in required" });
542
581
  return;
543
582
  }
583
+ const peer = socketPeerAddress(req);
584
+ if (!peer) {
585
+ sendJson(res, 400, UNKNOWN_PEER);
586
+ return;
587
+ }
544
588
  if (mutatingMethod(req.method)) {
545
589
  const presented = String(req.headers["x-abstractcode-csrf"] || "").trim();
546
590
  if (!session.csrfToken || presented !== session.csrfToken) {
@@ -563,9 +607,13 @@ function proxyGatewayRequest(req, res, config) {
563
607
  const headers = { ...req.headers, host: backend.url.host };
564
608
  delete headers.cookie;
565
609
  delete headers.authorization;
566
- delete headers["x-forwarded-for"];
567
- delete headers["x-forwarded-host"];
568
- delete headers["x-forwarded-proto"];
610
+ // Forwarding headers: drop every client-supplied spelling (any case, plus
611
+ // RFC 7239 `Forwarded` and `X-Real-IP`), then X-Forwarded-For = the socket
612
+ // peer, so the gateway can tell whether the browser is on its machine.
613
+ for (const key of Object.keys(headers))
614
+ if (FORWARDING_HEADERS.has(key.toLowerCase())) delete headers[key];
615
+ headers["x-forwarded-for"] = peer;
616
+ headers["x-abstractframework-app-proxy"] = APP_PROXY_NAME;
569
617
  delete headers["x-abstractcode-csrf"];
570
618
  headers["x-abstractgateway-session"] = session.sessionId;
571
619
  if (mutatingMethod(req.method))