@epoch-agent/server 0.9.0 → 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 +538 -33
- package/dist/index.js +393 -60
- 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-B0ky2TY1.css +0 -1
- package/dist/web/assets/index-C8ef1aOs.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, 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';
|
|
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, WIRE_PLUGIN_UPLOAD_FORM_FIELDS } 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) {
|
|
@@ -558,6 +585,10 @@ var SessionHub = class {
|
|
|
558
585
|
if (this.closed) return;
|
|
559
586
|
this.publish(sessionId, { type: "tasks-changed" });
|
|
560
587
|
}
|
|
588
|
+
notifySessionsChanged(sessionId) {
|
|
589
|
+
if (this.closed) return;
|
|
590
|
+
this.publish(sessionId, { type: "sessions-changed" });
|
|
591
|
+
}
|
|
561
592
|
shutdown() {
|
|
562
593
|
if (this.closed) return;
|
|
563
594
|
this.closed = true;
|
|
@@ -565,6 +596,7 @@ var SessionHub = class {
|
|
|
565
596
|
this.reap();
|
|
566
597
|
this.sinks.clear();
|
|
567
598
|
this.observers.clear();
|
|
599
|
+
this.counted.clear();
|
|
568
600
|
}
|
|
569
601
|
publish(sessionId, event) {
|
|
570
602
|
const frame = { seq: ++this.seq, sessionId, ts: this.clock(), event };
|
|
@@ -628,7 +660,7 @@ var SessionHub = class {
|
|
|
628
660
|
}
|
|
629
661
|
drainQueue(entry) {
|
|
630
662
|
if (this.closed || !this.registry.has(entry.session.sessionId)) return;
|
|
631
|
-
if (this.
|
|
663
|
+
if (this.present === 0) {
|
|
632
664
|
entry.queue.length = 0;
|
|
633
665
|
return;
|
|
634
666
|
}
|
|
@@ -649,7 +681,7 @@ var SessionHub = class {
|
|
|
649
681
|
this.graceTimer = null;
|
|
650
682
|
}
|
|
651
683
|
reap() {
|
|
652
|
-
if (this.
|
|
684
|
+
if (this.present > 0 && !this.closed) return;
|
|
653
685
|
for (const [, entry] of this.registry.all()) {
|
|
654
686
|
entry.queue.length = 0;
|
|
655
687
|
if (entry.state === "idle") continue;
|
|
@@ -767,6 +799,54 @@ async function readJsonBody(req, limit = JSON_BODY_LIMIT, lang) {
|
|
|
767
799
|
};
|
|
768
800
|
}
|
|
769
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
|
+
}
|
|
770
850
|
function describe(err) {
|
|
771
851
|
return err instanceof Error ? err.message : String(err);
|
|
772
852
|
}
|
|
@@ -933,7 +1013,7 @@ function estimateIndexTokens(text) {
|
|
|
933
1013
|
return Math.ceil(text.length / 4);
|
|
934
1014
|
}
|
|
935
1015
|
function toWireMcp(server) {
|
|
936
|
-
const state = server
|
|
1016
|
+
const state = mcpConnectorState(server);
|
|
937
1017
|
return {
|
|
938
1018
|
name: server.name,
|
|
939
1019
|
state,
|
|
@@ -943,7 +1023,7 @@ function toWireMcp(server) {
|
|
|
943
1023
|
source: server.source
|
|
944
1024
|
};
|
|
945
1025
|
}
|
|
946
|
-
function collectCapabilities(runtime, tools, bound, role) {
|
|
1026
|
+
function collectCapabilities(runtime, tools, bound, role, project) {
|
|
947
1027
|
const residency = runtime.skillIndexResidency(role);
|
|
948
1028
|
return {
|
|
949
1029
|
roles: runtime.agentRoles.map((r) => toWireRole(r, runtime.agentRoleNotices)),
|
|
@@ -952,7 +1032,10 @@ function collectCapabilities(runtime, tools, bound, role) {
|
|
|
952
1032
|
mcpServers: runtime.mcpServers.map(toWireMcp),
|
|
953
1033
|
project: {
|
|
954
1034
|
root: bound?.workspace.root ?? null,
|
|
955
|
-
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
|
|
956
1039
|
}
|
|
957
1040
|
};
|
|
958
1041
|
}
|
|
@@ -1022,6 +1105,81 @@ function replaceLeadingText(message, prompt) {
|
|
|
1022
1105
|
const head = { type: "text", text: prompt };
|
|
1023
1106
|
return [head, ...message.slice(1)];
|
|
1024
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: [] };
|
|
1025
1183
|
var MAX_CANDIDATES = 20;
|
|
1026
1184
|
var MAX_CANDIDATE_LIMIT = 50;
|
|
1027
1185
|
function visibilityOf(ctx, sessionId) {
|
|
@@ -1103,16 +1261,18 @@ function listFileCandidates(ctx, res, sessionId, rawUrl, lang) {
|
|
|
1103
1261
|
truncated: result.truncated
|
|
1104
1262
|
});
|
|
1105
1263
|
}
|
|
1106
|
-
function attachMentions(ctx, sessionId, message) {
|
|
1264
|
+
function attachMentions(ctx, sessionId, message, uploads = []) {
|
|
1107
1265
|
const text = leadingText2(message);
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
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) {
|
|
1111
1268
|
return { message, references: [], files: [] };
|
|
1112
1269
|
}
|
|
1113
1270
|
const parts = [];
|
|
1114
1271
|
let used = 0;
|
|
1115
|
-
const
|
|
1272
|
+
const uploaded = attachUploads(ctx, sessionId, uploads, used);
|
|
1273
|
+
parts.push(...uploaded.parts);
|
|
1274
|
+
used = uploaded.usedBytes;
|
|
1275
|
+
const fileReceipts = [...uploaded.receipts];
|
|
1116
1276
|
for (const [index, path] of mentions.files.entries()) {
|
|
1117
1277
|
const part = index >= MAX_ATTACH_FILES ? { type: "file", path, omitted: "over-limit" } : readOneFile(ctx, sessionId, path, used);
|
|
1118
1278
|
parts.push(part);
|
|
@@ -1249,11 +1409,14 @@ function toWirePolicy(policy) {
|
|
|
1249
1409
|
}));
|
|
1250
1410
|
return { dirs, projectSkipped: policy.projectSkipped, skippedDir: policy.skippedDir };
|
|
1251
1411
|
}
|
|
1252
|
-
function toWireSecurityWorkspace(bound) {
|
|
1412
|
+
function toWireSecurityWorkspace(bound, project) {
|
|
1253
1413
|
return {
|
|
1254
1414
|
...toWireSessionWorkspace(bound),
|
|
1255
1415
|
trustLevel: bound.trust.level,
|
|
1256
|
-
unloadedExtraInstructionFiles: bound.unloadedInstructions
|
|
1416
|
+
unloadedExtraInstructionFiles: bound.unloadedInstructions,
|
|
1417
|
+
execTrusted: bound.trust.exec,
|
|
1418
|
+
inventory: project.inventory,
|
|
1419
|
+
trustRequest: project.trustRequest
|
|
1257
1420
|
};
|
|
1258
1421
|
}
|
|
1259
1422
|
function toWireTrustList(known, workspaces, currentRoot) {
|
|
@@ -1264,11 +1427,13 @@ function toWireTrustList(known, workspaces, currentRoot) {
|
|
|
1264
1427
|
name: w.name,
|
|
1265
1428
|
level: decision.level,
|
|
1266
1429
|
trusted: decision.trusted,
|
|
1267
|
-
|
|
1430
|
+
execTrusted: decision.exec,
|
|
1431
|
+
current: currentRoot !== null && w.root === currentRoot,
|
|
1432
|
+
exists: existsSync(w.root)
|
|
1268
1433
|
};
|
|
1269
1434
|
});
|
|
1270
1435
|
}
|
|
1271
|
-
function collectSecurity(runtime, workspaces, bound, session) {
|
|
1436
|
+
function collectSecurity(runtime, workspaces, bound, session, project) {
|
|
1272
1437
|
const perms = runtime.permissions;
|
|
1273
1438
|
return {
|
|
1274
1439
|
sandbox: runtime.isolation ? toWireSandbox(runtime.isolation, runtime.config.sandbox?.terminal !== false) : null,
|
|
@@ -1281,7 +1446,7 @@ function collectSecurity(runtime, workspaces, bound, session) {
|
|
|
1281
1446
|
managed: toWireManaged(perms.managed()),
|
|
1282
1447
|
cached: perms.approvals().map(toWireApproval)
|
|
1283
1448
|
} : PERMISSIONS_UNAVAILABLE,
|
|
1284
|
-
workspace: bound ? toWireSecurityWorkspace(bound) : null,
|
|
1449
|
+
workspace: bound ? toWireSecurityWorkspace(bound, project) : null,
|
|
1285
1450
|
knownWorkspaces: toWireTrustList(workspaces.known(), workspaces, bound?.workspace.root ?? null),
|
|
1286
1451
|
policy: toWirePolicy(runtime.policy),
|
|
1287
1452
|
audit: perms ? toWireAudit(perms.audit()) : { rows: [], dropped: 0 }
|
|
@@ -1451,6 +1616,7 @@ function config(ctx, res, lang) {
|
|
|
1451
1616
|
usageScope: runtime.usageScope,
|
|
1452
1617
|
compression: runtime.compression,
|
|
1453
1618
|
...runtime.config.budget?.maxCostUsd !== void 0 ? { maxCostUsd: runtime.config.budget.maxCostUsd } : {},
|
|
1619
|
+
...runtime.config.display?.currency !== void 0 ? { currency: runtime.config.display.currency } : {},
|
|
1454
1620
|
...runtime.config.brand !== void 0 ? { brand: runtime.config.brand } : {},
|
|
1455
1621
|
...runtime.config.sidebarMenu !== void 0 ? { sidebarMenu: runtime.config.sidebarMenu } : {},
|
|
1456
1622
|
...runtime.config.userMenu !== void 0 ? { userMenu: runtime.config.userMenu } : {},
|
|
@@ -1514,6 +1680,7 @@ async function deleteSession(ctx, res, sessionId, lang) {
|
|
|
1514
1680
|
}
|
|
1515
1681
|
const outcome = await catalog.delete(sessionId);
|
|
1516
1682
|
if (!outcome.deleted && !inHub) return sendUnknownSession(res, sessionId, lang);
|
|
1683
|
+
ctx.hub.notifySessionsChanged(sessionId);
|
|
1517
1684
|
sendJson(res, 200, outcome);
|
|
1518
1685
|
}
|
|
1519
1686
|
async function patchSession(ctx, req, res, sessionId, lang) {
|
|
@@ -1566,7 +1733,9 @@ async function postMessage(ctx, req, res, sessionId, lang) {
|
|
|
1566
1733
|
}
|
|
1567
1734
|
const body = await readJsonBody(req, void 0, lang);
|
|
1568
1735
|
if (!body.ok) return sendError(res, body.status, "bad-body", body.message);
|
|
1569
|
-
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);
|
|
1570
1739
|
if (message === null) {
|
|
1571
1740
|
return sendError(res, 400, "bad-message", t("web.bad_message", void 0, lang));
|
|
1572
1741
|
}
|
|
@@ -1577,7 +1746,7 @@ async function postMessage(ctx, req, res, sessionId, lang) {
|
|
|
1577
1746
|
return sendError(res, mapped.status, mapped.code, mapped.message);
|
|
1578
1747
|
}
|
|
1579
1748
|
const expansion = expandUserContent(ctx.runtime, message, live?.model ?? null, picked.role);
|
|
1580
|
-
const attached = attachMentions(ctx, sessionId, expansion.message);
|
|
1749
|
+
const attached = attachMentions(ctx, sessionId, expansion.message, uploads);
|
|
1581
1750
|
const started = ctx.hub.start(sessionId, attached.message, expansion.decorate);
|
|
1582
1751
|
if (started.ok) {
|
|
1583
1752
|
return sendJson(res, 202, {
|
|
@@ -1668,7 +1837,8 @@ function listCapabilities(ctx, res, sessionId, lang) {
|
|
|
1668
1837
|
ctx.runtime,
|
|
1669
1838
|
ctx.runtime.tools,
|
|
1670
1839
|
ctx.runtime.workspaces.of(sessionId),
|
|
1671
|
-
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)
|
|
1672
1842
|
);
|
|
1673
1843
|
sendJson(res, 200, payload);
|
|
1674
1844
|
}
|
|
@@ -1687,7 +1857,8 @@ function getSecurity(ctx, res, sessionId, lang) {
|
|
|
1687
1857
|
ctx.runtime,
|
|
1688
1858
|
ctx.runtime.workspaces,
|
|
1689
1859
|
ctx.runtime.workspaces.of(sessionId),
|
|
1690
|
-
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)
|
|
1691
1862
|
);
|
|
1692
1863
|
sendJson(res, 200, payload);
|
|
1693
1864
|
}
|
|
@@ -1757,6 +1928,10 @@ function parseUserContent(raw) {
|
|
|
1757
1928
|
if (Array.isArray(raw) && raw.length > 0) return raw;
|
|
1758
1929
|
return null;
|
|
1759
1930
|
}
|
|
1931
|
+
function parseAttachmentNames(raw) {
|
|
1932
|
+
if (!Array.isArray(raw)) return [];
|
|
1933
|
+
return raw.filter((name) => typeof name === "string" && name.length > 0);
|
|
1934
|
+
}
|
|
1760
1935
|
async function revokeApprovalCache(ctx, req, res, sessionId, lang) {
|
|
1761
1936
|
if (!ctx.hub.has(sessionId)) return sendUnknownSession(res, sessionId, lang);
|
|
1762
1937
|
const perms = ctx.runtime.permissions;
|
|
@@ -2286,10 +2461,11 @@ function sessionHasImages(session) {
|
|
|
2286
2461
|
}
|
|
2287
2462
|
function getModel(ctx, res, sessionId, _lang) {
|
|
2288
2463
|
const { runtime } = ctx;
|
|
2289
|
-
const
|
|
2464
|
+
const live = runtime.sessionFactory.get(sessionId);
|
|
2465
|
+
const selection = live?.model.get();
|
|
2290
2466
|
sendJson(res, 200, {
|
|
2291
2467
|
selection: selection ? toWireSelection(selection) : null,
|
|
2292
|
-
configured: runtime.config.model || null
|
|
2468
|
+
configured: (live ? live.model.configured() : runtime.config.model) || null
|
|
2293
2469
|
});
|
|
2294
2470
|
}
|
|
2295
2471
|
async function setModel(ctx, req, res, sessionId, lang) {
|
|
@@ -2437,9 +2613,18 @@ function toWireCounts(counts) {
|
|
|
2437
2613
|
mcpServers: counts.mcpServers
|
|
2438
2614
|
};
|
|
2439
2615
|
}
|
|
2440
|
-
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) {
|
|
2441
2625
|
return {
|
|
2442
2626
|
name: entry.name,
|
|
2627
|
+
displayName: displayNames.get(`${entry.marketplace}/${entry.name}`) ?? null,
|
|
2443
2628
|
version: entry.version,
|
|
2444
2629
|
source: entry.source,
|
|
2445
2630
|
sourceType: entry.sourceType,
|
|
@@ -2554,8 +2739,9 @@ function pluginsPayload(control) {
|
|
|
2554
2739
|
const installedMap = new Map(
|
|
2555
2740
|
installed.map((p) => [p.name, { version: p.version, linked: p.linked }])
|
|
2556
2741
|
);
|
|
2742
|
+
const shown = displayNamesOf(control);
|
|
2557
2743
|
return {
|
|
2558
|
-
installed: installed.map(toWirePluginEntry),
|
|
2744
|
+
installed: installed.map((p) => toWirePluginEntry(p, shown)),
|
|
2559
2745
|
hits: control.search().map((hit) => toWirePluginHit(hit, installedMap)),
|
|
2560
2746
|
marketplaces: control.marketplaces().map(toWireMarketplace),
|
|
2561
2747
|
pendingRestart: control.pendingRestart,
|
|
@@ -2619,7 +2805,7 @@ function sendAction(res, control, outcome) {
|
|
|
2619
2805
|
const fresh = "record" in outcome ? control.list().find((p) => p.name === outcome.record.name) : void 0;
|
|
2620
2806
|
sendJson(res, 200, {
|
|
2621
2807
|
ok: true,
|
|
2622
|
-
entry: fresh ? toWirePluginEntry(fresh) : null,
|
|
2808
|
+
entry: fresh ? toWirePluginEntry(fresh, displayNamesOf(control)) : null,
|
|
2623
2809
|
pendingRestart: control.pendingRestart,
|
|
2624
2810
|
detail: "detail" in outcome ? outcome.detail : null,
|
|
2625
2811
|
reason: null
|
|
@@ -2651,6 +2837,51 @@ async function installLocalPlugin(ctx, req, res, lang) {
|
|
|
2651
2837
|
await control.installLocal(fieldOf(payload, "path"), fieldOf(payload, "token"), lang)
|
|
2652
2838
|
);
|
|
2653
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
|
+
}
|
|
2654
2885
|
async function updateInstalledPlugin(ctx, req, res, lang) {
|
|
2655
2886
|
if (refusedByLan(ctx, res, lang)) return;
|
|
2656
2887
|
const control = controlOf(ctx, res, lang);
|
|
@@ -2784,7 +3015,8 @@ function toWireProvider(option, lang) {
|
|
|
2784
3015
|
label: providerLabel(option, lang),
|
|
2785
3016
|
envVar: option.envVar,
|
|
2786
3017
|
hasKey: option.hasKey,
|
|
2787
|
-
keyHint: option.keyHint
|
|
3018
|
+
keyHint: option.keyHint,
|
|
3019
|
+
current: option.current
|
|
2788
3020
|
};
|
|
2789
3021
|
}
|
|
2790
3022
|
function toWireSuggestions(view) {
|
|
@@ -3349,6 +3581,33 @@ async function removeSkill(ctx, req, res, lang) {
|
|
|
3349
3581
|
reason: null
|
|
3350
3582
|
});
|
|
3351
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
|
+
}
|
|
3352
3611
|
// src/sse.ts
|
|
3353
3612
|
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
3354
3613
|
function parseLastEventId(req) {
|
|
@@ -3832,9 +4091,9 @@ var VIEWABLE = /* @__PURE__ */ new Map([
|
|
|
3832
4091
|
[".webm", { kind: "video", mime: "video/webm" }],
|
|
3833
4092
|
[".mov", { kind: "video", mime: "video/quicktime" }]
|
|
3834
4093
|
]);
|
|
3835
|
-
function kindOf(
|
|
3836
|
-
if (
|
|
3837
|
-
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";
|
|
3838
4097
|
}
|
|
3839
4098
|
function toWireRefusal(reason) {
|
|
3840
4099
|
return reason;
|
|
@@ -3882,15 +4141,15 @@ function statFiles(ctx, res, sessionId, rawUrl, lang) {
|
|
|
3882
4141
|
const asked = params.getAll("path").filter((p) => p.trim() !== "");
|
|
3883
4142
|
const taken = asked.slice(0, WIRE_FILE_STAT_MAX);
|
|
3884
4143
|
const files = taken.map((path) => {
|
|
3885
|
-
const
|
|
3886
|
-
if (!
|
|
4144
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4145
|
+
if (!stat3.ok) return { path, ok: false, refusal: toWireRefusal(stat3.reason) };
|
|
3887
4146
|
return {
|
|
3888
4147
|
path,
|
|
3889
4148
|
ok: true,
|
|
3890
|
-
kind: kindOf(
|
|
3891
|
-
bytes:
|
|
3892
|
-
mtimeMs:
|
|
3893
|
-
openable: ctx.sameMachine && isSystemOpenable(
|
|
4149
|
+
kind: kindOf(stat3),
|
|
4150
|
+
bytes: stat3.bytes,
|
|
4151
|
+
mtimeMs: stat3.mtimeMs,
|
|
4152
|
+
openable: ctx.sameMachine && isSystemOpenable(stat3.rel)
|
|
3894
4153
|
};
|
|
3895
4154
|
});
|
|
3896
4155
|
sendJson(res, 200, {
|
|
@@ -3904,18 +4163,18 @@ function readFileText(ctx, res, sessionId, rawUrl, lang) {
|
|
|
3904
4163
|
if (root === null) return;
|
|
3905
4164
|
const path = singlePath(res, rawUrl, lang);
|
|
3906
4165
|
if (path === null) return;
|
|
3907
|
-
const
|
|
3908
|
-
if (!
|
|
3909
|
-
const refusal = toWireRefusal(
|
|
4166
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4167
|
+
if (!stat3.ok) {
|
|
4168
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
3910
4169
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
3911
4170
|
}
|
|
3912
|
-
if (
|
|
4171
|
+
if (stat3.kind !== "text") {
|
|
3913
4172
|
return sendError(res, 415, "not-text", t("web.file_view_not_text", void 0, lang));
|
|
3914
4173
|
}
|
|
3915
|
-
if (
|
|
4174
|
+
if (stat3.bytes > MAX_TEXT_BYTES) {
|
|
3916
4175
|
return sendError(res, 413, "too-large", describeRefusal("too-large", lang));
|
|
3917
4176
|
}
|
|
3918
|
-
const out = ctx.runtime.workspaceFiles.readFile(
|
|
4177
|
+
const out = ctx.runtime.workspaceFiles.readFile(stat3.rel, root);
|
|
3919
4178
|
if (!out.ok) {
|
|
3920
4179
|
const refusal = out.reason === "binary" ? "unreadable" : out.reason;
|
|
3921
4180
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
@@ -3931,26 +4190,26 @@ function readFileBytes(ctx, res, sessionId, rawUrl, headOnly, lang) {
|
|
|
3931
4190
|
if (root === null) return;
|
|
3932
4191
|
const path = singlePath(res, rawUrl, lang);
|
|
3933
4192
|
if (path === null) return;
|
|
3934
|
-
const
|
|
3935
|
-
if (!
|
|
3936
|
-
const refusal = toWireRefusal(
|
|
4193
|
+
const stat3 = ctx.runtime.workspaceFiles.statFile(path, root);
|
|
4194
|
+
if (!stat3.ok) {
|
|
4195
|
+
const refusal = toWireRefusal(stat3.reason);
|
|
3937
4196
|
return sendError(res, statusFor(refusal), refusal, describeRefusal(refusal, lang));
|
|
3938
4197
|
}
|
|
3939
|
-
const known = VIEWABLE.get(extname(
|
|
3940
|
-
const inline = known !== void 0 &&
|
|
4198
|
+
const known = VIEWABLE.get(extname(stat3.rel).toLowerCase());
|
|
4199
|
+
const inline = known !== void 0 && stat3.kind === "binary";
|
|
3941
4200
|
const headers = {
|
|
3942
4201
|
"Content-Type": inline ? known.mime : "application/octet-stream",
|
|
3943
4202
|
"X-Content-Type-Options": "nosniff",
|
|
3944
|
-
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(
|
|
4203
|
+
"Content-Disposition": inline ? "inline" : `attachment; filename*=UTF-8''${encodeURIComponent(basenameOf(stat3.rel))}`
|
|
3945
4204
|
};
|
|
3946
4205
|
if (inline && known.kind === "pdf") headers["Content-Security-Policy"] = "sandbox";
|
|
3947
4206
|
if (headOnly) {
|
|
3948
4207
|
return sendEmpty(res, 200, {
|
|
3949
4208
|
...headers,
|
|
3950
|
-
"Content-Length": String(
|
|
4209
|
+
"Content-Length": String(stat3.bytes)
|
|
3951
4210
|
});
|
|
3952
4211
|
}
|
|
3953
|
-
sendFile(res,
|
|
4212
|
+
sendFile(res, stat3.absPath, headers);
|
|
3954
4213
|
}
|
|
3955
4214
|
function basenameOf(rel) {
|
|
3956
4215
|
const cut = rel.lastIndexOf("/");
|
|
@@ -4001,6 +4260,26 @@ async function openFileWithSystemApp(ctx, req, res, sessionId, lang) {
|
|
|
4001
4260
|
}
|
|
4002
4261
|
sendJson(res, 200, { ok: true });
|
|
4003
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
|
+
}
|
|
4004
4283
|
var GIT_TIMEOUT_MS = 1e4;
|
|
4005
4284
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
4006
4285
|
function runGit(cwd, args) {
|
|
@@ -4111,7 +4390,7 @@ async function gitDiff(root) {
|
|
|
4111
4390
|
for (const item of picked) {
|
|
4112
4391
|
const old = await gitSide(root, item.oldSha, sizes);
|
|
4113
4392
|
const fresh = await worktreeSide(join(root, ...item.path.split("/")));
|
|
4114
|
-
files.push(
|
|
4393
|
+
files.push(describe3(item.path, old, fresh));
|
|
4115
4394
|
}
|
|
4116
4395
|
return { source: "git", root, files, truncated, notes };
|
|
4117
4396
|
}
|
|
@@ -4157,7 +4436,7 @@ async function checkpointDiff(opts) {
|
|
|
4157
4436
|
for (const absolute of picked) {
|
|
4158
4437
|
const fresh = await worktreeSide(absolute);
|
|
4159
4438
|
files.push(
|
|
4160
|
-
|
|
4439
|
+
describe3(displayPath(opts.workDir, absolute), ABSENT, fresh, {
|
|
4161
4440
|
status: fresh.kind === "absent" ? "deleted" : "modified",
|
|
4162
4441
|
omitted: "no-snapshot"
|
|
4163
4442
|
})
|
|
@@ -4195,7 +4474,7 @@ async function worktreeSide(absolute) {
|
|
|
4195
4474
|
return ABSENT;
|
|
4196
4475
|
}
|
|
4197
4476
|
}
|
|
4198
|
-
function
|
|
4477
|
+
function describe3(path, old, fresh, override = {}) {
|
|
4199
4478
|
const status = override.status ?? (old.kind === "absent" ? "added" : fresh.kind === "absent" ? "deleted" : "modified");
|
|
4200
4479
|
const oldBytes = bytesOf(old);
|
|
4201
4480
|
const newBytes = bytesOf(fresh);
|
|
@@ -4278,6 +4557,7 @@ async function createSession(ctx, req, res, lang) {
|
|
|
4278
4557
|
}
|
|
4279
4558
|
ctx.hub.register(outcome.live.session);
|
|
4280
4559
|
if (defer) ctx.runtime.workspaces.defer(outcome.live.sessionId);
|
|
4560
|
+
if (outcome.created) ctx.hub.notifySessionsChanged(outcome.live.sessionId);
|
|
4281
4561
|
sendJson(res, 200, {
|
|
4282
4562
|
sessionId: outcome.live.sessionId,
|
|
4283
4563
|
created: outcome.created,
|
|
@@ -4362,6 +4642,36 @@ async function sessionDiff(ctx, res, sessionId, _lang) {
|
|
|
4362
4642
|
})
|
|
4363
4643
|
);
|
|
4364
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
|
+
}
|
|
4365
4675
|
// src/routes.ts
|
|
4366
4676
|
var DENY_KEY = {
|
|
4367
4677
|
"missing-credential": "web.route.deny_missing_credential",
|
|
@@ -4369,10 +4679,17 @@ var DENY_KEY = {
|
|
|
4369
4679
|
"bad-origin": "web.route.deny_bad_origin",
|
|
4370
4680
|
"bad-host": "web.route.deny_bad_host"
|
|
4371
4681
|
};
|
|
4682
|
+
var log = createLogger("server-route");
|
|
4372
4683
|
function createRequestHandler(opts) {
|
|
4373
4684
|
return (req, res) => {
|
|
4374
4685
|
const lang = langOf(req.url);
|
|
4375
|
-
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
|
+
});
|
|
4376
4693
|
if (!res.headersSent) {
|
|
4377
4694
|
sendError(res, 500, "internal", t("web.route.internal_error", void 0, lang));
|
|
4378
4695
|
} else res.destroy();
|
|
@@ -4457,6 +4774,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
|
|
|
4457
4774
|
if (resource === "skills" && first === "import" && second === void 0 && method === "POST") {
|
|
4458
4775
|
return importSkills(ctx, req, res, lang);
|
|
4459
4776
|
}
|
|
4777
|
+
if (resource === "skills" && first === "stage" && second === void 0 && method === "POST") {
|
|
4778
|
+
return stageSkillDrop(ctx, req, res, lang);
|
|
4779
|
+
}
|
|
4460
4780
|
if (resource === "skills" && first === "remove" && second === void 0 && method === "POST") {
|
|
4461
4781
|
return removeSkill(ctx, req, res, lang);
|
|
4462
4782
|
}
|
|
@@ -4499,6 +4819,9 @@ async function handleApi(opts, req, res, method, segments, lang) {
|
|
|
4499
4819
|
if (resource === "plugins" && first === "refresh" && second === void 0 && method === "POST") {
|
|
4500
4820
|
return refreshPlugins(ctx, res, lang);
|
|
4501
4821
|
}
|
|
4822
|
+
if (resource === "plugins" && first === "stage" && second === void 0 && method === "POST") {
|
|
4823
|
+
return stagePlugin(ctx, req, res, lang);
|
|
4824
|
+
}
|
|
4502
4825
|
if (resource === "providers" && first === void 0 && isRead(method)) {
|
|
4503
4826
|
return listProviders(ctx, res, lang);
|
|
4504
4827
|
}
|
|
@@ -4580,6 +4903,9 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
|
|
|
4580
4903
|
if (method === "POST") return postMessage(ctx, req, res, sessionId, lang);
|
|
4581
4904
|
return methodNotAllowed(res, "GET, POST", lang);
|
|
4582
4905
|
}
|
|
4906
|
+
if (action === "attachments" && method === "POST") {
|
|
4907
|
+
return uploadAttachment(ctx, req, res, sessionId, lang);
|
|
4908
|
+
}
|
|
4583
4909
|
if (action === "abort" && method === "POST") return abortSession(ctx, res, sessionId, lang);
|
|
4584
4910
|
if (action === "reference-candidates" && isRead(method)) {
|
|
4585
4911
|
return listReferenceCandidates(ctx, res, sessionId, req.url, lang);
|
|
@@ -4638,6 +4964,12 @@ async function handleSessions(opts, req, res, method, sessionId, action, detail,
|
|
|
4638
4964
|
if (action === "permission" && method === "POST") {
|
|
4639
4965
|
return setPermission(ctx, req, res, sessionId, lang);
|
|
4640
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
|
+
}
|
|
4641
4973
|
if (action === "approval-cache" && method === "POST") {
|
|
4642
4974
|
return revokeApprovalCache(ctx, req, res, sessionId, lang);
|
|
4643
4975
|
}
|
|
@@ -4752,7 +5084,7 @@ async function createWebServer(opts) {
|
|
|
4752
5084
|
host: binding.host,
|
|
4753
5085
|
token: binding.token,
|
|
4754
5086
|
...webRoot === void 0 ? {} : { webRoot },
|
|
4755
|
-
observe: (sink) => hub.observe(sink),
|
|
5087
|
+
observe: (sink, opts2) => hub.observe(sink, opts2 ?? {}),
|
|
4756
5088
|
snapshot: () => hub.listSessions(),
|
|
4757
5089
|
close: makeCloser(server, hub, opts.runtime, unsubscribeTasks)
|
|
4758
5090
|
}
|
|
@@ -4772,7 +5104,8 @@ function buildHandler(opts, hub, binding, port, webRoot) {
|
|
|
4772
5104
|
artifactsRoot: opts.artifactsRoot ?? opts.runtime.artifactsRoot,
|
|
4773
5105
|
lanExposed: binding.lanExposed,
|
|
4774
5106
|
nativeDirPicker: resolveNativeDirPicker(machineFacts),
|
|
4775
|
-
sameMachine: isSameMachine(machineFacts)
|
|
5107
|
+
sameMachine: isSameMachine(machineFacts),
|
|
5108
|
+
...opts.onTrustRequest === void 0 ? {} : { trustRequest: opts.onTrustRequest }
|
|
4776
5109
|
},
|
|
4777
5110
|
guard: createAuthGuard({ token: binding.token, port, lanExposed: binding.lanExposed }),
|
|
4778
5111
|
...webRoot === void 0 ? {} : { webRoot },
|