@someonegg/agent-hub 0.0.0-stage → 0.7.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/LICENSE +21 -0
- package/README.md +104 -2
- package/README.zh-CN.md +99 -0
- package/README.zh-TW.md +99 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/index-4ejyszaV.css +1 -0
- package/dist/client/assets/index-CKKT4cMK.js +309 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +6 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest.webmanifest +21 -0
- package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
- package/dist/server/server/adapters/claude/identity.js +28 -0
- package/dist/server/server/adapters/claude/interaction.js +68 -0
- package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
- package/dist/server/server/adapters/claude/ready-script.js +25 -0
- package/dist/server/server/adapters/claude/session-manager.js +106 -0
- package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
- package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
- package/dist/server/server/adapters/codex/io-errors.js +2 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
- package/dist/server/server/adapters/codex/limits.js +2 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
- package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +51 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
- package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/value-readers.js +4 -0
- package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
- package/dist/server/server/adapters/shared/io-errors.js +13 -0
- package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
- package/dist/server/server/adapters/shared/limits.js +4 -0
- package/dist/server/server/adapters/shared/session-management.js +43 -0
- package/dist/server/server/adapters/shared/source-cache.js +23 -0
- package/dist/server/server/api/session-api-mapper.js +65 -0
- package/dist/server/server/application/interaction-session-reader.js +17 -0
- package/dist/server/server/application/session-read-service.js +86 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/application/session-response.js +11 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +112 -0
- package/dist/server/server/cli/command-error.js +27 -0
- package/dist/server/server/cli/main.js +42 -0
- package/dist/server/server/cli/output.js +52 -0
- package/dist/server/server/cli/resource.js +104 -0
- package/dist/server/server/cli/session-client.js +64 -0
- package/dist/server/server/cli/session-follow.js +58 -0
- package/dist/server/server/cli/session-output.js +184 -0
- package/dist/server/server/cli/session-wait.js +44 -0
- package/dist/server/server/cli/session.js +318 -0
- package/dist/server/server/cli/skill.js +37 -0
- package/dist/server/server/config.js +196 -0
- package/dist/server/server/create-hub.js +91 -0
- package/dist/server/server/directory-lock.js +65 -0
- package/dist/server/server/domain/project-directory.js +5 -0
- package/dist/server/server/domain/session-domain.js +2 -0
- package/dist/server/server/domain/session-text.js +45 -0
- package/dist/server/server/hosting/diff-store.js +103 -0
- package/dist/server/server/hosting/routes.js +168 -0
- package/dist/server/server/hosting/store.js +162 -0
- package/dist/server/server/hosting/validation-error.js +4 -0
- package/dist/server/server/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +54 -0
- package/dist/server/server/http/create-server.js +218 -0
- package/dist/server/server/http/query-params.js +19 -0
- package/dist/server/server/http/request-body.js +48 -0
- package/dist/server/server/http/request-errors.js +59 -0
- package/dist/server/server/http/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +128 -0
- package/dist/server/server/http/session-management-routes.js +105 -0
- package/dist/server/server/http/session-path.js +10 -0
- package/dist/server/server/http/session-read-routes.js +143 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +124 -0
- package/dist/server/server/interaction/interaction-service.js +276 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +62 -0
- package/dist/server/server/interaction/pane-observation.js +31 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/runtime-capabilities.js +2 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-activity-service.js +81 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +184 -0
- package/dist/server/server/interaction/tmux-launcher.js +84 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
- package/dist/server/server/interaction/tmux-service.js +334 -0
- package/dist/server/server/interaction/tmux-target.js +13 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +54 -0
- package/dist/server/server/management/operation-service.js +307 -0
- package/dist/server/server/management/operation-state.js +3 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-launch-executor.js +164 -0
- package/dist/server/server/management/session-lifecycle-executor.js +158 -0
- package/dist/server/server/management/session-manager.js +64 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +72 -0
- package/dist/server/server/security/opaque-id.js +17 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +151 -0
- package/dist/server/server/updates/update-revision.js +22 -0
- package/dist/server/server/uploads/store.js +304 -0
- package/dist/server/server/version.js +21 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/diff.js +158 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +22 -0
- package/dist/server/shared/task-name.js +6 -0
- package/dist/server/shared/uploads.js +8 -0
- package/package.json +98 -4
- package/skills/ahub-control/SKILL.md +93 -0
- package/skills/ahub-publish/SKILL.md +54 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveHomeDirectory } from "../config.js";
|
|
3
|
+
import { UploadStore } from "../uploads/store.js";
|
|
4
|
+
import { HttpQueryError } from "./query-params.js";
|
|
5
|
+
import { RequestBodyError } from "./request-body.js";
|
|
6
|
+
import { reportRequestError, requestUrl } from "./request-errors.js";
|
|
7
|
+
import { sendJson } from "./router.js";
|
|
8
|
+
import { createSessionInteractionRoutes } from "./session-interaction-routes.js";
|
|
9
|
+
import { createSessionManagementRoutes } from "./session-management-routes.js";
|
|
10
|
+
import { createSessionReadRoutes } from "./session-read-routes.js";
|
|
11
|
+
import { createUploadRoutes } from "./upload-routes.js";
|
|
12
|
+
export function createApiRouter(dependencies, options = {}) {
|
|
13
|
+
const handlers = [
|
|
14
|
+
createUploadRoutes(dependencies.uploads ??
|
|
15
|
+
new UploadStore(dependencies.uploadDirectory ?? join(resolveHomeDirectory(), "uploads")), dependencies.sessions),
|
|
16
|
+
createSessionManagementRoutes(dependencies.operations),
|
|
17
|
+
createSessionInteractionRoutes(dependencies.interaction),
|
|
18
|
+
createSessionReadRoutes(dependencies),
|
|
19
|
+
];
|
|
20
|
+
const router = async (request, response) => {
|
|
21
|
+
const url = requestUrl(request);
|
|
22
|
+
if (!url.pathname.startsWith("/api/"))
|
|
23
|
+
return false;
|
|
24
|
+
const headOnly = request.method === "HEAD";
|
|
25
|
+
for (const handler of handlers.filter((candidate) => candidate.matches(url))) {
|
|
26
|
+
try {
|
|
27
|
+
if (await handler.handle(request, response, url))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (response.headersSent) {
|
|
32
|
+
reportRequestError(request, response, error, options, true);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (error instanceof RequestBodyError || error instanceof HttpQueryError) {
|
|
36
|
+
sendJson(response, error instanceof RequestBodyError ? error.status : 400, {
|
|
37
|
+
error: {
|
|
38
|
+
code: error instanceof RequestBodyError ? error.code : "invalid_query",
|
|
39
|
+
message: error.message,
|
|
40
|
+
},
|
|
41
|
+
}, headOnly);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (handler.mapError(error, request, response))
|
|
45
|
+
return true;
|
|
46
|
+
reportRequestError(request, response, error, options, true);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
return router;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=api-router.js.map
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createReadStream, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { createServer as createHttpServer, } from "node:http";
|
|
4
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
5
|
+
import { extname, resolve, sep } from "node:path";
|
|
6
|
+
import { DiffStore } from "../hosting/diff-store.js";
|
|
7
|
+
import { hostingRoutes } from "../hosting/routes.js";
|
|
8
|
+
import { decodePath, isMissingFile, MalformedRequestError, reportRequestError, requestUrl, } from "./request-errors.js";
|
|
9
|
+
import { emptyApiRouter, sendJson } from "./router.js";
|
|
10
|
+
import { applySecurityHeaders } from "./security.js";
|
|
11
|
+
const CONTENT_TYPES = {
|
|
12
|
+
".css": "text/css; charset=utf-8",
|
|
13
|
+
".html": "text/html; charset=utf-8",
|
|
14
|
+
".js": "text/javascript; charset=utf-8",
|
|
15
|
+
".json": "application/json; charset=utf-8",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".ico": "image/vnd.microsoft.icon",
|
|
19
|
+
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
20
|
+
};
|
|
21
|
+
function sendText(response, status, message, headOnly) {
|
|
22
|
+
response.statusCode = status;
|
|
23
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
24
|
+
response.setHeader("Content-Length", Buffer.byteLength(message));
|
|
25
|
+
response.end(headOnly ? undefined : message);
|
|
26
|
+
}
|
|
27
|
+
async function serveFile(response, filePath, headOnly, status = 200, onStreamError = () => { }) {
|
|
28
|
+
try {
|
|
29
|
+
const info = await stat(filePath);
|
|
30
|
+
if (!info.isFile())
|
|
31
|
+
return false;
|
|
32
|
+
response.statusCode = status;
|
|
33
|
+
response.setHeader("Content-Type", CONTENT_TYPES[extname(filePath)] ?? "application/octet-stream");
|
|
34
|
+
response.setHeader("Content-Length", info.size);
|
|
35
|
+
if (headOnly) {
|
|
36
|
+
response.end();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
createReadStream(filePath)
|
|
40
|
+
.on("error", (error) => {
|
|
41
|
+
response.destroy();
|
|
42
|
+
onStreamError(error);
|
|
43
|
+
})
|
|
44
|
+
.pipe(response);
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!isMissingFile(error))
|
|
50
|
+
throw error;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function createServer(config, apiRouter = emptyApiRouter, closeServices = () => { }, upgrades, options = {}) {
|
|
55
|
+
const clientRoot = realpathSync(config.clientDirectory);
|
|
56
|
+
const hosting = hostingRoutes(config.hostingDirectory, options);
|
|
57
|
+
const diffs = config.hostingDirectory ? new DiffStore(config.hostingDirectory) : undefined;
|
|
58
|
+
const clientRootPrefix = `${clientRoot}${sep}`;
|
|
59
|
+
const requestListener = async (request, response) => {
|
|
60
|
+
applySecurityHeaders(response, request);
|
|
61
|
+
const headOnly = request.method === "HEAD";
|
|
62
|
+
try {
|
|
63
|
+
if (await hosting(request, response))
|
|
64
|
+
return;
|
|
65
|
+
if (await apiRouter(request, response))
|
|
66
|
+
return;
|
|
67
|
+
if (request.method !== "GET" && !headOnly) {
|
|
68
|
+
response.setHeader("Allow", "GET, HEAD");
|
|
69
|
+
sendJson(response, 405, {
|
|
70
|
+
error: { code: "method_not_allowed", message: "Method is not allowed for this resource" },
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const url = requestUrl(request);
|
|
75
|
+
const decodedPath = decodePath(url.pathname);
|
|
76
|
+
const sessionPage = /^\/sessions\/[A-Za-z0-9_-]{20,100}(?:\/uploads)?\/?$/.test(decodedPath);
|
|
77
|
+
const diffPage = /^\/diffs\/([a-f0-9-]{36})$/.exec(decodedPath);
|
|
78
|
+
const appPage = decodedPath === "/" ||
|
|
79
|
+
sessionPage ||
|
|
80
|
+
((decodedPath === "/artifacts" || diffPage) && config.hostingDirectory);
|
|
81
|
+
const pageStatus = diffPage && diffs && !(await diffs.exists(diffPage[1])) ? 404 : 200;
|
|
82
|
+
if (diffPage)
|
|
83
|
+
response.setHeader("Cache-Control", "private, no-store");
|
|
84
|
+
const relativePath = appPage ? "index.html" : decodedPath.slice(1);
|
|
85
|
+
const requestedPath = resolve(clientRoot, relativePath);
|
|
86
|
+
let canonicalPath;
|
|
87
|
+
try {
|
|
88
|
+
canonicalPath = await realpath(requestedPath);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!isMissingFile(error))
|
|
92
|
+
throw error;
|
|
93
|
+
sendText(response, 404, "Not found", headOnly);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (canonicalPath.startsWith(clientRootPrefix) &&
|
|
97
|
+
(await serveFile(response, canonicalPath, headOnly, pageStatus, (error) => reportRequestError(request, response, error, options)))) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
sendText(response, 404, "Not found", headOnly);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!(error instanceof MalformedRequestError) || response.headersSent) {
|
|
104
|
+
reportRequestError(request, response, error, options);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
sendJson(response, 400, { error: { code: "bad_request", message: "Malformed request" } }, headOnly);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (!config.tls.enabled) {
|
|
111
|
+
const server = createHttpServer(requestListener);
|
|
112
|
+
return attachLifecycle(server, closeServices, upgrades);
|
|
113
|
+
}
|
|
114
|
+
const requireClientCertificate = config.tls.certificateAuthorityPath !== undefined;
|
|
115
|
+
const server = createHttpsServer({
|
|
116
|
+
cert: readFileSync(config.tls.certificatePath),
|
|
117
|
+
key: readFileSync(config.tls.privateKeyPath),
|
|
118
|
+
...(config.tls.certificateAuthorityPath
|
|
119
|
+
? { ca: readFileSync(config.tls.certificateAuthorityPath) }
|
|
120
|
+
: {}),
|
|
121
|
+
minVersion: "TLSv1.2",
|
|
122
|
+
requestCert: requireClientCertificate,
|
|
123
|
+
rejectUnauthorized: requireClientCertificate,
|
|
124
|
+
}, requestListener);
|
|
125
|
+
return attachLifecycle(server, closeServices, upgrades);
|
|
126
|
+
}
|
|
127
|
+
/** Own startup cleanup as well as the server's pre-drain shutdown hook. */
|
|
128
|
+
export async function startServer(config, apiRouter, closeServices, upgrades) {
|
|
129
|
+
let primary;
|
|
130
|
+
let local;
|
|
131
|
+
let cleaned = false;
|
|
132
|
+
const cleanup = () => {
|
|
133
|
+
if (cleaned)
|
|
134
|
+
return;
|
|
135
|
+
cleaned = true;
|
|
136
|
+
upgrades?.close();
|
|
137
|
+
closeServices();
|
|
138
|
+
};
|
|
139
|
+
// Listener shutdown must leave shared WebSocket cleanup to the owner above.
|
|
140
|
+
const listenerUpgrades = upgrades && {
|
|
141
|
+
handle: upgrades.handle.bind(upgrades),
|
|
142
|
+
close: () => { },
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
primary = createServer(config, apiRouter, () => { }, listenerUpgrades);
|
|
146
|
+
await listenServer(primary, config.port, config.host);
|
|
147
|
+
if (config.localPort !== undefined) {
|
|
148
|
+
local = createServer({ ...config, tls: { enabled: false } }, apiRouter, () => { }, listenerUpgrades);
|
|
149
|
+
await listenServer(local, config.localPort, "127.0.0.1");
|
|
150
|
+
const address = local.address();
|
|
151
|
+
if (address && typeof address === "object")
|
|
152
|
+
console.log(`Agent Hub local HTTP listening on http://127.0.0.1:${address.port}`);
|
|
153
|
+
}
|
|
154
|
+
const server = primary;
|
|
155
|
+
const close = server.close.bind(server);
|
|
156
|
+
server.close = ((callback) => {
|
|
157
|
+
let pending = local ? 2 : 1;
|
|
158
|
+
let failure;
|
|
159
|
+
const finished = (error) => {
|
|
160
|
+
failure ??= error;
|
|
161
|
+
if (--pending === 0)
|
|
162
|
+
callback?.(failure);
|
|
163
|
+
};
|
|
164
|
+
cleanup();
|
|
165
|
+
close(finished);
|
|
166
|
+
local?.close(finished);
|
|
167
|
+
return server;
|
|
168
|
+
});
|
|
169
|
+
return server;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
cleanup();
|
|
173
|
+
for (const server of [local, primary]) {
|
|
174
|
+
if (server?.listening) {
|
|
175
|
+
server.close();
|
|
176
|
+
server.closeAllConnections();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function listenServer(server, port, host) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const cleanup = () => {
|
|
185
|
+
server.removeListener("error", onError);
|
|
186
|
+
server.removeListener("listening", onListening);
|
|
187
|
+
};
|
|
188
|
+
const onError = (error) => {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(error);
|
|
191
|
+
};
|
|
192
|
+
const onListening = () => {
|
|
193
|
+
cleanup();
|
|
194
|
+
resolve();
|
|
195
|
+
};
|
|
196
|
+
server.once("error", onError);
|
|
197
|
+
server.once("listening", onListening);
|
|
198
|
+
server.listen(port, host);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function attachLifecycle(server, closeServices, upgrades) {
|
|
202
|
+
server.on("upgrade", (request, socket, head) => {
|
|
203
|
+
if (upgrades)
|
|
204
|
+
upgrades.handle(request, socket, head);
|
|
205
|
+
else
|
|
206
|
+
socket.destroy();
|
|
207
|
+
});
|
|
208
|
+
const close = server.close.bind(server);
|
|
209
|
+
const mutable = server;
|
|
210
|
+
mutable.close = (callback) => {
|
|
211
|
+
upgrades?.close();
|
|
212
|
+
closeServices();
|
|
213
|
+
close(callback);
|
|
214
|
+
return server;
|
|
215
|
+
};
|
|
216
|
+
return server;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=create-server.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class HttpQueryError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export function optional(params, name) {
|
|
4
|
+
const values = params.getAll(name);
|
|
5
|
+
if (values.length > 1)
|
|
6
|
+
invalid(`${name} must appear once`);
|
|
7
|
+
return values[0];
|
|
8
|
+
}
|
|
9
|
+
export function only(params, allowed) {
|
|
10
|
+
const accepted = new Set(allowed);
|
|
11
|
+
for (const name of params.keys()) {
|
|
12
|
+
if (!accepted.has(name))
|
|
13
|
+
invalid(`${name} is not supported`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function invalid(message) {
|
|
17
|
+
throw new HttpQueryError(message);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=query-params.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MAX_INTERACTION_MESSAGE_BYTES } from "../../shared/interaction.js";
|
|
2
|
+
const MAX_INTERACTION_JSON_BYTES = MAX_INTERACTION_MESSAGE_BYTES * 6 + 1_024;
|
|
3
|
+
export class RequestBodyError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
constructor(status, code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function readJsonObject(request) {
|
|
13
|
+
const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
14
|
+
if (contentType !== "application/json") {
|
|
15
|
+
invalidBody("Content-Type must be application/json");
|
|
16
|
+
}
|
|
17
|
+
const declared = Number(request.headers["content-length"]);
|
|
18
|
+
if (Number.isFinite(declared) && declared > MAX_INTERACTION_JSON_BYTES) {
|
|
19
|
+
bodyTooLarge();
|
|
20
|
+
}
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
for await (const chunk of request) {
|
|
24
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
25
|
+
bytes += value.byteLength;
|
|
26
|
+
if (bytes > MAX_INTERACTION_JSON_BYTES)
|
|
27
|
+
bodyTooLarge();
|
|
28
|
+
chunks.push(value);
|
|
29
|
+
}
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
invalidBody("Request body must be valid JSON");
|
|
36
|
+
}
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
38
|
+
invalidBody("Request body must be a JSON object");
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
function invalidBody(message) {
|
|
43
|
+
throw new RequestBodyError(400, "invalid_json", message);
|
|
44
|
+
}
|
|
45
|
+
function bodyTooLarge() {
|
|
46
|
+
throw new RequestBodyError(413, "body_too_large", "Request body is too large");
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=request-body.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { sendJson } from "./router.js";
|
|
3
|
+
export class MalformedRequestError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export function requestUrl(request) {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(request.url ?? "/", "http://localhost");
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new MalformedRequestError("Malformed URL");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function decodePath(path) {
|
|
14
|
+
try {
|
|
15
|
+
return decodeURIComponent(path);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new MalformedRequestError("Malformed path encoding");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isMissingFile(error) {
|
|
22
|
+
return ["ENOENT", "ENOTDIR"].includes(error?.code ?? "");
|
|
23
|
+
}
|
|
24
|
+
const ids = new WeakMap();
|
|
25
|
+
export function requestId(request, factory = randomUUID) {
|
|
26
|
+
let id = ids.get(request);
|
|
27
|
+
if (!id) {
|
|
28
|
+
id = factory();
|
|
29
|
+
ids.set(request, id);
|
|
30
|
+
}
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
export const consoleErrorLogger = {
|
|
34
|
+
error(message, context) {
|
|
35
|
+
console.error(message, context);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export function reportRequestError(request, response, error, options = {}, sessionApi = false) {
|
|
39
|
+
const id = requestId(request, options.requestId);
|
|
40
|
+
(options.logger ?? consoleErrorLogger).error(sessionApi ? "Session API request failed" : "HTTP request failed", {
|
|
41
|
+
requestId: id,
|
|
42
|
+
...(!sessionApi ? { method: request.method, path: request.url?.split("?")[0] ?? "/" } : {}),
|
|
43
|
+
error,
|
|
44
|
+
});
|
|
45
|
+
if (response.headersSent || response.destroyed) {
|
|
46
|
+
response.destroy();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
sendJson(response, 500, {
|
|
50
|
+
error: {
|
|
51
|
+
code: "internal_error",
|
|
52
|
+
message: sessionApi
|
|
53
|
+
? "The local session reader could not complete the request"
|
|
54
|
+
: "The server could not complete the request",
|
|
55
|
+
requestId: id,
|
|
56
|
+
},
|
|
57
|
+
}, request.method === "HEAD");
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=request-errors.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function sendJson(response, status, body, headOnly = false) {
|
|
2
|
+
const json = JSON.stringify(body);
|
|
3
|
+
response.statusCode = status;
|
|
4
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
5
|
+
response.setHeader("Cache-Control", response.getHeader("Cache-Control") ?? "no-store");
|
|
6
|
+
response.setHeader("Content-Length", Buffer.byteLength(json));
|
|
7
|
+
response.end(headOnly ? undefined : json);
|
|
8
|
+
}
|
|
9
|
+
export const emptyApiRouter = (request, response) => {
|
|
10
|
+
const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
11
|
+
if (!pathname.startsWith("/api/"))
|
|
12
|
+
return false;
|
|
13
|
+
sendJson(response, 404, { error: { code: "not_found", message: "API endpoint not found" } }, request.method === "HEAD");
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const SECURITY_HEADERS = {
|
|
2
|
+
"Content-Security-Policy": "default-src 'self'; base-uri 'none'; font-src 'self' data:; form-action 'none'; frame-ancestors 'none'; img-src 'self' data: blob:; object-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'",
|
|
3
|
+
"X-Content-Type-Options": "nosniff",
|
|
4
|
+
"Referrer-Policy": "no-referrer",
|
|
5
|
+
"X-Frame-Options": "DENY",
|
|
6
|
+
};
|
|
7
|
+
export function applySecurityHeaders(response, request) {
|
|
8
|
+
const secure = "encrypted" in request.socket && request.socket.encrypted;
|
|
9
|
+
const host = request.headers.host;
|
|
10
|
+
// Explicit WebSocket sources are needed by browsers that do not map 'self' to ws/wss.
|
|
11
|
+
if (host && /^[a-zA-Z0-9.:[\]-]+$/.test(host)) {
|
|
12
|
+
response.setHeader("Content-Security-Policy", `${SECURITY_HEADERS["Content-Security-Policy"]}; connect-src 'self' ${secure ? "wss" : "ws"}://${host}`);
|
|
13
|
+
}
|
|
14
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
|
|
15
|
+
if (!response.hasHeader(name))
|
|
16
|
+
response.setHeader(name, value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { INTERACTION_KEYS, MAX_INTERACTION_KEY_SEQUENCE_LENGTH, } from "../../shared/api-contract.js";
|
|
2
|
+
import { SessionInteractionError, } from "../interaction/interaction-service.js";
|
|
3
|
+
import { MessageValidationError, normalizeMessage } from "../interaction/message.js";
|
|
4
|
+
import { readJsonObject, RequestBodyError } from "./request-body.js";
|
|
5
|
+
import { sendJson } from "./router.js";
|
|
6
|
+
import { SESSION_ID_PATTERN, SESSION_ROOT, sessionPath } from "./session-path.js";
|
|
7
|
+
const INTERACTION_PATH = new RegExp(`^${SESSION_ROOT}/${SESSION_ID_PATTERN}/(?:terminal|terminal-preview|messages|keys|handoff-pane-binding)$`);
|
|
8
|
+
const INTERACTION_KEY_SET = new Set(INTERACTION_KEYS);
|
|
9
|
+
export function createSessionInteractionRoutes(interaction) {
|
|
10
|
+
return {
|
|
11
|
+
matches: (url) => INTERACTION_PATH.test(url.pathname),
|
|
12
|
+
async handle(request, response, url) {
|
|
13
|
+
const { id, segments: [operation = ""], } = sessionPath(url.pathname);
|
|
14
|
+
if (operation === "terminal" && request.method === "GET") {
|
|
15
|
+
if (!interaction?.captureTerminal)
|
|
16
|
+
return interactionUnavailable(response);
|
|
17
|
+
if ([...url.searchParams].length)
|
|
18
|
+
invalidBody("terminal does not accept query parameters");
|
|
19
|
+
response.setHeader("Cache-Control", "no-store");
|
|
20
|
+
sendJson(response, 200, await interaction.captureTerminal(id));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (operation === "terminal-preview") {
|
|
24
|
+
response.setHeader("Upgrade", "websocket");
|
|
25
|
+
sendJson(response, 426, {
|
|
26
|
+
error: { code: "websocket_required", message: "Terminal Preview requires WebSocket" },
|
|
27
|
+
}, request.method === "HEAD");
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (request.method !== "POST")
|
|
31
|
+
return false;
|
|
32
|
+
if (interaction === undefined)
|
|
33
|
+
return interactionUnavailable(response);
|
|
34
|
+
if (operation === "handoff-pane-binding") {
|
|
35
|
+
const body = await readJsonObject(request);
|
|
36
|
+
if (Object.keys(body).length !== 0)
|
|
37
|
+
invalidBody("Expected an empty object");
|
|
38
|
+
await interaction.handoffPaneBinding(id);
|
|
39
|
+
return noContent(response);
|
|
40
|
+
}
|
|
41
|
+
if (operation === "messages") {
|
|
42
|
+
const body = parseMessageBody(await readJsonObject(request));
|
|
43
|
+
if (body.requireIdle === undefined)
|
|
44
|
+
await interaction.sendMessage(id, body.message);
|
|
45
|
+
else
|
|
46
|
+
await interaction.sendMessage(id, body.message, body.requireIdle);
|
|
47
|
+
return noContent(response);
|
|
48
|
+
}
|
|
49
|
+
if (operation === "keys") {
|
|
50
|
+
const body = parseKeysBody(await readJsonObject(request));
|
|
51
|
+
if (body.requireIdle === undefined)
|
|
52
|
+
await interaction.sendKeys(id, body.keys);
|
|
53
|
+
else
|
|
54
|
+
await interaction.sendKeys(id, body.keys, body.requireIdle);
|
|
55
|
+
return noContent(response);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
},
|
|
59
|
+
mapError(error, request, response) {
|
|
60
|
+
const headOnly = request.method === "HEAD";
|
|
61
|
+
if (error instanceof MessageValidationError && error.code === "invalid_message") {
|
|
62
|
+
sendJson(response, 400, {
|
|
63
|
+
error: { code: "invalid_message", message: error.message },
|
|
64
|
+
}, headOnly);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof SessionInteractionError) {
|
|
68
|
+
const status = error.code === "session_not_found"
|
|
69
|
+
? 404
|
|
70
|
+
: error.code === "operation_result_unknown"
|
|
71
|
+
? 504
|
|
72
|
+
: error.code === "interaction_failed"
|
|
73
|
+
? 502
|
|
74
|
+
: 409;
|
|
75
|
+
sendJson(response, status, {
|
|
76
|
+
error: { code: error.code, message: error.message },
|
|
77
|
+
}, headOnly);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function invalidBody(message) {
|
|
85
|
+
throw new RequestBodyError(400, "invalid_json", message);
|
|
86
|
+
}
|
|
87
|
+
function isInteractionKey(value) {
|
|
88
|
+
return typeof value === "string" && INTERACTION_KEY_SET.has(value);
|
|
89
|
+
}
|
|
90
|
+
function noContent(response) {
|
|
91
|
+
response.statusCode = 204;
|
|
92
|
+
response.end();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function interactionUnavailable(response) {
|
|
96
|
+
sendJson(response, 409, {
|
|
97
|
+
error: {
|
|
98
|
+
code: "interaction_not_supported",
|
|
99
|
+
message: "Interaction is not enabled",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
export function parseMessageBody(body) {
|
|
105
|
+
if (Object.keys(body).some((key) => !["message", "requireIdle"].includes(key)) ||
|
|
106
|
+
typeof body.message !== "string" ||
|
|
107
|
+
(body.requireIdle !== undefined && typeof body.requireIdle !== "boolean")) {
|
|
108
|
+
invalidBody("body must contain only a string message field");
|
|
109
|
+
}
|
|
110
|
+
normalizeMessage(body.message);
|
|
111
|
+
return body;
|
|
112
|
+
}
|
|
113
|
+
export function parseKeysBody(body) {
|
|
114
|
+
if (Object.keys(body).some((key) => !["keys", "requireIdle"].includes(key)) ||
|
|
115
|
+
!Array.isArray(body.keys) ||
|
|
116
|
+
(body.requireIdle !== undefined && typeof body.requireIdle !== "boolean")) {
|
|
117
|
+
invalidBody("body must contain only a keys array");
|
|
118
|
+
}
|
|
119
|
+
if (body.keys.length === 0 ||
|
|
120
|
+
body.keys.length > MAX_INTERACTION_KEY_SEQUENCE_LENGTH ||
|
|
121
|
+
!body.keys.every(isInteractionKey)) {
|
|
122
|
+
invalidBody(`keys must contain 1-${MAX_INTERACTION_KEY_SEQUENCE_LENGTH} supported interaction keys`);
|
|
123
|
+
}
|
|
124
|
+
if (body.requireIdle === true && body.keys.includes("interrupt"))
|
|
125
|
+
invalidBody("interrupt cannot require idle");
|
|
126
|
+
return body;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=session-interaction-routes.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { TASK_NAME_ERROR, validTaskName } from "../../shared/task-name.js";
|
|
2
|
+
import { ManagementError } from "../management/operation-service.js";
|
|
3
|
+
import { readJsonObject, RequestBodyError } from "./request-body.js";
|
|
4
|
+
import { sendJson } from "./router.js";
|
|
5
|
+
import { SESSION_ID_PATTERN, SESSION_ROOT, sessionPath } from "./session-path.js";
|
|
6
|
+
const OPERATION_PATH = /^\/api\/v1\/operations\/[^/]+$/;
|
|
7
|
+
const SESSION_PATH = new RegExp(`^${SESSION_ROOT}/${SESSION_ID_PATTERN}(?:/(?:archive|exit))?$`);
|
|
8
|
+
export function createSessionManagementRoutes(operations) {
|
|
9
|
+
return {
|
|
10
|
+
matches: (url) => OPERATION_PATH.test(url.pathname) ||
|
|
11
|
+
url.pathname === SESSION_ROOT ||
|
|
12
|
+
SESSION_PATH.test(url.pathname),
|
|
13
|
+
async handle(request, response, url) {
|
|
14
|
+
if (!operations)
|
|
15
|
+
return false;
|
|
16
|
+
if (OPERATION_PATH.test(url.pathname)) {
|
|
17
|
+
if (request.method !== "GET")
|
|
18
|
+
return false;
|
|
19
|
+
const operationId = url.pathname.slice("/api/v1/operations/".length);
|
|
20
|
+
const operation = await operations.get(operationId);
|
|
21
|
+
if (!operation) {
|
|
22
|
+
throw new ManagementError(404, "Operation not found; the server may have restarted or the record expired");
|
|
23
|
+
}
|
|
24
|
+
sendJson(response, 200, operation);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const create = url.pathname === SESSION_ROOT;
|
|
28
|
+
const archive = url.pathname.endsWith("/archive");
|
|
29
|
+
const exit = url.pathname.endsWith("/exit");
|
|
30
|
+
const method = create || archive || exit ? "POST" : "DELETE";
|
|
31
|
+
if (request.method !== method)
|
|
32
|
+
return false;
|
|
33
|
+
const protocol = "encrypted" in request.socket && request.socket.encrypted ? "https" : "http";
|
|
34
|
+
if (request.headers.origin &&
|
|
35
|
+
request.headers.origin !== `${protocol}://${request.headers.host}`) {
|
|
36
|
+
throw new ManagementError(403, "Cross-origin management requests are not allowed");
|
|
37
|
+
}
|
|
38
|
+
const body = await readJsonObject(request);
|
|
39
|
+
if (create) {
|
|
40
|
+
if (typeof body.requestId !== "string")
|
|
41
|
+
throw new RequestBodyError(400, "invalid_json", "requestId is required");
|
|
42
|
+
if (body.resumeSessionId !== undefined) {
|
|
43
|
+
if (typeof body.resumeSessionId !== "string" ||
|
|
44
|
+
!body.resumeSessionId.trim() ||
|
|
45
|
+
Object.keys(body).some((key) => !["resumeSessionId", "yolo", "requestId"].includes(key)) ||
|
|
46
|
+
(body.yolo !== undefined && typeof body.yolo !== "boolean"))
|
|
47
|
+
throw new RequestBodyError(400, "invalid_json", "Resume accepts only resumeSessionId, yolo and requestId");
|
|
48
|
+
sendJson(response, 202, operations.create({
|
|
49
|
+
resumeSessionId: body.resumeSessionId,
|
|
50
|
+
yolo: body.yolo,
|
|
51
|
+
requestId: body.requestId,
|
|
52
|
+
}));
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(body).some((key) => ![
|
|
56
|
+
"projectDirectory",
|
|
57
|
+
"workingDirectory",
|
|
58
|
+
"taskName",
|
|
59
|
+
"worktree",
|
|
60
|
+
"yolo",
|
|
61
|
+
"requestId",
|
|
62
|
+
"agent",
|
|
63
|
+
].includes(key)) ||
|
|
64
|
+
(body.agent !== undefined && typeof body.agent !== "string") ||
|
|
65
|
+
typeof body.projectDirectory !== "string" ||
|
|
66
|
+
(body.workingDirectory !== undefined && typeof body.workingDirectory !== "string") ||
|
|
67
|
+
typeof body.worktree !== "boolean" ||
|
|
68
|
+
(body.yolo !== undefined && typeof body.yolo !== "boolean")) {
|
|
69
|
+
throw new RequestBodyError(400, "invalid_json", "projectDirectory and worktree are required; yolo must be a boolean if provided");
|
|
70
|
+
}
|
|
71
|
+
if (body.taskName !== undefined && !validTaskName(body.taskName))
|
|
72
|
+
throw new RequestBodyError(400, "invalid_json", TASK_NAME_ERROR);
|
|
73
|
+
sendJson(response, 202, operations.create({
|
|
74
|
+
...(typeof body.agent === "string" ? { agent: body.agent } : {}),
|
|
75
|
+
projectDirectory: body.projectDirectory,
|
|
76
|
+
...(typeof body.taskName === "string" ? { taskName: body.taskName.trim() } : {}),
|
|
77
|
+
...(typeof body.workingDirectory === "string"
|
|
78
|
+
? { workingDirectory: body.workingDirectory }
|
|
79
|
+
: {}),
|
|
80
|
+
worktree: body.worktree,
|
|
81
|
+
yolo: body.yolo ?? false,
|
|
82
|
+
requestId: body.requestId,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (Object.keys(body).length !== 0)
|
|
87
|
+
throw new RequestBodyError(400, "invalid_json", "Expected an empty object");
|
|
88
|
+
const { id: sessionId } = sessionPath(url.pathname);
|
|
89
|
+
await operations.assertSupported(exit ? "exit" : archive ? "archive" : "delete", sessionId);
|
|
90
|
+
sendJson(response, 202, operations.manage(exit ? "exit" : archive ? "archive" : "delete", sessionId));
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
},
|
|
94
|
+
mapError(error, request, response) {
|
|
95
|
+
if (error instanceof ManagementError) {
|
|
96
|
+
sendJson(response, error.status, {
|
|
97
|
+
error: { code: error.code, message: error.message },
|
|
98
|
+
}, request.method === "HEAD");
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=session-management-routes.js.map
|