@tea-agent/loop-agent 0.43.0-next.2 → 0.43.0-next.4

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 (92) hide show
  1. package/CHANGELOG.md +15 -12
  2. package/dist/application/dag/run-dag.js +5 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +274 -172
  5. package/dist/shared/frontend-execution-policy.js +1 -0
  6. package/dist/worker/console/chat/assistant-content.js +23 -2
  7. package/dist/worker/console/chat/explore-tools.js +13 -0
  8. package/dist/worker/console/chat/pi-runtime.js +18 -0
  9. package/dist/worker/console/chat/routes.js +11 -0
  10. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  11. package/dist/worker/console/chat/terminal-routes.js +216 -0
  12. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  13. package/dist/worker/console/chat/terminal-tools.js +199 -0
  14. package/dist/worker/console/chat/tool-preview.js +11 -0
  15. package/dist/worker/console/chat/tools.js +1 -0
  16. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CCj36aaC.js} +1 -1
  17. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-Cl3MIVFh.js} +1 -1
  18. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-C_n4kMJS.js} +1 -1
  19. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-DlWYJ7cK.js} +1 -1
  20. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-Ud5pS7vS.js} +1 -1
  21. package/dist/worker/console/static/assets/channel-CEMRpNm-.js +1 -0
  22. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-CQPwOv_j.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-CMzjxrWI.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-pcnXnD6t.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CEB3AvU2.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-DSKnbXbp.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-CS6-PjCZ.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-f6q8Tnzz.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-DkB0QX3q.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-BdB4jAv6.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-BVm1bnX6.js} +1 -1
  32. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C9lhaZkR.js +1 -0
  33. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C9lhaZkR.js +1 -0
  34. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-FV_P-IEp.js} +1 -1
  35. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BM7RtkA2.js} +1 -1
  36. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-C1BZwTG5.js} +1 -1
  37. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-C6McNKmY.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-C0aWRUaE.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-Bfk_SKE8.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-prx1T4qf.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-DBvty0eU.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-CF0em8e9.js} +1 -1
  43. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CF90b4Ws.js} +1 -1
  44. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-hfunxzof.js} +1 -1
  45. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-D-QSy4Nl.js} +1 -1
  46. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-DaBGNcvT.js} +1 -1
  47. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-BChcubGL.js} +1 -1
  48. package/dist/worker/console/static/assets/index-CJkCY1XR.js +464 -0
  49. package/dist/worker/console/static/assets/index-DUBCbiqv.css +1 -0
  50. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BkC67fIa.js} +1 -1
  51. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-ibhLczCD.js} +1 -1
  52. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DCRMitay.js} +1 -1
  53. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-DJ_hKaYI.js} +1 -1
  54. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-CIlLsYsA.js} +1 -1
  55. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-C-5IQMP_.js} +5 -5
  56. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-TCzV5TQT.js} +1 -1
  57. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-bfhd2BQH.js} +1 -1
  58. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-BxFRAm5N.js} +1 -1
  59. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-C-m12lxv.js} +1 -1
  60. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CwmLiGgx.js} +1 -1
  61. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-oc0Q1jiD.js} +1 -1
  62. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-9Ght6Yur.js} +1 -1
  63. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-ndm3lyz9.js} +1 -1
  64. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-7sPlKVcw.js} +1 -1
  65. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-Izl21-oS.js} +1 -1
  66. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CmElLFpP.js +1 -0
  67. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-CtO3D65o.js} +2 -2
  68. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DKRbGG8V.js +8 -0
  69. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-Cyg5cY7z.js} +1 -1
  70. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-CwtE_TFK.js} +1 -1
  71. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Dtt9v8OO.js} +1 -1
  72. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B2Y3ryb6.js} +1 -1
  73. package/dist/worker/console/static/index.html +2 -2
  74. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  75. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  76. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  77. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  78. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  79. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  80. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  81. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  82. package/dist/worker/console/workspace-context.js +9 -1
  83. package/dist/workflows/dag/frontend-shape.js +30 -1
  84. package/dist/workflows/dag/node-execution.js +6 -0
  85. package/package.json +6 -3
  86. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  87. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  88. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  89. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  90. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  91. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  92. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -71,6 +71,10 @@ export const SECRET_SCRUB_PATTERNS = [
71
71
  name: "private-key-block",
72
72
  re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/g,
73
73
  },
