@zq-silk/yui 2.3.0 → 2.4.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 +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +17 -8
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +25 -2
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/release-workflow.md +28 -0
- package/docs/release-workflow.zh-CN.md +21 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
package/dist/web/webServer.js
CHANGED
|
@@ -3,7 +3,10 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import WebSocket, { WebSocketServer } from "ws";
|
|
4
4
|
import { CliError, usageError } from "../errors/cliError.js";
|
|
5
5
|
import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
|
|
6
|
+
import { parseTaskSearchOptions, searchTaskBodies } from "../context/taskSearch.js";
|
|
7
|
+
import { readTaskUsage } from "../runtime/taskUsageQuery.js";
|
|
6
8
|
import { WebRequestRejected } from "./webMutation.js";
|
|
9
|
+
import { SETTINGS_HTML } from "./assets/shell/settings.js";
|
|
7
10
|
import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
8
11
|
import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
|
|
9
12
|
import { buildWebTaskCatalog, buildWebPageSessions, buildWebTaskDetail } from "./webSnapshot.js";
|
|
@@ -39,13 +42,14 @@ export function parseWebCommandOptions(args) {
|
|
|
39
42
|
export function createYuiWebServer(store, dependencies = {}) {
|
|
40
43
|
const now = dependencies.now ?? (() => new Date());
|
|
41
44
|
const token = dependencies.token ?? randomBytes(24).toString("base64url");
|
|
45
|
+
const httpDependencies = { ...dependencies, previews: dependencies.previews?.() };
|
|
42
46
|
const webSocketServer = new WebSocketServer({
|
|
43
47
|
noServer: true,
|
|
44
48
|
perMessageDeflate: false,
|
|
45
49
|
maxPayload: MAX_TERMINAL_MESSAGE_BYTES
|
|
46
50
|
});
|
|
47
51
|
const server = createServer((request, response) => {
|
|
48
|
-
void handleHttpRequest(request, response, store,
|
|
52
|
+
void handleHttpRequest(request, response, store, httpDependencies, token, now).catch(() => {
|
|
49
53
|
if (!response.headersSent)
|
|
50
54
|
setSecurityHeaders(response);
|
|
51
55
|
if (!response.writableEnded) {
|
|
@@ -100,7 +104,229 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
100
104
|
sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
|
|
101
105
|
return;
|
|
102
106
|
}
|
|
107
|
+
if (pathname.startsWith("/preview/") && dependencies.previews) {
|
|
108
|
+
await dependencies.previews.serve(request, response);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const previewMatch = /^\/api\/tasks\/([A-Za-z0-9_-]+)\/previews(?:\/([A-Za-z0-9_-]+)\/stop)?$/.exec(pathname);
|
|
112
|
+
if (previewMatch && dependencies.previews) {
|
|
113
|
+
try {
|
|
114
|
+
if (method === "GET" && !previewMatch[2]) {
|
|
115
|
+
sendJson(response, 200, await dependencies.previews.list(previewMatch[1]), false);
|
|
116
|
+
}
|
|
117
|
+
else if (method === "POST" && previewMatch[2]) {
|
|
118
|
+
const body = await readMutationBody(request);
|
|
119
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length) {
|
|
120
|
+
throw new WebRequestRejected("Stop accepts an empty object; the URL selects the exact service.");
|
|
121
|
+
}
|
|
122
|
+
sendJson(response, 200, dependencies.previews.stop(previewMatch[1], previewMatch[2]), false);
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
sendJson(response, error instanceof WebRequestRejected ? 409 : 500, {
|
|
129
|
+
error: error instanceof Error ? error.message : "Preview request failed.",
|
|
130
|
+
disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown"
|
|
131
|
+
}, false);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if ((pathname === "/api/tasks" || pathname === "/api/projects") && dependencies.surface) {
|
|
136
|
+
try {
|
|
137
|
+
if (pathname === "/api/projects" && method === "GET") {
|
|
138
|
+
const query = new URL(request.url, "http://localhost").searchParams;
|
|
139
|
+
if ([...query.keys()].some(key => key !== "after") || query.getAll("after").length > 1) {
|
|
140
|
+
throw new WebRequestRejected("Project listing accepts only one after cursor.");
|
|
141
|
+
}
|
|
142
|
+
sendJson(response, 200, dependencies.surface.projects(query.get("after") ?? undefined), false);
|
|
143
|
+
}
|
|
144
|
+
else if (pathname === "/api/tasks" && method === "POST") {
|
|
145
|
+
const body = await readMutationBody(request);
|
|
146
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
147
|
+
|| Object.keys(body).some(key => !["title", "requirements", "projectIds", "plan", "requestId"].includes(key))
|
|
148
|
+
|| !("title" in body) || typeof body.title !== "string" || !body.title.trim() || body.title.length > 200
|
|
149
|
+
|| !("requirements" in body) || typeof body.requirements !== "string" || !body.requirements.trim()
|
|
150
|
+
|| !("projectIds" in body) || !Array.isArray(body.projectIds) || body.projectIds.length > 32
|
|
151
|
+
|| body.projectIds.some(id => typeof id !== "string" || !/^project-[a-zA-Z0-9_-]+$/.test(id))
|
|
152
|
+
|| !("plan" in body) || typeof body.plan !== "boolean"
|
|
153
|
+
|| !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()) {
|
|
154
|
+
throw new WebRequestRejected("Expected title, requirements, projectIds, plan and requestId only.");
|
|
155
|
+
}
|
|
156
|
+
sendJson(response, 200, { ...dependencies.surface.create(body), requestId: body.requestId }, false);
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
sendJson(response, 409, { error: error instanceof Error ? error.message : "Creation unavailable.",
|
|
163
|
+
disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (pathname.startsWith("/api/skills/")) {
|
|
168
|
+
try {
|
|
169
|
+
const service = dependencies.skills;
|
|
170
|
+
if (!service)
|
|
171
|
+
throw new WebRequestRejected("Skill service unavailable.");
|
|
172
|
+
const q = new URL(request.url, "http://localhost").searchParams;
|
|
173
|
+
if ([...q.keys()].some(k => !["scope", "task", "role", "id", "resource", "frozen", "digest", "run", "q", "cursor"].includes(k)
|
|
174
|
+
|| q.getAll(k).length !== 1))
|
|
175
|
+
throw new WebRequestRejected("Unknown or duplicate Skill query.");
|
|
176
|
+
if (q.has("frozen") && !["true", "false"].includes(q.get("frozen"))
|
|
177
|
+
|| q.has("run") && q.get("frozen") !== "true")
|
|
178
|
+
throw new WebRequestRejected("A Run read requires an explicit frozen version.");
|
|
179
|
+
const selected = { scope: q.get("scope"), role: q.get("role"), ...(q.has("task") ? { task: q.get("task") } : {}) };
|
|
180
|
+
let value;
|
|
181
|
+
if (method === "GET" && pathname === "/api/skills/catalog")
|
|
182
|
+
value = service.catalog(q.get("q") ?? "", q.get("cursor") ?? "0");
|
|
183
|
+
else if (method === "GET" && pathname === "/api/skills/roles")
|
|
184
|
+
value = service.roles(q.get("scope") ?? "", q.get("task") ?? undefined);
|
|
185
|
+
else if (method === "GET" && pathname === "/api/skills/role")
|
|
186
|
+
value = service.role(selected);
|
|
187
|
+
else if (method === "GET" && pathname === "/api/skills/file")
|
|
188
|
+
value = service.file({
|
|
189
|
+
id: q.get("id") ?? "", resource: q.get("resource") ?? undefined,
|
|
190
|
+
frozen: q.get("frozen") === "true", digest: q.get("digest") ?? undefined, run: q.get("run") ?? undefined, target: selected
|
|
191
|
+
});
|
|
192
|
+
else if (method === "POST" && pathname === "/api/skills/role")
|
|
193
|
+
value = service.save(await readMutationBody(request));
|
|
194
|
+
else {
|
|
195
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
sendJson(response, 200, value, false);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
sendJson(response, 409, { error: error instanceof Error ? error.message : "Skill operation unavailable.",
|
|
202
|
+
disposition: method === "GET" || error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (pathname === "/api/settings" || pathname === "/api/settings/group" || pathname === "/api/settings/capabilities") {
|
|
207
|
+
try {
|
|
208
|
+
if (!dependencies.settings)
|
|
209
|
+
throw new WebRequestRejected("Settings service unavailable.");
|
|
210
|
+
const query = new URL(request.url, "http://localhost").searchParams;
|
|
211
|
+
if ([...query.keys()].some(k => !["id", "refresh", "q", "cursor"].includes(k)))
|
|
212
|
+
throw new WebRequestRejected("Unknown settings query.");
|
|
213
|
+
let value;
|
|
214
|
+
if (method === "GET" && pathname === "/api/settings")
|
|
215
|
+
value = dependencies.settings.index(query.get("q") ?? "", query.get("cursor") ?? "0");
|
|
216
|
+
else if (method === "GET" && pathname === "/api/settings/group")
|
|
217
|
+
value = dependencies.settings.read(query.get("id") ?? "");
|
|
218
|
+
else if (method === "GET" && pathname === "/api/settings/capabilities")
|
|
219
|
+
value = await dependencies.settings.capabilities(query.get("id") ?? "", query.get("refresh") === "true");
|
|
220
|
+
else if (method === "POST" && pathname === "/api/settings/group")
|
|
221
|
+
value = await dependencies.settings.save(await readMutationBody(request));
|
|
222
|
+
else {
|
|
223
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
sendJson(response, 200, value, false);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
sendJson(response, 409, { error: error instanceof Error ? error.message : "Settings unavailable.",
|
|
230
|
+
disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
103
234
|
const artifactTarget = /^\/api\/tasks\/([^/]+)\/(artifacts|evidence)$/.exec(pathname);
|
|
235
|
+
if (["/api/conversation", "/api/conversation/material", "/api/conversation/model"].includes(pathname) && dependencies.conversation) {
|
|
236
|
+
try {
|
|
237
|
+
const q = new URL(request.url, "http://localhost").searchParams;
|
|
238
|
+
if ([...q.keys()].some(k => !["scope", "task", "role", "session", "cursor", "offset", "requestId"].includes(k))) {
|
|
239
|
+
throw new WebRequestRejected("Unknown conversation parameter.");
|
|
240
|
+
}
|
|
241
|
+
const roleName = safeIdentity(q.get("role"), "Role");
|
|
242
|
+
const owner = q.get("scope") === "task"
|
|
243
|
+
? { scope: "task", taskId: safeIdentity(q.get("task"), "Task"), roleName }
|
|
244
|
+
: q.get("scope") === "global" && !q.has("task") ? { scope: "global", roleName }
|
|
245
|
+
: (() => { throw new WebRequestRejected("Invalid conversation scope."); })();
|
|
246
|
+
let result;
|
|
247
|
+
if (pathname.endsWith("/model")) {
|
|
248
|
+
const id = safeIdentity(q.get("session"), "Session");
|
|
249
|
+
if (method === "GET")
|
|
250
|
+
result = await dependencies.conversation.models(owner, id);
|
|
251
|
+
else if (method === "POST") {
|
|
252
|
+
const body = await readMutationBody(request);
|
|
253
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
254
|
+
|| Object.keys(body).some(k => k !== "model")
|
|
255
|
+
|| typeof body.model !== "string") {
|
|
256
|
+
throw new WebRequestRejected("Expected one native model selection.");
|
|
257
|
+
}
|
|
258
|
+
result = await dependencies.conversation.setModel(owner, id, body.model);
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
throw new WebRequestRejected("Model access requires GET or POST.");
|
|
262
|
+
}
|
|
263
|
+
else if (pathname.endsWith("/material")) {
|
|
264
|
+
if (method !== "POST")
|
|
265
|
+
throw new WebRequestRejected("Material upload requires POST.");
|
|
266
|
+
const body = await readMutationBody(request, 2 * 1024 * 1024);
|
|
267
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
268
|
+
|| Object.keys(body).some(k => !["requestId", "name", "content"].includes(k))) {
|
|
269
|
+
throw new WebRequestRejected("Expected material requestId, name and UTF-8 content only.");
|
|
270
|
+
}
|
|
271
|
+
const value = body;
|
|
272
|
+
if (typeof value.requestId !== "string" || typeof value.name !== "string" || typeof value.content !== "string") {
|
|
273
|
+
throw new WebRequestRejected("Invalid material fields.");
|
|
274
|
+
}
|
|
275
|
+
result = await dependencies.conversation.material(owner, safeIdentity(q.get("session"), "Session"), value);
|
|
276
|
+
}
|
|
277
|
+
else if (method === "GET" && q.has("requestId")) {
|
|
278
|
+
result = dependencies.conversation.receipt(owner, safeIdentity(q.get("requestId"), "Request"));
|
|
279
|
+
}
|
|
280
|
+
else if (method === "GET" && q.has("session")) {
|
|
281
|
+
const cursor = q.get("cursor");
|
|
282
|
+
if (cursor !== null && cursor.length > 16000)
|
|
283
|
+
throw new WebRequestRejected("Cursor too large.");
|
|
284
|
+
result = await dependencies.conversation.history(owner, safeIdentity(q.get("session"), "Session"), cursor ?? undefined);
|
|
285
|
+
}
|
|
286
|
+
else if (method === "GET") {
|
|
287
|
+
const offset = q.get("offset") ?? "0";
|
|
288
|
+
if (!/^\d{1,6}$/.test(offset))
|
|
289
|
+
throw new WebRequestRejected("Invalid session offset.");
|
|
290
|
+
result = dependencies.conversation.state(owner, Number(offset));
|
|
291
|
+
}
|
|
292
|
+
else if (method === "POST") {
|
|
293
|
+
const body = await readMutationBody(request);
|
|
294
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
295
|
+
|| Object.keys(body).some(k => !["action", "requestId", "body", "expectedTarget", "materials", "nativeRequestId", "result"].includes(k))) {
|
|
296
|
+
throw new WebRequestRejected("Invalid conversation input.");
|
|
297
|
+
}
|
|
298
|
+
const value = body;
|
|
299
|
+
if (value.action === "native-respond") {
|
|
300
|
+
if ((typeof value.nativeRequestId !== "string" && typeof value.nativeRequestId !== "number")
|
|
301
|
+
|| (typeof value.expectedTarget !== "string" && value.expectedTarget !== null)
|
|
302
|
+
|| !value.result || typeof value.result !== "object" || Array.isArray(value.result)) {
|
|
303
|
+
throw new WebRequestRejected("Expected exact native request and Turn response.");
|
|
304
|
+
}
|
|
305
|
+
result = await dependencies.conversation.respond(owner, safeIdentity(q.get("session"), "Session"), value.nativeRequestId, value.expectedTarget, value.result);
|
|
306
|
+
sendJson(response, 200, result, false);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!["queue", "steer", "interrupt"].includes(String(value.action))
|
|
310
|
+
|| typeof value.requestId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value.requestId)
|
|
311
|
+
|| value.body !== undefined && typeof value.body !== "string"
|
|
312
|
+
|| value.expectedTarget !== undefined && typeof value.expectedTarget !== "string"
|
|
313
|
+
|| value.materials !== undefined && (!Array.isArray(value.materials) || value.materials.length > 8)) {
|
|
314
|
+
throw new WebRequestRejected("Expected action, requestId, input and exact Turn.");
|
|
315
|
+
}
|
|
316
|
+
result = await dependencies.conversation.control(owner, safeIdentity(q.get("session"), "Session"), value);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
sendJson(response, 200, result, false);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
sendJson(response, 409, { error: error instanceof Error ? error.message : "Conversation unavailable.",
|
|
326
|
+
disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
104
330
|
if (artifactTarget && dependencies.surface) {
|
|
105
331
|
if (method !== "GET") {
|
|
106
332
|
sendJson(response, 405, { error: "Artifacts are read-only." }, false);
|
|
@@ -115,15 +341,21 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
115
341
|
sendJson(response, 200, dependencies.surface.evidence(taskId), false);
|
|
116
342
|
return;
|
|
117
343
|
}
|
|
118
|
-
if ([...query.keys()].some(key => !["path", "commit"].includes(key))) {
|
|
119
|
-
throw new WebRequestRejected("Artifact read accepts
|
|
344
|
+
if ([...query.keys()].some(key => !["path", "commit", "before", "offset"].includes(key) || query.getAll(key).length !== 1)) {
|
|
345
|
+
throw new WebRequestRejected("Artifact read accepts path, fixed commits and offset only.");
|
|
120
346
|
}
|
|
121
347
|
const path = query.get("path");
|
|
122
348
|
const commit = query.get("commit");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
349
|
+
const before = query.get("before");
|
|
350
|
+
const offset = query.get("offset") ?? "0";
|
|
351
|
+
if (!/^\d{1,9}$/u.test(offset))
|
|
352
|
+
throw new WebRequestRejected("Invalid artifact offset.");
|
|
353
|
+
if (path !== null && commit === null || before !== null && path === null
|
|
354
|
+
|| Number(offset) > 0 && commit === null)
|
|
355
|
+
throw new WebRequestRejected("Select a fixed commit for this read.");
|
|
356
|
+
const result = path === null ? await dependencies.surface.artifacts(taskId, commit ?? undefined, Number(offset))
|
|
357
|
+
: before === null ? await dependencies.surface.artifact(taskId, path, commit, Number(offset))
|
|
358
|
+
: await dependencies.surface.artifactDiff(taskId, path, commit, before, Number(offset));
|
|
127
359
|
sendJson(response, 200, result, false);
|
|
128
360
|
}
|
|
129
361
|
catch (error) {
|
|
@@ -194,7 +426,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
194
426
|
}
|
|
195
427
|
return;
|
|
196
428
|
}
|
|
197
|
-
const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|list|metadata|messages|control)$/.exec(pathname);
|
|
429
|
+
const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(workbench|context|delta|inspect|search-source|list|metadata|messages|control|activate|archive)$/.exec(pathname);
|
|
198
430
|
if (surfaceTarget && dependencies.surface) {
|
|
199
431
|
try {
|
|
200
432
|
let taskId;
|
|
@@ -207,7 +439,21 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
207
439
|
const action = surfaceTarget[2];
|
|
208
440
|
const query = new URL(request.url, "http://localhost").searchParams;
|
|
209
441
|
let value;
|
|
210
|
-
if (method === "
|
|
442
|
+
if (method === "POST" && (action === "activate" || action === "archive")) {
|
|
443
|
+
const body = await readMutationBody(request);
|
|
444
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
445
|
+
|| Object.keys(body).some(key => key !== "requestId")
|
|
446
|
+
|| !("requestId" in body) || typeof body.requestId !== "string" || !body.requestId.trim()) {
|
|
447
|
+
throw new WebRequestRejected("Expected requestId only.");
|
|
448
|
+
}
|
|
449
|
+
value = { requestId: body.requestId, result: action === "activate"
|
|
450
|
+
? dependencies.surface.activate(taskId, body.requestId)
|
|
451
|
+
: await dependencies.surface.archive(taskId) };
|
|
452
|
+
}
|
|
453
|
+
else if (method === "GET" && action === "workbench") {
|
|
454
|
+
value = dependencies.surface.workbench(taskId);
|
|
455
|
+
}
|
|
456
|
+
else if (method === "GET" && action === "context") {
|
|
211
457
|
value = await dependencies.surface.read(taskId);
|
|
212
458
|
}
|
|
213
459
|
else if (method === "GET" && action === "delta") {
|
|
@@ -216,6 +462,15 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
216
462
|
...(query.has("continuation") ? { continuation: query.get("continuation") } : {})
|
|
217
463
|
});
|
|
218
464
|
}
|
|
465
|
+
else if (method === "GET" && action === "search-source") {
|
|
466
|
+
const offset = query.get("offset") ?? "0";
|
|
467
|
+
if (!/^\d{1,12}$/.test(offset))
|
|
468
|
+
throw new WebRequestRejected("Invalid source offset.");
|
|
469
|
+
value = dependencies.surface.searchSource(taskId, {
|
|
470
|
+
store: query.get("store") ?? "", refId: query.get("ref") ?? "",
|
|
471
|
+
digest: query.get("digest") ?? "", field: query.get("field") ?? "", offset: Number(offset)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
219
474
|
else if (method === "GET" && action === "inspect") {
|
|
220
475
|
value = dependencies.surface.inspect(taskId, {
|
|
221
476
|
store: query.get("store") ?? "", refId: query.get("ref") ?? "",
|
|
@@ -273,12 +528,20 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
273
528
|
if (method === "GET" || method === "HEAD") {
|
|
274
529
|
try {
|
|
275
530
|
const asset = findWebAsset(pathname);
|
|
276
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
277
|
-
sendText(response, 200, "text/html; charset=utf-8", dashboardHtml(token), method === "HEAD");
|
|
531
|
+
if (pathname === "/" || pathname === "/index.html" || pathname === "/settings") {
|
|
532
|
+
sendText(response, 200, "text/html; charset=utf-8", pathname === "/settings" ? SETTINGS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), method === "HEAD");
|
|
278
533
|
}
|
|
279
534
|
else if (asset !== null) {
|
|
280
535
|
sendAsset(response, asset, method === "HEAD");
|
|
281
536
|
}
|
|
537
|
+
else if (pathname === "/api/search") {
|
|
538
|
+
const query = new URL(request.url, "http://localhost").searchParams;
|
|
539
|
+
if (query.getAll("query").length !== 1)
|
|
540
|
+
throw usageError("Search requires one query.");
|
|
541
|
+
const options = parseTaskSearchOptions([query.get("query"),
|
|
542
|
+
...[...query].filter(([key]) => key !== "query").flatMap(([key, value]) => [`--${key}`, value])]);
|
|
543
|
+
sendJson(response, 200, store.transaction(reader => searchTaskBodies(reader, options)), method === "HEAD");
|
|
544
|
+
}
|
|
282
545
|
else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
|
|
283
546
|
const query = new URL(request.url, "http://localhost").searchParams;
|
|
284
547
|
const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
|
|
@@ -289,11 +552,22 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
289
552
|
: buildWebTaskCatalog(store, options);
|
|
290
553
|
sendJson(response, 200, snapshot, method === "HEAD");
|
|
291
554
|
}
|
|
555
|
+
else if (/^\/api\/tasks\/[^/]+\/usage$/u.test(pathname)) {
|
|
556
|
+
const taskId = decodeURIComponent(pathname.split("/")[3]);
|
|
557
|
+
const query = new URL(request.url, "http://localhost").searchParams;
|
|
558
|
+
const offset = Number(query.get("offset") ?? 0), limit = Number(query.get("limit") ?? 0);
|
|
559
|
+
if ([...query.keys()].some(key => key !== "offset" && key !== "limit")
|
|
560
|
+
|| !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 0 || limit > 50) {
|
|
561
|
+
throw usageError("Usage requires offset >= 0 and limit 0..50.");
|
|
562
|
+
}
|
|
563
|
+
const value = store.readTransaction(reader => readTaskUsage(reader, taskId, { offset, limit, now: now() }));
|
|
564
|
+
sendJson(response, value === null ? 404 : 200, value ?? { error: "Task not found." }, method === "HEAD");
|
|
565
|
+
}
|
|
292
566
|
else if (pathname.startsWith("/api/tasks/")) {
|
|
293
567
|
const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
|
|
294
568
|
const detail = taskId.length === 0 || taskId.includes("/")
|
|
295
569
|
? null
|
|
296
|
-
: buildWebTaskDetail(store, taskId, now());
|
|
570
|
+
: buildWebTaskDetail(store, taskId, now(), new URL(request.url, "http://localhost").searchParams.get("compact") === "true");
|
|
297
571
|
sendJson(response, detail === null ? 404 : 200, detail ?? { error: "Task not found." }, method === "HEAD");
|
|
298
572
|
}
|
|
299
573
|
else {
|
|
@@ -466,10 +740,12 @@ function parseTerminalRequest(parameters) {
|
|
|
466
740
|
const roleName = safeIdentity(parameters.get("role"), "Role");
|
|
467
741
|
const columns = boundedInteger(parameters.get("cols"), 20, 400, "Terminal columns");
|
|
468
742
|
const rows = boundedInteger(parameters.get("rows"), 5, 200, "Terminal rows");
|
|
743
|
+
const selection = parameters.has("session")
|
|
744
|
+
? { nativeSessionId: safeIdentity(parameters.get("session"), "Session") } : {};
|
|
469
745
|
if (scope === "global") {
|
|
470
746
|
if (parameters.has("task"))
|
|
471
747
|
throw new Error("Global terminal cannot include a Task.");
|
|
472
|
-
return { scope, roleName, columns, rows };
|
|
748
|
+
return { scope, roleName, columns, rows, ...selection };
|
|
473
749
|
}
|
|
474
750
|
if (scope === "task") {
|
|
475
751
|
return {
|
|
@@ -477,7 +753,8 @@ function parseTerminalRequest(parameters) {
|
|
|
477
753
|
taskId: safeIdentity(parameters.get("task"), "Task"),
|
|
478
754
|
roleName,
|
|
479
755
|
columns,
|
|
480
|
-
rows
|
|
756
|
+
rows,
|
|
757
|
+
...selection
|
|
481
758
|
};
|
|
482
759
|
}
|
|
483
760
|
throw new Error("Terminal scope is invalid.");
|
|
@@ -616,9 +893,9 @@ function parseWebInputAnswer(value) {
|
|
|
616
893
|
}
|
|
617
894
|
throw new Error("Exactly one non-empty choiceKey or text is required.");
|
|
618
895
|
}
|
|
619
|
-
async function readMutationBody(request) {
|
|
896
|
+
async function readMutationBody(request, limit = MAX_JSON_BODY_BYTES) {
|
|
620
897
|
try {
|
|
621
|
-
return await readJsonBody(request);
|
|
898
|
+
return await readJsonBody(request, limit);
|
|
622
899
|
}
|
|
623
900
|
catch (error) {
|
|
624
901
|
throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
|
|
@@ -636,13 +913,13 @@ function webSubmissionIntent(body) {
|
|
|
636
913
|
}
|
|
637
914
|
throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
|
|
638
915
|
}
|
|
639
|
-
async function readJsonBody(request) {
|
|
916
|
+
async function readJsonBody(request, limit = MAX_JSON_BODY_BYTES) {
|
|
640
917
|
const chunks = [];
|
|
641
918
|
let size = 0;
|
|
642
919
|
for await (const chunk of request) {
|
|
643
920
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
644
921
|
size += buffer.length;
|
|
645
|
-
if (size >
|
|
922
|
+
if (size > limit)
|
|
646
923
|
throw new Error("Request body is too large.");
|
|
647
924
|
chunks.push(buffer);
|
|
648
925
|
}
|