@epoch-agent/server 0.8.1 → 0.10.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 +54 -0
- package/dist/index.d.ts +610 -43
- package/dist/index.js +452 -70
- package/dist/web/assets/index-D-mlA3qO.js +377 -0
- package/dist/web/assets/index-x_cigNDu.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-Cl7UCWRQ.css +0 -1
- package/dist/web/assets/index-De1G68kR.js +0 -376
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
-
import { t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
|
|
2
|
+
import { createLogger, t, ApprovalRelay, QuestionRelay, serializeStream, withRoleScope, withModelScope, withToolScope, nextRunAt, observeBackgroundTasks, skillIndexDescription, SANDBOX_EXCLUDES, SANDBOX_COVERS, isSystemOpenable, backgroundTaskOutput, listBackgroundTasks, providerLabel, MAX_REASON_CHARS, MAX_OBJECTIVE_CHARS, MAX_GOAL_MAX_ROUNDS, DEFAULT_GOAL_MAX_ROUNDS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_DEFAULTS, coalesceRecording, readRecording } from '@epoch-agent/runtime';
|
|
3
3
|
import { timingSafeEqual, randomBytes } from 'crypto';
|
|
4
|
-
import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType } from '@epoch-agent/protocol';
|
|
4
|
+
import { PLAN_OUTCOMES, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, extractAllMentions, MAX_ATTACH_FILES, utf8ByteLength, MAX_SESSION_REFS, attachSessionSurface, isWireRewindScope, WIRE_REWIND_SCOPES, mcpConnectorState, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_BYTES, truncateToBudget, WIRE_LANG_PARAM, isLang, isQuestionAnswerMap, isProviderType, apiKeyEnvVar, WIRE_FILE_STAT_MAX, WIRE_SETTING_WRITE_LAYERS, isWirePlanAction, WIRE_PLAN_ACTIONS, isPermissionLevel, PERMISSION_LEVELS, conformsToWire, WIRE_TRUST_REQUEST_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, collectPendingApprovals, unfixedRules, isWireSettingWriteLayer, parseModelRef, WIRE_GOAL_ACTIONS, OPERATION_TYPES, isOperationType, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } from '@epoch-agent/protocol';
|
|
5
5
|
import { resolve, join, relative, isAbsolute, sep, dirname, basename, extname } from 'path';
|
|
6
|
-
import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, accessSync, constants } from 'fs';
|
|
6
|
+
import { existsSync, realpathSync, statSync, mkdirSync, createReadStream, readdirSync, createWriteStream, accessSync, constants } from 'fs';
|
|
7
|
+
import { stat, readFile, rm } from 'fs/promises';
|
|
8
|
+
import { pipeline } from 'stream/promises';
|
|
7
9
|
import { homedir } from 'os';
|
|
8
10
|
import { execFile, spawn } from 'child_process';
|
|
9
|
-
import { stat, readFile } from 'fs/promises';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
12
|
|
|
12
13
|
// src/index.ts
|
|
@@ -271,6 +272,22 @@ function toWireSessionWorkspace(bound) {
|
|
|
271
272
|
skippedInstructionFiles: bound.projectContext.skippedInstructions
|
|
272
273
|
};
|
|
273
274
|
}
|
|
275
|
+
function projectInventoryOf(workspaces, sessionId, trustRequest) {
|
|
276
|
+
const root = workspaces.of(sessionId)?.workspace.root;
|
|
277
|
+
return {
|
|
278
|
+
inventory: root === void 0 ? EMPTY_INVENTORY : workspaces.inventoryOf(root),
|
|
279
|
+
trustRequest
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
var EMPTY_INVENTORY = {
|
|
283
|
+
instructionFiles: [],
|
|
284
|
+
skills: 0,
|
|
285
|
+
roles: 0,
|
|
286
|
+
commands: 0,
|
|
287
|
+
hookFiles: [],
|
|
288
|
+
policies: 0,
|
|
289
|
+
settingsFiles: []
|
|
290
|
+
};
|
|
274
291
|
function toWireBinding(state, bound) {
|
|
275
292
|
if (state === "bound" && bound)
|
|
276
293
|
return { state: "bound", workspace: toWireSessionWorkspace(bound) };
|
|
@@ -417,6 +434,7 @@ var SessionHub = class {
|
|
|
417
434
|
registry;
|
|
418
435
|
sinks = /* @__PURE__ */ new Set();
|
|
419
436
|
observers = /* @__PURE__ */ new Set();
|
|
437
|
+
counted = /* @__PURE__ */ new Set();
|
|
420
438
|
requestOwner = /* @__PURE__ */ new Map();
|
|
421
439
|
seq = 0;
|
|
422
440
|
graceTimer = null;
|
|
@@ -474,6 +492,9 @@ var SessionHub = class {
|
|
|
474
492
|
get connectionCount() {
|
|
475
493
|
return this.sinks.size;
|
|
476
494
|
}
|
|
495
|
+
get present() {
|
|
496
|
+
return this.sinks.size + this.counted.size;
|
|
497
|
+
}
|
|
477
498
|
subscribe(sink, lastEventId) {
|
|
478
499
|
sink(this.connectionFrame({ type: "connected" }));
|
|
479
500
|
if (lastEventId !== void 0) {
|
|
@@ -491,18 +512,24 @@ var SessionHub = class {
|
|
|
491
512
|
if (released) return;
|
|
492
513
|
released = true;
|
|
493
514
|
this.sinks.delete(sink);
|
|
494
|
-
if (this.
|
|
515
|
+
if (this.present === 0) this.armGrace();
|
|
495
516
|
};
|
|
496
517
|
}
|
|
497
|
-
observe(sink) {
|
|
518
|
+
observe(sink, opts = {}) {
|
|
498
519
|
if (this.closed) return () => {
|
|
499
520
|
};
|
|
500
521
|
this.observers.add(sink);
|
|
522
|
+
if (opts.counts === true) {
|
|
523
|
+
this.counted.add(sink);
|
|
524
|
+
this.clearGrace();
|
|
525
|
+
}
|
|
501
526
|
let released = false;
|
|
502
527
|
return () => {
|
|
503
528
|
if (released) return;
|
|
504
529
|
released = true;
|
|
505
530
|
this.observers.delete(sink);
|
|
531
|
+
const wasPresent = this.counted.delete(sink);
|
|
532
|
+
if (wasPresent && this.present === 0) this.armGrace();
|
|
506
533
|
};
|
|
507
534
|
}
|
|
508
535
|
start(sessionId, message, decorate) {
|
|
@@ -554,6 +581,14 @@ var SessionHub = class {
|
|
|
554
581
|
this.syncApprovalState(entry);
|
|
555
582
|
return true;
|
|
556
583
|
}
|
|
584
|
+
notifyTasksChanged(sessionId) {
|
|
585
|
+
if (this.closed) return;
|
|
586
|
+
this.publish(sessionId, { type: "tasks-changed" });
|
|
587
|
+
}
|
|
588
|
+
notifySessionsChanged(sessionId) {
|
|
589
|
+
if (this.closed) return;
|
|
590
|
+
this.publish(sessionId, { type: "sessions-changed" });
|
|
591
|
+
}
|
|
557
592
|
shutdown() {
|
|
558
593
|
if (this.closed) return;
|
|
559
594
|
this.closed = true;
|
|
@@ -561,6 +596,7 @@ var SessionHub = class {
|
|
|
561
596
|
this.reap();
|
|
562
597
|
this.sinks.clear();
|
|
563
598
|
this.observers.clear();
|
|
599
|
+
this.counted.clear();
|
|
564
600
|
}
|
|
565
601
|
publish(sessionId, event) {
|
|
566
602
|
const frame = { seq: ++this.seq, sessionId, ts: this.clock(), event };
|
|
@@ -624,7 +660,7 @@ var SessionHub = class {
|
|
|
624
660
|
}
|
|
625
661
|
drainQueue(entry) {
|
|
626
662
|
if (this.closed || !this.registry.has(entry.session.sessionId)) return;
|
|
627
|
-
if (this.
|
|
663
|
+
if (this.present === 0) {
|
|
628
664
|
entry.queue.length = 0;
|
|
629
665
|
return;
|
|
630
666
|
}
|
|
@@ -645,7 +681,7 @@ var SessionHub = class {
|
|
|
645
681
|
this.graceTimer = null;
|
|
646
682
|
}
|
|
647
683
|
reap() {
|
|
648
|
-
if (this.
|
|
684
|
+
if (this.present > 0 && !this.closed) return;
|
|
649
685
|
for (const [, entry] of this.registry.all()) {
|
|
650
686
|
entry.queue.length = 0;
|
|
651
687
|
if (entry.state === "idle") continue;
|
|
@@ -763,6 +799,54 @@ async function readJsonBody(req, limit = JSON_BODY_LIMIT, lang) {
|
|
|
763
799
|
};
|
|
764
800
|
}
|
|
765
801
|
}
|
|
802
|
+
var PLUGIN_UPLOAD_BODY_LIMIT = 50 * 1024 * 1024;
|
|
803
|
+
async function readRawBody(req, limit, lang) {
|
|
804
|
+
const chunks = [];
|
|
805
|
+
let size = 0;
|
|
806
|
+
try {
|
|
807
|
+
for await (const chunk of req) {
|
|
808
|
+
const buf = chunk;
|
|
809
|
+
size += buf.length;
|
|
810
|
+
if (size > limit) {
|
|
811
|
+
req.destroy();
|
|
812
|
+
return { ok: false, status: 413, message: t("web.body_too_large", { limit }, lang) };
|
|
813
|
+
}
|
|
814
|
+
chunks.push(buf);
|
|
815
|
+
}
|
|
816
|
+
} catch (err) {
|
|
817
|
+
return {
|
|
818
|
+
ok: false,
|
|
819
|
+
status: 400,
|
|
820
|
+
message: t("web.body_read_failed", { reason: describe(err) }, lang)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (size === 0) {
|
|
824
|
+
return { ok: false, status: 400, message: t("web.body_empty", void 0, lang) };
|
|
825
|
+
}
|
|
826
|
+
return { ok: true, value: Buffer.concat(chunks) };
|
|
827
|
+
}
|
|
828
|
+
async function streamBodyToFile(req, target, lang) {
|
|
829
|
+
const sink = createWriteStream(target, { flags: "wx" });
|
|
830
|
+
try {
|
|
831
|
+
await pipeline(req, sink);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
await rm(target, { force: true }).catch(() => void 0);
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
status: 400,
|
|
837
|
+
message: t("web.body_read_failed", { reason: describe(err) }, lang)
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
return { ok: true, bytes: (await stat(target)).size };
|
|
842
|
+
} catch (err) {
|
|
843
|
+
return {
|
|
844
|
+
ok: false,
|
|
845
|
+
status: 500,
|
|
846
|
+
message: t("web.body_read_failed", { reason: describe(err) }, lang)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
}
|
|
766
850
|
function describe(err) {
|
|
767
851
|
return err instanceof Error ? err.message : String(err);
|
|
768
852
|
}
|
|
@@ -929,7 +1013,7 @@ function estimateIndexTokens(text) {
|
|
|
929
1013
|
return Math.ceil(text.length / 4);
|
|
930
1014
|
}
|
|
931
1015
|
function toWireMcp(server) {
|
|
932
|
-
const state = server
|
|
1016
|
+
const state = mcpConnectorState(server);
|
|
933
1017
|
return {
|
|
934
1018
|
name: server.name,
|
|
935
1019
|
state,
|
|
@@ -939,7 +1023,7 @@ function toWireMcp(server) {
|
|
|
939
1023
|
source: server.source
|
|
940
1024
|
};
|
|
941
1025
|
}
|
|
942
|
-
function collectCapabilities(runtime, tools, bound, role) {
|
|
1026
|
+
function collectCapabilities(runtime, tools, bound, role, project) {
|
|
943
1027
|
const residency = runtime.skillIndexResidency(role);
|
|
944
1028
|
return {
|
|
945
1029
|
roles: runtime.agentRoles.map((r) => toWireRole(r, runtime.agentRoleNotices)),
|
|
@@ -948,7 +1032,10 @@ function collectCapabilities(runtime, tools, bound, role) {
|
|
|
948
1032
|
mcpServers: runtime.mcpServers.map(toWireMcp),
|
|
949
1033
|
project: {
|
|
950
1034
|
root: bound?.workspace.root ?? null,
|
|
951
|
-
trusted: bound?.trust.trusted ?? false
|
|
1035
|
+
trusted: bound?.trust.trusted ?? false,
|
|
1036
|
+
execTrusted: bound?.trust.exec ?? false,
|
|
1037
|
+
inventory: project.inventory,
|
|
1038
|
+
trustRequest: project.trustRequest
|
|
952
1039
|
}
|
|
953
1040
|
};
|
|
954
1041
|
}
|
|
@@ -1018,6 +1105,81 @@ function replaceLeadingText(message, prompt) {
|
|
|
1018
1105
|
const head = { type: "text", text: prompt };
|
|
1019
1106
|
return [head, ...message.slice(1)];
|
|
1020
1107
|
}
|
|
1108
|
+
async function uploadAttachment(ctx, req, res, sessionId, lang) {
|
|
1109
|
+
const raw = nameParam(req.url);
|
|
1110
|
+
if (raw === void 0) {
|
|
1111
|
+
return sendError(res, 400, "bad-request", t("web.attachment_name_required", void 0, lang));
|
|
1112
|
+
}
|
|
1113
|
+
const control = ctx.runtime.attachments;
|
|
1114
|
+
let placed;
|
|
1115
|
+
try {
|
|
1116
|
+
placed = control.place(sessionId, raw);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
return sendError(res, 500, "attachment-failed", describe2(err));
|
|
1119
|
+
}
|
|
1120
|
+
if (!placed) {
|
|
1121
|
+
return sendError(
|
|
1122
|
+
res,
|
|
1123
|
+
400,
|
|
1124
|
+
"bad-request",
|
|
1125
|
+
t("web.attachment_name_invalid", { name: raw }, lang)
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const out = await streamBodyToFile(req, placed.path, lang);
|
|
1129
|
+
if (!out.ok) return sendError(res, out.status, "bad-body", out.message);
|
|
1130
|
+
control.prune(sessionId);
|
|
1131
|
+
return sendJson(res, 200, {
|
|
1132
|
+
name: placed.name,
|
|
1133
|
+
bytes: out.bytes
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
function attachUploads(ctx, sessionId, names, usedBytes = 0) {
|
|
1137
|
+
const parts = [];
|
|
1138
|
+
const receipts = [];
|
|
1139
|
+
let used = usedBytes;
|
|
1140
|
+
for (const name of names) {
|
|
1141
|
+
const part = readOne(ctx, sessionId, name, used);
|
|
1142
|
+
parts.push(part);
|
|
1143
|
+
if (part.text !== void 0) used += utf8ByteLength(part.text);
|
|
1144
|
+
const { type: _type, text: _text, ...rest } = part;
|
|
1145
|
+
receipts.push(rest);
|
|
1146
|
+
}
|
|
1147
|
+
return { parts, receipts, usedBytes: used };
|
|
1148
|
+
}
|
|
1149
|
+
function readOne(ctx, sessionId, name, usedBytes) {
|
|
1150
|
+
const remaining = MAX_ATTACH_TOTAL_BYTES - usedBytes;
|
|
1151
|
+
const path = ctx.runtime.attachments.resolve(sessionId, name) ?? name;
|
|
1152
|
+
if (remaining <= 0) return { type: "file", path, omitted: "over-limit" };
|
|
1153
|
+
const budget = Math.min(MAX_ATTACH_BYTES, remaining);
|
|
1154
|
+
const out = ctx.runtime.attachments.read(sessionId, name);
|
|
1155
|
+
if (!out.ok) {
|
|
1156
|
+
return {
|
|
1157
|
+
type: "file",
|
|
1158
|
+
path,
|
|
1159
|
+
omitted: out.reason,
|
|
1160
|
+
...out.bytes === void 0 ? {} : { bytes: out.bytes }
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (out.bytes <= budget) {
|
|
1164
|
+
return { type: "file", path: out.path, text: out.text, bytes: out.bytes };
|
|
1165
|
+
}
|
|
1166
|
+
const { text, truncatedTo } = truncateToBudget(new TextEncoder().encode(out.text), budget);
|
|
1167
|
+
return { type: "file", path: out.path, text, truncatedTo, bytes: out.bytes };
|
|
1168
|
+
}
|
|
1169
|
+
function nameParam(url) {
|
|
1170
|
+
let raw;
|
|
1171
|
+
try {
|
|
1172
|
+
raw = new URL(url ?? "/", "http://127.0.0.1").searchParams.get("name");
|
|
1173
|
+
} catch {
|
|
1174
|
+
return void 0;
|
|
1175
|
+
}
|
|
1176
|
+
return raw && raw.length > 0 ? raw : void 0;
|
|
1177
|
+
}
|
|
1178
|
+
function describe2(err) {
|
|
1179
|
+
return err instanceof Error ? err.message : String(err);
|
|
1180
|
+
}
|
|
1181
|
+
// src/mentions.ts
|
|
1182
|
+
var EMPTY_MENTIONS = { files: [], sessions: [] };
|
|
1021
1183
|
var MAX_CANDIDATES = 20;
|
|
1022
1184
|
var MAX_CANDIDATE_LIMIT = 50;
|
|
1023
1185
|
function visibilityOf(ctx, sessionId) {
|
|
@@ -1099,16 +1261,18 @@ function listFileCandidates(ctx, res, sessionId, rawUrl, lang) {
|
|
|
1099
1261
|
truncated: result.truncated
|
|
1100
1262
|
});
|
|
1101
1263
|
}
|
|
1102
|
-
function attachMentions(ctx, sessionId, message) {
|
|
1264
|
+
function attachMentions(ctx, sessionId, message, uploads = []) {
|
|
1103
1265
|
const text = leadingText2(message);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (mentions.files.length === 0 && mentions.sessions.length === 0) {
|
|
1266
|
+
const mentions = text === null ? EMPTY_MENTIONS : extractAllMentions(text);
|
|
1267
|
+
if (uploads.length === 0 && mentions.files.length === 0 && mentions.sessions.length === 0) {
|
|
1107
1268
|
return { message, references: [], files: [] };
|
|
1108
1269
|
}
|
|
1109
1270
|
const parts = [];
|
|
1110
1271
|
let used = 0;
|
|
1111
|
-
const
|
|
1272
|
+
const uploaded = attachUploads(ctx, sessionId, uploads, used);
|
|
1273
|
+
parts.push(...uploaded.parts);
|
|
1274
|
+
used = uploaded.usedBytes;
|
|
1275
|
+
const fileReceipts = [...uploaded.receipts];
|
|
1112
1276
|
for (const [index, path] of mentions.files.entries()) {
|
|
1113
1277
|
const part = index >= MAX_ATTACH_FILES ? { type: "file", path, omitted: "over-limit" } : readOneFile(ctx, sessionId, path, used);
|
|
1114
1278
|
parts.push(part);
|
|
@@ -1245,11 +1409,14 @@ function toWirePolicy(policy) {
|
|
|
1245
1409
|
}));
|
|
1246
1410
|
return { dirs, projectSkipped: policy.projectSkipped, skippedDir: policy.skippedDir };
|
|
1247
1411
|
}
|
|
1248
|
-
function toWireSecurityWorkspace(bound) {
|
|
1412
|
+
function toWireSecurityWorkspace(bound, project) {
|
|
1249
1413
|
return {
|
|
1250
1414
|
...toWireSessionWorkspace(bound),
|
|
1251
1415
|
trustLevel: bound.trust.level,
|
|
1252
|
-
unloadedExtraInstructionFiles: bound.unloadedInstructions
|
|
1416
|
+
unloadedExtraInstructionFiles: bound.unloadedInstructions,
|
|
1417
|
+
execTrusted: bound.trust.exec,
|
|
1418
|
+
inventory: project.inventory,
|
|
1419
|
+
trustRequest: project.trustRequest
|
|
1253
1420
|
};
|
|
1254
1421
|
}
|
|
1255
1422
|
function toWireTrustList(known, workspaces, currentRoot) {
|
|
@@ -1260,11 +1427,13 @@ function toWireTrustList(known, workspaces, currentRoot) {
|
|
|
1260
1427
|
name: w.name,
|
|
1261
1428
|
level: decision.level,
|
|
1262
1429
|
trusted: decision.trusted,
|
|
1263
|
-
|
|
1430
|
+
execTrusted: decision.exec,
|
|
1431
|
+
current: currentRoot !== null && w.root === currentRoot,
|
|
1432
|
+
exists: existsSync(w.root)
|
|
1264
1433
|
};
|
|
1265
1434
|
});
|
|
1266
1435
|
}
|
|
1267
|
-
function collectSecurity(runtime, workspaces, bound, session) {
|
|
1436
|
+
function collectSecurity(runtime, workspaces, bound, session, project) {
|
|
1268
1437
|
const perms = runtime.permissions;
|
|
1269
1438
|
return {
|
|
1270
1439
|
sandbox: runtime.isolation ? toWireSandbox(runtime.isolation, runtime.config.sandbox?.terminal !== false) : null,
|
|
@@ -1277,7 +1446,7 @@ function collectSecurity(runtime, workspaces, bound, session) {
|
|
|
1277
1446
|
managed: toWireManaged(perms.managed()),
|
|
1278
1447
|
cached: perms.approvals().map(toWireApproval)
|
|
1279
1448
|
} : PERMISSIONS_UNAVAILABLE,
|
|
1280
|
-
workspace: bound ? toWireSecurityWorkspace(bound) : null,
|
|
1449
|
+
workspace: bound ? toWireSecurityWorkspace(bound, project) : null,
|
|
1281
1450
|
knownWorkspaces: toWireTrustList(workspaces.known(), workspaces, bound?.workspace.root ?? null),
|
|
1282
1451
|
policy: toWirePolicy(runtime.policy),
|
|
1283
1452
|
audit: perms ? toWireAudit(perms.audit()) : { rows: [], dropped: 0 }
|
|
@@ -1447,6 +1616,7 @@ function config(ctx, res, lang) {
|
|
|
1447
1616
|
usageScope: runtime.usageScope,
|
|
1448
1617
|
compression: runtime.compression,
|
|
1449
1618
|
...runtime.config.budget?.maxCostUsd !== void 0 ? { maxCostUsd: runtime.config.budget.maxCostUsd } : {},
|
|
1619
|
+
...runtime.config.display?.currency !== void 0 ? { currency: runtime.config.display.currency } : {},
|
|
1450
1620
|
...runtime.config.brand !== void 0 ? { brand: runtime.config.brand } : {},
|
|
1451
1621
|
...runtime.config.sidebarMenu !== void 0 ? { sidebarMenu: runtime.config.sidebarMenu } : {},
|
|
1452
1622
|
...runtime.config.userMenu !== void 0 ? { userMenu: runtime.config.userMenu } : {},
|
|
@@ -1510,6 +1680,7 @@ async function deleteSession(ctx, res, sessionId, lang) {
|
|
|
1510
1680
|
}
|
|
1511
1681
|
const outcome = await catalog.delete(sessionId);
|
|
1512
1682
|
if (!outcome.deleted && !inHub) return sendUnknownSession(res, sessionId, lang);
|
|
1683
|
+
ctx.hub.notifySessionsChanged(sessionId);
|
|
1513
1684
|
sendJson(res, 200, outcome);
|
|
1514
1685
|
}
|
|
1515
1686
|
async function patchSession(ctx, req, res, sessionId, lang) {
|
|
@@ -1562,7 +1733,9 @@ async function postMessage(ctx, req, res, sessionId, lang) {
|
|
|
1562
1733
|
}
|
|
1563
1734
|
const body = await readJsonBody(req, void 0, lang);
|
|
1564
1735
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
1565
|
-
const
|
|
1736
|
+
const uploads = parseAttachmentNames(asRecord(body.value)["attachments"]);
|
|
1737
|
+
const parsed = parseUserContent(asRecord(body.value).message);
|
|
1738
|
+
const message = parsed ?? (uploads.length > 0 ? [] : null);
|
|
1566
1739
|
if (message === null) {
|
|
1567
1740
|
return sendError(res, 400, "bad-message", t("web.bad_message", void 0, lang));
|
|
1568
1741
|
}
|
|
@@ -1573,7 +1746,7 @@ async function postMessage(ctx, req, res, sessionId, lang) {
|
|
|
1573
1746
|
return sendError(res, mapped.status, mapped.code, mapped.message);
|
|
1574
1747
|
}
|
|
1575
1748
|
const expansion = expandUserContent(ctx.runtime, message, live?.model ?? null, picked.role);
|
|
1576
|
-
const attached = attachMentions(ctx, sessionId, expansion.message);
|
|
1749
|
+
const attached = attachMentions(ctx, sessionId, expansion.message, uploads);
|
|
1577
1750
|
const started = ctx.hub.start(sessionId, attached.message, expansion.decorate);
|
|
1578
1751
|
if (started.ok) {
|
|
1579
1752
|
return sendJson(res, 202, {
|
|
@@ -1664,7 +1837,8 @@ function listCapabilities(ctx, res, sessionId, lang) {
|
|
|
1664
1837
|
ctx.runtime,
|
|
1665
1838
|
ctx.runtime.tools,
|
|
1666
1839
|
ctx.runtime.workspaces.of(sessionId),
|
|
1667
|
-
ctx.runtime.sessionFactory.get(sessionId)?.role?.name ?? null
|
|
1840
|
+
ctx.runtime.sessionFactory.get(sessionId)?.role?.name ?? null,
|
|
1841
|
+
projectInventoryOf(ctx.runtime.workspaces, sessionId, ctx.trustRequest !== void 0)
|
|
1668
1842
|
);
|
|
1669
1843
|
sendJson(res, 200, payload);
|
|
1670
1844
|
}
|
|
@@ -1683,7 +1857,8 @@ function getSecurity(ctx, res, sessionId, lang) {
|
|
|
1683
1857
|
ctx.runtime,
|
|
1684
1858
|
ctx.runtime.workspaces,
|
|
1685
1859
|
ctx.runtime.workspaces.of(sessionId),
|
|
1686
|
-
ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null
|
|
1860
|
+
ctx.runtime.sessionFactory.get(sessionId)?.permissions ?? null,
|
|
1861
|
+
projectInventoryOf(ctx.runtime.workspaces, sessionId, ctx.trustRequest !== void 0)
|
|
1687
1862
|
);
|
|
1688
1863
|
sendJson(res, 200, payload);
|
|
1689
1864
|
}
|
|
@@ -1753,6 +1928,10 @@ function parseUserContent(raw) {
|
|
|
1753
1928
|
if (Array.isArray(raw) && raw.length > 0) return raw;
|
|
1754
1929
|
return null;
|
|
1755
1930
|
}
|
|
1931
|
+
function parseAttachmentNames(raw) {
|
|
1932
|
+
if (!Array.isArray(raw)) return [];
|
|
1933
|
+
return raw.filter((name) => typeof name === "string" && name.length > 0);
|
|
1934
|
+
}
|
|
1756
1935
|
async function revokeApprovalCache(ctx, req, res, sessionId, lang) {
|
|
1757
1936
|
if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
|
|
1758
1937
|
const perms = ctx.runtime.permissions;
|
|
@@ -2282,10 +2461,11 @@ function sessionHasImages(session) {
|
|
|
2282
2461
|
}
|
|
2283
2462
|
function getModel(ctx, res, sessionId, _lang) {
|
|
2284
2463
|
const { runtime } = ctx;
|
|
2285
|
-
const
|
|
2464
|
+
const live = runtime.sessionFactory.get(sessionId);
|
|
2465
|
+
const selection = live?.model.get();
|
|
2286
2466
|
sendJson(res, 200, {
|
|
2287
2467
|
selection: selection ? toWireSelection(selection) : null,
|
|
2288
|
-
configured: runtime.config.model || null
|
|
2468
|
+
configured: (live ? live.model.configured() : runtime.config.model) || null
|
|
2289
2469
|
});
|
|
2290
2470
|
}
|
|
2291
2471
|
async function setModel(ctx, req, res, sessionId, lang) {
|
|
@@ -2433,9 +2613,18 @@ function toWireCounts(counts) {
|
|
|
2433
2613
|
mcpServers: counts.mcpServers
|
|
2434
2614
|
};
|
|
2435
2615
|
}
|
|
2436
|
-
function
|
|
2616
|
+
function displayNamesOf(control) {
|
|
2617
|
+
const out = /* @__PURE__ */ new Map();
|
|
2618
|
+
for (const hit of control.search()) {
|
|
2619
|
+
const shown = hit.entry.displayName;
|
|
2620
|
+
if (shown !== void 0 && shown !== "") out.set(`${hit.marketplace}/${hit.entry.name}`, shown);
|
|
2621
|
+
}
|
|
2622
|
+
return out;
|
|
2623
|
+
}
|
|
2624
|
+
function toWirePluginEntry(entry, displayNames) {
|
|
2437
2625
|
return {
|
|
2438
2626
|
name: entry.name,
|
|
2627
|
+
displayName: displayNames.get(`${entry.marketplace}/${entry.name}`) ?? null,
|
|
2439
2628
|
version: entry.version,
|
|
2440
2629
|
source: entry.source,
|
|
2441
2630
|
sourceType: entry.sourceType,
|
|
@@ -2470,6 +2659,7 @@ function toWirePluginHit(hit, installed) {
|
|
|
2470
2659
|
size: hit.entry.size ?? null,
|
|
2471
2660
|
homepage: hit.entry.homepage ?? null,
|
|
2472
2661
|
visibility: hit.entry.visibility ?? null,
|
|
2662
|
+
status: hit.entry.status ?? null,
|
|
2473
2663
|
updateAvailable: mine !== void 0 && !mine.linked && hit.entry.version !== void 0 && hit.entry.version !== mine.version
|
|
2474
2664
|
};
|
|
2475
2665
|
}
|
|
@@ -2526,21 +2716,39 @@ function fieldOf(payload, key) {
|
|
|
2526
2716
|
const value = payload[key];
|
|
2527
2717
|
return typeof value === "string" ? value.trim() : "";
|
|
2528
2718
|
}
|
|
2719
|
+
function uploadFormOf(payload) {
|
|
2720
|
+
const form = {};
|
|
2721
|
+
for (const name of WIRE_PLUGIN_UPLOAD_FORM_FIELDS) {
|
|
2722
|
+
const raw = fieldOf(payload, name);
|
|
2723
|
+
if (raw === "") continue;
|
|
2724
|
+
if (name === "visibility") {
|
|
2725
|
+
if (raw === "private" || raw === "public") form[name] = raw;
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
form[name] = raw;
|
|
2729
|
+
}
|
|
2730
|
+
return form;
|
|
2731
|
+
}
|
|
2529
2732
|
function listPlugins(ctx, res, lang) {
|
|
2530
2733
|
const control = controlOf(ctx, res, lang);
|
|
2531
2734
|
if (!control) return;
|
|
2735
|
+
sendJson(res, 200, pluginsPayload(control));
|
|
2736
|
+
}
|
|
2737
|
+
function pluginsPayload(control) {
|
|
2532
2738
|
const installed = control.list();
|
|
2533
2739
|
const installedMap = new Map(
|
|
2534
2740
|
installed.map((p) => [p.name, { version: p.version, linked: p.linked }])
|
|
2535
2741
|
);
|
|
2536
|
-
|
|
2537
|
-
|
|
2742
|
+
const shown = displayNamesOf(control);
|
|
2743
|
+
return {
|
|
2744
|
+
installed: installed.map((p) => toWirePluginEntry(p, shown)),
|
|
2538
2745
|
hits: control.search().map((hit) => toWirePluginHit(hit, installedMap)),
|
|
2539
2746
|
marketplaces: control.marketplaces().map(toWireMarketplace),
|
|
2540
2747
|
pendingRestart: control.pendingRestart,
|
|
2541
2748
|
uploadEnabled: control.uploadEnabled,
|
|
2542
|
-
uploadOptions: control.uploadOptions
|
|
2543
|
-
|
|
2749
|
+
uploadOptions: control.uploadOptions,
|
|
2750
|
+
refreshEnabled: control.refreshEnabled
|
|
2751
|
+
};
|
|
2544
2752
|
}
|
|
2545
2753
|
function sendPreview(res, outcome) {
|
|
2546
2754
|
if (!outcome.ok) {
|
|
@@ -2597,7 +2805,7 @@ function sendAction(res, control, outcome) {
|
|
|
2597
2805
|
const fresh = "record" in outcome ? control.list().find((p) => p.name === outcome.record.name) : void 0;
|
|
2598
2806
|
sendJson(res, 200, {
|
|
2599
2807
|
ok: true,
|
|
2600
|
-
entry: fresh ? toWirePluginEntry(fresh) : null,
|
|
2808
|
+
entry: fresh ? toWirePluginEntry(fresh, displayNamesOf(control)) : null,
|
|
2601
2809
|
pendingRestart: control.pendingRestart,
|
|
2602
2810
|
detail: "detail" in outcome ? outcome.detail : null,
|
|
2603
2811
|
reason: null
|
|
@@ -2629,6 +2837,51 @@ async function installLocalPlugin(ctx, req, res, lang) {
|
|
|
2629
2837
|
await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
|
|
2630
2838
|
);
|
|
2631
2839
|
}
|
|
2840
|
+
async function stagePlugin(ctx, req, res, lang) {
|
|
2841
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
2842
|
+
const control = controlOf(ctx, res, lang);
|
|
2843
|
+
if (!control) return;
|
|
2844
|
+
const asJson = (req.headers["content-type"] ?? "").toLowerCase().includes("application/json");
|
|
2845
|
+
if (asJson) {
|
|
2846
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
2847
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
2848
|
+
return sendStage(
|
|
2849
|
+
res,
|
|
2850
|
+
await control.stage({ kind: "path", path: fieldOf(asRecord(body.value), "path") }, lang)
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
|
|
2854
|
+
if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
|
|
2855
|
+
return sendStage(
|
|
2856
|
+
res,
|
|
2857
|
+
await control.stage({ kind: "bytes", name: nameParam2(req), data: raw.value }, lang)
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
function nameParam2(req) {
|
|
2861
|
+
try {
|
|
2862
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
|
|
2863
|
+
} catch {
|
|
2864
|
+
return "";
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function sendStage(res, outcome) {
|
|
2868
|
+
const payload = outcome.ok ? {
|
|
2869
|
+
ok: true,
|
|
2870
|
+
path: outcome.path,
|
|
2871
|
+
name: outcome.name,
|
|
2872
|
+
version: outcome.version,
|
|
2873
|
+
detail: null,
|
|
2874
|
+
reason: null
|
|
2875
|
+
} : {
|
|
2876
|
+
ok: false,
|
|
2877
|
+
path: null,
|
|
2878
|
+
name: null,
|
|
2879
|
+
version: null,
|
|
2880
|
+
detail: outcome.detail,
|
|
2881
|
+
reason: outcome.reason
|
|
2882
|
+
};
|
|
2883
|
+
sendJson(res, 200, payload);
|
|
2884
|
+
}
|
|
2632
2885
|
async function updateInstalledPlugin(ctx, req, res, lang) {
|
|
2633
2886
|
if (refusedByLan(ctx, res, lang)) return;
|
|
2634
2887
|
const control = controlOf(ctx, res, lang);
|
|
@@ -2686,10 +2939,7 @@ function uploadPlugin(ctx, req, res, lang) {
|
|
|
2686
2939
|
await control.upload(
|
|
2687
2940
|
fieldOf(payload, "path"),
|
|
2688
2941
|
fieldOf(payload, "token"),
|
|
2689
|
-
|
|
2690
|
-
...fieldOf(payload, "visibility") === "private" ? { visibility: "private" } : fieldOf(payload, "visibility") === "public" ? { visibility: "public" } : {},
|
|
2691
|
-
...fieldOf(payload, "changelog") ? { changelog: fieldOf(payload, "changelog") } : {}
|
|
2692
|
-
},
|
|
2942
|
+
uploadFormOf(payload),
|
|
2693
2943
|
lang
|
|
2694
2944
|
)
|
|
2695
2945
|
);
|
|
@@ -2739,13 +2989,34 @@ function sendUpload(res, outcome) {
|
|
|
2739
2989
|
reason: outcome.ok ? null : outcome.reason
|
|
2740
2990
|
});
|
|
2741
2991
|
}
|
|
2992
|
+
async function refreshPlugins(ctx, res, lang) {
|
|
2993
|
+
if (refusedByLan(ctx, res, lang)) return;
|
|
2994
|
+
const control = controlOf(ctx, res, lang);
|
|
2995
|
+
if (!control) return;
|
|
2996
|
+
const outcome = await control.refresh(lang);
|
|
2997
|
+
if (!outcome.ok) {
|
|
2998
|
+
return sendJson(res, 200, {
|
|
2999
|
+
ok: false,
|
|
3000
|
+
plugins: null,
|
|
3001
|
+
reason: outcome.reason,
|
|
3002
|
+
detail: outcome.detail
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
sendJson(res, 200, {
|
|
3006
|
+
ok: true,
|
|
3007
|
+
plugins: pluginsPayload(control),
|
|
3008
|
+
reason: null,
|
|
3009
|
+
detail: outcome.detail === "" ? null : outcome.detail
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
2742
3012
|
function toWireProvider(option, lang) {
|
|
2743
3013
|
return {
|
|
2744
3014
|
type: option.type,
|
|
2745
3015
|
label: providerLabel(option, lang),
|
|
2746
3016
|
envVar: option.envVar,
|
|
2747
3017
|
hasKey: option.hasKey,
|
|
2748
|
-
keyHint: option.keyHint
|
|
3018
|
+
keyHint: option.keyHint,
|
|
3019
|
+
current: option.current
|
|
2749
3020
|
};
|
|
2750
3021
|
}
|
|
2751
3022
|
function toWireSuggestions(view) {
|
|
@@ -3310,6 +3581,33 @@ async function removeSkill(ctx, req, res, lang) {
|
|
|
3310
3581
|
reason: null
|
|
3311
3582
|
});
|
|
3312
3583
|
}
|
|
3584
|
+
function refusedByLan2(ctx, res, lang) {
|
|
3585
|
+
if (!ctx.lanExposed) return false;
|
|
3586
|
+
sendError(res, 403, "skill-stage-lan-exposed", t("web.skill_stage_lan_exposed", void 0, lang));
|
|
3587
|
+
return true;
|
|
3588
|
+
}
|
|
3589
|
+
async function stageSkillDrop(ctx, req, res, lang) {
|
|
3590
|
+
if (refusedByLan2(ctx, res, lang)) return;
|
|
3591
|
+
const raw = await readRawBody(req, PLUGIN_UPLOAD_BODY_LIMIT, lang);
|
|
3592
|
+
if (!raw.ok) return sendError(res, raw.status, "bad-body", raw.message);
|
|
3593
|
+
const outcome = await ctx.runtime.skillStage.stage(
|
|
3594
|
+
{ name: nameParam3(req), data: raw.value },
|
|
3595
|
+
lang
|
|
3596
|
+
);
|
|
3597
|
+
sendJson(res, 200, {
|
|
3598
|
+
ok: outcome.ok,
|
|
3599
|
+
path: outcome.ok ? outcome.path : null,
|
|
3600
|
+
detail: outcome.ok ? null : outcome.detail,
|
|
3601
|
+
reason: outcome.ok ? null : outcome.reason
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
function nameParam3(req) {
|
|
3605
|
+
try {
|
|
3606
|
+
return new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("name") ?? "";
|
|
3607
|
+
} catch {
|
|
3608
|
+
return "";
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3313
3611
|
// src/sse.ts
|
|
3314
3612
|
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
3315
3613
|
function parseLastEventId(req) {
|
|
@@ -3793,9 +4091,9 @@ var VIEWABLE = /* @__PURE__ */ new Map([
|
|
|
3793
4091
|
[".webm", { kind: "video", mime: "video/webm" }],
|
|
3794
4092
|
[".mov", { kind: "video", mime: "video/quicktime" }]
|
|
3795
4093
|
]);
|
|
3796
|
-
function kindOf(
|
|
3797
|
-
if (
|
|
3798
|
-
return VIEWABLE.get(extname(
|
|
4094
|
+
function kindOf(stat3) {
|
|
4095
|
+
if (stat3.kind === "text") return "text";
|
|
4096
|
+
return VIEWABLE.get(extname(stat3.rel).toLowerCase())?.kind ?? "opaque";
|
|
3799
4097
|
}
|
|
3800
4098
|
function toWireRefusal(reason) {
|
|
3801
4099
|
return reason;
|
|
@@ -3843,15 +4141,15 @@ function statFiles(ctx, res, sessionId, rawUrl, lang) {
|
|
|
3843
4141
|
const asked = params.getAll("path").filter((p) => p.trim() !== "");
|
|
3844
4142
|
const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
|
|
3845
4143
|
const files = taken.map((path) => {
|
|
3846
|
-
const
|
|
3847
|
-
if (!
|
|
4144
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4145
|
+
if (!stat3.ok) return { path, ok: false, refusal: toWireRefusal(stat3.reason) };
|
|
3848
4146
|
return {
|
|
3849
4147
|
path,
|
|
3850
4148
|
ok: true,
|
|
3851
|
-
kind: kindOf(
|
|
3852
|
-
bytes:
|
|
3853
|
-
mtimeMs:
|
|
3854
|
-
openable: ctx.sameMachine && isSystemOpenable(
|
|
4149
|
+
kind: kindOf(stat3),
|
|
4150
|
+
bytes: stat3.bytes,
|
|
4151
|
+
mtimeMs: stat3.mtimeMs,
|
|
4152
|
+
openable: ctx.sameMachine && isSystemOpenable(stat3.rel)
|
|
3855
4153
|
};
|
|
3856
4154
|
});
|
|
3857
4155
|
sendJson(res, 200, {
|
|
@@ -3865,18 +4163,18 @@ function readFileText(ctx, res, sessionId, rawUrl, lang) {
|
|
|
3865
4163
|
if (root === null) return;
|
|
3866
4164
|
const path = singlePath(res, rawUrl, lang);
|
|
3867
4165
|
if (path === null) return;
|
|
3868
|
-
const
|
|
3869
|
-
if (!
|
|
3870
|
-
const refusal = toWireRefusal(
|
|
4166
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4167
|
+
if (!stat3.ok) {
|
|
4168
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
3871
4169
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
3872
4170
|
}
|
|
3873
|
-
if (
|
|
4171
|
+
if (stat3.kind !== "text") {
|
|
3874
4172
|
return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
|
|
3875
4173
|
}
|
|
3876
|
-
if (
|
|
4174
|
+
if (stat3.bytes > MAX_TEXT_BYTES) {
|
|
3877
4175
|
return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
|
|
3878
4176
|
}
|
|
3879
|
-
const out = ctx.runtime.workspaceFiles.readFile(
|
|
4177
|
+
const out = ctx.runtime.workspaceFiles.readFile(stat3.rel, root);
|
|
3880
4178
|
if (!out.ok) {
|
|
3881
4179
|
const refusal = out.reason === "binary" ? "unreadable" : out.reason;
|
|
3882
4180
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
@@ -3892,26 +4190,26 @@ function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
|
|
|
3892
4190
|
if (root === null) return;
|
|
3893
4191
|
const path = singlePath(res, rawUrl, lang);
|
|
3894
4192
|
if (path === null) return;
|
|
3895
|
-
const
|
|
3896
|
-
if (!
|
|
3897
|
-
const refusal = toWireRefusal(
|
|
4193
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4194
|
+
if (!stat3.ok) {
|
|
4195
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
3898
4196
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
3899
4197
|
}
|
|
3900
|
-
const known = VIEWABLE.get(extname(
|
|
3901
|
-
const inline = known !== void 0 &&
|
|
4198
|
+
const known = VIEWABLE.get(extname(stat3.rel).toLowerCase());
|
|
4199
|
+
const inline = known !== void 0 && stat3.kind === "binary";
|
|
3902
4200
|
const headers = {
|
|
3903
4201
|
"Content-Type": inline ? known.mime : "application/octet-stream",
|
|
3904
4202
|
"X-Content-Type-Options": "nosniff",
|
|
3905
|
-
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(
|
|
4203
|
+
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat3.rel))}`
|
|
3906
4204
|
};
|
|
3907
4205
|
if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
|
|
3908
4206
|
if (headOnly) {
|
|
3909
4207
|
return sendEmpty(res, 200, {
|
|
3910
4208
|
...headers,
|
|
3911
|
-
"Content-Length": String(
|
|
4209
|
+
"Content-Length": String(stat3.bytes)
|
|
3912
4210
|
});
|
|
3913
4211
|
}
|
|
3914
|
-
sendFile(res,
|
|
4212
|
+
sendFile(res, stat3.absPath, headers);
|
|
3915
4213
|
}
|
|
3916
4214
|
function basenameOf(rel) {
|
|
3917
4215
|
const cut = rel.lastIndexOf("/");
|
|
@@ -3962,6 +4260,26 @@ async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
|
|
|
3962
4260
|
}
|
|
3963
4261
|
sendJson(res, 200, { ok: true });
|
|
3964
4262
|
}
|
|
4263
|
+
async function forgetWorkspace(ctx, sessionId, req, res, lang) {
|
|
4264
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
4265
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4266
|
+
if (!conformsToWire(body.value, WIRE_FORGET_WORKSPACE_FIELDS)) {
|
|
4267
|
+
return sendError(res, 400, "bad-request", t("web.forget_bad_request", {}, lang));
|
|
4268
|
+
}
|
|
4269
|
+
const { root } = body.value;
|
|
4270
|
+
const workspaces = ctx.runtime.workspaces;
|
|
4271
|
+
if (!workspaces.known().some((w) => w.root === root)) {
|
|
4272
|
+
return sendError(res, 404, "unknown-workspace", t("web.forget_unknown", {}, lang));
|
|
4273
|
+
}
|
|
4274
|
+
const current = workspaces.of(sessionId)?.workspace.root;
|
|
4275
|
+
if (current !== void 0 && current === root) {
|
|
4276
|
+
return sendError(res, 409, "workspace-in-use", t("web.forget_in_use", {}, lang));
|
|
4277
|
+
}
|
|
4278
|
+
workspaces.forget(root);
|
|
4279
|
+
sendJson(res, 200, {
|
|
4280
|
+
known: toWireTrustList(workspaces.known(), workspaces, current ?? null)
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
3965
4283
|
var GIT_TIMEOUT_MS = 1e4;
|
|
3966
4284
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
3967
4285
|
function runGit(cwd, args) {
|
|
@@ -4072,7 +4390,7 @@ async function gitDiff(root) {
|
|
|
4072
4390
|
for (const item of picked) {
|
|
4073
4391
|
const old = await gitSide(root, item.oldSha, sizes);
|
|
4074
4392
|
const fresh = await worktreeSide(join(root, ...item.path.split("/")));
|
|
4075
|
-
files.push(
|
|
4393
|
+
files.push(describe3(item.path, old, fresh));
|
|
4076
4394
|
}
|
|
4077
4395
|
return { source: "git", root, files, truncated, notes };
|
|
4078
4396
|
}
|
|
@@ -4118,7 +4436,7 @@ async function checkpointDiff(opts) {
|
|
|
4118
4436
|
for (const absolute of picked) {
|
|
4119
4437
|
const fresh = await worktreeSide(absolute);
|
|
4120
4438
|
files.push(
|
|
4121
|
-
|
|
4439
|
+
describe3(displayPath(opts.workDir, absolute), ABSENT, fresh, {
|
|
4122
4440
|
status: fresh.kind === "absent" ? "deleted" : "modified",
|
|
4123
4441
|
omitted: "no-snapshot"
|
|
4124
4442
|
})
|
|
@@ -4156,7 +4474,7 @@ async function worktreeSide(absolute) {
|
|
|
4156
4474
|
return ABSENT;
|
|
4157
4475
|
}
|
|
4158
4476
|
}
|
|
4159
|
-
function
|
|
4477
|
+
function describe3(path, old, fresh, override = {}) {
|
|
4160
4478
|
const status = override.status ?? (old.kind === "absent" ? "added" : fresh.kind === "absent" ? "deleted" : "modified");
|
|
4161
4479
|
const oldBytes = bytesOf(old);
|
|
4162
4480
|
const newBytes = bytesOf(fresh);
|
|
@@ -4239,6 +4557,7 @@ async function createSession(ctx, req, res, lang) {
|
|
|
4239
4557
|
}
|
|
4240
4558
|
ctx.hub.register(outcome.live.session);
|
|
4241
4559
|
if (defer) ctx.runtime.workspaces.defer(outcome.live.sessionId);
|
|
4560
|
+
if (outcome.created) ctx.hub.notifySessionsChanged(outcome.live.sessionId);
|
|
4242
4561
|
sendJson(res, 200, {
|
|
4243
4562
|
sessionId: outcome.live.sessionId,
|
|
4244
4563
|
created: outcome.created,
|
|
@@ -4323,6 +4642,36 @@ async function sessionDiff(ctx, res, sessionId, _lang) {
|
|
|
4323
4642
|
})
|
|
4324
4643
|
);
|
|
4325
4644
|
}
|
|
4645
|
+
function isKnownRoot(ctx, sessionId, root) {
|
|
4646
|
+
if (ctx.runtime.workspaces.of(sessionId)?.workspace.root === root) return true;
|
|
4647
|
+
return ctx.runtime.workspaces.known().some((w) => w.root === root);
|
|
4648
|
+
}
|
|
4649
|
+
async function requestTrust(ctx, sessionId, req, res, lang) {
|
|
4650
|
+
const ask = ctx.trustRequest;
|
|
4651
|
+
if (ask === void 0) {
|
|
4652
|
+
return sendError(res, 404, "trust-request-unavailable", t("web.trust_unavailable", {}, lang));
|
|
4653
|
+
}
|
|
4654
|
+
const body = await readJsonBody(req, void 0, lang);
|
|
4655
|
+
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
4656
|
+
if (!conformsToWire(body.value, WIRE_TRUST_REQUEST_FIELDS)) {
|
|
4657
|
+
return sendError(res, 400, "bad-request", t("web.trust_bad_request", {}, lang));
|
|
4658
|
+
}
|
|
4659
|
+
const { root, want } = body.value;
|
|
4660
|
+
if (!isKnownRoot(ctx, sessionId, root)) {
|
|
4661
|
+
return sendError(res, 400, "unknown-workspace", t("web.trust_unknown_root", {}, lang));
|
|
4662
|
+
}
|
|
4663
|
+
try {
|
|
4664
|
+
await ask({ root, want: want === "exec" ? "exec" : "read" });
|
|
4665
|
+
} catch (err) {
|
|
4666
|
+
return sendError(
|
|
4667
|
+
res,
|
|
4668
|
+
500,
|
|
4669
|
+
"trust-request-failed",
|
|
4670
|
+
t("web.trust_failed", { detail: err instanceof Error ? err.message : String(err) }, lang)
|
|
4671
|
+
);
|
|
4672
|
+
}
|
|
4673
|
+
sendJson(res, 200, { asked: true });
|
|
4674
|
+
}
|
|
4326
4675
|
// src/routes.ts
|
|
4327
4676
|
var DENY_KEY = {
|
|
4328
4677
|
"missing-credential": "web.route.deny_missing_credential",
|
|
@@ -4330,10 +4679,17 @@ var DENY_KEY = {
|
|
|
4330
4679
|
"bad-origin": "web.route.deny_bad_origin",
|
|
4331
4680
|
"bad-host": "web.route.deny_bad_host"
|
|
4332
4681
|
};
|
|
4682
|
+
var log = createLogger("server-route");
|
|
4333
4683
|
function createRequestHandler(opts) {
|
|
4334
4684
|
return (req, res) => {
|
|
4335
4685
|
const lang = langOf(req.url);
|
|
4336
|
-
dispatch(opts, req, res, lang).catch(() => {
|
|
4686
|
+
dispatch(opts, req, res, lang).catch((err) => {
|
|
4687
|
+
log.error("unhandled handler exception", {
|
|
4688
|
+
method: req.method ?? "",
|
|
4689
|
+
path: (req.url ?? "").split("?")[0],
|
|
4690
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4691
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
4692
|
+
});
|
|
4337
4693
|
if (!res.headersSent) {
|
|
4338
4694
|
sendError(res, 500, "internal", t("web.route.internal_error", void 0, lang));
|
|
4339
4695
|
} else res.destroy();
|
|
@@ -4418,6 +4774,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
|
|
|
4418
4774
|
if (resource === "skills" && first === "import" && second === void 0 && method === "POST") {
|
|
4419
4775
|
return importSkills(ctx, req, res, lang);
|
|
4420
4776
|
}
|
|
4777
|
+
if (resource === "skills" && first === "stage" && second === void 0 && method === "POST") {
|
|
4778
|
+
return stageSkillDrop(ctx, req, res, lang);
|
|
4779
|
+
}
|
|
4421
4780
|
if (resource === "skills" && first === "remove" && second === void 0 && method === "POST") {
|
|
4422
4781
|
return removeSkill(ctx, req, res, lang);
|
|
4423
4782
|
}
|
|
@@ -4457,6 +4816,12 @@ async function handleApi(opts, req, res, method, segments, lang) {
|
|
|
4457
4816
|
if (resource === "plugins" && first === "upload" && second === void 0 && method === "POST") {
|
|
4458
4817
|
return uploadPlugin(ctx, req, res, lang);
|
|
4459
4818
|
}
|
|
4819
|
+
if (resource === "plugins" && first === "refresh" && second === void 0 && method === "POST") {
|
|
4820
|
+
return refreshPlugins(ctx, res, lang);
|
|
4821
|
+
}
|
|
4822
|
+
if (resource === "plugins" && first === "stage" && second === void 0 && method === "POST") {
|
|
4823
|
+
return stagePlugin(ctx, req, res, lang);
|
|
4824
|
+
}
|
|
4460
4825
|
if (resource === "providers" && first === void 0 && isRead(method)) {
|
|
4461
4826
|
return listProviders(ctx, res, lang);
|
|
4462
4827
|
}
|
|
@@ -4538,6 +4903,9 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
|
|
|
4538
4903
|
if (method === "POST") return postMessage(ctx, req, res, sessionId, lang);
|
|
4539
4904
|
return methodNotAllowed(res, "GET, POST", lang);
|
|
4540
4905
|
}
|
|
4906
|
+
if (action === "attachments" && method === "POST") {
|
|
4907
|
+
return uploadAttachment(ctx, req, res, sessionId, lang);
|
|
4908
|
+
}
|
|
4541
4909
|
if (action === "abort" && method === "POST") return abortSession(ctx, res, sessionId, lang);
|
|
4542
4910
|
if (action === "reference-candidates" && isRead(method)) {
|
|
4543
4911
|
return listReferenceCandidates(ctx, res, sessionId, req.url, lang);
|
|
@@ -4596,6 +4964,12 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
|
|
|
4596
4964
|
if (action === "permission" && method === "POST") {
|
|
4597
4965
|
return setPermission(ctx, req, res, sessionId, lang);
|
|
4598
4966
|
}
|
|
4967
|
+
if (action === "trust-request" && method === "POST") {
|
|
4968
|
+
return requestTrust(ctx, sessionId, req, res, lang);
|
|
4969
|
+
}
|
|
4970
|
+
if (action === "workspaces" && method === "POST" && detail === "forget") {
|
|
4971
|
+
return forgetWorkspace(ctx, sessionId, req, res, lang);
|
|
4972
|
+
}
|
|
4599
4973
|
if (action === "approval-cache" && method === "POST") {
|
|
4600
4974
|
return revokeApprovalCache(ctx, req, res, sessionId, lang);
|
|
4601
4975
|
}
|
|
@@ -4686,6 +5060,9 @@ async function createWebServer(opts) {
|
|
|
4686
5060
|
onCooled: (sessionId) => opts.runtime.sessionFactory.release(sessionId)
|
|
4687
5061
|
});
|
|
4688
5062
|
if (opts.runtime.session) hub.register(opts.runtime.session);
|
|
5063
|
+
const unsubscribeTasks = observeBackgroundTasks((sessionId) => {
|
|
5064
|
+
hub.notifyTasksChanged(sessionId);
|
|
5065
|
+
});
|
|
4689
5066
|
let handle = null;
|
|
4690
5067
|
const server = createServer((req, res) => {
|
|
4691
5068
|
if (handle) return handle(req, res);
|
|
@@ -4694,7 +5071,10 @@ async function createWebServer(opts) {
|
|
|
4694
5071
|
});
|
|
4695
5072
|
const webRoot = opts.webRoot ?? defaultWebRoot();
|
|
4696
5073
|
const listened = await listen(server, binding.host, binding.port);
|
|
4697
|
-
if (!listened.ok)
|
|
5074
|
+
if (!listened.ok) {
|
|
5075
|
+
unsubscribeTasks();
|
|
5076
|
+
return listened;
|
|
5077
|
+
}
|
|
4698
5078
|
handle = buildHandler(opts, hub, binding, listened.port, webRoot);
|
|
4699
5079
|
return {
|
|
4700
5080
|
ok: true,
|
|
@@ -4704,9 +5084,9 @@ async function createWebServer(opts) {
|
|
|
4704
5084
|
host: binding.host,
|
|
4705
5085
|
token: binding.token,
|
|
4706
5086
|
...webRoot === void 0 ? {} : { webRoot },
|
|
4707
|
-
observe: (sink) => hub.observe(sink),
|
|
5087
|
+
observe: (sink, opts2) => hub.observe(sink, opts2 ?? {}),
|
|
4708
5088
|
snapshot: () => hub.listSessions(),
|
|
4709
|
-
close: makeCloser(server, hub, opts.runtime)
|
|
5089
|
+
close: makeCloser(server, hub, opts.runtime, unsubscribeTasks)
|
|
4710
5090
|
}
|
|
4711
5091
|
};
|
|
4712
5092
|
}
|
|
@@ -4724,7 +5104,8 @@ function buildHandler(opts, hub, binding, port, webRoot) {
|
|
|
4724
5104
|
artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
|
|
4725
5105
|
lanExposed: binding.lanExposed,
|
|
4726
5106
|
nativeDirPicker: resolveNativeDirPicker(machineFacts),
|
|
4727
|
-
sameMachine: isSameMachine(machineFacts)
|
|
5107
|
+
sameMachine: isSameMachine(machineFacts),
|
|
5108
|
+
...opts.onTrustRequest === void 0 ? {} : { trustRequest: opts.onTrustRequest }
|
|
4728
5109
|
},
|
|
4729
5110
|
guard: createAuthGuard({ token: binding.token, port, lanExposed: binding.lanExposed }),
|
|
4730
5111
|
...webRoot === void 0 ? {} : { webRoot },
|
|
@@ -4757,10 +5138,11 @@ function describeListenError(err, host, port) {
|
|
|
4757
5138
|
}
|
|
4758
5139
|
return t("web.listen_failed", { host, port, reason: err.message }, void 0);
|
|
4759
5140
|
}
|
|
4760
|
-
function makeCloser(server, hub, runtime) {
|
|
5141
|
+
function makeCloser(server, hub, runtime, unsubscribeTasks) {
|
|
4761
5142
|
let closing = null;
|
|
4762
5143
|
return () => {
|
|
4763
5144
|
closing ??= (async () => {
|
|
5145
|
+
unsubscribeTasks();
|
|
4764
5146
|
hub.shutdown();
|
|
4765
5147
|
server.closeAllConnections();
|
|
4766
5148
|
await new Promise((resolve4) => server.close(() => resolve4()));
|