@zq-silk/yui 2.2.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 +19 -3
- 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 +25 -11
- package/dist/cli/interactionPolicy.js +4 -0
- 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 +30 -9
- package/dist/commands/taskIntegrationCommands.js +5 -16
- 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 +51 -39
- package/dist/controller/clientRuntime.js +3 -2
- 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 +15 -79
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobCleanupInspection.js +67 -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 +86 -80
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +18 -11
- 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/integrationRuntimeIsolation.js +21 -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/taskRuntimeIsolation.js +91 -21
- 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/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- 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} +28 -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} +62 -56
- 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/{icons.js → shared/icons.js} +4 -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} +15 -13
- 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 +306 -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 +114 -9
- 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/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +53 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +4 -3
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-leader/references/integration.md +20 -0
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +3 -0
- package/skills/yui-runtime/SKILL.md +4 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
- package/dist/web/assets/client/app.js +0 -804
- package/dist/web/assets/client/components.js +0 -418
- package/dist/web/assets/client/detail.js +0 -333
- package/dist/web/assets/client/dock.js +0 -225
- package/dist/web/assets/client/evidence.js +0 -172
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1161
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -63
- package/dist/web/assets/client/sections.js +0 -313
- 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 -169
- package/dist/web/assets/styles/views.js +0 -292
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|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") ?? "",
|
|
@@ -223,6 +478,17 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
223
478
|
...(query.has("cursor") ? { cursor: query.get("cursor") } : {})
|
|
224
479
|
});
|
|
225
480
|
}
|
|
481
|
+
else if (method === "GET" && action === "list") {
|
|
482
|
+
const limit = query.get("limit");
|
|
483
|
+
if (limit !== null && !/^\d{1,3}$/u.test(limit))
|
|
484
|
+
throw new WebRequestRejected("Invalid list limit.");
|
|
485
|
+
value = dependencies.surface.list(taskId, {
|
|
486
|
+
store: query.get("store") ?? "",
|
|
487
|
+
...(query.has("status") ? { status: query.get("status") } : {}),
|
|
488
|
+
...(query.has("cursor") ? { cursor: query.get("cursor") } : {}),
|
|
489
|
+
...(limit === null ? {} : { limit: Number(limit) })
|
|
490
|
+
});
|
|
491
|
+
}
|
|
226
492
|
else if (method === "POST" && action === "messages") {
|
|
227
493
|
const body = await readMutationBody(request);
|
|
228
494
|
if (typeof body !== "object" || body === null || Array.isArray(body)
|
|
@@ -262,12 +528,20 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
262
528
|
if (method === "GET" || method === "HEAD") {
|
|
263
529
|
try {
|
|
264
530
|
const asset = findWebAsset(pathname);
|
|
265
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
266
|
-
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");
|
|
267
533
|
}
|
|
268
534
|
else if (asset !== null) {
|
|
269
535
|
sendAsset(response, asset, method === "HEAD");
|
|
270
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
|
+
}
|
|
271
545
|
else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
|
|
272
546
|
const query = new URL(request.url, "http://localhost").searchParams;
|
|
273
547
|
const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
|
|
@@ -278,11 +552,22 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
278
552
|
: buildWebTaskCatalog(store, options);
|
|
279
553
|
sendJson(response, 200, snapshot, method === "HEAD");
|
|
280
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
|
+
}
|
|
281
566
|
else if (pathname.startsWith("/api/tasks/")) {
|
|
282
567
|
const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
|
|
283
568
|
const detail = taskId.length === 0 || taskId.includes("/")
|
|
284
569
|
? null
|
|
285
|
-
: buildWebTaskDetail(store, taskId, now());
|
|
570
|
+
: buildWebTaskDetail(store, taskId, now(), new URL(request.url, "http://localhost").searchParams.get("compact") === "true");
|
|
286
571
|
sendJson(response, detail === null ? 404 : 200, detail ?? { error: "Task not found." }, method === "HEAD");
|
|
287
572
|
}
|
|
288
573
|
else {
|
|
@@ -455,10 +740,12 @@ function parseTerminalRequest(parameters) {
|
|
|
455
740
|
const roleName = safeIdentity(parameters.get("role"), "Role");
|
|
456
741
|
const columns = boundedInteger(parameters.get("cols"), 20, 400, "Terminal columns");
|
|
457
742
|
const rows = boundedInteger(parameters.get("rows"), 5, 200, "Terminal rows");
|
|
743
|
+
const selection = parameters.has("session")
|
|
744
|
+
? { nativeSessionId: safeIdentity(parameters.get("session"), "Session") } : {};
|
|
458
745
|
if (scope === "global") {
|
|
459
746
|
if (parameters.has("task"))
|
|
460
747
|
throw new Error("Global terminal cannot include a Task.");
|
|
461
|
-
return { scope, roleName, columns, rows };
|
|
748
|
+
return { scope, roleName, columns, rows, ...selection };
|
|
462
749
|
}
|
|
463
750
|
if (scope === "task") {
|
|
464
751
|
return {
|
|
@@ -466,7 +753,8 @@ function parseTerminalRequest(parameters) {
|
|
|
466
753
|
taskId: safeIdentity(parameters.get("task"), "Task"),
|
|
467
754
|
roleName,
|
|
468
755
|
columns,
|
|
469
|
-
rows
|
|
756
|
+
rows,
|
|
757
|
+
...selection
|
|
470
758
|
};
|
|
471
759
|
}
|
|
472
760
|
throw new Error("Terminal scope is invalid.");
|
|
@@ -605,9 +893,9 @@ function parseWebInputAnswer(value) {
|
|
|
605
893
|
}
|
|
606
894
|
throw new Error("Exactly one non-empty choiceKey or text is required.");
|
|
607
895
|
}
|
|
608
|
-
async function readMutationBody(request) {
|
|
896
|
+
async function readMutationBody(request, limit = MAX_JSON_BODY_BYTES) {
|
|
609
897
|
try {
|
|
610
|
-
return await readJsonBody(request);
|
|
898
|
+
return await readJsonBody(request, limit);
|
|
611
899
|
}
|
|
612
900
|
catch (error) {
|
|
613
901
|
throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid request body.");
|
|
@@ -625,13 +913,13 @@ function webSubmissionIntent(body) {
|
|
|
625
913
|
}
|
|
626
914
|
throw new WebRequestRejected(`intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}.`);
|
|
627
915
|
}
|
|
628
|
-
async function readJsonBody(request) {
|
|
916
|
+
async function readJsonBody(request, limit = MAX_JSON_BODY_BYTES) {
|
|
629
917
|
const chunks = [];
|
|
630
918
|
let size = 0;
|
|
631
919
|
for await (const chunk of request) {
|
|
632
920
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
633
921
|
size += buffer.length;
|
|
634
|
-
if (size >
|
|
922
|
+
if (size > limit)
|
|
635
923
|
throw new Error("Request body is too large.");
|
|
636
924
|
chunks.push(buffer);
|
|
637
925
|
}
|