@wjarka/cezarion 0.14.8 → 0.14.9-dev.1004
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 +129 -2
- package/dist/application-update/bin-recovery.d.ts +5 -0
- package/dist/application-update/bin-recovery.js +96 -0
- package/dist/application-update/bin-recovery.js.map +1 -0
- package/dist/application-update/discovery.d.ts +22 -0
- package/dist/application-update/discovery.js +91 -0
- package/dist/application-update/discovery.js.map +1 -0
- package/dist/application-update/helper.d.ts +57 -0
- package/dist/application-update/helper.js +374 -0
- package/dist/application-update/helper.js.map +1 -0
- package/dist/application-update/launcher.d.ts +8 -0
- package/dist/application-update/launcher.js +53 -0
- package/dist/application-update/launcher.js.map +1 -0
- package/dist/application-update/lock.d.ts +4 -0
- package/dist/application-update/lock.js +70 -0
- package/dist/application-update/lock.js.map +1 -0
- package/dist/application-update/npm-process.d.ts +17 -0
- package/dist/application-update/npm-process.js +133 -0
- package/dist/application-update/npm-process.js.map +1 -0
- package/dist/application-update/service.d.ts +69 -0
- package/dist/application-update/service.js +456 -0
- package/dist/application-update/service.js.map +1 -0
- package/dist/artifacts/cli.d.ts +1 -0
- package/dist/artifacts/cli.js +32 -0
- package/dist/artifacts/cli.js.map +1 -0
- package/dist/artifacts/lifecycle.d.ts +4 -0
- package/dist/artifacts/lifecycle.js +42 -0
- package/dist/artifacts/lifecycle.js.map +1 -0
- package/dist/artifacts/resolve.d.ts +18 -0
- package/dist/artifacts/resolve.js +102 -0
- package/dist/artifacts/resolve.js.map +1 -0
- package/dist/artifacts/store.d.ts +8 -0
- package/dist/artifacts/store.js +323 -0
- package/dist/artifacts/store.js.map +1 -0
- package/dist/automations/scheduler.d.ts +6 -0
- package/dist/automations/scheduler.js +45 -17
- package/dist/automations/scheduler.js.map +1 -1
- package/dist/automations/store.d.ts +12 -4
- package/dist/automations/store.js +199 -22
- package/dist/automations/store.js.map +1 -1
- package/dist/automations/types.d.ts +2 -0
- package/dist/automations/types.js +1 -0
- package/dist/automations/types.js.map +1 -1
- package/dist/ci-wait/mcp.d.ts +3 -0
- package/dist/ci-wait/mcp.js +14 -3
- package/dist/ci-wait/mcp.js.map +1 -1
- package/dist/contract/application-update.d.ts +32 -0
- package/dist/contract/artifacts.d.ts +83 -0
- package/dist/contract/ask-schema.d.ts +32 -0
- package/dist/contract/ask.d.ts +2 -32
- package/dist/contract/automations.d.ts +6 -0
- package/dist/contract/conversations.d.ts +165 -0
- package/dist/contract/delegation.d.ts +68 -0
- package/dist/contract/discovery.d.ts +134 -0
- package/dist/contract/events.d.ts +49 -0
- package/dist/contract/health.d.ts +12 -0
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.js +1654 -1454
- package/dist/contract/projects.d.ts +40 -0
- package/dist/contract/runs.d.ts +186 -0
- package/dist/contract/skills.d.ts +33 -0
- package/dist/core/agent-runner.d.ts +33 -4
- package/dist/core/agent-runner.js +5 -0
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +6 -12
- package/dist/core/claude-cli-runner.js +90 -20
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.d.ts +2 -1
- package/dist/core/codex-app-server-runner.js +327 -92
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +13 -1
- package/dist/core/codex-app-server-transport.js +58 -17
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/cursor-acp-runner.d.ts +6 -1
- package/dist/core/cursor-acp-runner.js +162 -26
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/cursor-provider-error.d.ts +3 -0
- package/dist/core/cursor-provider-error.js +16 -1
- package/dist/core/cursor-provider-error.js.map +1 -1
- package/dist/core/input-submissions.d.ts +14 -0
- package/dist/core/input-submissions.js +26 -0
- package/dist/core/input-submissions.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +4 -1
- package/dist/core/opencode-server-runner.js +156 -26
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/pi-runner.d.ts +2 -1
- package/dist/core/pi-runner.js +88 -10
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/provider-auth.d.ts +99 -1
- package/dist/core/provider-auth.js +203 -2
- package/dist/core/provider-auth.js.map +1 -1
- package/dist/delegation/cli.d.ts +1 -0
- package/dist/delegation/cli.js +103 -7
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/conversations.js +13 -5
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/input.d.ts +2 -1
- package/dist/delegation/input.js +6 -1
- package/dist/delegation/input.js.map +1 -1
- package/dist/delegation/policy.d.ts +3 -2
- package/dist/delegation/policy.js +3 -1
- package/dist/delegation/policy.js.map +1 -1
- package/dist/delegation/process-liveness.d.ts +66 -0
- package/dist/delegation/process-liveness.js +215 -0
- package/dist/delegation/process-liveness.js.map +1 -0
- package/dist/delegation/provision.d.ts +1 -1
- package/dist/delegation/provision.js +10 -5
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/questions.d.ts +19 -0
- package/dist/delegation/questions.js +45 -0
- package/dist/delegation/questions.js.map +1 -0
- package/dist/delegation/results.d.ts +1 -1
- package/dist/delegation/results.js +33 -19
- package/dist/delegation/results.js.map +1 -1
- package/dist/delegation/routes.d.ts +185 -11
- package/dist/delegation/routes.js +6 -2
- package/dist/delegation/routes.js.map +1 -1
- package/dist/delegation/service.d.ts +16 -5
- package/dist/delegation/service.js +161 -13
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +228 -65
- package/dist/discovery/catalog.d.ts +14 -0
- package/dist/discovery/catalog.js +31 -0
- package/dist/discovery/catalog.js.map +1 -0
- package/dist/discovery/cli.d.ts +8 -0
- package/dist/discovery/cli.js +104 -0
- package/dist/discovery/cli.js.map +1 -0
- package/dist/git-worktree.d.ts +3 -1
- package/dist/git-worktree.js +26 -4
- package/dist/git-worktree.js.map +1 -1
- package/dist/index.js +92 -26
- package/dist/index.js.map +1 -1
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +20 -0
- package/dist/open-url.js.map +1 -0
- package/dist/release/snapshot.d.ts +3 -3
- package/dist/release/snapshot.js +6 -6
- package/dist/release/snapshot.js.map +1 -1
- package/dist/runs/agent-tmpdir.d.ts +2 -0
- package/dist/runs/agent-tmpdir.js +5 -2
- package/dist/runs/agent-tmpdir.js.map +1 -1
- package/dist/runs/client-request.d.ts +11 -0
- package/dist/runs/client-request.js +26 -0
- package/dist/runs/client-request.js.map +1 -0
- package/dist/runs/delegation-state.d.ts +17 -0
- package/dist/runs/event-history.js +6 -0
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/retention.d.ts +7 -2
- package/dist/runs/retention.js +39 -7
- package/dist/runs/retention.js.map +1 -1
- package/dist/runs/store.d.ts +100 -6
- package/dist/runs/store.js +245 -2
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/webhook.d.ts +125 -0
- package/dist/runs/webhook.js +256 -0
- package/dist/runs/webhook.js.map +1 -0
- package/dist/server/provider-auth-runtime.d.ts +30 -4
- package/dist/server/provider-auth-runtime.js +71 -6
- package/dist/server/provider-auth-runtime.js.map +1 -1
- package/dist/server/server.d.ts +8226 -4860
- package/dist/server/server.js +309 -15
- package/dist/server/server.js.map +1 -1
- package/dist/server-install/platforms/ubuntu-vps.d.ts +5 -1
- package/dist/server-install/platforms/ubuntu-vps.js +160 -18
- package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
- package/dist/server-install/steps.d.ts +4 -2
- package/dist/server-install/steps.js +5 -4
- package/dist/server-install/steps.js.map +1 -1
- package/dist/skills-remote.d.ts +30 -5
- package/dist/skills-remote.js +122 -32
- package/dist/skills-remote.js.map +1 -1
- package/dist/skills.js +15 -6
- package/dist/skills.js.map +1 -1
- package/dist/task-cli/cli.d.ts +38 -0
- package/dist/task-cli/cli.js +580 -0
- package/dist/task-cli/cli.js.map +1 -0
- package/dist/task-cli/discovery.d.ts +22 -0
- package/dist/task-cli/discovery.js +96 -0
- package/dist/task-cli/discovery.js.map +1 -0
- package/dist/task-cli/http.d.ts +46 -0
- package/dist/task-cli/http.js +101 -0
- package/dist/task-cli/http.js.map +1 -0
- package/dist/task-cli/projections.d.ts +40 -0
- package/dist/task-cli/projections.js +45 -0
- package/dist/task-cli/projections.js.map +1 -0
- package/dist/task-cli/watch.d.ts +78 -0
- package/dist/task-cli/watch.js +336 -0
- package/dist/task-cli/watch.js.map +1 -0
- package/dist/workflows/run.d.ts +160 -2
- package/dist/workflows/run.js +1141 -108
- package/dist/workflows/run.js.map +1 -1
- package/dist/workspace/agent-profiles.d.ts +22 -0
- package/dist/workspace/agent-profiles.js +15 -0
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.d.ts +9 -0
- package/dist/workspace/config.js +13 -1
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/projects-cli.js +1 -1
- package/dist/workspace/projects.d.ts +24 -1
- package/dist/workspace/projects.js +14 -1
- package/dist/workspace/projects.js.map +1 -1
- package/package.json +5 -5
- package/scripts/mock-claude.mjs +64 -2
- package/scripts/mock-codex-app-server.mjs +124 -1
- package/scripts/mock-cursor-acp.mjs +54 -3
- package/scripts/mock-cursor-hang-stdin-late.mjs +35 -0
- package/scripts/mock-cursor-hang-stdin.mjs +30 -0
- package/scripts/mock-opencode-serve.mjs +102 -2
- package/scripts/mock-pi-rpc.mjs +82 -3
- package/web/dist/assets/agent-accounts-CerclGiO.js +1 -0
- package/web/dist/assets/centered-state-DH8qtCz-.js +43 -0
- package/web/dist/assets/collapsible-De2G0xqQ.js +1 -0
- package/web/dist/assets/commit-list-CQubP1iS.js +1 -0
- package/web/dist/assets/compare-variants-C_NijJvg.js +1 -0
- package/web/dist/assets/{diff-tRE59UNb2.js → diff-8M5HlDxq2.js} +2 -2
- package/web/dist/assets/diff-controls-B6ok0TtQ.js +1 -0
- package/web/dist/assets/{diff-stat-Cgnl_hYF.js → diff-stat-D1AODN2a.js} +1 -1
- package/web/dist/assets/{diff-view-DN1ort7Q.js → diff-view-Bs5Gh7UF.js} +2 -2
- package/web/dist/assets/dropdown-menu-7KGQMoFj.js +1 -0
- package/web/dist/assets/git-toolbar-DqbapMXr.js +1 -0
- package/web/dist/assets/github-lMHiuYSx.js +1 -0
- package/web/dist/assets/{image-preview-C2JGcBlC.js → image-preview-jzHoEyLM.js} +1 -1
- package/web/dist/assets/index-D4OJTArJ.js +7 -0
- package/web/dist/assets/index-D5NkwqY7.css +1 -0
- package/web/dist/assets/markdown-DvbymqXz.js +2 -0
- package/web/dist/assets/pill-BL6ugwXo.js +1 -0
- package/web/dist/assets/project-router-ChC5Rb-Y.js +1 -0
- package/web/dist/assets/prompt-templates-j8qlK7c-.js +15 -0
- package/web/dist/assets/repo-git-BbPAgkqb.js +1 -0
- package/web/dist/assets/{run-diff-CyG0Nciv.js → run-diff-BuATgZpx.js} +2 -2
- package/web/dist/assets/run-header-DuAWswYM.js +1 -0
- package/web/dist/assets/skills-DiC2XrU1.js +1 -0
- package/web/dist/assets/{tab-link-BWNp3mLY.js → tab-link-BmGKj9Rt.js} +1 -1
- package/web/dist/assets/task-changes-CP9kPnsy.js +1 -0
- package/web/dist/assets/task-commits-D6NfTw5r.js +1 -0
- package/web/dist/assets/task-files-W-b_gtcb.js +2 -0
- package/web/dist/assets/task-thread-Bw_nQuTx.css +1 -0
- package/web/dist/assets/task-thread-Q7Nj33DH.js +9 -0
- package/web/dist/assets/textarea-ByZ-txS7.js +1 -0
- package/web/dist/assets/thread-loading-DHT9p5o5.js +1 -0
- package/web/dist/assets/token-metrics-k4XoovDS.js +1 -0
- package/web/dist/assets/utils-BTFBhZ55.js +64 -0
- package/web/dist/assets/workflows-DqQszPHJ.js +11 -0
- package/web/dist/assets/{zoomable-image-Z3JDnm0b.js → zoomable-image-Ba6y6Wyl.js} +1 -1
- package/web/dist/index.html +14 -14
- package/web/dist/assets/agent-accounts-BZm_2WAv.js +0 -1
- package/web/dist/assets/alert-dialog-B422RI-k.js +0 -43
- package/web/dist/assets/collapsible-Cy_Fmkrd.js +0 -1
- package/web/dist/assets/commit-list-Dcnk7TnI.js +0 -1
- package/web/dist/assets/compare-variants-Ciqwvd_f.js +0 -1
- package/web/dist/assets/diff-controls-BGrXGi7O.js +0 -1
- package/web/dist/assets/dropdown-menu-dwDGT8MM.js +0 -1
- package/web/dist/assets/git-toolbar-UiHnGvE0.js +0 -1
- package/web/dist/assets/github-D-MrkbJ7.js +0 -1
- package/web/dist/assets/ibm-plex-mono-regular-BF3vfe7_.ttf +0 -0
- package/web/dist/assets/index-BszfYvBq.js +0 -7
- package/web/dist/assets/index-CWh4pKP3.css +0 -1
- package/web/dist/assets/markdown-C_IcKVQd.js +0 -2
- package/web/dist/assets/pill-BsbO6G7y.js +0 -1
- package/web/dist/assets/project-router-DbLkll25.js +0 -1
- package/web/dist/assets/prompt-templates-BlTGJB5W.js +0 -15
- package/web/dist/assets/repo-git-Bm3s6Why.js +0 -1
- package/web/dist/assets/run-header-IFyIGCey.js +0 -1
- package/web/dist/assets/skills-C-C4DVFY.js +0 -1
- package/web/dist/assets/task-changes-D2PHtWUW.js +0 -1
- package/web/dist/assets/task-commits-N5u9U0_R.js +0 -1
- package/web/dist/assets/task-files-BS7c7CFm.js +0 -2
- package/web/dist/assets/task-thread-BQFdz1dJ.css +0 -1
- package/web/dist/assets/task-thread-DGM2dZV-.js +0 -9
- package/web/dist/assets/textarea-FjaU370i.js +0 -1
- package/web/dist/assets/thread-loading-77sJ8Yu_.js +0 -1
- package/web/dist/assets/token-metrics-Clxh03bE.js +0 -1
- package/web/dist/assets/utils-PYYcpCFZ.js +0 -64
- package/web/dist/assets/workflows-Czl0ZfXH.js +0 -11
package/dist/server/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DelegationService } from '../delegation/service.js';
|
|
2
2
|
import { delegationFailure } from '../delegation/routes.js';
|
|
3
|
-
import { workerEmptyRequestSchema, runRelationshipsSchema, runDelegationSummarySchema } from '../contract/index.js';
|
|
3
|
+
import { CLIENT_REQUEST_VARIANTS_ERROR, workerEmptyRequestSchema, runRelationshipsSchema, runDelegationSummarySchema } from '../contract/index.js';
|
|
4
4
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { AutomationStore } from '../automations/store.js';
|
|
@@ -17,9 +17,10 @@ import { serve } from '@hono/node-server';
|
|
|
17
17
|
import { bodyLimit } from 'hono/body-limit';
|
|
18
18
|
import { streamSSE } from 'hono/streaming';
|
|
19
19
|
import { jsonZodValidator, paramZodValidator, queryZodValidator } from './validators.js';
|
|
20
|
+
import { deliverOnce, TaskWebhooks } from '../runs/webhook.js';
|
|
20
21
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
21
22
|
import { z } from 'zod';
|
|
22
|
-
import { repoPullInputSchema, pinRunInputSchema, githubSearchQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
|
|
23
|
+
import { repoPullInputSchema, pinRunInputSchema, notifyRunInputSchema, githubSearchQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
|
|
23
24
|
// A contract VALUE, like `workspaceUiStateSchema` in workspace/migrations.ts — the request
|
|
24
25
|
// schema this route validates with is the same one the client compiles against.
|
|
25
26
|
import { MODEL_DISCOVERY_RUNNERS, attachmentInputSchema, attachmentFileParamSchema, modelDiscoveryRunnerSchema, openProjectInSchema, runnerDiscoversModels, updateProjectInputSchema, } from '../contract/index.js';
|
|
@@ -41,10 +42,14 @@ import { appendHandoffHeartbeat, handoffProgressExcerpt, readHandoff } from '../
|
|
|
41
42
|
import { markStarted, onTodosChanged, readTodos, removeTodo, todoTaskText } from '../todos.js';
|
|
42
43
|
import { HistoryCursorError, deriveRunContextEvents, readEventsAfterLiveCursor, readRunHistoryPage, validateLiveCursor, } from '../runs/event-history.js';
|
|
43
44
|
import { readRunIndexFromDisk } from '../runs/run-index.js';
|
|
45
|
+
import { clientRequestHash } from '../runs/client-request.js';
|
|
44
46
|
import { ColdRepoHandles } from './cold-repo-handles.js';
|
|
45
47
|
import { isV2WireEventType } from '../runs/ui-event-sink.js';
|
|
46
48
|
import { runEventsQuerySchema, runHistoryQuerySchema, runIdParamSchema, } from '../contract/index.js';
|
|
47
49
|
import { resolveAttachmentPath } from '../workflows/attachment-path.js';
|
|
50
|
+
import { artifactParamsSchema, fileLinkQuerySchema } from '../contract/index.js';
|
|
51
|
+
import { artifactDirectory, listArtifacts, readArtifact } from '../artifacts/store.js';
|
|
52
|
+
import { artifactPreview, loadFileLink, rasterMime } from '../artifacts/resolve.js';
|
|
48
53
|
import { isUntouchedCancelledRun, toPastedContent } from '../workflows/run.js';
|
|
49
54
|
import { removeWorktree, worktreeDiff, worktreeDiffStat, worktreeSizeBytes } from '../git-worktree.js';
|
|
50
55
|
import { isReclaimable, reclaimWorktrees } from '../runs/retention.js';
|
|
@@ -64,7 +69,7 @@ import { defaultAgentProfile, listAgentProfiles, profileDirState, resolveProfile
|
|
|
64
69
|
import { PROFILE_CAPABLE_PROVIDERS, profileEnv, supportsProfiles } from '../core/agent-profiles.js';
|
|
65
70
|
import { quoteExecutable, withEnvPrefix } from '../core/shell-env.js';
|
|
66
71
|
import { resolveCursorExecutable } from '../core/cursor-model-catalog.js';
|
|
67
|
-
import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectStatus, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
|
|
72
|
+
import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectStatus, toProjectListEntry, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
|
|
68
73
|
import { mergeWriteWorkspaceUiState, readWorkspaceUiState } from '../workspace/ui-state.js';
|
|
69
74
|
import { checkoutRepo } from './checkout.js';
|
|
70
75
|
import { ProjectContextError, ProjectContexts } from './project-context.js';
|
|
@@ -72,6 +77,8 @@ import { reviewGateEnabled } from '../runs/review-gate.js';
|
|
|
72
77
|
import { readUiState, uiStatePath } from '../ui-state.js';
|
|
73
78
|
import { agentHomePaths, expandTilde } from '../paths.js';
|
|
74
79
|
import { isLoopbackHostHeader, normalizeHostname, resolveCapabilities } from './capabilities.js';
|
|
80
|
+
import { applicationUpdateInputSchema } from '../contract/index.js';
|
|
81
|
+
import { ApplicationUpdateConflictError, ApplicationUpdateFailureError } from '../application-update/service.js';
|
|
75
82
|
import { createSocketHub } from './ws.js';
|
|
76
83
|
import { browseDirectory, isInsideBrowseRoot, isLexicallyInsideBrowseRoot, resolveBrowseRoot } from './fs-browse.js';
|
|
77
84
|
import { parseRemote, resolveForge } from './forge/index.js';
|
|
@@ -304,9 +311,16 @@ const startRunSchema = z
|
|
|
304
311
|
// audit trail survives the composer detour. Bounded like every other
|
|
305
312
|
// string here; a todo id is a short generated key.
|
|
306
313
|
todoId: z.string().min(1).max(200, 'must be at most 200 characters').optional(),
|
|
314
|
+
// Idempotent start (#504): the same id and payload answer 200 with the existing run.
|
|
315
|
+
clientRequestId: z.string().uuid().optional(),
|
|
316
|
+
// Task webhook opt-in (#589). Absent = off; not part of the idempotency hash.
|
|
317
|
+
notify: z.boolean().optional(),
|
|
307
318
|
})
|
|
308
319
|
.refine((b) => Boolean(b.workflow) !== Boolean(b.steps), {
|
|
309
320
|
message: 'provide either "workflow" or "steps", not both',
|
|
321
|
+
})
|
|
322
|
+
.refine((b) => !(b.clientRequestId && (b.variants ?? 1) > 1), {
|
|
323
|
+
message: CLIENT_REQUEST_VARIANTS_ERROR,
|
|
310
324
|
});
|
|
311
325
|
const pickSchema = z.object({
|
|
312
326
|
runId: z.string().min(1),
|
|
@@ -811,6 +825,11 @@ export function createApp(deps) {
|
|
|
811
825
|
automationStore: deps.automationStore ?? AutomationStore.open(bootDataDir),
|
|
812
826
|
launchKey: ensureLaunchKey(bootDataDir), // bookmarklet auto-start secret (spec 011)
|
|
813
827
|
};
|
|
828
|
+
// The task webhook (#589): attached before any recovery — the boot store by `index.ts`, lazy
|
|
829
|
+
// projects through `prepareManager` below. Attaching is idempotent, so the calls here only
|
|
830
|
+
// cover an app built without boot's wiring (tests, embedded servers).
|
|
831
|
+
const taskWebhooks = deps.taskWebhooks ?? new TaskWebhooks({ fetch: deps.taskWebhookFetch, origin: '' });
|
|
832
|
+
taskWebhooks.attach(bootContext);
|
|
814
833
|
// Non-boot projects build lazily on first scoped request; their managers
|
|
815
834
|
// count against the same workspace semaphore as the boot manager (step 2.5).
|
|
816
835
|
const contexts = deps.contexts ?? new ProjectContexts({
|
|
@@ -821,7 +840,10 @@ export function createApp(deps) {
|
|
|
821
840
|
return listProjects(selector);
|
|
822
841
|
},
|
|
823
842
|
semaphore: deps.semaphore,
|
|
824
|
-
prepareManager: project =>
|
|
843
|
+
prepareManager: (project) => {
|
|
844
|
+
taskWebhooks.attach(project);
|
|
845
|
+
return deps.delegation?.attachProject(project);
|
|
846
|
+
},
|
|
825
847
|
});
|
|
826
848
|
// Workspace-level SSE bus (step 2.8) — the registry mutators and the
|
|
827
849
|
// checkout flow (Phase 4) emit here; /api/workspace/events relays.
|
|
@@ -845,6 +867,17 @@ export function createApp(deps) {
|
|
|
845
867
|
}
|
|
846
868
|
contexts.onStoreCreated((store) => providerRuntimeAuth.watch(store));
|
|
847
869
|
contexts.onContextBuilt((ctx) => providerRuntimeAuth.watch(ctx.store));
|
|
870
|
+
// Injected contexts (tests) skip `prepareManager`; attach those when they are built.
|
|
871
|
+
for (const id of contexts.ids()) {
|
|
872
|
+
const ctx = contexts.peek(id);
|
|
873
|
+
if (ctx)
|
|
874
|
+
taskWebhooks.attach(ctx);
|
|
875
|
+
}
|
|
876
|
+
contexts.onContextBuilt((ctx) => taskWebhooks.attach(ctx));
|
|
877
|
+
const projectRegistryEntry = async (projectId) => {
|
|
878
|
+
const id = projectId === 'default' ? await resolveBootProject() : projectId;
|
|
879
|
+
return (await loadWorkspaceConfig()).projects.find((entry) => entry.id === id);
|
|
880
|
+
};
|
|
848
881
|
const app = new Hono();
|
|
849
882
|
// Reject oversized request bodies before they reach any handler (#429). GETs
|
|
850
883
|
// and SSE carry no body, so this only ever gates the mutating routes.
|
|
@@ -1088,6 +1121,9 @@ export function createApp(deps) {
|
|
|
1088
1121
|
// the key as always-present, which is a shape no client ever receives. The contract schema
|
|
1089
1122
|
// says `.optional()`, and contract-parity.test.ts holds the two together.
|
|
1090
1123
|
...(update?.latest !== undefined ? { latestVersion: update.latest } : {}),
|
|
1124
|
+
...(deps.applicationUpdate ? { applicationUpdate: caps.localHandoff
|
|
1125
|
+
? deps.applicationUpdate.snapshot()
|
|
1126
|
+
: { status: 'idle', supported: false, message: 'Update this installation manually.' } } : {}),
|
|
1091
1127
|
// Health is CORS-open and, in hosted mode, reachable off the loopback —
|
|
1092
1128
|
// so any site/host that reads it would learn the developer's absolute
|
|
1093
1129
|
// checkout path and username (#431). Local mode keeps the full path (the
|
|
@@ -1163,6 +1199,10 @@ export function createApp(deps) {
|
|
|
1163
1199
|
})();
|
|
1164
1200
|
return healthInFlight;
|
|
1165
1201
|
};
|
|
1202
|
+
deps.applicationUpdate?.onStateChange?.(() => {
|
|
1203
|
+
healthCache = undefined;
|
|
1204
|
+
void refreshHealth();
|
|
1205
|
+
});
|
|
1166
1206
|
// The read the GET and the topic snapshot share. On the live server (a hub is
|
|
1167
1207
|
// injected) it answers from cache instantly and revalidates behind the
|
|
1168
1208
|
// response; without a hub — a bare app in tests — there is no refresher
|
|
@@ -2039,7 +2079,7 @@ export function createApp(deps) {
|
|
|
2039
2079
|
const parsed = { data: c.req.valid('json') };
|
|
2040
2080
|
// `default` is the boot alias the cockpit is allowed to use everywhere else.
|
|
2041
2081
|
const id = raw === 'default' ? await resolveBootProject() : raw;
|
|
2042
|
-
const { maxParallel, tags } = parsed.data;
|
|
2082
|
+
const { maxParallel, tags, webhook } = parsed.data;
|
|
2043
2083
|
// Read-first (mirroring DELETE, server.ts:1252-1258): a well-formed but
|
|
2044
2084
|
// unknown id must 404 WITHOUT rewriting the config — otherwise it would both
|
|
2045
2085
|
// do a needless full-config tmp+rename and, on a read-only home, surface the
|
|
@@ -2081,6 +2121,16 @@ export function createApp(deps) {
|
|
|
2081
2121
|
else
|
|
2082
2122
|
entry.tags = normalized;
|
|
2083
2123
|
}
|
|
2124
|
+
if (webhook !== undefined) {
|
|
2125
|
+
// The token is write-only (#589): the form never has it back, so an omitted token
|
|
2126
|
+
// keeps the stored one and only `''` clears it.
|
|
2127
|
+
if (webhook === null)
|
|
2128
|
+
delete entry.webhook;
|
|
2129
|
+
else {
|
|
2130
|
+
const token = webhook.token === undefined ? entry.webhook?.token : webhook.token || undefined;
|
|
2131
|
+
entry.webhook = { url: webhook.url, ...(token ? { token } : {}) };
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2084
2134
|
updated = entry;
|
|
2085
2135
|
});
|
|
2086
2136
|
}
|
|
@@ -2096,7 +2146,41 @@ export function createApp(deps) {
|
|
|
2096
2146
|
// `PUT /api/workspace/config` fires for a workspace-cap change.
|
|
2097
2147
|
await deps.semaphore?.refresh();
|
|
2098
2148
|
const body = {
|
|
2099
|
-
project:
|
|
2149
|
+
project: toProjectListEntry(updated, await probeProjectStatus(updated.root)),
|
|
2150
|
+
};
|
|
2151
|
+
return c.json(body);
|
|
2152
|
+
})
|
|
2153
|
+
// "Send test" (#589): one `task.test` delivery to the STORED webhook, now, with the timeout
|
|
2154
|
+
// the real deliveries use and no retries — the settings page wants the answer, not a queue.
|
|
2155
|
+
// Refused in single-project mode like every other registry edit route.
|
|
2156
|
+
.post('/projects/:projectId/webhook/test', async (c) => {
|
|
2157
|
+
if (capabilities().singleProject) {
|
|
2158
|
+
return c.json(singleProjectRefusal('editing projects'), 409);
|
|
2159
|
+
}
|
|
2160
|
+
const raw = c.req.param('projectId');
|
|
2161
|
+
if (!projectIdSchema.safeParse(raw).success) {
|
|
2162
|
+
return c.json({ error: `unknown project: ${raw}` }, 404);
|
|
2163
|
+
}
|
|
2164
|
+
const entry = await projectRegistryEntry(raw).catch(() => undefined);
|
|
2165
|
+
if (!entry)
|
|
2166
|
+
return c.json({ error: `unknown project: ${raw}` }, 404);
|
|
2167
|
+
if (!entry.webhook)
|
|
2168
|
+
return c.json({ error: NO_WEBHOOK_ERROR }, 400);
|
|
2169
|
+
const payload = {
|
|
2170
|
+
event: 'task.test',
|
|
2171
|
+
deliveryId: randomUUID(),
|
|
2172
|
+
projectId: entry.id,
|
|
2173
|
+
occurredAt: new Date().toISOString(),
|
|
2174
|
+
};
|
|
2175
|
+
if (process.env.CEZ_DRY_RUN === '1') {
|
|
2176
|
+
const body = { ok: true, dryRun: true };
|
|
2177
|
+
return c.json(body);
|
|
2178
|
+
}
|
|
2179
|
+
const result = await deliverOnce(entry.webhook, payload, { fetch: deps.taskWebhookFetch });
|
|
2180
|
+
const body = {
|
|
2181
|
+
ok: result.ok,
|
|
2182
|
+
...(result.status === undefined ? {} : { status: result.status }),
|
|
2183
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
2100
2184
|
};
|
|
2101
2185
|
return c.json(body);
|
|
2102
2186
|
})
|
|
@@ -2232,7 +2316,7 @@ export function createApp(deps) {
|
|
|
2232
2316
|
let project;
|
|
2233
2317
|
try {
|
|
2234
2318
|
const entry = await registerProject(requested, source);
|
|
2235
|
-
project =
|
|
2319
|
+
project = toProjectListEntry(entry, await probeProjectStatus(entry.root));
|
|
2236
2320
|
}
|
|
2237
2321
|
catch (err) {
|
|
2238
2322
|
// e.g. a read-only home — nothing was persisted (atomic tmp+rename).
|
|
@@ -2342,6 +2426,32 @@ export function createApp(deps) {
|
|
|
2342
2426
|
throw error;
|
|
2343
2427
|
}
|
|
2344
2428
|
});
|
|
2429
|
+
// Workspace-level, server-selected target and executable. The empty JSON body is strict.
|
|
2430
|
+
const applicationUpdateRoutes = new Hono()
|
|
2431
|
+
.post('/workspace/application-update/apply', jsonZodValidator(applicationUpdateInputSchema), async (c) => {
|
|
2432
|
+
if (!capabilities().localHandoff)
|
|
2433
|
+
return c.json({ error: 'Application updates require a local cockpit.' }, 409);
|
|
2434
|
+
if (!deps.applicationUpdate)
|
|
2435
|
+
return c.json({ error: 'Application update is unavailable.' }, 409);
|
|
2436
|
+
try {
|
|
2437
|
+
return c.json({ state: await deps.applicationUpdate.apply() });
|
|
2438
|
+
}
|
|
2439
|
+
catch (error) {
|
|
2440
|
+
return c.json({ error: error instanceof ApplicationUpdateConflictError || error instanceof ApplicationUpdateFailureError ? error.message : 'Application update preparation failed.' }, error instanceof ApplicationUpdateConflictError ? 409 : error instanceof ApplicationUpdateFailureError ? error.status : 500);
|
|
2441
|
+
}
|
|
2442
|
+
})
|
|
2443
|
+
.post('/workspace/application-update/restart', jsonZodValidator(applicationUpdateInputSchema), async (c) => {
|
|
2444
|
+
if (!capabilities().localHandoff)
|
|
2445
|
+
return c.json({ error: 'Application updates require a local cockpit.' }, 409);
|
|
2446
|
+
if (!deps.applicationUpdate)
|
|
2447
|
+
return c.json({ error: 'Application update is unavailable.' }, 409);
|
|
2448
|
+
try {
|
|
2449
|
+
return c.json({ state: await deps.applicationUpdate.restart() });
|
|
2450
|
+
}
|
|
2451
|
+
catch (error) {
|
|
2452
|
+
return c.json({ error: error instanceof ApplicationUpdateConflictError ? error.message : 'Application restart is unavailable.' }, error instanceof ApplicationUpdateConflictError ? 409 : 500);
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2345
2455
|
// ---- GUI clone (multi-project spec, step 4.3) ----------------------------
|
|
2346
2456
|
// "Add project → Clone from GitHub": clone into the checkout root, then
|
|
2347
2457
|
// register the result through `registerFolder` above (same guards, same
|
|
@@ -2891,7 +3001,7 @@ export function createApp(deps) {
|
|
|
2891
3001
|
automationsChanged();
|
|
2892
3002
|
return c.body(null, 204);
|
|
2893
3003
|
})
|
|
2894
|
-
.post('/automations/:id/enable', (c) => {
|
|
3004
|
+
.post('/automations/:id/enable', async (c) => {
|
|
2895
3005
|
const store = c.get('project').automationStore;
|
|
2896
3006
|
const current = store.get(c.req.param('id'));
|
|
2897
3007
|
if (!current)
|
|
@@ -2905,7 +3015,7 @@ export function createApp(deps) {
|
|
|
2905
3015
|
cursor: { timestamp: baselineAt },
|
|
2906
3016
|
nextCheckAt: new Date(Date.now() + automation.intervalSeconds * 1_000).toISOString(),
|
|
2907
3017
|
});
|
|
2908
|
-
store.appendLog({ automationId: automation.id, revision: automation.revision, result: 'baseline', reason: 'Enabled from a current-time baseline; existing records were not launched.' });
|
|
3018
|
+
await store.appendLog({ automationId: automation.id, revision: automation.revision, result: 'baseline', reason: 'Enabled from a current-time baseline; existing records were not launched.' });
|
|
2909
3019
|
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
2910
3020
|
automationsChanged();
|
|
2911
3021
|
return c.json({ automation });
|
|
@@ -3044,6 +3154,7 @@ export function createApp(deps) {
|
|
|
3044
3154
|
};
|
|
3045
3155
|
// ---- chained family: runs lifecycle + artifacts (project-scoped) ----
|
|
3046
3156
|
const delegationService = deps.delegation?.service ?? new DelegationService();
|
|
3157
|
+
delegationService.setDiscovery({ models: modelCatalog, providers: providerStatus });
|
|
3047
3158
|
const runsRoutes = new Hono()
|
|
3048
3159
|
.get('/runs', (c) => c.json(c.get('project').store.listRuns().map(run => withUsage(run))))
|
|
3049
3160
|
.get('/runs/:id/relationships', paramZodValidator(runIdParamSchema), queryZodValidator(workerEmptyRequestSchema), (c) => {
|
|
@@ -3105,6 +3216,34 @@ export function createApp(deps) {
|
|
|
3105
3216
|
const { store } = c.get('project');
|
|
3106
3217
|
const run = store.setPinned(c.req.valid('param').id, c.req.valid('json').pinned !== false);
|
|
3107
3218
|
return run ? c.json(run) : c.json({ error: 'not found' }, 404);
|
|
3219
|
+
})
|
|
3220
|
+
// Task webhook opt-in for one run (#589), valid in every state — handing a finished task to a
|
|
3221
|
+
// bot for follow-up is a real case. Turning it on sends one `task.subscribed` delivery with
|
|
3222
|
+
// the note; the note never reaches the agent session. Either direction leaves a durable
|
|
3223
|
+
// `handoff` event so the thread shows when and why. Idempotent: repeating `true` with a
|
|
3224
|
+
// new note is how "Send a note…" works.
|
|
3225
|
+
.post('/runs/:id/notify', paramZodValidator(runIdParamSchema), jsonZodValidator(notifyRunInputSchema), async (c) => {
|
|
3226
|
+
const project = c.get('project');
|
|
3227
|
+
const { id } = c.req.valid('param');
|
|
3228
|
+
const { notify, message } = c.req.valid('json');
|
|
3229
|
+
if (!project.store.getRun(id))
|
|
3230
|
+
return c.json({ error: 'not found' }, 404);
|
|
3231
|
+
if (notify) {
|
|
3232
|
+
const entry = await projectRegistryEntry(project.id).catch(() => undefined);
|
|
3233
|
+
if (!entry?.webhook)
|
|
3234
|
+
return c.json({ error: NO_WEBHOOK_ERROR }, 400);
|
|
3235
|
+
}
|
|
3236
|
+
const run = project.store.updateRun(id, { notify: notify ? true : undefined });
|
|
3237
|
+
if (!run)
|
|
3238
|
+
return c.json({ error: 'not found' }, 404);
|
|
3239
|
+
project.store.appendEvent(id, {
|
|
3240
|
+
type: 'handoff',
|
|
3241
|
+
notify,
|
|
3242
|
+
...(message === undefined ? {} : { message }),
|
|
3243
|
+
});
|
|
3244
|
+
if (notify)
|
|
3245
|
+
taskWebhooks.attach(project).subscribed(id, message);
|
|
3246
|
+
return c.json(run);
|
|
3108
3247
|
})
|
|
3109
3248
|
// The per-task off switch for that resume (the workspace setting is Settings → Resources).
|
|
3110
3249
|
// Idempotent: a run with nothing pending answers 200 too, because "this task will not
|
|
@@ -3131,8 +3270,21 @@ export function createApp(deps) {
|
|
|
3131
3270
|
return run ? c.json(run) : c.json({ error: 'not found' }, 404);
|
|
3132
3271
|
})
|
|
3133
3272
|
.post('/runs', jsonZodValidator(startRunSchema), async (c) => {
|
|
3134
|
-
const { root: repoRoot, dataDir, manager } = c.get('project');
|
|
3273
|
+
const { root: repoRoot, dataDir, manager, store } = c.get('project');
|
|
3135
3274
|
const parsed = { data: c.req.valid('json') };
|
|
3275
|
+
const clientRequest = parsed.data.clientRequestId
|
|
3276
|
+
? { id: parsed.data.clientRequestId, hash: clientRequestHash(parsed.data) }
|
|
3277
|
+
: undefined;
|
|
3278
|
+
// A retry only retrieves a persisted result: mutable start prerequisites (workflow,
|
|
3279
|
+
// provider, account and model policy) must not invalidate an already accepted request.
|
|
3280
|
+
if (clientRequest) {
|
|
3281
|
+
const existing = store.findRunByClientRequestId(clientRequest.id);
|
|
3282
|
+
if (existing) {
|
|
3283
|
+
if (existing.clientRequestHash !== clientRequest.hash)
|
|
3284
|
+
return c.json({ error: 'request id payload conflict' }, 409);
|
|
3285
|
+
return c.json(existing, 200);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3136
3288
|
if (agentModelsLocked(repoRoot) &&
|
|
3137
3289
|
(parsed.data.model?.trim() || parsed.data.effort?.trim())) {
|
|
3138
3290
|
return c.json({ error: AGENT_MODELS_LOCKED_ERROR }, 409);
|
|
@@ -3168,9 +3320,15 @@ export function createApp(deps) {
|
|
|
3168
3320
|
if ('error' in account)
|
|
3169
3321
|
return c.json({ error: account.error }, 400);
|
|
3170
3322
|
}
|
|
3323
|
+
// Task webhook (#589): an explicit `true` on a project with no webhook is refused, so a
|
|
3324
|
+
// bot learns in one round trip. `cez task start`'s implicit default never sends it there.
|
|
3325
|
+
if (parsed.data.notify === true && !(await projectRegistryEntry(c.get('project').id).catch(() => undefined))?.webhook) {
|
|
3326
|
+
return c.json({ error: NO_WEBHOOK_ERROR }, 400);
|
|
3327
|
+
}
|
|
3171
3328
|
const images = parsed.data.images?.map(toPastedContent);
|
|
3172
3329
|
const input = {
|
|
3173
3330
|
task: parsed.data.task,
|
|
3331
|
+
notify: parsed.data.notify,
|
|
3174
3332
|
model: parsed.data.model,
|
|
3175
3333
|
effort: parsed.data.effort,
|
|
3176
3334
|
runner: parsed.data.runner,
|
|
@@ -3186,6 +3344,18 @@ export function createApp(deps) {
|
|
|
3186
3344
|
// CEZ_TODOS_FILE alike (RunManager.agentEnv).
|
|
3187
3345
|
generateFollowups: capabilities().followups ? parsed.data.generateFollowups : false,
|
|
3188
3346
|
};
|
|
3347
|
+
if (clientRequest) {
|
|
3348
|
+
// Recheck after every await: concurrent first requests may both miss the early lookup.
|
|
3349
|
+
// The manager's lookup and create are synchronous, so exactly one can create (#504).
|
|
3350
|
+
const result = manager.startRunIdempotent(workflow, input, clientRequest);
|
|
3351
|
+
if ('conflict' in result)
|
|
3352
|
+
return c.json({ error: 'request id payload conflict' }, 409);
|
|
3353
|
+
if (!result.created)
|
|
3354
|
+
return c.json(result.run, 200);
|
|
3355
|
+
if (parsed.data.todoId)
|
|
3356
|
+
await noteTodoStarted(dataDir, parsed.data.todoId, result.run.id);
|
|
3357
|
+
return c.json(result.run, 201);
|
|
3358
|
+
}
|
|
3189
3359
|
const variants = parsed.data.variants ?? 1;
|
|
3190
3360
|
if (variants > 1) {
|
|
3191
3361
|
// Variants live in worktrees — without git there's nothing to isolate
|
|
@@ -3703,6 +3873,70 @@ export function createApp(deps) {
|
|
|
3703
3873
|
if (!result.ok)
|
|
3704
3874
|
return c.json({ error: result.error }, 409);
|
|
3705
3875
|
return c.json(result.commit);
|
|
3876
|
+
})
|
|
3877
|
+
.get('/runs/:id/file-link', paramZodValidator(artifactParamsSchema.omit({ artifactId: true })), queryZodValidator(fileLinkQuerySchema), async (c) => {
|
|
3878
|
+
const { root, dataDir, store } = c.get('project');
|
|
3879
|
+
const run = store.getRun(c.req.valid('param').id);
|
|
3880
|
+
if (!run)
|
|
3881
|
+
return c.json({ error: 'not found' }, 404);
|
|
3882
|
+
const query = c.req.valid('query');
|
|
3883
|
+
const loaded = await loadFileLink({ dataDir, runId: run.id, projectRoot: root, workingDirectory: run.worktreePath ?? root, path: query.path });
|
|
3884
|
+
if (query.raw === '1') {
|
|
3885
|
+
if (!loaded.imageBytes || loaded.preview.type !== 'file')
|
|
3886
|
+
return c.json({ error: 'Only bounded raster images can be served inline.' }, 409);
|
|
3887
|
+
return c.body(new Uint8Array(loaded.imageBytes).buffer, 200, {
|
|
3888
|
+
'content-type': rasterMime(loaded.preview.path), 'x-content-type-options': 'nosniff',
|
|
3889
|
+
'content-security-policy': "default-src 'none'; sandbox", 'cache-control': 'private, no-store',
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
return c.json(loaded.preview);
|
|
3893
|
+
})
|
|
3894
|
+
.get('/runs/:id/artifacts', paramZodValidator(artifactParamsSchema.omit({ artifactId: true })), async (c) => {
|
|
3895
|
+
const { dataDir, store } = c.get('project');
|
|
3896
|
+
const id = c.req.valid('param').id;
|
|
3897
|
+
if (!store.getRun(id))
|
|
3898
|
+
return c.json({ error: 'not found' }, 404);
|
|
3899
|
+
return c.json({ artifacts: await listArtifacts(artifactDirectory(dataDir, id), id) });
|
|
3900
|
+
})
|
|
3901
|
+
.get('/runs/:id/artifacts/:artifactId', paramZodValidator(artifactParamsSchema), async (c) => {
|
|
3902
|
+
const { dataDir, store } = c.get('project');
|
|
3903
|
+
const { id, artifactId } = c.req.valid('param');
|
|
3904
|
+
if (!store.getRun(id))
|
|
3905
|
+
return c.json({ error: 'not found' }, 404);
|
|
3906
|
+
const entry = await readArtifact(artifactDirectory(dataDir, id), id, artifactId);
|
|
3907
|
+
if (!entry)
|
|
3908
|
+
return c.json({ error: 'Published file is missing or unavailable.' }, 404);
|
|
3909
|
+
return c.json(artifactPreview(entry.metadata, entry.bytes));
|
|
3910
|
+
})
|
|
3911
|
+
.get('/runs/:id/artifacts/:artifactId/download', paramZodValidator(artifactParamsSchema), async (c) => {
|
|
3912
|
+
const { dataDir, store } = c.get('project');
|
|
3913
|
+
const { id, artifactId } = c.req.valid('param');
|
|
3914
|
+
if (!store.getRun(id))
|
|
3915
|
+
return c.json({ error: 'not found' }, 404);
|
|
3916
|
+
const entry = await readArtifact(artifactDirectory(dataDir, id), id, artifactId);
|
|
3917
|
+
if (!entry)
|
|
3918
|
+
return c.json({ error: 'Published file is missing or unavailable.' }, 404);
|
|
3919
|
+
return c.body(new Uint8Array(entry.bytes).buffer, 200, {
|
|
3920
|
+
'content-type': 'application/octet-stream', 'x-content-type-options': 'nosniff',
|
|
3921
|
+
'content-disposition': `attachment; filename="${entry.metadata.name.replace(/[^A-Za-z0-9._-]/g, '_')}"`,
|
|
3922
|
+
'content-security-policy': "default-src 'none'; sandbox", 'cache-control': 'private, no-store',
|
|
3923
|
+
});
|
|
3924
|
+
})
|
|
3925
|
+
.get('/runs/:id/artifacts/:artifactId/image', paramZodValidator(artifactParamsSchema), async (c) => {
|
|
3926
|
+
const { dataDir, store } = c.get('project');
|
|
3927
|
+
const { id, artifactId } = c.req.valid('param');
|
|
3928
|
+
if (!store.getRun(id))
|
|
3929
|
+
return c.json({ error: 'not found' }, 404);
|
|
3930
|
+
const entry = await readArtifact(artifactDirectory(dataDir, id), id, artifactId);
|
|
3931
|
+
if (!entry)
|
|
3932
|
+
return c.json({ error: 'Published file is missing or unavailable.' }, 404);
|
|
3933
|
+
const mime = rasterMime(entry.metadata.name);
|
|
3934
|
+
if (!mime)
|
|
3935
|
+
return c.json({ error: 'Only raster images can be served inline.' }, 409);
|
|
3936
|
+
return c.body(new Uint8Array(entry.bytes).buffer, 200, {
|
|
3937
|
+
'content-type': mime, 'x-content-type-options': 'nosniff',
|
|
3938
|
+
'content-security-policy': "default-src 'none'; sandbox", 'cache-control': 'private, no-store',
|
|
3939
|
+
});
|
|
3706
3940
|
})
|
|
3707
3941
|
// Files tab: directory listing (path omitted or a dir) or file content
|
|
3708
3942
|
// (size-capped, binary flagged). Traversal-safe — readWorktreePath rejects
|
|
@@ -4052,7 +4286,10 @@ export function createApp(deps) {
|
|
|
4052
4286
|
// The keep-limit the panel reports is the one the enforcer will actually
|
|
4053
4287
|
// apply — inherited from the workspace default when this repo sets none.
|
|
4054
4288
|
const keep = await resolveWorktreeRetention(repoRoot);
|
|
4055
|
-
const
|
|
4289
|
+
const allRuns = store.listRuns();
|
|
4290
|
+
// Listing is on-disk dirs only; parent liveness for #575 uses the full store
|
|
4291
|
+
// so a live `worktree: false` parent is not treated as gone (#570 honesty).
|
|
4292
|
+
const runs = allRuns.filter((r) => r.worktreePath && existsSync(r.worktreePath));
|
|
4056
4293
|
const worktrees = await Promise.all(runs.map(async (r) => ({
|
|
4057
4294
|
runId: r.id,
|
|
4058
4295
|
title: r.title ?? r.id,
|
|
@@ -4061,7 +4298,7 @@ export function createApp(deps) {
|
|
|
4061
4298
|
// POSIX `du` — degrades to null (Windows / du missing / error); never blocks.
|
|
4062
4299
|
sizeBytes: await worktreeSizeBytes(r.worktreePath),
|
|
4063
4300
|
finishedAt: r.finishedAt ?? null,
|
|
4064
|
-
reclaimable: isReclaimable(r),
|
|
4301
|
+
reclaimable: isReclaimable(r, allRuns),
|
|
4065
4302
|
})));
|
|
4066
4303
|
// Total is null when any size degraded, so the panel never shows a wrong sum.
|
|
4067
4304
|
const totalBytes = worktrees.some((w) => w.sizeBytes === null)
|
|
@@ -5080,6 +5317,7 @@ export function createApp(deps) {
|
|
|
5080
5317
|
.route('/', projectsRoutes)
|
|
5081
5318
|
.route('/', agentProfilesRoutes)
|
|
5082
5319
|
.route('/', skillsUpdateRoutes)
|
|
5320
|
+
.route('/', applicationUpdateRoutes)
|
|
5083
5321
|
.route('/', workspaceConfigRoutes)
|
|
5084
5322
|
.route('/', fsBrowseRoutes)
|
|
5085
5323
|
.route('/', automationChecksRoutes)
|
|
@@ -5110,6 +5348,13 @@ export function createApp(deps) {
|
|
|
5110
5348
|
routed.get('*', (c) => serveShell(c) ?? c.notFound());
|
|
5111
5349
|
return routed;
|
|
5112
5350
|
}
|
|
5351
|
+
/** The host a thread link names (#589): the bound one, or loopback for a wildcard bind. */
|
|
5352
|
+
function taskWebhookHost(bindHost) {
|
|
5353
|
+
if (!bindHost || bindHost === '0.0.0.0' || bindHost === '::')
|
|
5354
|
+
return '127.0.0.1';
|
|
5355
|
+
return bindHost.includes(':') ? `[${bindHost}]` : bindHost;
|
|
5356
|
+
}
|
|
5357
|
+
const NO_WEBHOOK_ERROR = 'this project has no task webhook — set one in Settings → General → Task webhook, or start without notify';
|
|
5113
5358
|
export function startServer(deps, port) {
|
|
5114
5359
|
const workspaceEvents = deps.workspaceEvents ?? new WorkspaceEventBus();
|
|
5115
5360
|
const skillsUpdate = deps.skillsUpdate ?? new SkillsUpdateService({ invalidateCatalog: refreshTeamSkills });
|
|
@@ -5119,11 +5364,15 @@ export function startServer(deps, port) {
|
|
|
5119
5364
|
const automationCoordinator = new AutomationCoordinator({ listProjects });
|
|
5120
5365
|
const bootProjectId = deps.bootProjectId ?? 'default';
|
|
5121
5366
|
const bootAutomationStore = automationCoordinator.store(bootProjectId, deps.repoRoot);
|
|
5367
|
+
const taskWebhooks = deps.taskWebhooks ?? new TaskWebhooks({ fetch: deps.taskWebhookFetch });
|
|
5122
5368
|
const sharedContexts = deps.contexts ?? new ProjectContexts({
|
|
5123
5369
|
listProjects,
|
|
5124
5370
|
semaphore: deps.semaphore,
|
|
5125
5371
|
automationStore: (projectId, root) => automationCoordinator.store(projectId, root),
|
|
5126
|
-
prepareManager: project =>
|
|
5372
|
+
prepareManager: (project) => {
|
|
5373
|
+
taskWebhooks.attach(project);
|
|
5374
|
+
return deps.delegation?.attachProject(project);
|
|
5375
|
+
},
|
|
5127
5376
|
});
|
|
5128
5377
|
// #801: GitHub automations are opt-in. Off, the flag must remove the BEHAVIOR and not merely
|
|
5129
5378
|
// the UI — no scheduler, no GitHub polling, no launched runs — so every entry point into the
|
|
@@ -5133,6 +5382,7 @@ export function startServer(deps, port) {
|
|
|
5133
5382
|
let rescheduleAutomations = () => { };
|
|
5134
5383
|
const app = createApp({
|
|
5135
5384
|
...deps,
|
|
5385
|
+
taskWebhooks,
|
|
5136
5386
|
contexts: sharedContexts,
|
|
5137
5387
|
automationStore: bootAutomationStore,
|
|
5138
5388
|
workspaceEvents,
|
|
@@ -5146,7 +5396,28 @@ export function startServer(deps, port) {
|
|
|
5146
5396
|
// hosted/VPS deployment (which also flips CEZ_REMOTE to gate the local-handoff endpoints) —
|
|
5147
5397
|
// src/index.ts never passes it, so the loopback guarantee holds for the normal CLI.
|
|
5148
5398
|
const server = serve({
|
|
5149
|
-
fetch:
|
|
5399
|
+
fetch: async (request, env) => {
|
|
5400
|
+
const result = await app.fetch(request, env);
|
|
5401
|
+
if (request.method === 'POST' && new URL(request.url).pathname === '/api/v1/workspace/application-update/restart'
|
|
5402
|
+
&& result.status === 200) {
|
|
5403
|
+
// Arming is server-owned. A connected caller must receive the complete
|
|
5404
|
+
// acknowledgement before shutdown; a caller that already disconnected
|
|
5405
|
+
// cannot leave its explicitly requested, armed restart stranded.
|
|
5406
|
+
const response = env.outgoing;
|
|
5407
|
+
const handoff = () => {
|
|
5408
|
+
response.off('finish', handoff);
|
|
5409
|
+
response.off('close', handoff);
|
|
5410
|
+
deps.applicationUpdate?.afterResponse?.();
|
|
5411
|
+
};
|
|
5412
|
+
if (response.destroyed)
|
|
5413
|
+
handoff();
|
|
5414
|
+
else {
|
|
5415
|
+
response.once('finish', handoff);
|
|
5416
|
+
response.once('close', handoff);
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
return result;
|
|
5420
|
+
},
|
|
5150
5421
|
port,
|
|
5151
5422
|
hostname: deps.bindHost ?? '127.0.0.1',
|
|
5152
5423
|
});
|
|
@@ -5239,7 +5510,30 @@ export function startServer(deps, port) {
|
|
|
5239
5510
|
});
|
|
5240
5511
|
server.once('close', () => { unsubscribe(); coordinator.stop(); automationScheduler.stop(); sharedContexts.disposeAll(); void deps.delegation?.close(); });
|
|
5241
5512
|
socketHub.attach(server, (req) => verifyWsUpgrade(req, deps.bindHost));
|
|
5242
|
-
|
|
5513
|
+
const shutdownForRestart = async () => {
|
|
5514
|
+
sharedContexts.disposeAll(); // flush every built secondary project before old process exits
|
|
5515
|
+
deps.store.flush();
|
|
5516
|
+
socketHub.close();
|
|
5517
|
+
await deps.delegation?.close();
|
|
5518
|
+
await new Promise((resolve) => {
|
|
5519
|
+
server.close(() => resolve());
|
|
5520
|
+
if ('closeAllConnections' in server)
|
|
5521
|
+
server.closeAllConnections();
|
|
5522
|
+
});
|
|
5523
|
+
};
|
|
5524
|
+
// The task webhook's thread links name the port the listener BOUND (#594 review): `--port 0`
|
|
5525
|
+
// asks for an ephemeral one, so the requested number is not an address. Deliveries queue
|
|
5526
|
+
// behind this, so none goes out before it is set.
|
|
5527
|
+
const setTaskWebhookOrigin = () => {
|
|
5528
|
+
const address = server.address();
|
|
5529
|
+
const bound = address && typeof address === 'object' ? address.port : port;
|
|
5530
|
+
taskWebhooks.setOrigin(`http://${taskWebhookHost(deps.bindHost)}:${bound}`);
|
|
5531
|
+
};
|
|
5532
|
+
if (server.listening)
|
|
5533
|
+
setTaskWebhookOrigin();
|
|
5534
|
+
else
|
|
5535
|
+
server.once('listening', setTaskWebhookOrigin);
|
|
5536
|
+
return Object.assign(server, { shutdownForRestart });
|
|
5243
5537
|
}
|
|
5244
5538
|
/**
|
|
5245
5539
|
* The WebSocket twin of the `/api/*` request-origin guard (#426), applied
|