74
+ {
75
+ name: "aimax-auth-token",
76
+ re: /\bAIMAX_AUTH_TOKEN=["']?[^\s"']+/g,
77
+ },
74
78
  ];
75
79
  /** Throw a typed explore error (kept as a value object so callers can read .code). */
76
80
  export class ExploreDeniedError extends Error {
@@ -89,6 +93,10 @@ export class ExploreDeniedError extends Error {
89
93
  */
90
94
  export function scrubSecrets(input) {
91
95
  let out = input;
96
+ const liveToken = process.env.AIMAX_AUTH_TOKEN;
97
+ if (liveToken && liveToken.length >= 8 && out.includes(liveToken)) {
98
+ out = out.split(liveToken).join("[REDACTED:aimax-auth-token]");
99
+ }
92
100
  const redactions = [];
93
101
  for (const { name, re } of SECRET_SCRUB_PATTERNS) {
94
102
  re.lastIndex = 0;
@@ -98,6 +106,11 @@ export function scrubSecrets(input) {
98
106
  redactions.push({ name, count: matches.length });
99
107
  }
100
108
  }
109
+ if (liveToken && liveToken.length >= 8 && input.includes(liveToken)) {
110
+ const existing = redactions.find((entry) => entry.name === "aimax-auth-token");
111
+ if (!existing)
112
+ redactions.push({ name: "aimax-auth-token", count: 1 });
113
+ }
101
114
  return { scrubbed: out, redactions };
102
115
  }
103
116
  /** True if a repo-relative path matches a sensitive-file denylist pattern. */
@@ -43,6 +43,7 @@ import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.
43
43
  import { filterActiveInterviewTools } from "../interview/tools.js";
44
44
  import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
45
45
  import { buildTodoContinuationContext, } from "./todos.js";
46
+ import { buildPersistentTerminalTools } from "./terminal-tools.js";
46
47
  import { buildGoalContinuationContext, } from "./goals.js";
47
48
  import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
48
49
  /**
@@ -1054,6 +1055,9 @@ export class ConsolePiRuntime {
1054
1055
  },
1055
1056
  });
1056
1057
  }
1058
+ async syncAimaxAuthToken() {
1059
+ await this.options.sddDataAlignment?.auth?.getToken();
1060
+ }
1057
1061
  /** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
1058
1062
  get resourcePreferencesPath() {
1059
1063
  return this.options.resourcePreferencesPath;
@@ -1341,6 +1345,7 @@ export class ConsolePiRuntime {
1341
1345
  * `initializing: true`; dependent routes wait on the same promise.
1342
1346
  */
1343
1347
  async createSession(init) {
1348
+ await this.syncAimaxAuthToken();
1344
1349
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
1345
1350
  this.disposedSessions.delete(sessionId);
1346
1351
  const sessionManager = await this.bindings.createSessionManager({
@@ -1438,6 +1443,7 @@ export class ConsolePiRuntime {
1438
1443
  async materializeSession(sessionId, init) {
1439
1444
  if (this.disposedSessions.has(sessionId))
1440
1445
  return;
1446
+ await this.syncAimaxAuthToken();
1441
1447
  const agentDir = await safeGetAgentDir(this.bindings);
1442
1448
  // Capture before services parse models.json. A concurrent edit can therefore
1443
1449
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
@@ -1547,6 +1553,16 @@ export class ConsolePiRuntime {
1547
1553
  const goalTools = this.options.goalRegistry
1548
1554
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
1549
1555
  : [];
1556
+ const terminalTools = this.options.terminalRegistry
1557
+ ? await buildPersistentTerminalTools({
1558
+ ownerSessionId: sessionId,
1559
+ registry: this.options.terminalRegistry,
1560
+ defaultCwd: this.options.cwd,
1561
+ }).catch((error) => {
1562
+ process.stderr.write(`[console] chat terminal-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1563
+ return [];
1564
+ })
1565
+ : [];
1550
1566
  const agentTool = await buildAgentCustomTool({
1551
1567
  parentSessionId: sessionId,
1552
1568
  orchestrator: this.exploreOrchestrator,
@@ -1571,6 +1587,7 @@ export class ConsolePiRuntime {
1571
1587
  ...(questionTool ? [questionTool] : []),
1572
1588
  ...(todoTool ? [todoTool] : []),
1573
1589
  ...goalTools,
1590
+ ...terminalTools,
1574
1591
  ...(agentTool ? [agentTool] : []),
1575
1592
  ];
1576
1593
  }
@@ -2347,6 +2364,7 @@ export class ConsolePiRuntime {
2347
2364
  * than silently creating a fresh empty session and losing prior context.
2348
2365
  */
2349
2366
  async reopenSession(init) {
2367
+ await this.syncAimaxAuthToken();
2350
2368
  const sessionId = init.sessionId;
2351
2369
  // Wait for in-flight create materialization rather than racing reopen.
2352
2370
  const initState = this.sessionInit.get(sessionId);
@@ -44,6 +44,8 @@ import { buildGoalCompactionInstructions, buildGoalContinuationContext, } from "
44
44
  import { classifyGoalRoundAdmission, } from "./goal-round-driver.js";
45
45
  import { isTerminalOperationState } from "../../../infrastructure/console/operation-store.js";
46
46
  import { SddDataAlignmentError, } from "./sdd-data-alignment.js";
47
+ import { TerminalSessionRegistry } from "./terminal-sessions.js";
48
+ import { handleTerminalRequest } from "./terminal-routes.js";
47
49
  function projectBrowserChatSession(record) {
48
50
  const { telemetrySessionId, ...projected } = record;
49
51
  void telemetrySessionId;
@@ -4146,6 +4148,15 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4146
4148
  export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
4147
4149
  if (!deps)
4148
4150
  return false;
4151
+ // Terminal management API (model + Console UI shared PTY registry).
4152
+ if (await handleTerminalRequest(req, res, {
4153
+ terminals: deps.terminals ?? new TerminalSessionRegistry(),
4154
+ gateMutation: (r) => gateMutation(r, deps),
4155
+ // Terminals open in the workspace repo root, not the Console process cwd.
4156
+ defaultCwd: deps.appData.repoRoot,
4157
+ }, pathname)) {
4158
+ return true;
4159
+ }
4149
4160
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
4150
4161
  !pathname.startsWith("/api/operator/v1/pi/"))
4151
4162
  return false;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createDecipheriv, randomBytes } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -10,8 +10,49 @@ export const SDD_SKILL_NAMES = [
10
10
  /** Backward-compatible name for callers that need a stable representative value. */
11
11
  export const SDD_DEFAULT_SKILL_NAME = "SDD-implementation-test-review";
12
12
  export const SDD_TELEMETRY_URL = "https://wizard-ai.paic.com.cn/code_pilot/api/skill-usage-log";
13
- export const SDD_TELEMETRY_SIGN = "52aad0c09e6b46818a11ee702d6fe0ba";
13
+ /** SM4 key hex; also the SDD telemetry `ws-sign` value. */
14
+ export const DEFAULT_AI_MA_PUB_SALT_KEY = "52aad0c09e6b46818a11ee702d6fe0ba";
15
+ export const SDD_TELEMETRY_SIGN = DEFAULT_AI_MA_PUB_SALT_KEY;
16
+ export const AIMAX_AUTH_TOKEN_ENV = "AIMAX_AUTH_TOKEN";
14
17
  export const WIZARD_LOCAL_DEFAULT_USER_AGENT = "Builder/OpenCode ai-sdk/provider-utils/4.0.21 runtime/bun/1.3.11";
18
+ const SM4_V1_PREFIX = "sm4:v1:";
19
+ /** Decode a plaintext ticket or `sm4:v1:` ciphertext. Unknown prefixes and crypto errors fail closed. */
20
+ export function decodeAimaxAuthToken(raw, keyHex = DEFAULT_AI_MA_PUB_SALT_KEY) {
21
+ const candidate = raw.trim();
22
+ if (!candidate)
23
+ return undefined;
24
+ if (!candidate.startsWith("sm4:"))
25
+ return candidate;
26
+ if (!candidate.startsWith(SM4_V1_PREFIX))
27
+ return undefined;
28
+ const hex = candidate.slice(SM4_V1_PREFIX.length);
29
+ if (!hex || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex))
30
+ return undefined;
31
+ const key = Buffer.from(keyHex.trim(), "hex");
32
+ if (key.length !== 16)
33
+ return undefined;
34
+ try {
35
+ const encrypted = Buffer.from(hex, "hex");
36
+ if (encrypted.length === 0 || encrypted.length % 16 !== 0)
37
+ return undefined;
38
+ const decipher = createDecipheriv("sm4-ecb", key, null);
39
+ decipher.setAutoPadding(true);
40
+ const plain = Buffer.concat([decipher.update(encrypted), decipher.final()])
41
+ .toString("utf8")
42
+ .trim();
43
+ return plain || undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** Set or delete `AIMAX_AUTH_TOKEN`. Empty string is treated as absent. */
50
+ export function syncAimaxAuthTokenEnv(env, token) {
51
+ if (token)
52
+ env[AIMAX_AUTH_TOKEN_ENV] = token;
53
+ else
54
+ delete env[AIMAX_AUTH_TOKEN_ENV];
55
+ }
15
56
  function sameAuthFileVersion(left, right) {
16
57
  if (!left.exists || !right.exists)
17
58
  return left.exists === right.exists;
@@ -21,9 +62,22 @@ function sameAuthFileVersion(left, right) {
21
62
  /** Reads ~/.aima-go/auth.json with metadata-based parse caching and fail-closed semantics. */
22
63
  export class AimaxAuthReader {
23
64
  authFile;
65
+ env;
24
66
  cached;
25
- constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json")) {
67
+ constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json"), env = process.env) {
26
68
  this.authFile = authFile;
69
+ this.env = env;
70
+ }
71
+ resolveKeyHex() {
72
+ const injected = typeof this.env.AI_MA_PUB_SALT_KEY === "string" ? this.env.AI_MA_PUB_SALT_KEY.trim() : "";
73
+ if (injected)
74
+ return injected;
75
+ const host = process.env.AI_MA_PUB_SALT_KEY?.trim();
76
+ return host || DEFAULT_AI_MA_PUB_SALT_KEY;
77
+ }
78
+ finish(token) {
79
+ syncAimaxAuthTokenEnv(this.env, token);
80
+ return token;
27
81
  }
28
82
  async getToken() {
29
83
  let version;
@@ -34,22 +88,24 @@ export class AimaxAuthReader {
34
88
  }
35
89
  catch {
36
90
  this.cached = { version: { exists: false } };
37
- return undefined;
91
+ return this.finish(undefined);
92
+ }
93
+ if (this.cached && sameAuthFileVersion(this.cached.version, version)) {
94
+ return this.finish(this.cached.token);
38
95
  }
39
- if (this.cached && sameAuthFileVersion(this.cached.version, version))
40
- return this.cached.token;
41
96
  let token;
42
97
  try {
43
98
  const parsed = JSON.parse(await readFile(this.authFile, "utf8"));
44
99
  const candidate = parsed["aimax-auth-token"];
45
- if (typeof candidate === "string" && candidate.trim())
46
- token = candidate;
100
+ if (typeof candidate === "string" && candidate.trim()) {
101
+ token = decodeAimaxAuthToken(candidate, this.resolveKeyHex());
102
+ }
47
103
  }
48
104
  catch {
49
105
  // Malformed or unreadable files are intentionally ineligible.
50
106
  }
51
107
  this.cached = { version, ...(token ? { token } : {}) };
52
- return token;
108
+ return this.finish(token);
53
109
  }
54
110
  async isEligible() {
55
111
  return Boolean(await this.getToken());
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Operator Chat — terminal management HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /terminals list terminal sessions (all owners)
6
+ * POST /terminals spawn { shell, name?, cwd?, ownerSessionId? }
7
+ * GET /terminals/:id session info + buffered output
8
+ * POST /terminals/:id/input { data } write to the PTY
9
+ * POST /terminals/:id/resize { cols, rows }
10
+ * POST /terminals/:id/interrupt send Ctrl+C
11
+ * POST /terminals/:id/kill stop and await full exit
12
+ * GET /terminals/:id/stream SSE: buffered replay + live chunks
13
+ *
14
+ * Mutating routes require the Console boot token (same gate as other chat
15
+ * mutations). The stream is read-only and uses the same origin checks as the
16
+ * chat event stream.
17
+ */
18
+ import { TerminalSessionError, availableShellKinds, } from "./terminal-sessions.js";
19
+ function sendJson(res, status, body) {
20
+ const payload = JSON.stringify(body);
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(payload);
26
+ }
27
+ function mapTerminalError(res, error) {
28
+ if (error instanceof TerminalSessionError) {
29
+ const status = error.code === "TERMINAL_NOT_FOUND"
30
+ ? 404
31
+ : error.code === "TERMINAL_EXITED" || error.code === "TERMINAL_SEND_ACTIVE"
32
+ ? 409
33
+ : 500;
34
+ sendJson(res, status, {
35
+ ok: false,
36
+ error: { code: error.code, message: error.message },
37
+ });
38
+ return;
39
+ }
40
+ sendJson(res, 500, {
41
+ ok: false,
42
+ error: {
43
+ code: "TERMINAL_INTERNAL",
44
+ message: error instanceof Error ? error.message : String(error),
45
+ },
46
+ });
47
+ }
48
+ async function readJsonBody(req, maxBytes = 512 * 1024) {
49
+ const chunks = [];
50
+ let total = 0;
51
+ for await (const chunk of req) {
52
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
53
+ total += buf.length;
54
+ if (total > maxBytes)
55
+ throw new Error("request body too large");
56
+ chunks.push(buf);
57
+ }
58
+ if (chunks.length === 0)
59
+ return {};
60
+ const raw = Buffer.concat(chunks).toString("utf8");
61
+ if (!raw.trim())
62
+ return {};
63
+ return JSON.parse(raw);
64
+ }
65
+ /** Route dispatcher; returns true when the request was handled. */
66
+ export async function handleTerminalRequest(req, res, deps, pathname) {
67
+ if (!pathname.startsWith("/api/operator/v1/chat/terminals"))
68
+ return false;
69
+ const method = (req.method ?? "GET").toUpperCase();
70
+ const rest = pathname.slice("/api/operator/v1/chat/terminals".length);
71
+ const parts = rest.split("/").filter(Boolean);
72
+ try {
73
+ // GET /terminals — list (+ available shell kinds for this platform)
74
+ if (method === "GET" && parts.length === 0) {
75
+ sendJson(res, 200, {
76
+ ok: true,
77
+ data: {
78
+ terminals: deps.terminals.list(),
79
+ availableShells: availableShellKinds().kinds,
80
+ },
81
+ });
82
+ return true;
83
+ }
84
+ // POST /terminals — spawn
85
+ if (method === "POST" && parts.length === 0) {
86
+ const gate = deps.gateMutation(req);
87
+ if (!gate.ok) {
88
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
89
+ return true;
90
+ }
91
+ const body = await readJsonBody(req);
92
+ const kinds = availableShellKinds().kinds;
93
+ const shell = kinds.includes(body.shell)
94
+ ? body.shell
95
+ : undefined;
96
+ if (!shell) {
97
+ sendJson(res, 400, {
98
+ ok: false,
99
+ error: {
100
+ code: "INVALID_INPUT",
101
+ message: `shell must be one of: ${kinds.join(", ")} (available on this platform)`,
102
+ },
103
+ });
104
+ return true;
105
+ }
106
+ const info = deps.terminals.spawn({
107
+ ownerSessionId: typeof body.ownerSessionId === "string" && body.ownerSessionId
108
+ ? body.ownerSessionId
109
+ : "console-ui",
110
+ name: typeof body.name === "string" && body.name ? body.name : `${shell}-ui`,
111
+ shell: shell,
112
+ cwd: typeof body.cwd === "string" && body.cwd
113
+ ? body.cwd
114
+ : (deps.defaultCwd ?? undefined),
115
+ cols: typeof body.cols === "number" ? body.cols : undefined,
116
+ rows: typeof body.rows === "number" ? body.rows : undefined,
117
+ });
118
+ sendJson(res, 201, { ok: true, data: info });
119
+ return true;
120
+ }
121
+ const sessionId = parts[0];
122
+ if (!sessionId)
123
+ return false;
124
+ const sub = parts[1] ?? "";
125
+ // GET /terminals/:id — info + buffered output
126
+ if (method === "GET" && parts.length === 1) {
127
+ const info = deps.terminals.get(sessionId);
128
+ const output = deps.terminals.readAll(sessionId);
129
+ sendJson(res, 200, { ok: true, data: { ...info, output } });
130
+ return true;
131
+ }
132
+ // GET /terminals/:id/stream — SSE
133
+ if (method === "GET" && sub === "stream") {
134
+ // Validate existence first (404 before opening the stream).
135
+ const info = deps.terminals.get(sessionId);
136
+ void info;
137
+ res.writeHead(200, {
138
+ "content-type": "text/event-stream; charset=utf-8",
139
+ "cache-control": "no-cache, no-store",
140
+ connection: "keep-alive",
141
+ "x-accel-buffering": "no",
142
+ });
143
+ const writeFrame = (event, data) => {
144
+ if (res.writableEnded)
145
+ return;
146
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
147
+ };
148
+ const buffered = deps.terminals.readAll(sessionId);
149
+ writeFrame("snapshot", {
150
+ truncated: buffered.truncated,
151
+ totalOutputChars: buffered.totalOutputChars,
152
+ text: buffered.text,
153
+ info: deps.terminals.get(sessionId),
154
+ });
155
+ const unsubscribe = deps.terminals.subscribe(sessionId, (chunk) => {
156
+ writeFrame("data", { chunk });
157
+ });
158
+ const heartbeat = setInterval(() => {
159
+ if (!res.writableEnded)
160
+ res.write(": heartbeat\n\n");
161
+ }, 15_000);
162
+ res.once("close", () => {
163
+ clearInterval(heartbeat);
164
+ unsubscribe();
165
+ });
166
+ return true;
167
+ }
168
+ // Everything else mutates → gate first.
169
+ const gate = deps.gateMutation(req);
170
+ if (!gate.ok) {
171
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
172
+ return true;
173
+ }
174
+ if (method === "POST" && sub === "input" && parts.length === 2) {
175
+ const body = await readJsonBody(req);
176
+ if (typeof body.data !== "string") {
177
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "data (string) is required" } });
178
+ return true;
179
+ }
180
+ deps.terminals.send(sessionId, body.data);
181
+ sendJson(res, 200, { ok: true });
182
+ return true;
183
+ }
184
+ if (method === "POST" && sub === "resize" && parts.length === 2) {
185
+ const body = await readJsonBody(req);
186
+ const cols = Number(body.cols);
187
+ const rows = Number(body.rows);
188
+ if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 2 || rows < 2 || cols > 500 || rows > 200) {
189
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "cols and rows (integers 2..500/200) are required" } });
190
+ return true;
191
+ }
192
+ deps.terminals.resize(sessionId, cols, rows);
193
+ sendJson(res, 200, { ok: true });
194
+ return true;
195
+ }
196
+ if (method === "POST" && sub === "interrupt" && parts.length === 2) {
197
+ deps.terminals.interrupt(sessionId);
198
+ sendJson(res, 200, { ok: true });
199
+ return true;
200
+ }
201
+ if (method === "POST" && sub === "kill" && parts.length === 2) {
202
+ await deps.terminals.kill(sessionId);
203
+ sendJson(res, 200, { ok: true });
204
+ return true;
205
+ }
206
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown terminal route: ${method} ${pathname}` } });
207
+ return true;
208
+ }
209
+ catch (error) {
210
+ if (!res.headersSent)
211
+ mapTerminalError(res, error);
212
+ else
213
+ res.end();
214
+ return true;
215
+ }
216
+ }