@zhanguym/chatroom 1.0.0 → 1.1.0-beta.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/dist/app/application.d.ts +0 -1
- package/dist/app/application.js +8 -7
- package/dist/app/event-bus.d.ts +13 -10
- package/dist/app/git-access.d.ts +50 -0
- package/dist/app/git-access.js +2 -0
- package/dist/app/service-registry.d.ts +11 -0
- package/dist/app/service-registry.js +17 -0
- package/dist/app/workspace-access.d.ts +24 -0
- package/dist/app/workspace-access.js +2 -0
- package/dist/auth/ingress-policy.d.ts +2 -0
- package/dist/auth/ingress-policy.js +14 -2
- package/dist/auth/passkey-service.js +1 -1
- package/dist/cli/run-cli.js +1 -1
- package/dist/core/fs/safe-directory.d.ts +9 -0
- package/dist/core/fs/safe-directory.js +129 -0
- package/dist/core/operations/repository.d.ts +2 -0
- package/dist/core/runtime/line-process.d.ts +19 -0
- package/dist/core/runtime/line-process.js +51 -0
- package/dist/infrastructure/database/operation-repository.js +8 -2
- package/dist/infrastructure/database/schema.d.ts +1 -1
- package/dist/infrastructure/database/schema.js +62 -0
- package/dist/infrastructure/http/http-server.js +1 -1
- package/dist/native/linux/chatroom-computer-helper +0 -0
- package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
- package/dist/native/windows/chatroom-computer-helper.exe +0 -0
- package/dist/operations/operation-log.js +16 -4
- package/dist/plugins/agent/async-queue.d.ts +10 -0
- package/dist/plugins/agent/async-queue.js +49 -0
- package/dist/plugins/agent/mcp.d.ts +3 -0
- package/dist/plugins/agent/mcp.js +502 -0
- package/dist/plugins/agent/plugin.d.ts +4 -0
- package/dist/plugins/agent/plugin.js +35 -0
- package/dist/plugins/agent/provider.d.ts +132 -0
- package/dist/plugins/agent/providers/codex/mapping.d.ts +56 -0
- package/dist/plugins/agent/providers/codex/mapping.js +427 -0
- package/dist/plugins/agent/providers/codex/provider.d.ts +84 -0
- package/dist/plugins/agent/providers/codex/provider.js +828 -0
- package/dist/plugins/agent/providers/codex/rpc.d.ts +40 -0
- package/dist/plugins/agent/providers/codex/rpc.js +227 -0
- package/dist/plugins/agent/providers/common.d.ts +11 -0
- package/dist/plugins/agent/providers/common.js +75 -0
- package/dist/plugins/agent/providers/index.d.ts +3 -0
- package/dist/plugins/agent/providers/index.js +4 -0
- package/dist/plugins/agent/repository.d.ts +21 -0
- package/dist/plugins/agent/repository.js +226 -0
- package/dist/plugins/agent/runtime-config.d.ts +5 -0
- package/dist/plugins/agent/runtime-config.js +60 -0
- package/dist/plugins/agent/runtime-files.d.ts +8 -0
- package/dist/plugins/agent/runtime-files.js +170 -0
- package/dist/plugins/agent/runtime-state.d.ts +17 -0
- package/dist/plugins/agent/runtime-state.js +133 -0
- package/dist/plugins/agent/runtime.d.ts +60 -0
- package/dist/plugins/agent/runtime.js +928 -0
- package/dist/plugins/agent/store.d.ts +17 -0
- package/dist/plugins/agent/store.js +1 -0
- package/dist/plugins/agent/types.d.ts +320 -0
- package/dist/plugins/agent/types.js +13 -0
- package/dist/plugins/cloud/plugin.d.ts +1 -1
- package/dist/plugins/cloud/plugin.js +3 -3
- package/dist/plugins/cloud/tunnel-client.js +6 -7
- package/dist/plugins/computer/computer-schemas.d.ts +7 -7
- package/dist/plugins/computer/computer-service.d.ts +3 -0
- package/dist/plugins/computer/computer-service.js +33 -1
- package/dist/plugins/computer/plugin.d.ts +1 -1
- package/dist/plugins/computer/plugin.js +1 -1
- package/dist/plugins/git/git-service.d.ts +3 -2
- package/dist/plugins/git/git-service.js +37 -1
- package/dist/plugins/git/plugin.d.ts +0 -2
- package/dist/plugins/git/plugin.js +3 -3
- package/dist/plugins/process/plugin.d.ts +1 -1
- package/dist/plugins/process/plugin.js +3 -3
- package/dist/plugins/process/process-supervisor.d.ts +1 -0
- package/dist/plugins/process/process-supervisor.js +15 -4
- package/dist/plugins/types.d.ts +1 -11
- package/dist/plugins/types.js +1 -17
- package/dist/plugins/web/api-types.d.ts +20 -5
- package/dist/plugins/web/http/agent-api-router.d.ts +3 -0
- package/dist/plugins/web/http/agent-api-router.js +254 -0
- package/dist/plugins/web/http/api-router.js +9 -0
- package/dist/plugins/web/http/computer-api-router.js +21 -6
- package/dist/plugins/web/http/process-api-router.js +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +79 -6
- package/dist/plugins/web/plugin.d.ts +1 -1
- package/dist/plugins/web/plugin.js +5 -4
- package/dist/plugins/web/runtime.d.ts +6 -3
- package/dist/plugins/web/runtime.js +6 -1
- package/dist/plugins/workspace/mcp.js +16 -0
- package/dist/plugins/workspace/metadata.d.ts +1 -1
- package/dist/plugins/workspace/plugin.d.ts +1 -1
- package/dist/plugins/workspace/plugin.js +5 -2
- package/dist/plugins/workspace/types.d.ts +1 -18
- package/dist/plugins/workspace/workspace-fs.d.ts +9 -3
- package/dist/plugins/workspace/workspace-fs.js +78 -125
- package/dist/plugins/workspace/workspace-root-policy.d.ts +9 -0
- package/dist/plugins/workspace/workspace-root-policy.js +56 -0
- package/dist/plugins/workspace/workspace-service.d.ts +11 -6
- package/dist/plugins/workspace/workspace-service.js +39 -49
- package/dist/web/assets/AgentView-DKuspiki.js +4 -0
- package/dist/web/assets/AgentView-DYJeMJLX.css +1 -0
- package/dist/web/assets/{CloudView-CHjVbp-5.css → CloudView-DVVvwuWW.css} +1 -1
- package/dist/web/assets/CloudView-PbREbJgz.js +1 -0
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-CiFNEpWo.js +3 -0
- package/dist/web/assets/ComputerView-BM0u3wSF.css +1 -0
- package/dist/web/assets/ComputerView-BRb2sE0Z.js +1 -0
- package/dist/web/assets/McpToolsView-B8NSFdf4.js +1 -0
- package/dist/web/assets/McpToolsView-C22ZbSuV.css +1 -0
- package/dist/web/assets/OperationsView-BjTY2lIh.css +1 -0
- package/dist/web/assets/OperationsView-Cavi-6SZ.js +4 -0
- package/dist/web/assets/ProcessesView-Ce6iVwjA.css +1 -0
- package/dist/web/assets/ProcessesView-UO48Nh2q.js +4 -0
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-Bvc9wRyA.js +1 -0
- package/dist/web/assets/SystemLogsView-BXmOrmu5.js +1 -0
- package/dist/web/assets/SystemLogsView-CofP4Jvi.css +1 -0
- package/dist/web/assets/VSelect-BPRXx8KM.js +1 -0
- package/dist/web/assets/VSpacer-Cl-QiwaM.js +1 -0
- package/dist/web/assets/VSwitch-C0XNj1lR.js +1 -0
- package/dist/web/assets/{VTabs-qBcuuXh3.js → VTabs-CHj39Ket.js} +1 -1
- package/dist/web/assets/VTextarea-BIcUWOU6.js +16 -0
- package/dist/web/assets/VTextarea-DG1zE1as.css +1 -0
- package/dist/web/assets/WorkspacesView-DPCt6kYy.css +1 -0
- package/dist/web/assets/WorkspacesView-rY6RXeiR.js +2 -0
- package/dist/web/assets/entity-refresh-queue-wQii8Dl5.js +1 -0
- package/dist/web/assets/{index-CkjqXRSj.css → index-BPpl3-sP.css} +1 -1
- package/dist/web/assets/index-Dt-pWiW1.js +19 -0
- package/dist/web/assets/{syntax-highlight-B5ArnyrC.js → syntax-highlight-C_fuMcwC.js} +1 -1
- package/dist/web/assets/useMasterDetailFocus-D0j-KcXv.js +1 -0
- package/dist/web/assets/useOperationLabels-DR8r6Vrl.js +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/plugins/git/types.d.ts +0 -32
- package/dist/web/assets/CloudView-CvX0dkgu.js +0 -1
- package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +0 -3
- package/dist/web/assets/ComputerView-91Z9Cefw.css +0 -1
- package/dist/web/assets/ComputerView-OOyEqWml.js +0 -1
- package/dist/web/assets/McpToolsView-BTnolyS6.css +0 -1
- package/dist/web/assets/McpToolsView-DjXWTKrZ.js +0 -1
- package/dist/web/assets/OperationsView-DqTY51Cr.js +0 -4
- package/dist/web/assets/ProcessesView-C13BykIa.js +0 -4
- package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +0 -1
- package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +0 -1
- package/dist/web/assets/VSelect-DrRpKyYI.js +0 -1
- package/dist/web/assets/VSpacer-C3wAY3F0.js +0 -1
- package/dist/web/assets/VSwitch-CxusR4iO.js +0 -1
- package/dist/web/assets/WorkspacesView-BzQ0aH74.css +0 -1
- package/dist/web/assets/WorkspacesView-CijIvfsp.js +0 -2
- package/dist/web/assets/index-BegKQ3Yd.js +0 -19
- package/dist/web/assets/useOperationLabels-BDjzGDkT.js +0 -1
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.d.ts +0 -0
- /package/dist/{plugins/process → core/runtime}/head-tail-buffer.js +0 -0
- /package/dist/plugins/{git/types.js → agent/provider.js} +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { createServiceToken } from "#
|
|
1
|
+
import { createServiceToken } from "#app/service-registry";
|
|
2
2
|
import { PipeProcessBackend } from "./infrastructure/pipe-process-backend.js";
|
|
3
3
|
import { PtyProcessBackend } from "./infrastructure/pty-process-backend.js";
|
|
4
4
|
import { registerProcessTools } from "./mcp.js";
|
|
5
5
|
import { ProcessSupervisor } from "./process-supervisor.js";
|
|
6
|
-
export const
|
|
6
|
+
export const ProcessServiceToken = createServiceToken("process");
|
|
7
7
|
export function createProcessPlugin() {
|
|
8
8
|
let service = null;
|
|
9
9
|
return {
|
|
10
10
|
id: "process",
|
|
11
11
|
activate(context) {
|
|
12
12
|
service = new ProcessSupervisor({ pipe: new PipeProcessBackend(), pty: new PtyProcessBackend() }, context.operations, context.events, context.config.process.maxOutputBytes, context.config.process.defaultTimeoutMs, context.config.process.maxCompletedProcesses);
|
|
13
|
-
context.services.provide(
|
|
13
|
+
context.services.provide(ProcessServiceToken, service);
|
|
14
14
|
},
|
|
15
15
|
registerMcp(mcp) {
|
|
16
16
|
if (!service)
|
|
@@ -23,6 +23,7 @@ export declare class ProcessSupervisor {
|
|
|
23
23
|
}, operations: OperationLog, eventBus: RuntimeEventBus, maxOutputBytes: number, defaultTimeoutMs: number, maxCompletedProcesses: number);
|
|
24
24
|
start(request: ProcessStartRequest, operationContext: ProcessOperationContext): Promise<ProcessSnapshot>;
|
|
25
25
|
read(processId: ProcessId): ProcessSnapshot;
|
|
26
|
+
summary(processId: ProcessId): ProcessSummary;
|
|
26
27
|
list(): ProcessSnapshot[];
|
|
27
28
|
summaries(): ProcessSummary[];
|
|
28
29
|
write(processId: ProcessId, data: string): ProcessSnapshot;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
3
|
import { childEnvironment } from "#core/runtime/child-environment";
|
|
4
|
-
import { HeadTailBuffer } from "
|
|
4
|
+
import { HeadTailBuffer } from "#core/runtime/head-tail-buffer";
|
|
5
5
|
export class ProcessSupervisor {
|
|
6
6
|
backends;
|
|
7
7
|
operations;
|
|
@@ -54,6 +54,12 @@ export class ProcessSupervisor {
|
|
|
54
54
|
this.operations.finish(operationId, "error", null, error);
|
|
55
55
|
throw new ChatRoomError("PROCESS_FAILED", `Failed to start process: ${request.command}`, undefined, { cause: error });
|
|
56
56
|
}
|
|
57
|
+
if (this.shuttingDown) {
|
|
58
|
+
backend.kill("SIGKILL");
|
|
59
|
+
if (!adoptedOperation)
|
|
60
|
+
this.operations.finish(operationId, "cancelled", null, new Error("Process supervisor shut down during process startup"));
|
|
61
|
+
throw new ChatRoomError("CONFLICT", "Process supervisor is shutting down");
|
|
62
|
+
}
|
|
57
63
|
let resolveSettled;
|
|
58
64
|
const settled = new Promise((resolve) => {
|
|
59
65
|
resolveSettled = resolve;
|
|
@@ -97,13 +103,17 @@ export class ProcessSupervisor {
|
|
|
97
103
|
}, 2000).unref();
|
|
98
104
|
}, timeoutMs).unref();
|
|
99
105
|
const snapshot = this.snapshotOf(processId, managed);
|
|
100
|
-
this.eventBus.emit({ type: "process",
|
|
106
|
+
this.eventBus.emit({ type: "process", processId });
|
|
101
107
|
return snapshot;
|
|
102
108
|
}
|
|
103
109
|
read(processId) {
|
|
104
110
|
const item = this.require(processId);
|
|
105
111
|
return this.snapshotOf(processId, item);
|
|
106
112
|
}
|
|
113
|
+
summary(processId) {
|
|
114
|
+
const item = this.require(processId);
|
|
115
|
+
return this.summaryOf(processId, item);
|
|
116
|
+
}
|
|
107
117
|
list() {
|
|
108
118
|
return [...this.processes.entries()]
|
|
109
119
|
.map(([id, item]) => this.snapshotOf(id, item))
|
|
@@ -155,7 +165,7 @@ export class ProcessSupervisor {
|
|
|
155
165
|
item.outputEventTimer = setTimeout(() => {
|
|
156
166
|
item.outputEventTimer = null;
|
|
157
167
|
if (item.state === "running")
|
|
158
|
-
this.eventBus.emit({ type: "process
|
|
168
|
+
this.eventBus.emit({ type: "process", processId, output: true });
|
|
159
169
|
}, 500).unref();
|
|
160
170
|
}
|
|
161
171
|
}
|
|
@@ -185,7 +195,7 @@ export class ProcessSupervisor {
|
|
|
185
195
|
}, exitCode === 0 && !signal
|
|
186
196
|
? undefined
|
|
187
197
|
: { exitCode, signal, timedOut: snapshot.timedOut });
|
|
188
|
-
this.eventBus.emit({ type: "process",
|
|
198
|
+
this.eventBus.emit({ type: "process", processId });
|
|
189
199
|
item.resolveSettled(snapshot);
|
|
190
200
|
this.trimCompletedProcesses();
|
|
191
201
|
}
|
|
@@ -195,6 +205,7 @@ export class ProcessSupervisor {
|
|
|
195
205
|
.sort(([, a], [, b]) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
196
206
|
for (const [processId] of completed.slice(this.maxCompletedProcesses)) {
|
|
197
207
|
this.processes.delete(processId);
|
|
208
|
+
this.eventBus.emit({ type: "process", processId, deleted: true });
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
require(processId) {
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -6,17 +6,7 @@ import type { McpToolControl } from "#mcp/server/tool-control";
|
|
|
6
6
|
import type { RuntimeEventBus } from "#app/event-bus";
|
|
7
7
|
import type { ExternalAccessRegistry } from "#app/external-access-registry";
|
|
8
8
|
import type { LogWriter } from "#core/logging/types";
|
|
9
|
-
|
|
10
|
-
readonly key: symbol;
|
|
11
|
-
readonly name: string;
|
|
12
|
-
readonly __type?: T;
|
|
13
|
-
}
|
|
14
|
-
export declare function createServiceToken<T>(name: string): ServiceToken<T>;
|
|
15
|
-
export declare class ServiceRegistry {
|
|
16
|
-
private readonly values;
|
|
17
|
-
provide<T>(token: ServiceToken<T>, value: T): void;
|
|
18
|
-
require<T>(token: ServiceToken<T>): T;
|
|
19
|
-
}
|
|
9
|
+
import type { ServiceRegistry } from "#app/service-registry";
|
|
20
10
|
export interface PluginContext {
|
|
21
11
|
config: ChatRoomConfig;
|
|
22
12
|
database: AppDatabase;
|
package/dist/plugins/types.js
CHANGED
|
@@ -1,17 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
return { key: Symbol(name), name };
|
|
3
|
-
}
|
|
4
|
-
export class ServiceRegistry {
|
|
5
|
-
values = new Map();
|
|
6
|
-
provide(token, value) {
|
|
7
|
-
if (this.values.has(token.key))
|
|
8
|
-
throw new Error(`Service already provided: ${token.name}`);
|
|
9
|
-
this.values.set(token.key, value);
|
|
10
|
-
}
|
|
11
|
-
require(token) {
|
|
12
|
-
const value = this.values.get(token.key);
|
|
13
|
-
if (value === undefined)
|
|
14
|
-
throw new Error(`Required service unavailable: ${token.name}`);
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Operation as DomainOperation } from "#core/operations/types";
|
|
2
|
-
import type { GitBranch, GitChange, GitCommit, GitDiff, GitStatus } from "#
|
|
2
|
+
import type { GitBranch, GitChange, GitCommit, GitDiff, GitStatus } from "#app/git-access";
|
|
3
3
|
import type { ProcessSnapshot, ProcessSummary } from "#plugins/process/types";
|
|
4
|
+
import type { AgentAttachment, AgentHistory, AgentHistoryUpdate, AgentInteractionResponse, AgentItem, AgentModel, AgentProviderId, AgentProviderDetails, AgentReviewTarget, AgentProviderStatus, AgentSession, AgentTurn, AgentTurnSource } from "#plugins/agent/types";
|
|
4
5
|
import type { McpToolSummary as DomainMcpToolSummary } from "#mcp/server/tool-control";
|
|
5
6
|
import type { CloudServiceId, CloudStatus as DomainCloudStatus } from "#plugins/cloud/types";
|
|
6
|
-
import type { ComputerDisplay, ComputerPermission,
|
|
7
|
-
import type { WorkspaceEntry,
|
|
8
|
-
|
|
7
|
+
import type { ComputerDisplay, ComputerPermission, ComputerStatus } from "#plugins/computer/types";
|
|
8
|
+
import type { WorkspaceEntry, WorkspaceInfo, WorkspaceSkill } from "#app/workspace-access";
|
|
9
|
+
import type { WorkspaceFile } from "#plugins/workspace/types";
|
|
10
|
+
export type { AgentAttachment, AgentHistory, AgentHistoryUpdate, AgentInteractionResponse, AgentItem, AgentModel, AgentProviderId, AgentProviderDetails, AgentReviewTarget, AgentProviderStatus, AgentSession, AgentTurn, AgentTurnSource, ComputerDisplay, ComputerPermission, ComputerStatus, GitBranch, GitChange, GitCommit, GitDiff, GitStatus, ProcessSnapshot, ProcessSummary, WorkspaceEntry, WorkspaceFile, WorkspaceInfo, WorkspaceSkill, };
|
|
9
11
|
export interface ComputerPreviewView {
|
|
10
12
|
snapshotId: string;
|
|
11
13
|
revision: number;
|
|
@@ -18,7 +20,9 @@ export interface ComputerPreviewView {
|
|
|
18
20
|
y: number;
|
|
19
21
|
} | null;
|
|
20
22
|
elementCount: number;
|
|
21
|
-
screenshot:
|
|
23
|
+
screenshot: {
|
|
24
|
+
mimeType: "image/jpeg" | "image/png";
|
|
25
|
+
} | null;
|
|
22
26
|
}
|
|
23
27
|
export type Operation = DomainOperation;
|
|
24
28
|
export interface AuthStatus {
|
|
@@ -46,6 +50,17 @@ export type CloudService = CloudServiceId;
|
|
|
46
50
|
export type CloudStatus = DomainCloudStatus;
|
|
47
51
|
export interface WorkspaceFileContent {
|
|
48
52
|
content: string;
|
|
53
|
+
bytes: number;
|
|
54
|
+
truncated: boolean;
|
|
55
|
+
modifiedAt: string;
|
|
56
|
+
}
|
|
57
|
+
export interface WorkspaceFilePage {
|
|
58
|
+
items: WorkspaceFile[];
|
|
59
|
+
nextOffset: number | null;
|
|
60
|
+
}
|
|
61
|
+
export interface WorkspaceProjectFiles {
|
|
62
|
+
paths: string[];
|
|
63
|
+
truncated: boolean;
|
|
49
64
|
}
|
|
50
65
|
export interface CloudManagementSession {
|
|
51
66
|
url: string;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
|
+
import { isAgentProviderId, isAgentPermissionMode } from "#plugins/agent/types";
|
|
4
|
+
import { asyncRoute, bodyRecord, requireString, } from "#presentation/http/http-utils";
|
|
5
|
+
export function createAgentApiRouter(agents) {
|
|
6
|
+
const router = Router();
|
|
7
|
+
router.get("/agents/providers", asyncRoute(async (_req, res) => {
|
|
8
|
+
res.json(await agents.providerStatuses());
|
|
9
|
+
}));
|
|
10
|
+
router.get("/agents/providers/:provider/models", asyncRoute(async (req, res) => {
|
|
11
|
+
res.json(await agents.models(requireProvider(requireString(req.params.provider, "provider"))));
|
|
12
|
+
}));
|
|
13
|
+
router.get("/agents/providers/:provider/details", asyncRoute(async (req, res) => {
|
|
14
|
+
res.json(await agents.providerDetails(requireProvider(requireString(req.params.provider, "provider"))));
|
|
15
|
+
}));
|
|
16
|
+
router.get("/agents/sessions", (_req, res) => {
|
|
17
|
+
res.json(agents.listSessions());
|
|
18
|
+
});
|
|
19
|
+
router.post("/agents/sessions", asyncRoute(async (req, res) => {
|
|
20
|
+
const body = bodyRecord(req.body);
|
|
21
|
+
res.json(await agents.createSession({
|
|
22
|
+
workspaceRoot: requireString(body.workspaceRoot, "workspaceRoot"),
|
|
23
|
+
provider: requireProvider(requireString(body.provider, "provider")),
|
|
24
|
+
model: null,
|
|
25
|
+
reasoningEffort: null,
|
|
26
|
+
reasoningSummary: null,
|
|
27
|
+
serviceTier: null,
|
|
28
|
+
approvalPolicy: null,
|
|
29
|
+
approvalsReviewer: null,
|
|
30
|
+
permissionMode: null,
|
|
31
|
+
...parseConfig(body),
|
|
32
|
+
}));
|
|
33
|
+
}));
|
|
34
|
+
router.get("/agents/sessions/:sessionId/history", (req, res) => {
|
|
35
|
+
res.json(agents.history(requireString(req.params.sessionId, "sessionId")));
|
|
36
|
+
});
|
|
37
|
+
router.get("/agents/sessions/:sessionId/update", (req, res) => {
|
|
38
|
+
res.json(agents.historyUpdate(requireString(req.params.sessionId, "sessionId"), parseIdList(req.query.turnIds), parseIdList(req.query.itemIds)));
|
|
39
|
+
});
|
|
40
|
+
router.put("/agents/sessions/:sessionId/config", asyncRoute(async (req, res) => {
|
|
41
|
+
const body = bodyRecord(req.body);
|
|
42
|
+
res.json(await agents.configureSession(requireString(req.params.sessionId, "sessionId"), parseConfig(body)));
|
|
43
|
+
}));
|
|
44
|
+
router.post("/agents/sessions/:sessionId/turns", asyncRoute(async (req, res) => {
|
|
45
|
+
const body = bodyRecord(req.body);
|
|
46
|
+
res.json(await agents.startTurn(requireString(req.params.sessionId, "sessionId"), {
|
|
47
|
+
text: typeof body.text === "string" ? body.text : "",
|
|
48
|
+
attachments: parseAttachments(body.attachments),
|
|
49
|
+
outputSchema: body.outputSchema ?? null,
|
|
50
|
+
}, "human"));
|
|
51
|
+
}));
|
|
52
|
+
router.post("/agents/sessions/:sessionId/steer", asyncRoute(async (req, res) => {
|
|
53
|
+
const body = bodyRecord(req.body);
|
|
54
|
+
requireKeys(body, ["text", "attachments"]);
|
|
55
|
+
if (body.text !== undefined && typeof body.text !== "string")
|
|
56
|
+
throw new ChatRoomError("INVALID_INPUT", "text must be a string");
|
|
57
|
+
await agents.steer(requireString(req.params.sessionId, "sessionId"), {
|
|
58
|
+
text: typeof body.text === "string" ? body.text : "",
|
|
59
|
+
attachments: parseAttachments(body.attachments),
|
|
60
|
+
}, "human");
|
|
61
|
+
res.status(204).end();
|
|
62
|
+
}));
|
|
63
|
+
router.post("/agents/sessions/:sessionId/review", asyncRoute(async (req, res) => {
|
|
64
|
+
const body = bodyRecord(req.body);
|
|
65
|
+
requireKeys(body, ["target"]);
|
|
66
|
+
res.json(await agents.startReview(requireString(req.params.sessionId, "sessionId"), parseReviewTarget(body.target), "human"));
|
|
67
|
+
}));
|
|
68
|
+
router.post("/agents/sessions/:sessionId/interrupt", asyncRoute(async (req, res) => {
|
|
69
|
+
await agents.interrupt(requireString(req.params.sessionId, "sessionId"));
|
|
70
|
+
res.status(204).end();
|
|
71
|
+
}));
|
|
72
|
+
router.post("/agents/sessions/:sessionId/interactions/:itemId", asyncRoute(async (req, res) => {
|
|
73
|
+
res.json(await agents.respondInteraction(requireString(req.params.sessionId, "sessionId"), requireString(req.params.itemId, "itemId"), parseInteractionResponse(bodyRecord(req.body))));
|
|
74
|
+
}));
|
|
75
|
+
router.post("/agents/sessions/:sessionId/provider", asyncRoute(async (req, res) => {
|
|
76
|
+
const body = bodyRecord(req.body);
|
|
77
|
+
res.json(await agents.switchProvider(requireString(req.params.sessionId, "sessionId"), requireProvider(requireString(body.provider, "provider")), parseConfig(body)));
|
|
78
|
+
}));
|
|
79
|
+
router.delete("/agents/sessions/:sessionId", asyncRoute(async (req, res) => {
|
|
80
|
+
await agents.deleteSession(requireString(req.params.sessionId, "sessionId"));
|
|
81
|
+
res.status(204).end();
|
|
82
|
+
}));
|
|
83
|
+
return router;
|
|
84
|
+
}
|
|
85
|
+
function parseIdList(value) {
|
|
86
|
+
const values = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
87
|
+
const output = new Set();
|
|
88
|
+
for (const entry of values) {
|
|
89
|
+
if (typeof entry !== "string")
|
|
90
|
+
continue;
|
|
91
|
+
for (const id of entry.split(",")) {
|
|
92
|
+
if (!id)
|
|
93
|
+
continue;
|
|
94
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
95
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid Agent update id");
|
|
96
|
+
output.add(id);
|
|
97
|
+
if (output.size > 256)
|
|
98
|
+
throw new ChatRoomError("INVALID_INPUT", "Too many Agent update ids");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...output];
|
|
102
|
+
}
|
|
103
|
+
function requireProvider(value) {
|
|
104
|
+
if (isAgentProviderId(value))
|
|
105
|
+
return value;
|
|
106
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid Agent provider id");
|
|
107
|
+
}
|
|
108
|
+
function optionalString(value) {
|
|
109
|
+
if (value === null || value === undefined || value === "")
|
|
110
|
+
return null;
|
|
111
|
+
if (typeof value !== "string")
|
|
112
|
+
throw new ChatRoomError("INVALID_INPUT", "Expected a string");
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function parseAttachments(value) {
|
|
116
|
+
if (value === undefined || value === null)
|
|
117
|
+
return [];
|
|
118
|
+
if (!Array.isArray(value))
|
|
119
|
+
throw new ChatRoomError("INVALID_INPUT", "attachments must be an array");
|
|
120
|
+
return value.map((entry, index) => {
|
|
121
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
122
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid attachment at index " + index);
|
|
123
|
+
const record = entry;
|
|
124
|
+
const kind = requireString(record.kind, "attachments.kind");
|
|
125
|
+
if (!["image", "audio", "skill", "file"].includes(kind))
|
|
126
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid attachment kind");
|
|
127
|
+
return {
|
|
128
|
+
kind: kind,
|
|
129
|
+
name: requireString(record.name, "attachments.name"),
|
|
130
|
+
path: requireString(record.path, "attachments.path"),
|
|
131
|
+
mimeType: optionalString(record.mimeType),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function parseInteractionResponse(body) {
|
|
136
|
+
const type = requireString(body.type, "type");
|
|
137
|
+
if (type === "approval") {
|
|
138
|
+
const decision = requireString(body.decision, "decision");
|
|
139
|
+
return { type, decision };
|
|
140
|
+
}
|
|
141
|
+
if (type === "input") {
|
|
142
|
+
if (!body.answers ||
|
|
143
|
+
typeof body.answers !== "object" ||
|
|
144
|
+
Array.isArray(body.answers))
|
|
145
|
+
throw new ChatRoomError("INVALID_INPUT", "answers must be an object");
|
|
146
|
+
const answers = {};
|
|
147
|
+
for (const [key, value] of Object.entries(body.answers)) {
|
|
148
|
+
if (!Array.isArray(value) ||
|
|
149
|
+
value.some((entry) => typeof entry !== "string"))
|
|
150
|
+
throw new ChatRoomError("INVALID_INPUT", "Interaction answers must be string arrays");
|
|
151
|
+
answers[key] = value;
|
|
152
|
+
}
|
|
153
|
+
return { type, answers };
|
|
154
|
+
}
|
|
155
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported interaction response");
|
|
156
|
+
}
|
|
157
|
+
function parseConfig(body) {
|
|
158
|
+
const config = {};
|
|
159
|
+
for (const key of ["model", "reasoningEffort", "serviceTier"]) {
|
|
160
|
+
if (Object.hasOwn(body, key))
|
|
161
|
+
config[key] = optionalString(body[key]);
|
|
162
|
+
}
|
|
163
|
+
if (Object.hasOwn(body, "approvalPolicy"))
|
|
164
|
+
config.approvalPolicy = parseApprovalPolicy(body.approvalPolicy);
|
|
165
|
+
if (Object.hasOwn(body, "approvalsReviewer")) {
|
|
166
|
+
const value = optionalString(body.approvalsReviewer);
|
|
167
|
+
if (value !== null &&
|
|
168
|
+
!["user", "auto_review", "guardian_subagent"].includes(value))
|
|
169
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid approvalsReviewer");
|
|
170
|
+
config.approvalsReviewer = value;
|
|
171
|
+
}
|
|
172
|
+
if (Object.hasOwn(body, "permissionMode")) {
|
|
173
|
+
const value = optionalString(body.permissionMode);
|
|
174
|
+
if (value !== null && !isAgentPermissionMode(value))
|
|
175
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid permissionMode");
|
|
176
|
+
config.permissionMode = value;
|
|
177
|
+
}
|
|
178
|
+
if (Object.hasOwn(body, "reasoningSummary")) {
|
|
179
|
+
const value = optionalString(body.reasoningSummary);
|
|
180
|
+
if (value !== null &&
|
|
181
|
+
!["auto", "concise", "detailed", "none"].includes(value))
|
|
182
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid reasoning summary");
|
|
183
|
+
config.reasoningSummary = value;
|
|
184
|
+
}
|
|
185
|
+
return config;
|
|
186
|
+
}
|
|
187
|
+
function parseApprovalPolicy(value) {
|
|
188
|
+
if (value === null || value === undefined)
|
|
189
|
+
return null;
|
|
190
|
+
if (value === "untrusted" || value === "on-request" || value === "never")
|
|
191
|
+
return value;
|
|
192
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
193
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid approvalPolicy");
|
|
194
|
+
const record = value;
|
|
195
|
+
requireKeys(record, ["granular"]);
|
|
196
|
+
const granularValue = record.granular;
|
|
197
|
+
if (!granularValue ||
|
|
198
|
+
typeof granularValue !== "object" ||
|
|
199
|
+
Array.isArray(granularValue))
|
|
200
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid granular approvalPolicy");
|
|
201
|
+
const granular = granularValue;
|
|
202
|
+
const keys = [
|
|
203
|
+
"sandbox_approval",
|
|
204
|
+
"rules",
|
|
205
|
+
"skill_approval",
|
|
206
|
+
"request_permissions",
|
|
207
|
+
"mcp_elicitations",
|
|
208
|
+
];
|
|
209
|
+
requireKeys(granular, [...keys]);
|
|
210
|
+
if (keys.some((key) => !Object.hasOwn(granular, key) || typeof granular[key] !== "boolean"))
|
|
211
|
+
throw new ChatRoomError("INVALID_INPUT", "Granular approvalPolicy fields must be booleans");
|
|
212
|
+
return {
|
|
213
|
+
granular: {
|
|
214
|
+
sandbox_approval: granular.sandbox_approval,
|
|
215
|
+
rules: granular.rules,
|
|
216
|
+
skill_approval: granular.skill_approval,
|
|
217
|
+
request_permissions: granular.request_permissions,
|
|
218
|
+
mcp_elicitations: granular.mcp_elicitations,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function requireKeys(body, allowed) {
|
|
223
|
+
if (Object.keys(body).some((key) => !allowed.includes(key)))
|
|
224
|
+
throw new ChatRoomError("INVALID_INPUT", "Unexpected request field");
|
|
225
|
+
}
|
|
226
|
+
function parseReviewTarget(value) {
|
|
227
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
228
|
+
throw new ChatRoomError("INVALID_INPUT", "target must be an object");
|
|
229
|
+
const target = value;
|
|
230
|
+
const type = requireString(target.type, "target.type");
|
|
231
|
+
switch (type) {
|
|
232
|
+
case "uncommittedChanges":
|
|
233
|
+
requireKeys(target, ["type"]);
|
|
234
|
+
return { type };
|
|
235
|
+
case "baseBranch":
|
|
236
|
+
requireKeys(target, ["type", "branch"]);
|
|
237
|
+
return { type, branch: requireString(target.branch, "target.branch") };
|
|
238
|
+
case "commit":
|
|
239
|
+
requireKeys(target, ["type", "sha", "title"]);
|
|
240
|
+
return {
|
|
241
|
+
type,
|
|
242
|
+
sha: requireString(target.sha, "target.sha"),
|
|
243
|
+
title: optionalString(target.title),
|
|
244
|
+
};
|
|
245
|
+
case "custom":
|
|
246
|
+
requireKeys(target, ["type", "instructions"]);
|
|
247
|
+
return {
|
|
248
|
+
type,
|
|
249
|
+
instructions: requireString(target.instructions, "target.instructions"),
|
|
250
|
+
};
|
|
251
|
+
default:
|
|
252
|
+
throw new ChatRoomError("INVALID_INPUT", "Unsupported review target");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -9,6 +9,7 @@ import { createComputerApiRouter } from "./computer-api-router.js";
|
|
|
9
9
|
import { apiAuthentication, createPrivateAuthApiRouter, createPublicAuthApiRouter, } from "./auth-api-router.js";
|
|
10
10
|
import { createRuntimeApiRouter } from "./runtime-api-router.js";
|
|
11
11
|
import { createLogsApiRouter } from "./logs-api-router.js";
|
|
12
|
+
import { createAgentApiRouter } from "./agent-api-router.js";
|
|
12
13
|
export function createApiRouter(application, eventBus, auth, passkeys, ingress, cloud, logs, runtimeStatus) {
|
|
13
14
|
const router = Router();
|
|
14
15
|
router.use(createPublicAuthApiRouter(auth, passkeys, ingress, logs));
|
|
@@ -20,6 +21,13 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
|
|
|
20
21
|
res.json(application.operations.list({
|
|
21
22
|
limit: boundedIntegerQuery(req.query.limit, 100, 0, 500),
|
|
22
23
|
offset: boundedIntegerQuery(req.query.offset, 0, 0, 100000),
|
|
24
|
+
...(typeof req.query.beforeStartedAt === "string" &&
|
|
25
|
+
typeof req.query.beforeOperationId === "string"
|
|
26
|
+
? {
|
|
27
|
+
beforeStartedAt: req.query.beforeStartedAt,
|
|
28
|
+
beforeOperationId: req.query.beforeOperationId,
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
23
31
|
...(typeof req.query.pluginId === "string"
|
|
24
32
|
? { pluginId: req.query.pluginId }
|
|
25
33
|
: {}),
|
|
@@ -41,6 +49,7 @@ export function createApiRouter(application, eventBus, auth, passkeys, ingress,
|
|
|
41
49
|
router.use(createWorkspaceApiRouter(application));
|
|
42
50
|
router.use(createGitApiRouter(application));
|
|
43
51
|
router.use(createProcessApiRouter(application));
|
|
52
|
+
router.use(createAgentApiRouter(application.agents));
|
|
44
53
|
router.use(createComputerApiRouter(application.computer, application.operations, ingress));
|
|
45
54
|
router.use(createCloudApiRouter(cloud, application.operations));
|
|
46
55
|
return router;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { asyncRoute } from "#presentation/http/http-utils";
|
|
3
|
+
import { asyncRoute, requireString } from "#presentation/http/http-utils";
|
|
4
4
|
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
5
5
|
const settingsPatchSchema = z
|
|
6
6
|
.object({
|
|
@@ -20,8 +20,8 @@ export function createComputerApiRouter(computer, operations, ingress) {
|
|
|
20
20
|
res.json(await computer.requestPermission("screenRecording"));
|
|
21
21
|
}));
|
|
22
22
|
function assertLocalPermissionRequest(req) {
|
|
23
|
-
if (ingress.
|
|
24
|
-
throw new ChatRoomError("FORBIDDEN", "Computer permissions can be requested only from the local
|
|
23
|
+
if (ingress.isRemoteWeb(req))
|
|
24
|
+
throw new ChatRoomError("FORBIDDEN", "Computer permissions can be requested only from the local Web UI");
|
|
25
25
|
}
|
|
26
26
|
router.patch("/computer/settings", asyncRoute(async (req, res) => {
|
|
27
27
|
const parsed = settingsPatchSchema.parse(req.body);
|
|
@@ -39,11 +39,24 @@ export function createComputerApiRouter(computer, operations, ingress) {
|
|
|
39
39
|
}, async () => computer.setSettings(patch)));
|
|
40
40
|
}));
|
|
41
41
|
router.get("/computer/preview", (req, res) => {
|
|
42
|
-
const scope = ingress.
|
|
42
|
+
const scope = ingress.isRemoteWeb(req) ? "remote" : "local";
|
|
43
43
|
res.json(presentPreview(computer.latestSnapshot(scope), computer.latestSnapshotTimestamp()));
|
|
44
44
|
});
|
|
45
|
+
router.get("/computer/preview/image", (req, res) => {
|
|
46
|
+
const scope = ingress.isRemoteWeb(req) ? "remote" : "local";
|
|
47
|
+
const snapshotId = requireString(req.query.snapshotId, "snapshotId");
|
|
48
|
+
const screenshot = computer.screenshot(scope, snapshotId);
|
|
49
|
+
if (!screenshot)
|
|
50
|
+
throw new ChatRoomError("NOT_FOUND", "Computer screenshot not found");
|
|
51
|
+
const data = Buffer.from(screenshot.data, "base64");
|
|
52
|
+
res.setHeader("Content-Type", screenshot.mimeType);
|
|
53
|
+
res.setHeader("Content-Length", String(data.byteLength));
|
|
54
|
+
res.setHeader("Cache-Control", "private, max-age=31536000, immutable");
|
|
55
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
56
|
+
res.send(data);
|
|
57
|
+
});
|
|
45
58
|
router.post("/computer/snapshot", asyncRoute(async (req, res) => {
|
|
46
|
-
const scope = ingress.
|
|
59
|
+
const scope = ingress.isRemoteWeb(req) ? "remote" : "local";
|
|
47
60
|
const value = await computer.snapshot(scope, {
|
|
48
61
|
includeScreenshot: true,
|
|
49
62
|
includeElements: true,
|
|
@@ -64,6 +77,8 @@ function presentPreview(value, capturedAt) {
|
|
|
64
77
|
activeWindow: value.activeWindow,
|
|
65
78
|
cursor: value.cursor,
|
|
66
79
|
elementCount: value.elements.length,
|
|
67
|
-
screenshot: value.screenshot
|
|
80
|
+
screenshot: value.screenshot
|
|
81
|
+
? { mimeType: value.screenshot.mimeType }
|
|
82
|
+
: null,
|
|
68
83
|
};
|
|
69
84
|
}
|
|
@@ -3,6 +3,9 @@ import { asyncRoute, requireString } from "#presentation/http/http-utils";
|
|
|
3
3
|
export function createProcessApiRouter(application) {
|
|
4
4
|
const router = Router();
|
|
5
5
|
router.get("/processes", (_req, res) => res.json(application.listProcesses()));
|
|
6
|
+
router.get("/processes/:processId/summary", (req, res) => {
|
|
7
|
+
res.json(application.getProcessSummary(requireString(req.params.processId, "processId")));
|
|
8
|
+
});
|
|
6
9
|
router.get("/processes/:processId", (req, res) => {
|
|
7
10
|
res.json(application.getProcess(requireString(req.params.processId, "processId")));
|
|
8
11
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import express, { Router } from "express";
|
|
2
3
|
import { ChatRoomError } from "#core/errors/chatroom-error";
|
|
3
|
-
import { asyncRoute, bodyRecord, requireString, } from "#presentation/http/http-utils";
|
|
4
|
+
import { asyncRoute, bodyRecord, boundedIntegerQuery, requireString, } from "#presentation/http/http-utils";
|
|
4
5
|
const MAX_WRITE_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
const MAX_IMAGE_PREVIEW_BYTES = 32 * 1024 * 1024;
|
|
5
8
|
export function createWorkspaceApiRouter(application) {
|
|
6
9
|
const router = Router();
|
|
7
10
|
router.get("/workspaces", asyncRoute(async (_req, res) => {
|
|
@@ -27,7 +30,21 @@ export function createWorkspaceApiRouter(application) {
|
|
|
27
30
|
router.get("/workspace/files", asyncRoute(async (req, res) => {
|
|
28
31
|
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
29
32
|
const filePath = typeof req.query.path === "string" ? req.query.path : ".";
|
|
30
|
-
|
|
33
|
+
const limit = boundedIntegerQuery(req.query.limit, 300, 1, 500);
|
|
34
|
+
const offset = boundedIntegerQuery(req.query.offset, 0, 0, 100000);
|
|
35
|
+
const page = await fs.list(filePath, {
|
|
36
|
+
recursive: req.query.recursive === "1",
|
|
37
|
+
offset,
|
|
38
|
+
maxEntries: limit + 1,
|
|
39
|
+
});
|
|
40
|
+
const hasMore = page.length > limit;
|
|
41
|
+
res.json({
|
|
42
|
+
items: page.slice(0, limit),
|
|
43
|
+
nextOffset: hasMore ? offset + limit : null,
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
router.get("/workspace/project-files", asyncRoute(async (req, res) => {
|
|
47
|
+
res.json(await application.workspaces.projectFilesResult(requireString(req.query.root, "root")));
|
|
31
48
|
}));
|
|
32
49
|
router.get("/workspace/file", asyncRoute(async (req, res) => {
|
|
33
50
|
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
@@ -51,19 +68,70 @@ export function createWorkspaceApiRouter(application) {
|
|
|
51
68
|
input: { root: fs.root, path: filePath, bytes },
|
|
52
69
|
}, () => fs.write(filePath, body.content)));
|
|
53
70
|
}));
|
|
71
|
+
router.post("/workspace/attachment", express.raw({
|
|
72
|
+
type: "application/octet-stream",
|
|
73
|
+
limit: MAX_ATTACHMENT_BYTES,
|
|
74
|
+
}), asyncRoute(async (req, res) => {
|
|
75
|
+
const root = requireString(req.query.root, "root");
|
|
76
|
+
const sessionId = requireString(req.query.sessionId, "sessionId");
|
|
77
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId))
|
|
78
|
+
throw new ChatRoomError("INVALID_INPUT", "Invalid attachment session id");
|
|
79
|
+
const originalName = requireString(req.query.name, "name");
|
|
80
|
+
if (!Buffer.isBuffer(req.body))
|
|
81
|
+
throw new ChatRoomError("INVALID_INPUT", "Attachment body must be binary");
|
|
82
|
+
const fs = await application.workspaces.fs(root);
|
|
83
|
+
const session = application.agents.getSession(sessionId);
|
|
84
|
+
if (session.workspaceRoot !== fs.root)
|
|
85
|
+
throw new ChatRoomError("FORBIDDEN", "Agent session belongs to another workspace");
|
|
86
|
+
const filePath = ".chatroom/agent-attachments/" +
|
|
87
|
+
sessionId +
|
|
88
|
+
"/" +
|
|
89
|
+
randomUUID() +
|
|
90
|
+
"-" +
|
|
91
|
+
attachmentFileName(originalName);
|
|
92
|
+
const file = await application.operations.run({
|
|
93
|
+
pluginId: "workspace",
|
|
94
|
+
source: "gui",
|
|
95
|
+
action: "attachment.upload",
|
|
96
|
+
input: { root: fs.root, path: filePath, bytes: req.body.byteLength },
|
|
97
|
+
}, () => fs.writeBytes(filePath, req.body));
|
|
98
|
+
res.json({
|
|
99
|
+
...file,
|
|
100
|
+
name: originalName,
|
|
101
|
+
mimeType: typeof req.headers["x-chatroom-mime-type"] === "string"
|
|
102
|
+
? req.headers["x-chatroom-mime-type"]
|
|
103
|
+
: null,
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
54
106
|
router.get("/workspace/file/image", asyncRoute(async (req, res) => {
|
|
55
107
|
const filePath = requireString(req.query.path, "path");
|
|
56
108
|
const mime = imageMime(filePath);
|
|
57
109
|
if (!mime)
|
|
58
110
|
throw new ChatRoomError("INVALID_INPUT", "File type is not supported for image preview");
|
|
59
111
|
const fs = await application.workspaces.fs(requireString(req.query.root, "root"));
|
|
60
|
-
const file = await fs.readBytes(filePath, {
|
|
112
|
+
const file = await fs.readBytes(filePath, {
|
|
113
|
+
maxBytes: MAX_IMAGE_PREVIEW_BYTES,
|
|
114
|
+
});
|
|
61
115
|
if (file.truncated)
|
|
62
116
|
throw new ChatRoomError("INVALID_INPUT", "Image is too large to preview");
|
|
117
|
+
const etag = 'W/"' +
|
|
118
|
+
file.bytes.toString(16) +
|
|
119
|
+
"-" +
|
|
120
|
+
Date.parse(file.modifiedAt).toString(16) +
|
|
121
|
+
'"';
|
|
122
|
+
const requestedVersion = typeof req.query.v === "string" ? req.query.v : null;
|
|
63
123
|
res.setHeader("Content-Type", mime);
|
|
64
|
-
res.setHeader("
|
|
65
|
-
res.setHeader("
|
|
124
|
+
res.setHeader("ETag", etag);
|
|
125
|
+
res.setHeader("Last-Modified", file.modifiedAt);
|
|
126
|
+
res.setHeader("Cache-Control", requestedVersion === file.modifiedAt
|
|
127
|
+
? "private, max-age=31536000, immutable"
|
|
128
|
+
: "private, no-cache");
|
|
66
129
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
130
|
+
if (req.headers["if-none-match"] === etag) {
|
|
131
|
+
res.status(304).end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
res.setHeader("Content-Length", String(file.data.byteLength));
|
|
67
135
|
res.send(file.data);
|
|
68
136
|
}));
|
|
69
137
|
return router;
|
|
@@ -89,3 +157,8 @@ function imageMime(filePath) {
|
|
|
89
157
|
return null;
|
|
90
158
|
}
|
|
91
159
|
}
|
|
160
|
+
function attachmentFileName(value) {
|
|
161
|
+
const name = value.split(/[\\/]/).pop()?.trim() ?? "";
|
|
162
|
+
const safe = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
163
|
+
return (safe || "attachment").slice(0, 160);
|
|
164
|
+
}
|