@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.
- package/CHANGELOG.md +15 -12
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +274 -172
- package/dist/shared/frontend-execution-policy.js +1 -0
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +18 -0
- package/dist/worker/console/chat/routes.js +11 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CCj36aaC.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-Cl3MIVFh.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-C_n4kMJS.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-DlWYJ7cK.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-Ud5pS7vS.js} +1 -1
- package/dist/worker/console/static/assets/channel-CEMRpNm-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-CQPwOv_j.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-CMzjxrWI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-pcnXnD6t.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CEB3AvU2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-DSKnbXbp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-CS6-PjCZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-f6q8Tnzz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-DkB0QX3q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-BdB4jAv6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-BVm1bnX6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C9lhaZkR.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C9lhaZkR.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-FV_P-IEp.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-BM7RtkA2.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-C1BZwTG5.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-C6McNKmY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-C0aWRUaE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-Bfk_SKE8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-prx1T4qf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-DBvty0eU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-CF0em8e9.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CF90b4Ws.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-hfunxzof.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-D-QSy4Nl.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-DaBGNcvT.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-BChcubGL.js} +1 -1
- package/dist/worker/console/static/assets/index-CJkCY1XR.js +464 -0
- package/dist/worker/console/static/assets/index-DUBCbiqv.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BkC67fIa.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-ibhLczCD.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DCRMitay.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-DJ_hKaYI.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-CIlLsYsA.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-C-5IQMP_.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-TCzV5TQT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-bfhd2BQH.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-BxFRAm5N.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-C-m12lxv.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-CwmLiGgx.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-oc0Q1jiD.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-9Ght6Yur.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-ndm3lyz9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-7sPlKVcw.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-Izl21-oS.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CmElLFpP.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-CtO3D65o.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DKRbGG8V.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-Cyg5cY7z.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-CwtE_TFK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-Dtt9v8OO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-B2Y3ryb6.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +9 -1
- package/dist/workflows/dag/frontend-shape.js +30 -1
- package/dist/workflows/dag/node-execution.js +6 -0
- package/package.json +6 -3
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- 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
|
-
|
|
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
|
+
}
|