@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,2738 @@
|
|
|
1
|
+
import { HISTORY_STATUSES } from "./run-status.js";
|
|
2
|
+
import { modelCatalogStore } from "./model-catalog.js";
|
|
3
|
+
import { permissionProviderProblem, createRunPermissions as catalogPermissions } from "./run-permissions.js";
|
|
4
|
+
import { inheritRunEnvironment } from "./run-environment.js";
|
|
5
|
+
import { teamTemplateStore } from "./team-template-store.js";
|
|
6
|
+
import { fillTeamTemplate, validateTeamTemplate } from "./team-templates.js";
|
|
7
|
+
import { antigravityExecution, AGY_NATIVE_LIMITATION, createRunPermissions, narrowerTrust, resolveRunPermissions, permissionRequirements } from "./run-permissions.js";
|
|
8
|
+
import { TeamTerminals, handleTeamTerminals } from "./collab/terminals.js";
|
|
9
|
+
import { HostExecution, handleHostExecution } from "./collab/host-execution.js";
|
|
10
|
+
import { closeCodeIntelligence, codeIntelligenceHealth } from "./code-intelligence.js";
|
|
11
|
+
import { sendAsset } from "./assets.js";
|
|
12
|
+
import { transcriptPage, transcriptPreview, TranscriptCursorError } from "./transcript.js";
|
|
13
|
+
import { createSseWriter, streamHealth } from "./sse-writer.js";
|
|
14
|
+
import { PerformanceMetrics } from "./performance.js";
|
|
15
|
+
import { JourneyStore } from "./journey-store.js";
|
|
16
|
+
import { journeyApi } from "./journey-api.js";
|
|
17
|
+
import { taskIntent, outcomeMetrics, workflowRecommendations } from "./experience.js";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { CertificateRenewal } from "./certificate-renewal.js";
|
|
20
|
+
import { loadRemoteCertificate, saveRemoteCertificate, validateRemoteCertificate, certificateHostname } from "./remote-tls.js";
|
|
21
|
+
import { requestTailscaleCertificate } from "./tailscale.js";
|
|
22
|
+
import { ProjectTeams } from "./project-teams.js";
|
|
23
|
+
import { handleProjectTeams } from "./project-teams-api.js";
|
|
24
|
+
import { SshProjects } from "./ssh-projects/manager.js";
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { ProjectFilePolicies, FilePolicyError, assertPolicyExecution, configureFilePolicyResolver, filterPolicyDiff, policyIsRestrictive } from "./filesystem-policy.js";
|
|
27
|
+
import { FilesystemAccess } from "./filesystem-access.js";
|
|
28
|
+
import { createCodeHostsApi } from "./code-hosts-api.js";
|
|
29
|
+
import { FilteredTasks, FilteredTaskError } from "./filtered-tasks.js";
|
|
30
|
+
import { handleFilteredTasksApi } from "./filtered-tasks-api.js";
|
|
31
|
+
import { createServer as createSecureServer } from "node:https";
|
|
32
|
+
import { PhoneSessions } from "./phone-sessions.js";
|
|
33
|
+
import { createServer } from "node:http";
|
|
34
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
35
|
+
import { readFile } from "node:fs/promises";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
37
|
+
import { promisify } from "node:util";
|
|
38
|
+
import { brotliCompress, constants as zlib, gzip } from "node:zlib";
|
|
39
|
+
import {
|
|
40
|
+
agentAvailability,
|
|
41
|
+
AGENT_IDS,
|
|
42
|
+
registerProviderAdapters,
|
|
43
|
+
replaceAcpAdapters,
|
|
44
|
+
agentCatalog,
|
|
45
|
+
adapters,
|
|
46
|
+
getAdapter,
|
|
47
|
+
isAgentId,
|
|
48
|
+
validateAgentSettings
|
|
49
|
+
} from "./adapters/index.js";
|
|
50
|
+
import { listTree, readRepoFile, searchFiles } from "./repo-tree.js";
|
|
51
|
+
import { checkGit, doctor } from "./doctor.js";
|
|
52
|
+
import { readRepoConfig } from "./repo-config.js";
|
|
53
|
+
import { openFolder } from "./serve.js";
|
|
54
|
+
import { LaunchProfiles, LaunchProfileError, readLaunchProfiles } from "./launch-profiles.js";
|
|
55
|
+
import { TeamRerunError } from "./team-reruns.js";
|
|
56
|
+
import { ConventionsStore, ConventionsError } from "./conventions.js";
|
|
57
|
+
import { discoverDependencies } from "./dependency-health.js";
|
|
58
|
+
import { DeferredRunConflictError, sanitizeDeferredRequest } from "./deferred-runs.js";
|
|
59
|
+
import { createDeferredRuns, persistDeferredReceipt, recoverDeferredRuns } from "./deferred-launch.js";
|
|
60
|
+
import { validateContextBudget } from "./context-budget.js";
|
|
61
|
+
import { RetentionPolicyStore, RetentionPolicyConflictError, previewRetention } from "./retention-policy.js";
|
|
62
|
+
import { SharedAccounts, SharedAccountError } from "./shared-accounts.js";
|
|
63
|
+
import { canonicalSharedPath, SharedPathError } from "./shared-access.js";
|
|
64
|
+
import { CollaborationReviews, CollaborationReviewError } from "./collaboration-reviews.js";
|
|
65
|
+
import { handleSharedApi } from "./shared-api.js";
|
|
66
|
+
import { CollabStore, memberActor, ownerActor } from "./collab/store.js";
|
|
67
|
+
import { handleRunnerConnect, handleTeamApi, looksLikeRunnerToken, runnerAllowed, teamRoutePolicy } from "./collab/api.js";
|
|
68
|
+
import { CollabError } from "./collab/model.js";
|
|
69
|
+
import { TeamConnections } from "./collab/connections.js";
|
|
70
|
+
import { handleTeamLinkApi, linkInfo } from "./collab/link-api.js";
|
|
71
|
+
import { userInfo } from "node:os";
|
|
72
|
+
import { RemoteWorkers } from "./remote-workers/client.js";
|
|
73
|
+
import { createCoordinatedApi } from "./coordinated-api.js";
|
|
74
|
+
import { Extensions } from "./extensions.js";
|
|
75
|
+
import { ShellLimitError, Shells } from "./shell.js";
|
|
76
|
+
import { cursorSeq, StreamHub, TopicError } from "./stream.js";
|
|
77
|
+
import { replayEvents } from "./run-log.js";
|
|
78
|
+
import { previewTarget, Previews } from "./preview.js";
|
|
79
|
+
import { SnapshotError, takeSnapshot } from "./snapshot.js";
|
|
80
|
+
import { Integrations } from "./integrations.js";
|
|
81
|
+
import { Notifications } from "./notifications.js";
|
|
82
|
+
import { GithubReviewThreads, ReviewThreadError } from "./github-review-threads.js";
|
|
83
|
+
import { approvals } from "./approvals.js";
|
|
84
|
+
import { validateAgentContainer } from "./agent-container.js";
|
|
85
|
+
import { Notes, NotesConflictError } from "./notes.js";
|
|
86
|
+
import { HttpError, readJson, readLoginJson, send } from "./http.js";
|
|
87
|
+
import { handleWorkspaceApi } from "./workspace-api.js";
|
|
88
|
+
import { TeamPlanEditError } from "./workflow-control.js";
|
|
89
|
+
import { analyzeProjectMemory } from "./memory-analysis.js";
|
|
90
|
+
import { redactExport } from "./privacy.js";
|
|
91
|
+
import { validateVerificationProfile, renderVerificationReport } from "./verification.js";
|
|
92
|
+
import { CODEFLOW_HOME, autoTeamEnabled, envNumber, PORT } from "./config.js";
|
|
93
|
+
import { RunManager, RunConflictError, UntrustedSourceError } from "./engine.js";
|
|
94
|
+
import { detectCheckCommand } from "./checks.js";
|
|
95
|
+
import { findPattern, patterns } from "./patterns/index.js";
|
|
96
|
+
import { listDirectories } from "./fs.js";
|
|
97
|
+
import { git, listBranches, repoRoot, worktreeRoot } from "./git.js";
|
|
98
|
+
import { stat } from "node:fs/promises";
|
|
99
|
+
import { folderSizes, remoteBranches, removeWorktrees } from "./ship.js";
|
|
100
|
+
import { computeStats, rankAgents } from "./stats.js";
|
|
101
|
+
import { acquireLock, writeHealth, writeQueueStats, loadJournal } from "./store.js";
|
|
102
|
+
import { reapOrphanGroups } from "./adapters/process.js";
|
|
103
|
+
import { logError, logInfo, logWarn } from "./log.js";
|
|
104
|
+
import { listIssues, listPullRequests, MERGE_METHODS } from "./github.js";
|
|
105
|
+
import { listSweep, runSweep, sourceKeys } from "./sweeps.js";
|
|
106
|
+
import { parseImproveSettings, IMPROVE_AGENTS } from "./improve-settings.js";
|
|
107
|
+
import { exhausted, resolveAuto } from "./routing.js";
|
|
108
|
+
import { DocsHelp, serveDocs, parseHelpRequest } from "./docs-help.js";
|
|
109
|
+
import { improvePrompt, assertImproveEnvironment, ImproveError, MAX_IMPROVE_CHARS } from "./improve.js";
|
|
110
|
+
import { templates } from "./templates.js";
|
|
111
|
+
import { PrWatcher } from "./watch.js";
|
|
112
|
+
import { Automations, describeSchedule, readWebhook, validateSchedule, previewSchedule } from "./automation.js";
|
|
113
|
+
import { detectTailscale, isTailscaleIPv4 } from "./tailscale.js";
|
|
114
|
+
import { changeRemote, isOwnAddress, lanAddresses, loadRemote, renderRemote, saveRemote, tokenMatches } from "./remote.js";
|
|
115
|
+
import { projectOf, Projects } from "./projects.js";
|
|
116
|
+
import { memoryStore, MemoryConflictError } from "./memory.js";
|
|
117
|
+
import { adapterTeamCapabilities } from "./adapters/team-tools.js";
|
|
118
|
+
import { createAcpAdapter, probeAcp } from "./adapters/acp.js";
|
|
119
|
+
import { AcpConnections, validateAcpConnection } from "./acp-connections.js";
|
|
120
|
+
import { teamToolBridge } from "./team-tools.js";
|
|
121
|
+
import { teamEvaluationStore, buildTeamEvaluation, assessTeamRollout } from "./team-evaluation.js";
|
|
122
|
+
const releaseLock = await acquireLock().catch((err) => {
|
|
123
|
+
console.error(err.message);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
const reviewThreads = new GithubReviewThreads();
|
|
127
|
+
process.once("exit", releaseLock);
|
|
128
|
+
const recentCrashes = [];
|
|
129
|
+
process.on("unhandledRejection", (reason) => logError("Unhandled promise rejection", reason));
|
|
130
|
+
process.on("uncaughtException", (err) => {
|
|
131
|
+
logError("Uncaught exception", err);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
recentCrashes.push(now);
|
|
134
|
+
while (recentCrashes[0] < now - 6e4) recentCrashes.shift();
|
|
135
|
+
if (recentCrashes.length >= 10) void stop("too many uncaught exceptions in a minute", 1);
|
|
136
|
+
});
|
|
137
|
+
const reaped = await reapOrphanGroups();
|
|
138
|
+
if (reaped) logInfo(`Stopped ${reaped} process group(s) a previous code-flow left running`);
|
|
139
|
+
const extensions = new Extensions({ register: (providers, workflows) => {
|
|
140
|
+
const ids = workflows.map((workflow) => workflow.id);
|
|
141
|
+
if (new Set(ids).size !== ids.length || ids.some((id) => !!findPattern(id))) throw new Error("Workflow IDs must be unique and cannot replace installed patterns");
|
|
142
|
+
const available = /* @__PURE__ */ new Set([...AGENT_IDS, ...providers.map((provider) => provider.id)]);
|
|
143
|
+
if (workflows.some((workflow) => workflow.roles.some((role) => role.default.some((id) => !available.has(id))))) throw new Error("Workflow default roles must name registered providers");
|
|
144
|
+
registerProviderAdapters(providers);
|
|
145
|
+
patterns.push(...workflows);
|
|
146
|
+
} });
|
|
147
|
+
const projects = new Projects();
|
|
148
|
+
const acpConnections = new AcpConnections((connections) => replaceAcpAdapters(connections.map(createAcpAdapter)));
|
|
149
|
+
let acpChecks = 0;
|
|
150
|
+
const remoteWorkers = new RemoteWorkers();
|
|
151
|
+
await remoteWorkers.load().catch((error) => logError("SSH workers disabled; repair their configuration to enable remote execution", error));
|
|
152
|
+
const manager = new RunManager(findPattern, projects, remoteWorkers);
|
|
153
|
+
const filePolicies = new ProjectFilePolicies();
|
|
154
|
+
const fileAccess = new FilesystemAccess(filePolicies, projects, manager);
|
|
155
|
+
const filteredTasks = new FilteredTasks({
|
|
156
|
+
authorize: async (project, repo) => {
|
|
157
|
+
if (!projects.get(project)?.repos.includes(repo)) throw new FilteredTaskError("The project or its explicit repository membership changed", 409);
|
|
158
|
+
const policies = await fileAccess.forRepo(repo);
|
|
159
|
+
if (!projects.get(project)?.repos.includes(repo)) throw new FilteredTaskError("The project or its explicit repository membership changed", 409);
|
|
160
|
+
return policies;
|
|
161
|
+
},
|
|
162
|
+
withRepository: (repo, operation) => manager.withIdleRepository(repo, operation, { kind: "filtered task", owner: "filtered-tasks" })
|
|
163
|
+
});
|
|
164
|
+
let filteredTasksReady = false;
|
|
165
|
+
configureFilePolicyResolver((repo) => fileAccess.forRepo(repo), { suppressRepositoryCommands: () => fileAccess.hasRestrictions() });
|
|
166
|
+
manager.setFilesystemPolicyGuard((repo, run) => fileAccess.execution(repo, run));
|
|
167
|
+
let filesystemFeedRevision = 0;
|
|
168
|
+
filePolicies.onChange(() => {
|
|
169
|
+
filesystemFeedRevision++;
|
|
170
|
+
void closeCodeIntelligence();
|
|
171
|
+
});
|
|
172
|
+
const policyPreparations = /* @__PURE__ */ new Set();
|
|
173
|
+
function trackPolicyPreparation(operation) {
|
|
174
|
+
const work = Promise.resolve().then(operation);
|
|
175
|
+
policyPreparations.add(work);
|
|
176
|
+
void work.finally(() => policyPreparations.delete(work)).catch(() => void 0);
|
|
177
|
+
return work;
|
|
178
|
+
}
|
|
179
|
+
const conventions = new ConventionsStore();
|
|
180
|
+
const retentionPolicy = new RetentionPolicyStore();
|
|
181
|
+
const sharedAccounts = new SharedAccounts();
|
|
182
|
+
await sharedAccounts.load().catch((error) => logError("Shared accounts unavailable; owner recovery remains available", error));
|
|
183
|
+
const collaborationReviews = new CollaborationReviews();
|
|
184
|
+
const teamMembers = () => sharedAccounts.available ? sharedAccounts.snapshot().users.map((user) => ({ userId: user.id, username: user.username, displayName: user.displayName, enabled: user.enabled, roles: user.roles })) : [];
|
|
185
|
+
const osUser = (() => {
|
|
186
|
+
try {
|
|
187
|
+
return userInfo().username || void 0;
|
|
188
|
+
} catch {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
const collab = new CollabStore({ members: teamMembers, projects: () => projects.list().map((project) => ({ id: project.id, name: project.name })), ownerName: () => osUser });
|
|
193
|
+
const teamConnections = new TeamConnections({
|
|
194
|
+
store: collab,
|
|
195
|
+
manager,
|
|
196
|
+
findPattern,
|
|
197
|
+
projectFor: (repo) => projects.forRepo(repo)?.id,
|
|
198
|
+
onEvent: (id, event) => teamBroadcast(event, void 0, id)
|
|
199
|
+
});
|
|
200
|
+
const hostExecution = new HostExecution({
|
|
201
|
+
store: collab,
|
|
202
|
+
manager,
|
|
203
|
+
findPattern,
|
|
204
|
+
members: teamMembers,
|
|
205
|
+
projectRepos: (id) => projects.get(id)?.repos,
|
|
206
|
+
resolveRepo: (project, key) => teamConnections.get("local").link.resolveRepo(project, key),
|
|
207
|
+
availableAgents: async () => (await agentAvailability()).filter((agent) => agent.available).map((agent) => agent.id),
|
|
208
|
+
enabled: () => !fileAccess.hasRestrictions()
|
|
209
|
+
});
|
|
210
|
+
const resolveQueuedProviders = (request) => trackPolicyPreparation(async () => {
|
|
211
|
+
const pattern = findPattern(request.pattern);
|
|
212
|
+
if (!pattern) throw new Error("This pattern no longer exists; cancel and review the saved request");
|
|
213
|
+
const selection = request.loop?.remoteWorker;
|
|
214
|
+
const { roles } = await validateRoles(pattern, request.roles, request.repo, selection);
|
|
215
|
+
const inventory = selection ? (await manager.remoteInventory(selection)).providers : await agentAvailability();
|
|
216
|
+
return { roles, installed: new Set(inventory.filter((entry) => entry.available).map((entry) => entry.id)) };
|
|
217
|
+
});
|
|
218
|
+
const deferredRuns = createDeferredRuns(manager, { build: buildStartInput, resolveProviders: resolveQueuedProviders });
|
|
219
|
+
const coordinatedApi = createCoordinatedApi({ manager, projects, build: buildStartInput, resolveProviders: resolveQueuedProviders });
|
|
220
|
+
const launchProfiles = new LaunchProfiles(manager.devServers);
|
|
221
|
+
manager.onWorkspaceStop((runId) => launchProfiles.stopMatching(`${runId}/`));
|
|
222
|
+
const integrations = new Integrations();
|
|
223
|
+
const codeHostsApi = createCodeHostsApi({ integrations, manager, projects, build: buildStartInput });
|
|
224
|
+
await integrations.load().catch((error) => logError("External sources disabled: invalid integration configuration", error));
|
|
225
|
+
const notifications = new Notifications();
|
|
226
|
+
const notificationsReady = await notifications.load().then(() => true, (error) => {
|
|
227
|
+
logError("Notifications disabled: configuration or delivery state cannot be read", error);
|
|
228
|
+
return false;
|
|
229
|
+
});
|
|
230
|
+
const shells = new Shells();
|
|
231
|
+
const teamTerminals = new TeamTerminals({
|
|
232
|
+
users: () => sharedAccounts.available ? sharedAccounts.snapshot().users : [],
|
|
233
|
+
projectRepos: (id) => projects.get(id)?.repos,
|
|
234
|
+
resolveRepo: (project, key) => teamConnections.get("local").link.resolveRepo(project, key),
|
|
235
|
+
enabled: () => !fileAccess.hasRestrictions(),
|
|
236
|
+
changed: (projectId) => collab.hostExecutionChanged(projectId),
|
|
237
|
+
activity: () => manager.noteActivity()
|
|
238
|
+
});
|
|
239
|
+
sharedAccounts.onChange(() => {
|
|
240
|
+
void teamTerminals.reap().catch((error) => logError("Team terminal revocation failed", error));
|
|
241
|
+
});
|
|
242
|
+
const notes = new Notes();
|
|
243
|
+
const watcher = new PrWatcher(manager);
|
|
244
|
+
const automations = new Automations(manager, (body, extra) => trackPolicyPreparation(() => startFromBody(body, extra)), (repo, kind) => trackPolicyPreparation(async () => {
|
|
245
|
+
assertPolicyExecution(await fileAccess.execution(repo), "Automation source discovery");
|
|
246
|
+
return listSweep(repo, kind, manager.list());
|
|
247
|
+
}));
|
|
248
|
+
const projectTeams = new ProjectTeams({ manager, projects, automations, build: buildStartInput, allowed: async (repo) => assertPolicyExecution(await fileAccess.execution(repo), "Standing project teams") });
|
|
249
|
+
async function pausePolicyBackground() {
|
|
250
|
+
const results = await Promise.allSettled([watcher.stop(), automations.stop(), projectTeams.stop(), deferredRuns.stop(), coordinatedApi.service.stop(), ...notificationsReady ? [notifications.stop()] : []]);
|
|
251
|
+
while (policyPreparations.size) await Promise.allSettled([...policyPreparations]);
|
|
252
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
253
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
254
|
+
}
|
|
255
|
+
function startPolicyBackground() {
|
|
256
|
+
projectTeams.start();
|
|
257
|
+
watcher.start();
|
|
258
|
+
automations.start(envNumber("CODEFLOW_SCHEDULER_TICK_MS", 3e4, { min: 100 }));
|
|
259
|
+
deferredRuns.start();
|
|
260
|
+
coordinatedApi.service.start();
|
|
261
|
+
if (notificationsReady) notifications.start();
|
|
262
|
+
}
|
|
263
|
+
let remote;
|
|
264
|
+
let changingRemote = Promise.resolve();
|
|
265
|
+
function changeSavedRemote(patch) {
|
|
266
|
+
const change = changingRemote.catch(() => void 0).then(async () => {
|
|
267
|
+
const before = remote;
|
|
268
|
+
const next = changeRemote(before, patch);
|
|
269
|
+
if (next.control && (!phoneTls || remoteTlsIssue()) && !insecurePhoneDevelopment && patch.control === true) {
|
|
270
|
+
throw new HttpError(400, "Full phone control requires HTTPS. Choose Set up secure remote access in Settings \u2192 Remote access.");
|
|
271
|
+
}
|
|
272
|
+
if ((!phoneTls || remoteTlsIssue()) && !insecurePhoneDevelopment) next.control = false;
|
|
273
|
+
await saveRemote(next);
|
|
274
|
+
remote = next;
|
|
275
|
+
return before;
|
|
276
|
+
});
|
|
277
|
+
changingRemote = change.catch(() => void 0);
|
|
278
|
+
return change;
|
|
279
|
+
}
|
|
280
|
+
const REMOTE_PORT = envNumber("CODEFLOW_REMOTE_PORT", PORT + 1, { min: 1, max: 65535 });
|
|
281
|
+
const docsDirectory = fileURLToPath(new URL("../public/docs/", import.meta.url));
|
|
282
|
+
const docsHelp = new DocsHelp(docsDirectory, (agent, event) => {
|
|
283
|
+
if (event.kind === "quota") manager.recordQuota(agent, event.status, event.windows);
|
|
284
|
+
}, (agent, error) => {
|
|
285
|
+
if (error.kind === "usage_limit") manager.recordLimit(agent, error.resetsAt);
|
|
286
|
+
if (error.kind === "auth") manager.recordQuota(agent, "signed_out", {});
|
|
287
|
+
});
|
|
288
|
+
const indexHtml = fileURLToPath(new URL("../public/index.html", import.meta.url));
|
|
289
|
+
const HOST = "127.0.0.1";
|
|
290
|
+
const sshProjects = process.env.CODEFLOW_SSH_HOST ? void 0 : new SshProjects({ localProjects: () => projects.list(), canBrowse: () => !fileAccess.hasRestrictions() });
|
|
291
|
+
const tlsConfigured = !!process.env.CODEFLOW_TLS_CERT && !!process.env.CODEFLOW_TLS_KEY;
|
|
292
|
+
if (!!process.env.CODEFLOW_TLS_CERT !== !!process.env.CODEFLOW_TLS_KEY) throw new Error("Configure both CODEFLOW_TLS_CERT and CODEFLOW_TLS_KEY");
|
|
293
|
+
let phoneTls = tlsConfigured ? { cert: readFileSync(process.env.CODEFLOW_TLS_CERT), key: readFileSync(process.env.CODEFLOW_TLS_KEY) } : void 0;
|
|
294
|
+
const insecurePhoneDevelopment = process.env.CODEFLOW_ALLOW_INSECURE_PHONE_CONTROL === "1";
|
|
295
|
+
let phoneProtocol = phoneTls ? "https" : "http";
|
|
296
|
+
const phoneSessions = new PhoneSessions();
|
|
297
|
+
let savedCertificate;
|
|
298
|
+
let tlsLoadError;
|
|
299
|
+
let configuringTls = false;
|
|
300
|
+
const certificateRenewal = new CertificateRenewal(join(CODEFLOW_HOME, "remote-tls-renewal.json"));
|
|
301
|
+
function remoteTlsIssue() {
|
|
302
|
+
return tlsLoadError || (savedCertificate && savedCertificate.expiresAt <= Date.now() ? "HTTPS certificate expired. Renew or import a certificate in Remote access settings." : void 0);
|
|
303
|
+
}
|
|
304
|
+
const allowedHosts = /* @__PURE__ */ new Set([`${HOST}:${PORT}`, `localhost:${PORT}`]);
|
|
305
|
+
const journeys = new JourneyStore(join(CODEFLOW_HOME, "journeys"));
|
|
306
|
+
const handleJourneys = journeyApi(journeys, manager, [PORT, REMOTE_PORT]);
|
|
307
|
+
const previews = new Previews(
|
|
308
|
+
() => [`http://localhost:${PORT}`, `http://${HOST}:${PORT}`],
|
|
309
|
+
() => [PORT, REMOTE_PORT]
|
|
310
|
+
);
|
|
311
|
+
const sshPreviews = new Previews(() => [], () => [PORT, REMOTE_PORT], void 0, false);
|
|
312
|
+
function checkOrigin(req) {
|
|
313
|
+
if (!allowedHosts.has(req.headers.host ?? "")) throw new HttpError(403, "Bad Host header");
|
|
314
|
+
const origin = req.headers.origin;
|
|
315
|
+
if (origin && !allowedHosts.has(origin.replace(/^https?:\/\//, ""))) throw new HttpError(403, "Cross-origin request refused");
|
|
316
|
+
}
|
|
317
|
+
const auth = {
|
|
318
|
+
token: process.env.CODEFLOW_TOKEN || randomBytes(24).toString("hex"),
|
|
319
|
+
claimable: !process.env.CODEFLOW_SSH_HOST && sharedAccounts.available && !sharedAccounts.hasUsers
|
|
320
|
+
};
|
|
321
|
+
sharedAccounts.onChange(() => {
|
|
322
|
+
auth.claimable = false;
|
|
323
|
+
});
|
|
324
|
+
manager.onActivity(() => auth.claimable = false);
|
|
325
|
+
const COOKIE = "codeflow_session";
|
|
326
|
+
const SHARED_COOKIE = `codeflow_shared_${PORT}`;
|
|
327
|
+
const MAX_HEADER_SIZE = 1024 * 1024;
|
|
328
|
+
function mayClaim(req) {
|
|
329
|
+
const forwarded = ["forwarded", "x-forwarded-for", "x-forwarded-host", "x-real-ip", "cf-connecting-ip", "true-client-ip"].some((h) => req.headers[h] !== void 0);
|
|
330
|
+
const address = (req.socket.remoteAddress ?? "").replace(/^::ffff:/i, "");
|
|
331
|
+
return auth.claimable && !forwarded && (address === "::1" || address.startsWith("127."));
|
|
332
|
+
}
|
|
333
|
+
function sharedTransport(req) {
|
|
334
|
+
const forwarded = ["forwarded", "x-forwarded-for", "x-forwarded-host", "x-real-ip", "cf-connecting-ip", "true-client-ip"].some((header) => req.headers[header] !== void 0);
|
|
335
|
+
const address = (req.socket.remoteAddress ?? "").replace(/^::ffff:/i, "");
|
|
336
|
+
return { address, secure: req.socket.encrypted === true, loopback: !forwarded && (address === "::1" || /^127\./.test(address)) };
|
|
337
|
+
}
|
|
338
|
+
function sharedMutation(req) {
|
|
339
|
+
if (!req.headers.origin || !/^application\/json(?:\s*;|$)/i.test(String(req.headers["content-type"] ?? ""))) throw new HttpError(403, "Shared sign-in and mutations require same-origin JSON requests");
|
|
340
|
+
}
|
|
341
|
+
const clearCookie = (name) => `${name}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`;
|
|
342
|
+
function sharedCookie(token, secure) {
|
|
343
|
+
return `${SHARED_COOKIE}=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200${secure ? "; Secure" : ""}`;
|
|
344
|
+
}
|
|
345
|
+
function sharedSession(req) {
|
|
346
|
+
const transport = sharedTransport(req);
|
|
347
|
+
if (!transport.secure && !transport.loopback) return;
|
|
348
|
+
for (const token of readCookies(req, SHARED_COOKIE)) {
|
|
349
|
+
const identity = sharedAccounts.authenticate(token);
|
|
350
|
+
if (identity) return { token, identity };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function memberOf(identity) {
|
|
354
|
+
return { userId: identity.userId, username: identity.username, displayName: identity.displayName, enabled: true, roles: identity.roles };
|
|
355
|
+
}
|
|
356
|
+
function runnerActor(token) {
|
|
357
|
+
const credential = collab.authenticateRunner(token);
|
|
358
|
+
if (!credential) return void 0;
|
|
359
|
+
const member = teamMembers().find((entry) => entry.userId === credential.userId && entry.enabled);
|
|
360
|
+
return member && memberActor(member, "runner", credential.runnerId);
|
|
361
|
+
}
|
|
362
|
+
function runnerMutation(req) {
|
|
363
|
+
const hasBody = Number(req.headers["content-length"] ?? 0) > 0 || req.headers["transfer-encoding"] !== void 0;
|
|
364
|
+
if (hasBody && !/^application\/(?:json|octet-stream)(?:\s*;|$)/i.test(String(req.headers["content-type"] ?? ""))) throw new HttpError(415, "Runner requests send application/json, or application/octet-stream for a checkpoint bundle");
|
|
365
|
+
}
|
|
366
|
+
async function handleRunnerRequest(req, res, url, parts, token) {
|
|
367
|
+
const transport = sharedTransport(req);
|
|
368
|
+
if (!transport.secure && !transport.loopback) throw new HttpError(403, "Runner credentials are accepted over HTTPS or on this computer only");
|
|
369
|
+
const actor = runnerActor(token);
|
|
370
|
+
if (!actor) throw new HttpError(401, "This runner credential is not valid; connect this computer again in Settings \u2192 Team");
|
|
371
|
+
const route = teamRoutePolicy(req.method ?? "GET", parts);
|
|
372
|
+
if (parts[1] !== "team" || route && !runnerAllowed(route)) throw new HttpError(403, "This credential only reaches the team API");
|
|
373
|
+
await attempt(() => fileAccess.sharedGate(req));
|
|
374
|
+
return handleTeamApi(req, res, url, parts, { store: collab, actor, principal: "runner", reauthorize: () => {
|
|
375
|
+
const current = runnerActor(token);
|
|
376
|
+
if (!current) throw new HttpError(401, "This runner credential was revoked");
|
|
377
|
+
return current;
|
|
378
|
+
}, mutation: runnerMutation });
|
|
379
|
+
}
|
|
380
|
+
function sessionCookie() {
|
|
381
|
+
return `${COOKIE}=${auth.token}; HttpOnly; SameSite=Strict; Path=/`;
|
|
382
|
+
}
|
|
383
|
+
function readCookies(req, name) {
|
|
384
|
+
return String(req.headers.cookie ?? "").split(";").map((c) => c.trim()).filter((c) => c.startsWith(`${name}=`)).map((c) => c.slice(name.length + 1)).filter(Boolean);
|
|
385
|
+
}
|
|
386
|
+
function signedIn(req) {
|
|
387
|
+
const bearer = String(req.headers.authorization ?? "").replace(/^Bearer\s+/i, "");
|
|
388
|
+
if (bearer && tokenMatches(bearer, auth.token)) return true;
|
|
389
|
+
return readCookies(req, COOKIE).some((cookie) => tokenMatches(cookie, auth.token));
|
|
390
|
+
}
|
|
391
|
+
const PHONE_COOKIE = `codeflow_phone_${REMOTE_PORT}`;
|
|
392
|
+
const PHONE_COOKIE_DAYS = 30;
|
|
393
|
+
const phoneControl = (s = remote) => s.enabled && s.lan && s.control && !remoteTlsIssue() && (!!phoneTls || insecurePhoneDevelopment);
|
|
394
|
+
function phoneCookie(token) {
|
|
395
|
+
return `${PHONE_COOKIE}=${token}; HttpOnly; SameSite=Strict; Path=/${phoneTls ? "; Secure" : ""}; Max-Age=${PHONE_COOKIE_DAYS * 86400}`;
|
|
396
|
+
}
|
|
397
|
+
function phoneSignedIn(req) {
|
|
398
|
+
return phoneControl() && readCookies(req, PHONE_COOKIE).some((cookie) => phoneSessions.valid(cookie, remote.controlToken));
|
|
399
|
+
}
|
|
400
|
+
function checkLanOrigin(req) {
|
|
401
|
+
const origin = req.headers.origin;
|
|
402
|
+
if (origin && origin !== `${phoneProtocol}://${req.headers.host}`) throw new HttpError(403, "Cross-origin request refused");
|
|
403
|
+
}
|
|
404
|
+
const lockedPage = (title, body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
405
|
+
<title>code-flow \u2014 ${title}</title><style>
|
|
406
|
+
:root{--bg:#f8f7f3;--raised:#fdfcf9;--sunken:#f0efe9;--border:#e4e2da;--text:#292a26;--muted:#65675e;--link:#2d6a4b;--shadow:0 2px 6px rgba(34,35,24,.06),0 24px 64px rgba(34,35,24,.16);color-scheme:light}
|
|
407
|
+
@media (prefers-color-scheme:dark){:root{--bg:#161713;--raised:#1e1f1a;--sunken:#121310;--border:#2e3029;--text:#ecebe4;--muted:#9c9d92;--link:#8fe3c4;--shadow:0 2px 6px rgba(0,0,0,.35),0 24px 64px rgba(0,0,0,.55);color-scheme:dark}}
|
|
408
|
+
body{margin:0;padding:0 16px;background:var(--bg);color:var(--text);font:14px/1.55 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
409
|
+
main{max-width:480px;margin:12vh auto;padding:24px;background:var(--raised);border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow)}
|
|
410
|
+
.brand{font-size:15px;font-weight:600;letter-spacing:-.4px;margin:0 0 18px}.brand span{font-weight:400}
|
|
411
|
+
h1{font:400 24px/1.2 Georgia,"Times New Roman",serif;letter-spacing:-.5px;margin:0 0 12px}p{color:var(--muted);margin:8px 0}b{color:var(--text);font-weight:600}a{color:var(--link);font-weight:500}
|
|
412
|
+
code{font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text);background:var(--sunken);padding:1px 5px;border-radius:6px;overflow-wrap:anywhere}
|
|
413
|
+
</style></head><body><main><div class="brand">code<span>-flow</span></div>${body}</main></body></html>`;
|
|
414
|
+
const LOCKED_PAGE = lockedPage(
|
|
415
|
+
"sign in",
|
|
416
|
+
`<h1>Open code-flow from its link</h1>
|
|
417
|
+
<p>This code-flow server is already in use, so it only accepts browsers that open it with its link.</p>
|
|
418
|
+
<p>Find the line <code>Open http://localhost:${PORT}/?token=\u2026</code> where code-flow was started (the terminal, or the preview's server log) and open that link here.</p>
|
|
419
|
+
<p>Restarting code-flow and reloading this page also works, as long as no run has started yet.</p>`
|
|
420
|
+
);
|
|
421
|
+
const PHONE_LOCKED_PAGE = lockedPage(
|
|
422
|
+
"scan the QR code",
|
|
423
|
+
`<h1>Scan the QR code again</h1>
|
|
424
|
+
<p>This phone isn't signed in to code-flow. On your computer, open code-flow's <b>Automations</b> page and scan the QR code under <b>Remote access</b>.</p>
|
|
425
|
+
<p>Making a new link, or turning off full control from your phone, signs phones out.</p>`
|
|
426
|
+
);
|
|
427
|
+
const signingInPage = (target) => lockedPage("signing in", `<h1>Signing in\u2026</h1><p><a href="${target}">Open code-flow</a></p><script>location.replace(${JSON.stringify(target)})</script>`);
|
|
428
|
+
const NO_FRAMES = { "x-frame-options": "DENY", "content-security-policy": "frame-ancestors 'none'" };
|
|
429
|
+
let appPage;
|
|
430
|
+
const compressBrotli = promisify(brotliCompress);
|
|
431
|
+
const compressGzip = promisify(gzip);
|
|
432
|
+
async function loadAppPage() {
|
|
433
|
+
const file = await stat(indexHtml);
|
|
434
|
+
const version = `${file.mtimeMs}:${file.size}`;
|
|
435
|
+
if (appPage?.version !== version) {
|
|
436
|
+
const page = (async () => {
|
|
437
|
+
const identity = await readFile(indexHtml);
|
|
438
|
+
const [br, gz] = await Promise.all([
|
|
439
|
+
// Quality 9 rather than brotli's 11: about as small, and ready in a fraction of the time after a rebuild.
|
|
440
|
+
compressBrotli(identity, { params: { [zlib.BROTLI_PARAM_QUALITY]: 9, [zlib.BROTLI_PARAM_SIZE_HINT]: identity.length } }),
|
|
441
|
+
compressGzip(identity, { level: 9 })
|
|
442
|
+
]);
|
|
443
|
+
return { hash: createHash("sha256").update(identity).digest("base64url").slice(0, 32), identity, br, gzip: gz };
|
|
444
|
+
})();
|
|
445
|
+
appPage = { version, page };
|
|
446
|
+
page.catch(() => appPage?.page === page && (appPage = void 0));
|
|
447
|
+
}
|
|
448
|
+
return appPage.page;
|
|
449
|
+
}
|
|
450
|
+
function acceptsCoding(req, coding) {
|
|
451
|
+
let any = false;
|
|
452
|
+
for (const part of String(req.headers["accept-encoding"] ?? "").split(",")) {
|
|
453
|
+
const [name, ...params] = part.split(";").map((p) => p.trim().toLowerCase());
|
|
454
|
+
const q = params.find((p) => p.startsWith("q="));
|
|
455
|
+
const ok = !q || Number(q.slice(2)) > 0;
|
|
456
|
+
if (name === coding) return ok;
|
|
457
|
+
if (name === "*") any = ok;
|
|
458
|
+
}
|
|
459
|
+
return any;
|
|
460
|
+
}
|
|
461
|
+
async function sendAppPage(req, res, headers) {
|
|
462
|
+
const page = await loadAppPage();
|
|
463
|
+
const coding = acceptsCoding(req, "br") ? "br" : acceptsCoding(req, "gzip") ? "gzip" : void 0;
|
|
464
|
+
const etag = `"${page.hash}${coding ? `-${coding}` : ""}"`;
|
|
465
|
+
const common = { ...headers, etag, vary: "Accept-Encoding", "cache-control": "private, no-cache" };
|
|
466
|
+
const cached = String(req.headers["if-none-match"] ?? "").split(",").some((tag) => tag.trim().replace(/^W\//, "") === etag);
|
|
467
|
+
if (cached && !headers["set-cookie"]) {
|
|
468
|
+
res.writeHead(304, common);
|
|
469
|
+
return res.end();
|
|
470
|
+
}
|
|
471
|
+
const body = coding ? page[coding] : page.identity;
|
|
472
|
+
res.writeHead(200, { ...common, "content-type": "text/html; charset=utf-8", "content-length": body.length, ...coding ? { "content-encoding": coding } : {} });
|
|
473
|
+
res.end(body);
|
|
474
|
+
}
|
|
475
|
+
async function attempt(fn) {
|
|
476
|
+
try {
|
|
477
|
+
return await fn();
|
|
478
|
+
} catch (err) {
|
|
479
|
+
throw err instanceof HttpError ? err : new HttpError(err instanceof CollabError || err instanceof TeamPlanEditError || err instanceof TeamRerunError || err instanceof ConventionsError || err instanceof SharedAccountError || err instanceof SharedPathError || err instanceof CollaborationReviewError || err instanceof FilePolicyError ? err.status : err instanceof MemoryConflictError || err instanceof RunConflictError || err instanceof DeferredRunConflictError || err instanceof RetentionPolicyConflictError ? 409 : 400, err.message);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function requestedRepo(url) {
|
|
483
|
+
return validRepo(url.searchParams.get("repo") ?? "");
|
|
484
|
+
}
|
|
485
|
+
async function validRepo(requested) {
|
|
486
|
+
if (!requested.startsWith("/")) throw new HttpError(400, "Repository must be an absolute path");
|
|
487
|
+
return repoRoot(requested).catch(() => {
|
|
488
|
+
throw new HttpError(400, "Not a git repository");
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async function requestedFolder(url) {
|
|
492
|
+
const requested = url.searchParams.get("repo") ?? "";
|
|
493
|
+
const worktree = requested.startsWith("/") ? await manager.worktreeAt(requested).catch((err) => {
|
|
494
|
+
throw new HttpError(400, err.message);
|
|
495
|
+
}) : void 0;
|
|
496
|
+
return worktree ?? requestedRepo(url);
|
|
497
|
+
}
|
|
498
|
+
function describePattern(p) {
|
|
499
|
+
const { run: _run, ...rest } = p;
|
|
500
|
+
return rest;
|
|
501
|
+
}
|
|
502
|
+
function requestedRoles(pattern, input) {
|
|
503
|
+
const raw = input && typeof input === "object" ? input : {};
|
|
504
|
+
const requested = {};
|
|
505
|
+
for (const spec of pattern.roles) {
|
|
506
|
+
const value = raw[spec.id];
|
|
507
|
+
const list = (Array.isArray(value) ? value : value ? [value] : []).filter((v) => v === "auto" || isAgentId(v));
|
|
508
|
+
requested[spec.id] = spec.multiple ? list.slice(0, 6) : list.slice(0, 1);
|
|
509
|
+
}
|
|
510
|
+
return requested;
|
|
511
|
+
}
|
|
512
|
+
async function validateRoles(pattern, input, repo, remoteWorker, requestedTrust, policy) {
|
|
513
|
+
assertPolicyExecution(await fileAccess.execution(repo ?? ""), "Provider selection");
|
|
514
|
+
const requested = requestedRoles(pattern, input);
|
|
515
|
+
const inventory = remoteWorker ? (await manager.remoteInventory(remoteWorker)).providers : await agentAvailability();
|
|
516
|
+
const installed = new Set(inventory.filter((a) => a.available).map((a) => a.id));
|
|
517
|
+
const runtimeProblems = new Map(await Promise.all([...installed].map(async (id) => [id, !remoteWorker && policy && id === "agy" && policy.antigravityExecution !== "native" ? await getAdapter(id).managedAvailability?.() : void 0])));
|
|
518
|
+
const trust = requestedTrust ?? (repo ? (await readRepoConfig(repo)).config.trust : void 0);
|
|
519
|
+
const eligible = new Set([...installed].filter((id) => {
|
|
520
|
+
if (runtimeProblems.get(id)) return false;
|
|
521
|
+
if (policy) {
|
|
522
|
+
try {
|
|
523
|
+
const runPolicy = createRunPermissions(trust ?? "standard", policy.tools, pattern.id === "solo", policy.required, policy.antigravityExecution);
|
|
524
|
+
resolveRunPermissions({ policy: runPolicy, provider: id, mode: "write", currentTrust: runPolicy.trust, currentTools: policy.tools, remote: !!remoteWorker, container: policy.container });
|
|
525
|
+
return true;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return ["auto-team", "solo"].includes(pattern.id) ? id === "claude" || id === "codex" : !id.startsWith("acp:") || trust === "full" && !remoteWorker;
|
|
531
|
+
}));
|
|
532
|
+
if (pattern.id === "auto-team") {
|
|
533
|
+
const supplied = input && typeof input === "object" ? input : {};
|
|
534
|
+
if (!Array.isArray(supplied.coordinator)) requested.coordinator = ["auto"];
|
|
535
|
+
if (!Array.isArray(supplied.pool)) requested.pool = [...eligible];
|
|
536
|
+
}
|
|
537
|
+
const wantsAuto = Object.values(requested).some((list) => list.includes("auto"));
|
|
538
|
+
const rank = wantsAuto && repo ? rankAgents(manager.list(), repo, { pattern: pattern.id }) : void 0;
|
|
539
|
+
const resolved = resolveAuto(requested, manager.quotaForWorker(remoteWorker?.workerId), eligible, rank);
|
|
540
|
+
const roles = {};
|
|
541
|
+
const needed = /* @__PURE__ */ new Set();
|
|
542
|
+
for (const spec of pattern.roles) {
|
|
543
|
+
const list = resolved.roles[spec.id] ?? [];
|
|
544
|
+
const agents = spec.multiple ? [...new Set(list)] : list.slice(0, 1);
|
|
545
|
+
if (agents.length === 0 && !spec.optional) throw new HttpError(400, `Pick an agent for "${spec.label}"`);
|
|
546
|
+
if (pattern.id !== "auto-team" || spec.id !== "pool") agents.forEach((a) => needed.add(a));
|
|
547
|
+
roles[spec.id] = agents;
|
|
548
|
+
}
|
|
549
|
+
if (pattern.id === "auto-team" && !roles.pool?.some((agent) => installed.has(agent))) throw new HttpError(400, "Select at least one available provider for the worker pool");
|
|
550
|
+
for (const agent of needed) {
|
|
551
|
+
if (runtimeProblems.get(agent)) throw new HttpError(400, runtimeProblems.get(agent));
|
|
552
|
+
if (!installed.has(agent)) {
|
|
553
|
+
throw new HttpError(400, `${getAdapter(agent).displayName} (\`${getAdapter(agent).binary}\`) is not installed or not on PATH`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return { roles, notes: resolved.notes };
|
|
557
|
+
}
|
|
558
|
+
let improving = 0;
|
|
559
|
+
const MAX_IMPROVING = 3;
|
|
560
|
+
const improvingControllers = /* @__PURE__ */ new Set();
|
|
561
|
+
async function improveTask(body, res) {
|
|
562
|
+
if (body.execution !== void 0 && body.execution !== "local" || body.remoteWorker || body.agentContainer || body.loop?.remoteWorker || body.loop?.agentContainer) throw new HttpError(400, "Prompt improvement supports local execution only; remote and container profiles cannot fall back to a host CLI");
|
|
563
|
+
const text = typeof body.text === "string" ? body.text : "";
|
|
564
|
+
if (!text.trim()) throw new HttpError(400, "Write a task to improve first");
|
|
565
|
+
if (text.length > MAX_IMPROVE_CHARS) throw new HttpError(400, `The task is too long to improve (${MAX_IMPROVE_CHARS.toLocaleString("en")} characters at most)`);
|
|
566
|
+
if (body.mode !== void 0 && body.mode !== "quick" && body.mode !== "repo") throw new HttpError(400, "Choose Quick or Repo for prompt improvement");
|
|
567
|
+
let preferences;
|
|
568
|
+
try {
|
|
569
|
+
preferences = parseImproveSettings(body.settings ?? { agent: body.agent ?? "auto", defaultMode: body.mode ?? "quick", providers: {} });
|
|
570
|
+
} catch (error) {
|
|
571
|
+
throw new HttpError(400, error.message);
|
|
572
|
+
}
|
|
573
|
+
const stop2 = new AbortController();
|
|
574
|
+
res.once("close", () => stop2.abort());
|
|
575
|
+
if (res.destroyed || res.writableEnded) stop2.abort();
|
|
576
|
+
const request = {
|
|
577
|
+
text,
|
|
578
|
+
repo: await validRepo(typeof body.repo === "string" ? body.repo : ""),
|
|
579
|
+
mode: body.mode === void 0 ? preferences.defaultMode : body.mode === "repo" ? "repo" : "quick",
|
|
580
|
+
context: Array.isArray(body.context) ? body.context.filter((p) => typeof p === "string" && !!p.trim()).slice(0, 50).map((p) => p.slice(0, 300)) : [],
|
|
581
|
+
source: body.source && ["issue", "pr", "alert"].includes(body.source.kind) && Number.isInteger(body.source.number) ? { kind: body.source.kind, number: body.source.number, title: typeof body.source.title === "string" ? body.source.title.slice(0, 200) : void 0 } : void 0
|
|
582
|
+
};
|
|
583
|
+
if (improving >= MAX_IMPROVING) throw new HttpError(429, "Other tasks are being improved right now. Try again in a moment.");
|
|
584
|
+
improving++;
|
|
585
|
+
improvingControllers.add(stop2);
|
|
586
|
+
try {
|
|
587
|
+
return await attempt(() => manager.withIdleRepository(request.repo, async () => {
|
|
588
|
+
await manager.assertExecutionAllowed(request.repo, void 0, "Prompt improvement");
|
|
589
|
+
stop2.signal.throwIfAborted();
|
|
590
|
+
const installed = new Set((await agentAvailability()).filter((a) => a.available && IMPROVE_AGENTS.includes(a.id)).map((a) => a.id));
|
|
591
|
+
stop2.signal.throwIfAborted();
|
|
592
|
+
const agent = preferences.agent === "auto" ? resolveAuto({ improve: ["auto"] }, manager.quota, installed).roles.improve?.[0] : preferences.agent;
|
|
593
|
+
if (!agent) throw new HttpError(400, "Install the Claude Code, Codex or Antigravity CLI to improve tasks");
|
|
594
|
+
if (!installed.has(agent)) throw new HttpError(400, `${getAdapter(agent).displayName} (\`${getAdapter(agent).binary}\`) is not installed or not on PATH`);
|
|
595
|
+
if (preferences.agent !== "auto") {
|
|
596
|
+
const unavailable = exhausted(agent, manager.quota);
|
|
597
|
+
if (unavailable) throw new HttpError(400, `${getAdapter(agent).displayName} ${unavailable}. Choose another Improve agent in Settings or try again later.`);
|
|
598
|
+
}
|
|
599
|
+
request.settings = preferences.providers[agent]?.[request.mode];
|
|
600
|
+
await manager.assertExecutionAllowed(request.repo, void 0, "Prompt improvement");
|
|
601
|
+
stop2.signal.throwIfAborted();
|
|
602
|
+
manager.noteActivity();
|
|
603
|
+
try {
|
|
604
|
+
const result = await improvePrompt(agent, request, { signal: stop2.signal, onEvent: (event) => {
|
|
605
|
+
if (event.kind === "quota") manager.recordQuota(agent, event.status, event.windows);
|
|
606
|
+
} });
|
|
607
|
+
return send(res, 200, { ...result, agent });
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (!(error instanceof ImproveError)) throw error;
|
|
610
|
+
if (error.kind === "usage_limit") manager.recordLimit(agent, error.resetsAt);
|
|
611
|
+
if (error.kind === "auth") manager.recordQuota(agent, "signed_out", {});
|
|
612
|
+
throw new HttpError(502, error.message);
|
|
613
|
+
}
|
|
614
|
+
}, { kind: "prompt improvement", owner: "composer" }));
|
|
615
|
+
} finally {
|
|
616
|
+
improving--;
|
|
617
|
+
improvingControllers.delete(stop2);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function sideTurn(input) {
|
|
621
|
+
const v = input ?? {};
|
|
622
|
+
const chat = typeof v.chat === "string" ? v.chat : "";
|
|
623
|
+
if (!/^[a-z0-9-]{1,40}$/.test(chat)) throw new HttpError(400, "A side chat needs an id: up to 40 lowercase letters, digits or dashes");
|
|
624
|
+
if (v.mode !== "ask" && v.mode !== "change") throw new HttpError(400, 'A side chat is "ask" or "change"');
|
|
625
|
+
const title = typeof v.title === "string" ? v.title.replace(/\s+/g, " ").trim().slice(0, 80) : "";
|
|
626
|
+
return { chat, mode: v.mode, title: title || "Side chat" };
|
|
627
|
+
}
|
|
628
|
+
function validateSource(input) {
|
|
629
|
+
if (!input || typeof input !== "object") return void 0;
|
|
630
|
+
const raw = input;
|
|
631
|
+
const number = Number(raw.number);
|
|
632
|
+
if (raw.kind !== "issue" && raw.kind !== "pr" && raw.kind !== "alert") throw new HttpError(400, "Source must be an issue, a pull request or an alert");
|
|
633
|
+
if (!Number.isInteger(number) || number < 1) throw new HttpError(400, "Source needs an issue, PR or alert number");
|
|
634
|
+
if (raw.kind !== "alert") return { kind: raw.kind, number, ciLogs: raw.ciLogs === true };
|
|
635
|
+
if (raw.tool !== "dependabot" && raw.tool !== "code-scanning") throw new HttpError(400, "An alert comes from Dependabot or code scanning");
|
|
636
|
+
const numbers = Array.isArray(raw.numbers) ? [...new Set(raw.numbers.map(Number).filter((n) => Number.isInteger(n) && n > 0))].slice(0, 50) : [];
|
|
637
|
+
return { kind: "alert", tool: raw.tool, number, numbers: numbers.length ? numbers : void 0 };
|
|
638
|
+
}
|
|
639
|
+
const SWEEP_KINDS = ["issues", "dependabot", "alerts"];
|
|
640
|
+
const MAX_SWEEP = 25;
|
|
641
|
+
async function startSweep(body) {
|
|
642
|
+
const kind = body.kind;
|
|
643
|
+
if (!SWEEP_KINDS.includes(kind)) throw new HttpError(400, "Unknown kind of sweep");
|
|
644
|
+
const name = String(body.name ?? "").trim().slice(0, 80) || "Sweep";
|
|
645
|
+
const base = body.run && typeof body.run === "object" ? body.run : {};
|
|
646
|
+
const items = [];
|
|
647
|
+
for (const i of Array.isArray(body.items) ? body.items : []) {
|
|
648
|
+
const item = { key: String(i?.key ?? "").slice(0, 200), task: String(i?.task ?? "").trim().slice(0, 4e3), source: validateSource(i?.source) };
|
|
649
|
+
if (!item.key || !item.task || !item.source) throw new HttpError(400, "Every item needs a task and a source");
|
|
650
|
+
items.push({ ...item, source: item.source });
|
|
651
|
+
}
|
|
652
|
+
if (!items.length) throw new HttpError(400, "Pick at least one item");
|
|
653
|
+
if (items.length > MAX_SWEEP) throw new HttpError(400, `Pick at most ${MAX_SWEEP} items; the rest can go in a later sweep`);
|
|
654
|
+
const repo = (await buildStartInput({ ...base, task: items[0].task })).repo;
|
|
655
|
+
const root = await repoRoot(repo);
|
|
656
|
+
const schedule = body.schedule ? await attempt(async () => validateSchedule(body.schedule)) : void 0;
|
|
657
|
+
const id = `s-${randomBytes(4).toString("hex")}`;
|
|
658
|
+
const saved = schedule ? await automations.create({ name, enabled: true, run: { ...automationRun({ ...base, task: templates.find((t) => t.sweep === kind)?.task ?? name }), source: void 0 }, schedule, webhook: false, isolate: false, sweep: { kind, max: Math.min(10, Math.max(1, Number(body.max) || 5)) } }) : void 0;
|
|
659
|
+
const trigger = { id: saved?.id ?? id, name, via: "sweep" };
|
|
660
|
+
const outcome = await runSweep({
|
|
661
|
+
repo: root,
|
|
662
|
+
items,
|
|
663
|
+
runs: manager.list(),
|
|
664
|
+
start: (item) => startFromBody({ ...base, task: item.task, source: item.source }, { trigger })
|
|
665
|
+
});
|
|
666
|
+
if (saved) {
|
|
667
|
+
const started = new Set(outcome.started.map((s) => s.key));
|
|
668
|
+
await automations.markHandled(saved.id, items.filter((i) => started.has(i.key)).flatMap((i) => sourceKeys(i.source)));
|
|
669
|
+
}
|
|
670
|
+
return { sweep: trigger.id, automation: saved?.id, ...outcome };
|
|
671
|
+
}
|
|
672
|
+
function validateRoleSettings(input) {
|
|
673
|
+
const raw = input && typeof input === "object" ? input : {};
|
|
674
|
+
const out = {};
|
|
675
|
+
for (const [role, settings] of Object.entries(raw)) {
|
|
676
|
+
if (!/^[\w-]{1,40}$/.test(role)) continue;
|
|
677
|
+
try {
|
|
678
|
+
const valid = validateAgentSettings(settings);
|
|
679
|
+
if (Object.keys(valid).length) out[role] = valid;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
throw new HttpError(400, `${role}: ${err.message}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function validateLoop(input) {
|
|
687
|
+
const raw = input && typeof input === "object" ? input : {};
|
|
688
|
+
let remoteWorker;
|
|
689
|
+
if (raw.remoteWorker !== void 0) {
|
|
690
|
+
const value = raw.remoteWorker;
|
|
691
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["workerId", "repoMapping"].includes(key)) || typeof value.workerId !== "string" || typeof value.repoMapping !== "string" || !remoteWorkers.list().some((worker) => worker.id === value.workerId && worker.repoMappings.includes(value.repoMapping))) throw new HttpError(400, "Choose an explicitly registered remote worker and repository mapping");
|
|
692
|
+
remoteWorker = { workerId: value.workerId, repoMapping: value.repoMapping };
|
|
693
|
+
}
|
|
694
|
+
const text = (key) => {
|
|
695
|
+
const value = typeof raw[key] === "string" ? raw[key].trim() : "";
|
|
696
|
+
if (value.length > 1e3) throw new HttpError(400, `${key} command is too long`);
|
|
697
|
+
if (/[\r\n]/.test(value)) throw new HttpError(400, `${key} command must be a single line`);
|
|
698
|
+
return value || void 0;
|
|
699
|
+
};
|
|
700
|
+
const number = (key, min, max) => {
|
|
701
|
+
const n = Number(raw[key]);
|
|
702
|
+
return raw[key] === void 0 || raw[key] === "" || !Number.isFinite(n) ? void 0 : Math.min(max, Math.max(min, n));
|
|
703
|
+
};
|
|
704
|
+
return {
|
|
705
|
+
remoteWorker,
|
|
706
|
+
check: text("check"),
|
|
707
|
+
setup: text("setup"),
|
|
708
|
+
copy: Array.isArray(raw.copy) ? raw.copy.filter((p) => typeof p === "string" && !!p.trim()).slice(0, 50) : void 0,
|
|
709
|
+
maxMinutes: number("maxMinutes", 1, 720),
|
|
710
|
+
maxSteps: number("maxSteps", 1, 200),
|
|
711
|
+
maxInvocations: number("maxInvocations", 1, 200),
|
|
712
|
+
maxTokens: number("maxTokens", 1, 1e9),
|
|
713
|
+
maxCostUsd: number("maxCostUsd", 1e-3, 1e6),
|
|
714
|
+
contextBudget: validateContextBudget(raw.contextBudget),
|
|
715
|
+
checkTimeoutMin: number("checkTimeoutMin", 1, 120)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function validateOptions(pattern, input) {
|
|
719
|
+
const raw = input && typeof input === "object" ? input : {};
|
|
720
|
+
const options = {};
|
|
721
|
+
for (const spec of pattern.options) {
|
|
722
|
+
const value = raw[spec.id];
|
|
723
|
+
if (spec.type === "boolean") {
|
|
724
|
+
options[spec.id] = typeof value === "boolean" ? value : spec.default;
|
|
725
|
+
} else {
|
|
726
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
727
|
+
const fallback = spec.default;
|
|
728
|
+
const clamped = Number.isFinite(n) ? Math.min(spec.max ?? n, Math.max(spec.min ?? n, Math.round(n))) : fallback;
|
|
729
|
+
options[spec.id] = clamped;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return options;
|
|
733
|
+
}
|
|
734
|
+
async function buildStartInput(body) {
|
|
735
|
+
if (body.deferred !== void 0 || body.teamRerun !== void 0 || body.coordination !== void 0 || body.projectTeam !== void 0 || body.beforeDispatch !== void 0 || body.requireProjectRepo !== void 0) throw new HttpError(400, "Launch identities and selective rerun seeds are assigned by the server");
|
|
736
|
+
if (body.projectSelection !== void 0 && body.projectSelection !== "explicit") throw new HttpError(400, "Unknown project selection mode");
|
|
737
|
+
if (body.projectSelection === "explicit" && (!projects.available || typeof body.project !== "string" || !projects.get(body.project))) throw new HttpError(409, "Choose an available project before starting");
|
|
738
|
+
if (body.templateCatalog !== void 0 || body.teamTemplate !== void 0 || body.permissions !== void 0) throw new HttpError(400, "Template snapshots and run authority are assigned by the server");
|
|
739
|
+
let teamTemplate;
|
|
740
|
+
if (body.templateRef) {
|
|
741
|
+
const ref = body.templateRef;
|
|
742
|
+
const template = await attempt(() => teamTemplateStore.resolve(String(ref.id), Number(ref.version), typeof body.project === "string" ? body.project : void 0));
|
|
743
|
+
if (!ref.inputs || typeof ref.inputs !== "object" || Array.isArray(ref.inputs)) throw new HttpError(400, "Supply template inputs");
|
|
744
|
+
fillTeamTemplate(template, ref.inputs);
|
|
745
|
+
if (body.pattern !== (template.workflow ?? "auto-team")) throw new HttpError(400, "Selected template does not match the execution mode");
|
|
746
|
+
teamTemplate = { template, inputs: ref.inputs };
|
|
747
|
+
}
|
|
748
|
+
const pattern = findPattern(String(body.pattern ?? "auto-team"));
|
|
749
|
+
if (!pattern) throw new HttpError(400, "Unknown pattern");
|
|
750
|
+
let task = String(body.task ?? "").trim();
|
|
751
|
+
if (teamTemplate) {
|
|
752
|
+
const filled = fillTeamTemplate(teamTemplate.template, teamTemplate.inputs);
|
|
753
|
+
task += `
|
|
754
|
+
|
|
755
|
+
Selected template: ${filled.name} v${filled.version}
|
|
756
|
+
${filled.task}
|
|
757
|
+
${filled.criteria.map((c) => `- ${c}`).join("\n")}`;
|
|
758
|
+
}
|
|
759
|
+
const repo = String(body.repo ?? "").trim();
|
|
760
|
+
if (!task) throw new HttpError(400, "Describe the task");
|
|
761
|
+
if (!repo.startsWith("/")) throw new HttpError(400, "Repository must be an absolute path");
|
|
762
|
+
if (body.parentRunId !== void 0 && (typeof body.parentRunId !== "string" || !manager.get(body.parentRunId))) throw new HttpError(409, "The original run is no longer available. Reload before starting a continuation so its source restrictions can be retained.");
|
|
763
|
+
await attempt(async () => assertPolicyExecution(await fileAccess.execution(repo), "Run preparation"));
|
|
764
|
+
if (body.projectSelection === "explicit" && !projects.get(body.project)?.repos.includes(await repoRoot(repo))) throw new HttpError(409, "This repository is no longer an explicit member of the selected project");
|
|
765
|
+
if (body.externalSource !== void 0 || body.externalSourceId !== void 0 && typeof body.externalSourceId !== "string") throw new HttpError(400, "Import the external source through its configured integration first");
|
|
766
|
+
if (body.externalSourceId !== void 0 && body.externalSourceRef !== void 0) throw new HttpError(400, "Choose one external source reference");
|
|
767
|
+
if (body.intent !== void 0 && !taskIntent(body.intent)) throw new HttpError(400, "Choose a supported task type");
|
|
768
|
+
if (body.journeyId !== void 0) {
|
|
769
|
+
const journey = await attempt(() => journeys.get(String(body.journeyId)));
|
|
770
|
+
if (journey.repo !== await repoRoot(repo)) throw new HttpError(400, "The scenario belongs to another repository");
|
|
771
|
+
}
|
|
772
|
+
const parent = typeof body.parentRunId === "string" ? manager.get(body.parentRunId) : void 0;
|
|
773
|
+
if (body.parentRunId !== void 0 && !parent) throw new HttpError(400, "Parent run not found; cannot inherit its permissions");
|
|
774
|
+
const inheritedLoop = inheritRunEnvironment(body.loop ?? {}, parent);
|
|
775
|
+
const loop = validateLoop(inheritedLoop);
|
|
776
|
+
if (inheritedLoop.agentContainer) loop.agentContainer = await attempt(async () => validateAgentContainer(inheritedLoop.agentContainer));
|
|
777
|
+
const requiredPermissions = [.../* @__PURE__ */ new Set([...parent?.permissions?.required ?? [], ...permissionRequirements(body.permissionRequirements)])];
|
|
778
|
+
const agyExecution = antigravityExecution(body.antigravityExecution, parent?.permissions);
|
|
779
|
+
const runTrust = parent?.permissions ? narrowerTrust(narrowerTrust(parent.permissions.trust, parent.trust), body.trust ?? parent.trust) : body.trust;
|
|
780
|
+
const runTools = parent?.permissions ? parent.permissions.toolApprovals && !!parent.approvals.tools && body.approvals?.tools !== false : body.approvals?.tools === true;
|
|
781
|
+
const { roles, notes: notes2 } = await validateRoles(pattern, body.roles, await repoRoot(repo).catch(() => repo), loop.remoteWorker, runTrust, { required: requiredPermissions, container: !!loop.agentContainer, tools: runTools, antigravityExecution: agyExecution });
|
|
782
|
+
let externalSource = typeof body.externalSourceId === "string" ? await attempt(async () => integrations.resolveSource(body.externalSourceId)) : void 0;
|
|
783
|
+
if (body.externalSourceRef) {
|
|
784
|
+
const ref = body.externalSourceRef;
|
|
785
|
+
if (typeof ref.integration !== "string" || !["issue", "pr"].includes(ref.kind) || typeof ref.key !== "string") throw new HttpError(400, "Choose a configured external source");
|
|
786
|
+
externalSource = await attempt(() => integrations.importSource(ref.integration, ref.kind, ref.key));
|
|
787
|
+
}
|
|
788
|
+
if (externalSource && body.source) throw new HttpError(400, "Choose one external source for this run");
|
|
789
|
+
if (body.loop?.verification) loop.verification = await attempt(async () => validateVerificationProfile(body.loop.verification));
|
|
790
|
+
if (loop.remoteWorker && (body.approvals?.tools || loop.agentContainer)) throw new HttpError(400, "Remote workers use their provisioned isolation profile and cannot wait for native tool approvals");
|
|
791
|
+
const container = inheritedLoop.container;
|
|
792
|
+
if (container && typeof container.image === "string" && /^[\w./:@-]{1,200}$/.test(container.image)) {
|
|
793
|
+
loop.container = { image: container.image, network: container.network !== false };
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
pattern,
|
|
797
|
+
teamTemplate,
|
|
798
|
+
task,
|
|
799
|
+
intent: taskIntent(body.intent),
|
|
800
|
+
journeyId: typeof body.journeyId === "string" ? body.journeyId : void 0,
|
|
801
|
+
repo,
|
|
802
|
+
project: typeof body.project === "string" && body.project ? body.project : void 0,
|
|
803
|
+
requireProjectRepo: body.projectSelection === "explicit",
|
|
804
|
+
baseRef: body.baseRef ? String(body.baseRef) : void 0,
|
|
805
|
+
branchPrefix: body.branchPrefix ? String(body.branchPrefix) : void 0,
|
|
806
|
+
context: body.context && typeof body.context === "object" ? body.context : void 0,
|
|
807
|
+
loop,
|
|
808
|
+
approvals: { plan: typeof body.approvals?.plan === "boolean" ? body.approvals.plan : pattern.id === "auto-team", tools: runTools },
|
|
809
|
+
parentRunId: body.parentRunId,
|
|
810
|
+
trust: ["safe", "standard", "trusted", "full"].includes(runTrust) ? runTrust : void 0,
|
|
811
|
+
permissionRequirements: requiredPermissions,
|
|
812
|
+
antigravityExecution: agyExecution,
|
|
813
|
+
roleSettings: validateRoleSettings(body.roleSettings),
|
|
814
|
+
agentSettings: (() => {
|
|
815
|
+
try {
|
|
816
|
+
return validateAgentSettings(body.agentSettings);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
throw new HttpError(400, err.message);
|
|
819
|
+
}
|
|
820
|
+
})(),
|
|
821
|
+
source: validateSource(body.source),
|
|
822
|
+
externalSource,
|
|
823
|
+
allowUntrusted: body.allowUntrusted === true,
|
|
824
|
+
routing: notes2,
|
|
825
|
+
roles,
|
|
826
|
+
options: validateOptions(pattern, body.options)
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async function startFromBody(body, extra = {}) {
|
|
830
|
+
const input = await buildStartInput(body);
|
|
831
|
+
return attempt(() => manager.start({ ...input, ...extra }).catch((err) => {
|
|
832
|
+
if (err instanceof UntrustedSourceError) throw Object.assign(new HttpError(409, err.message), { confirm: "allowUntrusted" });
|
|
833
|
+
throw err;
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
function automationRun(body) {
|
|
837
|
+
const keep = ["templateRef", "permissionRequirements", "repo", "project", "projectSelection", "pattern", "task", "roles", "options", "baseRef", "loop", "trust", "agentSettings", "roleSettings", "source"];
|
|
838
|
+
const run = Object.fromEntries(keep.filter((k) => body[k] !== void 0).map((k) => [k, body[k]]));
|
|
839
|
+
if (typeof body.externalSourceId === "string") {
|
|
840
|
+
try {
|
|
841
|
+
const source = integrations.resolveSource(body.externalSourceId);
|
|
842
|
+
run.externalSourceRef = { integration: source.integration, kind: source.kind, key: source.key };
|
|
843
|
+
} catch (error) {
|
|
844
|
+
throw new HttpError(400, error.message);
|
|
845
|
+
}
|
|
846
|
+
} else if (body.externalSourceRef) run.externalSourceRef = body.externalSourceRef;
|
|
847
|
+
if (Array.isArray(body.context?.paths)) run.context = { paths: body.context.paths.map((p) => ({ path: String(p.path ?? "") })) };
|
|
848
|
+
return run;
|
|
849
|
+
}
|
|
850
|
+
function describeAutomation(t) {
|
|
851
|
+
return { ...t, scheduleText: describeSchedule(t.schedule), hookPath: `/hooks/${t.id}` };
|
|
852
|
+
}
|
|
853
|
+
async function handlePublic(req, res, url, parts, via) {
|
|
854
|
+
if (parts[0] === "hooks" && parts[1] && req.method === "POST") {
|
|
855
|
+
const chunks = [];
|
|
856
|
+
let size = 0;
|
|
857
|
+
for await (const chunk of req) {
|
|
858
|
+
size += chunk.length;
|
|
859
|
+
if (size > 1024 * 1024) throw new HttpError(413, "Payload too large");
|
|
860
|
+
chunks.push(chunk);
|
|
861
|
+
}
|
|
862
|
+
const raw = Buffer.concat(chunks);
|
|
863
|
+
let caller;
|
|
864
|
+
try {
|
|
865
|
+
caller = automations.authorize(parts[1], req.headers, raw);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
throw new HttpError(err.status ?? 401, err.message);
|
|
868
|
+
}
|
|
869
|
+
if (req.headers["x-github-event"] === "ping") return send(res, 200, { ok: true, pong: true }), true;
|
|
870
|
+
let payload = {};
|
|
871
|
+
try {
|
|
872
|
+
payload = raw.length ? JSON.parse(raw.toString("utf8")) : {};
|
|
873
|
+
} catch {
|
|
874
|
+
throw new HttpError(400, "Expected a JSON body");
|
|
875
|
+
}
|
|
876
|
+
const wanted = readWebhook(req.headers, payload);
|
|
877
|
+
if (!wanted) return send(res, 200, { ignored: "This event doesn't start a run" }), true;
|
|
878
|
+
const delivery = typeof req.headers["x-github-delivery"] === "string" ? req.headers["x-github-delivery"].slice(0, 100) : void 0;
|
|
879
|
+
const accepted = automations.accept(caller.trigger.id, wanted, delivery, caller.signed ? raw : void 0);
|
|
880
|
+
send(res, "ignored" in accepted ? 200 : 202, "ignored" in accepted ? accepted : { accepted: true, automation: caller.trigger.id });
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
if (parts[0] === "r" && req.method === "GET") {
|
|
884
|
+
const token = parts[1] ?? "";
|
|
885
|
+
if (!remote.enabled) throw new HttpError(404, "Not found");
|
|
886
|
+
const base = `/r/${token}`;
|
|
887
|
+
if (via === "lan" && phoneControl() && tokenMatches(token, remote.controlToken)) {
|
|
888
|
+
if (Date.now() >= (remote.pairingUntil ?? 0)) throw new HttpError(403, "This pairing link expired. Choose New link on this computer to pair a phone.");
|
|
889
|
+
const paired = await phoneSessions.pair(String(req.headers["user-agent"] ?? "Phone"), remote.controlToken);
|
|
890
|
+
const run = url.pathname.slice(base.length).match(/^\/run\/([\w-]+)$/)?.[1];
|
|
891
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "set-cookie": phoneCookie(paired.token), "cache-control": "no-store", ...NO_FRAMES, "referrer-policy": "no-referrer" });
|
|
892
|
+
res.end(signingInPage(run ? `/#/run/${run}` : "/"));
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
if (!tokenMatches(token, remote.token)) throw new HttpError(404, "Not found");
|
|
896
|
+
if (fileAccess.hasRestrictions()) throw new HttpError(403, "Phone views are unavailable under custom filesystem restrictions");
|
|
897
|
+
const html = renderRemote(manager, projects.list(), base, url.pathname.slice(base.length));
|
|
898
|
+
if (!html) throw new HttpError(404, "Not found");
|
|
899
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...NO_FRAMES, "referrer-policy": "no-referrer" });
|
|
900
|
+
res.end(html);
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
function remoteInfo(via = "local") {
|
|
906
|
+
const phonePath = `/r/${phoneControl() ? remote.controlToken : remote.token}`;
|
|
907
|
+
const ready = remote.enabled && remote.lan && lanServer?.listening && lanHost === networkHost() && !lanError;
|
|
908
|
+
const tailscaleHost = phoneTls && tailscaleStatus.dnsName ? tailscaleStatus.dnsName : tailscaleStatus.ip;
|
|
909
|
+
return {
|
|
910
|
+
enabled: remote.enabled,
|
|
911
|
+
lan: remote.lan,
|
|
912
|
+
transport: remote.transport ?? "lan",
|
|
913
|
+
tailscale: tailscaleStatus,
|
|
914
|
+
tailscaleUrl: ready && remote.transport === "tailscale" ? `${phoneProtocol}://${tailscaleHost}:${REMOTE_PORT}${phonePath}` : void 0,
|
|
915
|
+
control: phoneControl(),
|
|
916
|
+
secure: !!phoneTls && !remoteTlsIssue(),
|
|
917
|
+
tls: { editable: via === "local" && !tlsConfigured && !process.env.CODEFLOW_SSH_HOST, source: tlsConfigured ? "environment" : savedCertificate?.source, hostname: savedCertificate?.hostname, expiresAt: savedCertificate?.expiresAt, error: remoteTlsIssue(), renewal: tlsConfigured ? void 0 : certificateRenewal.status(savedCertificate) },
|
|
918
|
+
insecureDevelopment: insecurePhoneDevelopment,
|
|
919
|
+
pairingUntil: remote.pairingUntil ?? 0,
|
|
920
|
+
path: `/r/${remote.token}`,
|
|
921
|
+
phonePath,
|
|
922
|
+
local: `http://localhost:${PORT}/r/${remote.token}`,
|
|
923
|
+
lanUrls: ready && remote.transport !== "tailscale" ? (savedCertificate ? [savedCertificate.hostname] : lanAddresses()).map((ip) => `${phoneProtocol}://${ip.includes(":") ? `[${ip}]` : ip}:${REMOTE_PORT}${phonePath}`) : [],
|
|
924
|
+
lanError: remote.enabled && remote.lan ? remoteTlsIssue() || (savedCertificate && remote.transport === "tailscale" && tailscaleStatus.state === "connected" && savedCertificate.hostname !== tailscaleStatus.dnsName ? "The HTTPS certificate belongs to a different Tailscale name. Set up secure access again for this computer." : lanError) : void 0
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
let lanServer;
|
|
928
|
+
const lanSockets = /* @__PURE__ */ new Set();
|
|
929
|
+
function closeLanConnections() {
|
|
930
|
+
for (const socket of lanSockets) socket.destroy();
|
|
931
|
+
}
|
|
932
|
+
let lanHost;
|
|
933
|
+
let lanTls;
|
|
934
|
+
let remoteStopping = false;
|
|
935
|
+
let lanError;
|
|
936
|
+
const LAN_ALLOW_LOCAL = process.env.CODEFLOW_LAN_ALLOW_LOCAL === "1";
|
|
937
|
+
let tailscaleStatus = { state: "unavailable", message: "Tailscale has not been checked yet." };
|
|
938
|
+
let detectingTailscale;
|
|
939
|
+
let detectedTailscaleAt = 0;
|
|
940
|
+
let syncingLan = Promise.resolve();
|
|
941
|
+
async function refreshTailscale(force = false) {
|
|
942
|
+
if (process.env.CODEFLOW_SSH_HOST) return;
|
|
943
|
+
if (detectingTailscale) return detectingTailscale;
|
|
944
|
+
if (!force && Date.now() - detectedTailscaleAt < 5e3) return;
|
|
945
|
+
detectingTailscale = detectTailscale().then((status) => {
|
|
946
|
+
tailscaleStatus = status;
|
|
947
|
+
detectedTailscaleAt = Date.now();
|
|
948
|
+
}).finally(() => {
|
|
949
|
+
detectingTailscale = void 0;
|
|
950
|
+
});
|
|
951
|
+
return detectingTailscale;
|
|
952
|
+
}
|
|
953
|
+
function networkHost() {
|
|
954
|
+
if (process.env.CODEFLOW_SSH_HOST || remoteStopping || remoteTlsIssue() || !remote.enabled || !remote.lan) return void 0;
|
|
955
|
+
if (savedCertificate && remote.transport === "tailscale" && savedCertificate.hostname !== tailscaleStatus.dnsName) return void 0;
|
|
956
|
+
if (remote.transport === "tailscale") return tailscaleStatus.state === "connected" ? tailscaleStatus.ip : void 0;
|
|
957
|
+
return "0.0.0.0";
|
|
958
|
+
}
|
|
959
|
+
function syncLanServer(signOut = false) {
|
|
960
|
+
const change = syncingLan.catch(() => void 0).then(async () => {
|
|
961
|
+
let host = networkHost();
|
|
962
|
+
if (lanServer && (host !== lanHost || lanTls !== phoneTls)) {
|
|
963
|
+
const previous = lanServer;
|
|
964
|
+
lanServer = void 0;
|
|
965
|
+
lanHost = void 0;
|
|
966
|
+
await new Promise((resolve) => {
|
|
967
|
+
previous.close(() => resolve());
|
|
968
|
+
previous.closeAllConnections();
|
|
969
|
+
closeLanConnections();
|
|
970
|
+
});
|
|
971
|
+
host = networkHost();
|
|
972
|
+
}
|
|
973
|
+
if (!host) {
|
|
974
|
+
lanError = void 0;
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (lanServer) {
|
|
978
|
+
if (signOut) {
|
|
979
|
+
lanServer.closeAllConnections();
|
|
980
|
+
closeLanConnections();
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
lanError = void 0;
|
|
985
|
+
const transport = remote.transport ?? "lan";
|
|
986
|
+
const servingTls = phoneTls;
|
|
987
|
+
const receive = listener("lan");
|
|
988
|
+
const handleNetwork = (req, res) => {
|
|
989
|
+
if (host !== networkHost() || servingTls !== phoneTls || transport !== (remote.transport ?? "lan")) {
|
|
990
|
+
return send(res, 403, { error: "Remote access changed. Reconnect using the current link." });
|
|
991
|
+
}
|
|
992
|
+
receive(req, res);
|
|
993
|
+
};
|
|
994
|
+
const server2 = phoneTls ? createSecureServer({ ...phoneTls, maxHeaderSize: MAX_HEADER_SIZE }, handleNetwork) : createServer({ maxHeaderSize: MAX_HEADER_SIZE }, handleNetwork);
|
|
995
|
+
server2.on("connection", (socket) => {
|
|
996
|
+
lanSockets.add(socket);
|
|
997
|
+
socket.once("close", () => lanSockets.delete(socket));
|
|
998
|
+
const source = (socket.remoteAddress ?? "").replace(/^::ffff:/i, "");
|
|
999
|
+
if (!LAN_ALLOW_LOCAL && (isOwnAddress(source) || source === host || transport === "tailscale" && !isTailscaleIPv4(source))) socket.destroy();
|
|
1000
|
+
});
|
|
1001
|
+
lanServer = server2;
|
|
1002
|
+
lanHost = host;
|
|
1003
|
+
lanTls = servingTls;
|
|
1004
|
+
await new Promise((resolve) => {
|
|
1005
|
+
server2.once("listening", resolve);
|
|
1006
|
+
server2.on("error", (err) => {
|
|
1007
|
+
lanError = err.code === "EADDRINUSE" ? `Port ${REMOTE_PORT} is already in use by another program. Set CODEFLOW_REMOTE_PORT to a free port, then restart code-flow.` : transport === "tailscale" ? `Codeflow couldn't listen on Tailscale (${host}:${REMOTE_PORT}). Check that Tailscale is connected, then refresh.` : `code-flow couldn't listen on your network (port ${REMOTE_PORT}): ${err.message}`;
|
|
1008
|
+
logError(lanError, err);
|
|
1009
|
+
if (lanServer === server2) {
|
|
1010
|
+
lanServer = void 0;
|
|
1011
|
+
lanHost = void 0;
|
|
1012
|
+
}
|
|
1013
|
+
server2.close();
|
|
1014
|
+
resolve();
|
|
1015
|
+
});
|
|
1016
|
+
server2.listen(REMOTE_PORT, host);
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
syncingLan = change;
|
|
1020
|
+
return change;
|
|
1021
|
+
}
|
|
1022
|
+
async function renewRemoteCertificate() {
|
|
1023
|
+
if (tlsConfigured || process.env.CODEFLOW_SSH_HOST || remoteStopping || configuringTls || !remote.enabled || !remote.lan || remote.transport !== "tailscale") return;
|
|
1024
|
+
const status = certificateRenewal.status(savedCertificate);
|
|
1025
|
+
if (!status || status.running || status.nextAttempt > Date.now()) return;
|
|
1026
|
+
configuringTls = true;
|
|
1027
|
+
try {
|
|
1028
|
+
const change = changingRemote.catch(() => void 0).then(async () => {
|
|
1029
|
+
await certificateRenewal.tick(savedCertificate, async () => {
|
|
1030
|
+
const previous = savedCertificate;
|
|
1031
|
+
if (!previous) throw new Error("Certificate missing");
|
|
1032
|
+
await refreshTailscale(true);
|
|
1033
|
+
if (remoteStopping || tailscaleStatus.state !== "connected" || tailscaleStatus.dnsName !== previous.hostname) throw new Error("Tailscale identity unavailable");
|
|
1034
|
+
const certificate = await requestTailscaleCertificate(previous.hostname);
|
|
1035
|
+
const details = validateRemoteCertificate(certificate);
|
|
1036
|
+
if (remoteStopping || details.hostname !== previous.hostname || details.expiresAt <= previous.expiresAt) throw new Error("Certificate was not extended");
|
|
1037
|
+
await saveRemoteCertificate(certificate);
|
|
1038
|
+
phoneTls = { cert: certificate.cert, key: certificate.key };
|
|
1039
|
+
phoneProtocol = "https";
|
|
1040
|
+
savedCertificate = details;
|
|
1041
|
+
tlsLoadError = void 0;
|
|
1042
|
+
await syncLanServer();
|
|
1043
|
+
return details;
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
changingRemote = change;
|
|
1047
|
+
await change;
|
|
1048
|
+
} finally {
|
|
1049
|
+
configuringTls = false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const EPOCH = randomBytes(6).toString("hex");
|
|
1053
|
+
const STALL_MS = envNumber("CODEFLOW_SSE_STALL_MS", 3e4, { min: 100 });
|
|
1054
|
+
function openStream(res, epoch = EPOCH) {
|
|
1055
|
+
res.writeHead(200, {
|
|
1056
|
+
"content-type": "text/event-stream",
|
|
1057
|
+
"cache-control": "no-cache",
|
|
1058
|
+
connection: "keep-alive"
|
|
1059
|
+
});
|
|
1060
|
+
const writer = createSseWriter(res, { stallMs: STALL_MS, maxBytes: envNumber("CODEFLOW_SSE_BUFFER_BYTES", 8 * 1024 * 1024, { min: 1024 }) });
|
|
1061
|
+
const write = (chunk) => writer.write(chunk);
|
|
1062
|
+
const ping = setInterval(() => write(": ping\n\n"), 15e3);
|
|
1063
|
+
res.on("close", () => {
|
|
1064
|
+
clearInterval(ping);
|
|
1065
|
+
});
|
|
1066
|
+
return { send: (event, seq) => write(`${seq === void 0 ? "" : `id: ${epoch}:${seq}
|
|
1067
|
+
`}data: ${JSON.stringify(event)}
|
|
1068
|
+
|
|
1069
|
+
`) };
|
|
1070
|
+
}
|
|
1071
|
+
function lastEventSeq(req, expectedEpoch = EPOCH) {
|
|
1072
|
+
return cursorSeq(String(req.headers["last-event-id"] ?? ""), expectedEpoch);
|
|
1073
|
+
}
|
|
1074
|
+
const runFeed = { seq: 0, bytes: 0, recent: [], clients: /* @__PURE__ */ new Set() };
|
|
1075
|
+
const RUN_FEED_KEEP = 500;
|
|
1076
|
+
let runHistoryRevision = 0;
|
|
1077
|
+
manager.subscribeAll((run) => {
|
|
1078
|
+
if (run.deleted) runHistoryRevision++;
|
|
1079
|
+
const entry = { seq: ++runFeed.seq, run, bytes: Buffer.byteLength(JSON.stringify(run)) };
|
|
1080
|
+
runFeed.recent.push(entry);
|
|
1081
|
+
runFeed.bytes += entry.bytes;
|
|
1082
|
+
while (runFeed.recent.length > RUN_FEED_KEEP || runFeed.bytes > 32 * 1024 * 1024) runFeed.bytes -= runFeed.recent.shift().bytes;
|
|
1083
|
+
for (const client of runFeed.clients) client(entry);
|
|
1084
|
+
});
|
|
1085
|
+
function streamAllRuns(req, res, limit) {
|
|
1086
|
+
const page = limit === void 0 ? void 0 : fileAccess.page({ limit });
|
|
1087
|
+
const epoch = `${EPOCH}-fs${filesystemFeedRevision}-history${runHistoryRevision}`;
|
|
1088
|
+
const stream = openStream(res, epoch);
|
|
1089
|
+
const after = lastEventSeq(req, epoch);
|
|
1090
|
+
const oldest = runFeed.recent[0]?.seq ?? runFeed.seq + 1;
|
|
1091
|
+
if (after !== void 0 && after >= oldest - 1 && after <= runFeed.seq) {
|
|
1092
|
+
stream.send({ type: "hello", epoch, seq: runFeed.seq, resumed: true });
|
|
1093
|
+
for (const e of runFeed.recent) if (e.seq > after && !fileAccess.restrictedRun(e.run)) stream.send({ type: "run", seq: e.seq, run: e.run }, e.seq);
|
|
1094
|
+
} else {
|
|
1095
|
+
const attention = page ? manager.attentionPage({ limit: 100, predicate: (run) => !fileAccess.restrictedRun(run) }) : void 0;
|
|
1096
|
+
const runs = page ? [...new Map([...page.runs, ...attention.runs].map((run) => [run.id, run])).values()] : fileAccess.list();
|
|
1097
|
+
stream.send({ type: "hello", epoch, seq: runFeed.seq, runs, ...page ? { nextCursor: page.nextCursor, total: page.total, attentionTotal: attention.total, attentionTruncated: attention.truncated, attentionNextCursor: attention.nextCursor } : {} }, runFeed.seq);
|
|
1098
|
+
}
|
|
1099
|
+
const client = (e) => {
|
|
1100
|
+
if (!fileAccess.restrictedRun(e.run)) stream.send({ type: "run", seq: e.seq, run: e.run }, e.seq);
|
|
1101
|
+
};
|
|
1102
|
+
runFeed.clients.add(client);
|
|
1103
|
+
const stopPolicy = filePolicies.onChange(() => res.end());
|
|
1104
|
+
res.on("close", () => {
|
|
1105
|
+
runFeed.clients.delete(client);
|
|
1106
|
+
stopPolicy();
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
const teamFeed = /* @__PURE__ */ new Map();
|
|
1110
|
+
function teamBroadcast(frame, seq, id = "local") {
|
|
1111
|
+
for (const deliver of teamFeed.get(id) ?? []) deliver(frame, seq);
|
|
1112
|
+
}
|
|
1113
|
+
function teamFrame(event) {
|
|
1114
|
+
const frame = { type: "change", seq: event.seq, kind: event.kind };
|
|
1115
|
+
if (event.taskId !== void 0) frame.taskId = event.taskId;
|
|
1116
|
+
if (event.projectId !== void 0) frame.projectId = event.projectId;
|
|
1117
|
+
return frame;
|
|
1118
|
+
}
|
|
1119
|
+
collab.onChange((event) => {
|
|
1120
|
+
if (event.kind !== "inbox") return teamBroadcast(teamFrame(event), event.seq);
|
|
1121
|
+
if (event.userIds?.includes("owner")) void collab.inbox(ownerActor(collab.ownerName)).then(({ unread }) => teamBroadcast({ type: "inbox", seq: event.seq, unread }, event.seq), () => void 0);
|
|
1122
|
+
});
|
|
1123
|
+
sharedAccounts.onChange(() => teamBroadcast({ type: "resync" }));
|
|
1124
|
+
projects.onChange(() => teamBroadcast({ type: "resync" }));
|
|
1125
|
+
const topics = {
|
|
1126
|
+
approvals(_name, _after, deliver) {
|
|
1127
|
+
if (fileAccess.hasRestrictions()) throw new TopicError(403, "Approvals are unavailable under filesystem restrictions");
|
|
1128
|
+
const generation = filesystemFeedRevision;
|
|
1129
|
+
const publish = () => {
|
|
1130
|
+
if (generation === filesystemFeedRevision && !fileAccess.hasRestrictions()) {
|
|
1131
|
+
deliver({ type: "snapshot", records: redactExport(approvals.list().filter((record) => record.status === "pending")) });
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const stop2 = approvals.subscribe(publish);
|
|
1135
|
+
publish();
|
|
1136
|
+
return stop2;
|
|
1137
|
+
},
|
|
1138
|
+
"project-teams"(projectId, _after, deliver) {
|
|
1139
|
+
if (fileAccess.hasRestrictions() || !projects.get(projectId)) throw new TopicError(403, "Standing team scope is unavailable");
|
|
1140
|
+
const generation = filesystemFeedRevision;
|
|
1141
|
+
const publish = () => {
|
|
1142
|
+
if (generation === filesystemFeedRevision && !fileAccess.hasRestrictions() && projects.get(projectId)) deliver({ type: "snapshot", teams: projectTeams.list(projectId), ...projectTeams.configuration() });
|
|
1143
|
+
};
|
|
1144
|
+
const off = projectTeams.store.subscribe(publish);
|
|
1145
|
+
publish();
|
|
1146
|
+
return off;
|
|
1147
|
+
},
|
|
1148
|
+
/**
|
|
1149
|
+
* Every run's status changes, for the sidebar and notifications. A first connection (or one
|
|
1150
|
+
* from before a restart, or that missed too much) starts with the whole list, in step with the
|
|
1151
|
+
* stream; a reconnect gets the snapshots it missed.
|
|
1152
|
+
*/
|
|
1153
|
+
runs(_name, after, deliver) {
|
|
1154
|
+
const epoch = `${EPOCH}-fs${filesystemFeedRevision}-history${runHistoryRevision}`;
|
|
1155
|
+
const from = cursorSeq(after, epoch);
|
|
1156
|
+
const oldest = runFeed.recent[0]?.seq ?? runFeed.seq + 1;
|
|
1157
|
+
if (from !== void 0 && from >= oldest - 1 && from <= runFeed.seq) {
|
|
1158
|
+
deliver({ type: "hello", epoch, seq: runFeed.seq, resumed: true });
|
|
1159
|
+
for (const e of runFeed.recent) if (e.seq > from && !fileAccess.restrictedRun(e.run)) deliver({ type: "run", seq: e.seq, run: e.run }, e.seq);
|
|
1160
|
+
} else {
|
|
1161
|
+
const page = fileAccess.page({ limit: 100 });
|
|
1162
|
+
const attention = manager.attentionPage({ limit: 100, predicate: (run) => !fileAccess.restrictedRun(run) });
|
|
1163
|
+
const runs = [...new Map([...page.runs, ...attention.runs].map((run) => [run.id, run])).values()];
|
|
1164
|
+
deliver({ type: "hello", epoch, seq: runFeed.seq, runs, total: page.total, nextCursor: page.nextCursor, attentionTotal: attention.total, attentionTruncated: attention.truncated, attentionNextCursor: attention.nextCursor }, runFeed.seq);
|
|
1165
|
+
}
|
|
1166
|
+
const client = (e) => {
|
|
1167
|
+
if (!fileAccess.restrictedRun(e.run)) deliver({ type: "run", seq: e.seq, run: e.run }, e.seq);
|
|
1168
|
+
};
|
|
1169
|
+
runFeed.clients.add(client);
|
|
1170
|
+
return () => runFeed.clients.delete(client);
|
|
1171
|
+
},
|
|
1172
|
+
async "run-page"(id, after, deliver) {
|
|
1173
|
+
const run = await manager.getAsync(id);
|
|
1174
|
+
if (!run) throw new TopicError(404, "This run no longer exists");
|
|
1175
|
+
if (fileAccess.restrictedRun(run)) throw new TopicError(403, "This run is unavailable under its filesystem policy");
|
|
1176
|
+
const generation = filesystemFeedRevision, epoch = `run-${id}`;
|
|
1177
|
+
let waiting = [], waitingBytes = 0, overflow = false, last = 0;
|
|
1178
|
+
const allowed = () => generation === filesystemFeedRevision && !fileAccess.restrictedRun(run);
|
|
1179
|
+
const send2 = (event) => {
|
|
1180
|
+
if (event.seq > last && allowed()) {
|
|
1181
|
+
last = event.seq;
|
|
1182
|
+
deliver(transcriptPreview(event), event.seq);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
const stop2 = manager.subscribe(id, (event) => {
|
|
1186
|
+
if (!waiting) {
|
|
1187
|
+
send2(event);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const preview = transcriptPreview(event);
|
|
1191
|
+
waitingBytes += Buffer.byteLength(JSON.stringify(preview));
|
|
1192
|
+
if (waitingBytes > 2 * 1024 * 1024) {
|
|
1193
|
+
overflow = true;
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
waiting.push(preview);
|
|
1197
|
+
});
|
|
1198
|
+
try {
|
|
1199
|
+
const page = await transcriptPage(id, { after: cursorSeq(after, epoch) });
|
|
1200
|
+
if (!allowed()) throw new TopicError(409, "Filesystem policy changed; reconnect to refresh authorization");
|
|
1201
|
+
if (overflow) throw new TopicError(503, "Run updates exceeded the replay buffer; reconnect to catch up");
|
|
1202
|
+
const current = await manager.getAsync(id);
|
|
1203
|
+
if (!current || !allowed()) throw new TopicError(404, "This run is no longer available");
|
|
1204
|
+
const { steps, ...snapshot } = current;
|
|
1205
|
+
const newest = Math.max(page.latestSeq, waiting.at(-1)?.seq ?? 0);
|
|
1206
|
+
deliver({ type: "hello", epoch, resumed: page.resumed, snapshot: { run: snapshot, steps, seq: newest }, transcript: page, persistence: manager.persistence.get(id) });
|
|
1207
|
+
last = page.latestSeq;
|
|
1208
|
+
const queued = waiting;
|
|
1209
|
+
waiting = void 0;
|
|
1210
|
+
for (const event of queued) send2(event);
|
|
1211
|
+
const stopPersistence = manager.persistence.subscribe(id, (status) => {
|
|
1212
|
+
if (allowed()) deliver({ type: "persistence", status });
|
|
1213
|
+
});
|
|
1214
|
+
return () => {
|
|
1215
|
+
stop2();
|
|
1216
|
+
stopPersistence();
|
|
1217
|
+
};
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
stop2();
|
|
1220
|
+
throw error;
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
/**
|
|
1224
|
+
* A run's events. The hello carries the run as it is now (with its steps), so a window never
|
|
1225
|
+
* depends on the log holding the latest state (an event may have reached it before a crash lost
|
|
1226
|
+
* its best-effort write). Then what it takes to show the run (its latest snapshots, every agent
|
|
1227
|
+
* event and log line; see replayEvents), then live ones. Run event numbers are recovered from
|
|
1228
|
+
* disk, so a cursor is the run's (`run-<id>:<seq>`), not this server start's: a window that
|
|
1229
|
+
* reconnects, even to a restarted server, gets only what it missed. Live events that arrive
|
|
1230
|
+
* while the history is still being read wait for it, so the window never gets an older snapshot
|
|
1231
|
+
* after a newer one.
|
|
1232
|
+
*/
|
|
1233
|
+
async run(id, after, deliver) {
|
|
1234
|
+
if (!manager.get(id)) throw new TopicError(404, "This run doesn't exist. It may have been started by another code-flow, or its files were removed.");
|
|
1235
|
+
if (fileAccess.restrictedRun(manager.get(id))) throw new TopicError(403, "This run is unavailable under its filesystem policy");
|
|
1236
|
+
const generation = filesystemFeedRevision;
|
|
1237
|
+
const allowed = () => {
|
|
1238
|
+
const current = manager.get(id);
|
|
1239
|
+
return generation === filesystemFeedRevision && !!current && !fileAccess.restrictedRun(current);
|
|
1240
|
+
};
|
|
1241
|
+
const epoch = `run-${id}`;
|
|
1242
|
+
const requested = cursorSeq(after, epoch);
|
|
1243
|
+
let last = 0;
|
|
1244
|
+
const send2 = (e) => {
|
|
1245
|
+
if (e.seq <= last || !allowed()) return;
|
|
1246
|
+
last = e.seq;
|
|
1247
|
+
deliver(e, e.seq);
|
|
1248
|
+
};
|
|
1249
|
+
let waiting = [];
|
|
1250
|
+
const stop2 = manager.subscribe(id, (e) => waiting ? waiting.push(e) : send2(e));
|
|
1251
|
+
try {
|
|
1252
|
+
const history = await manager.events(id);
|
|
1253
|
+
if (!allowed()) throw new TopicError(409, "Filesystem policy changed; reconnect to refresh authorization");
|
|
1254
|
+
const newest = Math.max(0, history.at(-1)?.seq ?? 0, waiting.at(-1)?.seq ?? 0);
|
|
1255
|
+
const from = requested !== void 0 && requested <= newest ? requested : void 0;
|
|
1256
|
+
last = from ?? 0;
|
|
1257
|
+
const { steps, ...run } = manager.get(id);
|
|
1258
|
+
deliver({ type: "hello", epoch, resumed: from !== void 0, snapshot: { run, steps, seq: newest } });
|
|
1259
|
+
for (const event of replayEvents(history.filter((e) => e.seq > last))) send2(event);
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
stop2();
|
|
1262
|
+
throw err;
|
|
1263
|
+
} finally {
|
|
1264
|
+
const live = waiting.sort((a, b) => a.seq - b.seq);
|
|
1265
|
+
waiting = void 0;
|
|
1266
|
+
for (const event of live) send2(event);
|
|
1267
|
+
}
|
|
1268
|
+
return stop2;
|
|
1269
|
+
},
|
|
1270
|
+
/** A terminal of the side panel: what it showed so far, then its output as it comes. */
|
|
1271
|
+
shell(id, _after, deliver) {
|
|
1272
|
+
if (fileAccess.hasRestrictions()) throw new TopicError(403, "Terminals are paused under filesystem restrictions");
|
|
1273
|
+
const shell = shells.get(id);
|
|
1274
|
+
if (!shell) throw new TopicError(404, "That terminal is gone (code-flow restarted?)");
|
|
1275
|
+
deliver({ type: "hello", shell, history: shells.history(id) });
|
|
1276
|
+
return shells.subscribe(id, (e) => deliver(e)) ?? (() => {
|
|
1277
|
+
});
|
|
1278
|
+
},
|
|
1279
|
+
/** The team board: hub mode replays the store's ring from the cursor (or asks for a reload), linked mode relays the hub's stream. */
|
|
1280
|
+
team(name, after, deliver) {
|
|
1281
|
+
let entry;
|
|
1282
|
+
try {
|
|
1283
|
+
entry = teamConnections.get(name);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
throw new TopicError(404, error.message);
|
|
1286
|
+
}
|
|
1287
|
+
if (entry.link.linked) deliver({ type: "hello", epoch: `link-${entry.id}-${EPOCH}`, seq: 0, linked: true, connection: entry.link.state().connection });
|
|
1288
|
+
else {
|
|
1289
|
+
const page = collab.eventsSince(after);
|
|
1290
|
+
deliver({ type: "hello", epoch: page.epoch, seq: page.seq, linked: false }, page.seq);
|
|
1291
|
+
if (page.resync) deliver({ type: "resync" });
|
|
1292
|
+
for (const event of page.events) if (event.kind !== "inbox") deliver(teamFrame(event), event.seq);
|
|
1293
|
+
}
|
|
1294
|
+
const feed = teamFeed.get(entry.id) ?? /* @__PURE__ */ new Set();
|
|
1295
|
+
teamFeed.set(entry.id, feed);
|
|
1296
|
+
feed.add(deliver);
|
|
1297
|
+
return () => {
|
|
1298
|
+
feed.delete(deliver);
|
|
1299
|
+
if (!feed.size) teamFeed.delete(entry.id);
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const hub = new StreamHub(topics);
|
|
1304
|
+
const ownerHubStreams = /* @__PURE__ */ new Set();
|
|
1305
|
+
let streamSharing = { channelKey: randomBytes(32).toString("hex"), lockId: randomBytes(16).toString("hex") };
|
|
1306
|
+
function closeOwnerStreams(rotate = false) {
|
|
1307
|
+
if (rotate) streamSharing = { channelKey: randomBytes(32).toString("hex"), lockId: randomBytes(16).toString("hex") };
|
|
1308
|
+
for (const response of ownerHubStreams) response.end();
|
|
1309
|
+
}
|
|
1310
|
+
filePolicies.onChange(() => closeOwnerStreams());
|
|
1311
|
+
function openHubStream(res, url) {
|
|
1312
|
+
const raw = url.searchParams.get("subs") ?? "[]";
|
|
1313
|
+
if (raw.length > 16384) throw new HttpError(400, "Too many subscriptions");
|
|
1314
|
+
let subs;
|
|
1315
|
+
try {
|
|
1316
|
+
subs = JSON.parse(raw);
|
|
1317
|
+
} catch {
|
|
1318
|
+
throw new HttpError(400, "subs must be JSON");
|
|
1319
|
+
}
|
|
1320
|
+
const stream = openStream(res);
|
|
1321
|
+
ownerHubStreams.add(res);
|
|
1322
|
+
const stopPolicy = filePolicies.onChange(() => res.end());
|
|
1323
|
+
const client = hub.open((message) => stream.send(message), subs);
|
|
1324
|
+
res.on("close", () => {
|
|
1325
|
+
ownerHubStreams.delete(res);
|
|
1326
|
+
stopPolicy();
|
|
1327
|
+
hub.close(client);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
async function streamTopic(req, res, kind, name = "") {
|
|
1331
|
+
const epoch = kind === "run" ? `run-${name}` : `${EPOCH}-fs${filesystemFeedRevision}-history${runHistoryRevision}`;
|
|
1332
|
+
const after = lastEventSeq(req, epoch);
|
|
1333
|
+
const stream = openStream(res, epoch);
|
|
1334
|
+
let closed = false;
|
|
1335
|
+
const stopPolicy = filePolicies.onChange(() => res.end());
|
|
1336
|
+
res.on("close", () => {
|
|
1337
|
+
closed = true;
|
|
1338
|
+
stopPolicy();
|
|
1339
|
+
});
|
|
1340
|
+
try {
|
|
1341
|
+
const stop2 = await topics[kind](name, after === void 0 ? void 0 : `${epoch}:${after}`, (data, seq) => stream.send(data, kind === "shell" ? void 0 : seq));
|
|
1342
|
+
if (closed) stop2();
|
|
1343
|
+
else res.on("close", stop2);
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
if (err instanceof TopicError) return res.end();
|
|
1346
|
+
throw err;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const performanceMetrics = new PerformanceMetrics();
|
|
1350
|
+
async function handle(req, res, via) {
|
|
1351
|
+
performanceMetrics.observe(req, res);
|
|
1352
|
+
if (stopping) throw new HttpError(503, "The backend is stopping");
|
|
1353
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1354
|
+
let parts = url.pathname.split("/").filter(Boolean);
|
|
1355
|
+
if (parts[0] === "api") parts = await attempt(async () => canonicalSharedPath(req.url ?? "/").parts);
|
|
1356
|
+
if (await handlePublic(req, res, url, parts, via)) return;
|
|
1357
|
+
if (via === "lan") checkLanOrigin(req);
|
|
1358
|
+
else checkOrigin(req);
|
|
1359
|
+
const bearer = String(req.headers.authorization ?? "").replace(/^Bearer\s+/i, "");
|
|
1360
|
+
const explicitOwner = via === "local" && !!bearer && tokenMatches(bearer, auth.token);
|
|
1361
|
+
const runnerBearer = !explicitOwner && looksLikeRunnerToken(bearer);
|
|
1362
|
+
const shared = explicitOwner || runnerBearer ? void 0 : sharedSession(req);
|
|
1363
|
+
const sharedPresent = readCookies(req, SHARED_COOKIE).length > 0;
|
|
1364
|
+
const owner = explicitOwner || !sharedPresent && !runnerBearer && (via === "lan" ? phoneSignedIn(req) : signedIn(req));
|
|
1365
|
+
if (parts.join("/") === "api/auth/login" && req.method === "POST") {
|
|
1366
|
+
sharedMutation(req);
|
|
1367
|
+
const body = await readLoginJson(req);
|
|
1368
|
+
const session = await attempt(() => sharedAccounts.login({ username: body.username, password: body.password, ...sharedTransport(req) }));
|
|
1369
|
+
if (owner) closeOwnerStreams(true);
|
|
1370
|
+
res.setHeader("set-cookie", [sharedCookie(session.token, sharedTransport(req).secure), clearCookie(COOKIE), clearCookie(PHONE_COOKIE)]);
|
|
1371
|
+
return send(res, 200, { user: session.user, expiresAt: session.expiresAt });
|
|
1372
|
+
}
|
|
1373
|
+
if (parts.join("/") === "api/auth/self" && req.method === "GET") return send(res, 200, owner ? { kind: "owner", streamSharing } : shared?.identity ?? { kind: "anonymous" });
|
|
1374
|
+
if (parts.join("/") === "api/auth/logout" && req.method === "POST") {
|
|
1375
|
+
sharedMutation(req);
|
|
1376
|
+
for (const token of readCookies(req, SHARED_COOKIE)) await attempt(() => sharedAccounts.logout(token));
|
|
1377
|
+
auth.claimable = false;
|
|
1378
|
+
if (owner) closeOwnerStreams(true);
|
|
1379
|
+
res.setHeader("set-cookie", [clearCookie(SHARED_COOKIE), clearCookie(COOKIE), clearCookie(PHONE_COOKIE)]);
|
|
1380
|
+
return send(res, 200, { signedOut: true });
|
|
1381
|
+
}
|
|
1382
|
+
if (parts.join("/") === "api/team/runners/connect" && req.method === "POST") return handleRunnerConnect(req, res, { accounts: sharedAccounts, store: collab, transport: sharedTransport(req) });
|
|
1383
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
1384
|
+
const given = url.searchParams.get("token");
|
|
1385
|
+
if (given) {
|
|
1386
|
+
if (via === "lan") throw new HttpError(403, "Use a phone pairing link or shared-project account on this listener");
|
|
1387
|
+
if (!tokenMatches(given, auth.token)) throw new HttpError(403, "That link is from an earlier code-flow session; use the one printed when it last started");
|
|
1388
|
+
auth.claimable = false;
|
|
1389
|
+
res.writeHead(302, { location: "/", "set-cookie": [sessionCookie(), clearCookie(SHARED_COOKIE)], "cache-control": "no-store", "referrer-policy": "no-referrer" });
|
|
1390
|
+
return res.end();
|
|
1391
|
+
}
|
|
1392
|
+
const sharedLogin = sharedAccounts.available && sharedAccounts.hasUsers && (via === "local" || !!phoneTls);
|
|
1393
|
+
const claim = via === "local" && !owner && !sharedPresent && mayClaim(req);
|
|
1394
|
+
if (!owner && !shared && !claim && !sharedLogin) {
|
|
1395
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...NO_FRAMES });
|
|
1396
|
+
return res.end(via === "lan" ? PHONE_LOCKED_PAGE : LOCKED_PAGE);
|
|
1397
|
+
}
|
|
1398
|
+
if (via === "lan" && owner && !phoneControl()) throw new HttpError(404, "Not found");
|
|
1399
|
+
if (owner || shared || claim) auth.claimable = false;
|
|
1400
|
+
return sendAppPage(req, res, { ...NO_FRAMES, ...claim ? { "set-cookie": sessionCookie() } : {} });
|
|
1401
|
+
}
|
|
1402
|
+
if (parts[0] === "docs" && req.method === "GET") return serveDocs(docsDirectory, url.pathname, res);
|
|
1403
|
+
if (parts[0] === "assets" && parts.length === 2 && req.method === "GET") return sendAsset(fileURLToPath(new URL("../public/assets/", import.meta.url)), parts[1], req, res);
|
|
1404
|
+
if (parts[0] === "computer") {
|
|
1405
|
+
if (via !== "local" || !owner || !sshProjects) throw new HttpError(403, "Use the local owner app to open remote projects");
|
|
1406
|
+
const id = parts[1] ?? "";
|
|
1407
|
+
if (!/^[a-f0-9]{16}$/.test(id)) throw new HttpError(404, "Computer not found");
|
|
1408
|
+
const context = await attempt(() => sshProjects.context(id));
|
|
1409
|
+
if (parts.length === 2 && req.method === "GET") {
|
|
1410
|
+
const prefix = `/computer/${id}`;
|
|
1411
|
+
if (!url.pathname.endsWith("/")) {
|
|
1412
|
+
res.writeHead(302, { location: prefix + "/" });
|
|
1413
|
+
res.end();
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const html = (await readFile(indexHtml, "utf8")).replace(/<script\b/, `<script>window.__CODEFLOW_WORKSPACE=${JSON.stringify(id)};</script><script`);
|
|
1417
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...NO_FRAMES });
|
|
1418
|
+
res.end(html);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const path = url.pathname.slice(`/computer/${id}`.length) + url.search;
|
|
1422
|
+
if (path === "/api/ssh-context" && req.method === "GET") return send(res, 200, context);
|
|
1423
|
+
if (parts[2] !== "api") throw new HttpError(404, "Unknown remote operation");
|
|
1424
|
+
ownerHubStreams.add(res);
|
|
1425
|
+
res.once("close", () => ownerHubStreams.delete(res));
|
|
1426
|
+
return attempt(() => sshProjects.forward(id, req, res, path, `http://${req.headers.host}`));
|
|
1427
|
+
}
|
|
1428
|
+
if (parts[0] !== "api") throw new HttpError(404, "Not found");
|
|
1429
|
+
if (runnerBearer) return handleRunnerRequest(req, res, url, parts, bearer);
|
|
1430
|
+
if (shared) {
|
|
1431
|
+
await attempt(() => fileAccess.sharedGate(req));
|
|
1432
|
+
if (parts[1] === "team-terminals") return handleTeamTerminals(req, res, parts, teamTerminals, () => {
|
|
1433
|
+
const current = sharedAccounts.authenticate(shared.token, { touch: false });
|
|
1434
|
+
if (!current) throw new HttpError(401, "Your session ended; sign in again");
|
|
1435
|
+
return memberActor(memberOf(current), "browser");
|
|
1436
|
+
}, () => sharedMutation(req));
|
|
1437
|
+
if (parts[1] === "host-execution") return handleHostExecution(req, res, parts, hostExecution, memberActor(memberOf(shared.identity), "browser"), () => sharedMutation(req));
|
|
1438
|
+
if (parts[1] === "team") return handleTeamApi(req, res, url, parts, { store: collab, actor: memberActor(memberOf(shared.identity), "browser"), principal: "browser", reauthorize: () => {
|
|
1439
|
+
const current = sharedAccounts.authenticate(shared.token, { touch: false });
|
|
1440
|
+
if (!current) throw new HttpError(401, "Your session ended; sign in again");
|
|
1441
|
+
return memberActor(memberOf(current), "browser");
|
|
1442
|
+
}, mutation: sharedMutation });
|
|
1443
|
+
if (req.method !== "GET") sharedMutation(req);
|
|
1444
|
+
return attempt(() => handleSharedApi(req, res, url, parts, { accounts: sharedAccounts, token: shared.token, identity: shared.identity, manager, projects, comments: collaborationReviews, canReadFilePolicy: (run) => !fileAccess.restrictedRun(run), onFilePolicyChange: (listener2) => filePolicies.onChange(listener2), patterns: patterns.map(describePattern), templates }));
|
|
1445
|
+
}
|
|
1446
|
+
if (via === "lan" && !phoneControl()) throw new HttpError(404, "Not found");
|
|
1447
|
+
if (!owner) throw Object.assign(new HttpError(401, "Not signed in: use the owner link or sign in to shared projects"), { signIn: true });
|
|
1448
|
+
if (parts.join("/") === "api/ssh-context" && req.method === "GET") return send(res, 200, { kind: process.env.CODEFLOW_SSH_HOST ? "host" : "local" });
|
|
1449
|
+
if (process.env.CODEFLOW_TEAM_HOST && parts.join("/") === "api/team-host-runtime" && req.method === "GET") return send(res, 200, { instance: process.env.CODEFLOW_TEAM_HOST });
|
|
1450
|
+
if (process.env.CODEFLOW_SSH_HOST && parts.join("/") === "api/ssh-runtime" && req.method === "GET") return send(res, 200, { instance: process.env.CODEFLOW_SSH_HOST });
|
|
1451
|
+
if (parts[1] === "projects" && parts[2] && parts[3] === "filesystem-policy") {
|
|
1452
|
+
const project = projects.get(parts[2]);
|
|
1453
|
+
if (!project) throw new HttpError(404, "Project not found");
|
|
1454
|
+
if (parts.length !== 4) throw new HttpError(404, "Unknown filesystem policy operation");
|
|
1455
|
+
if (req.method === "GET") return send(res, 200, await attempt(() => filePolicies.get(project.id)));
|
|
1456
|
+
if (req.method === "PUT") {
|
|
1457
|
+
const body = await readJson(req, 64 * 1024);
|
|
1458
|
+
return send(res, 200, await attempt(() => fileAccess.update(project.id, body, async (commit, next) => {
|
|
1459
|
+
if (!filteredTasksReady || filteredTasks.active()) throw new FilePolicyError("Finish or explicitly cancel and reconcile filtered tasks before changing policy; unavailable task storage must be repaired first", 409);
|
|
1460
|
+
if (policyIsRestrictive(next) && extensions.list().length) throw new FilePolicyError("Disable configured extensions and restart before applying a restriction. Previously imported trusted JavaScript can retain background host authority, including a bundle rejected after initialization.", 409);
|
|
1461
|
+
if (shells.list().length || teamTerminals.active() || manager.devServers.list().some((service) => service.status === "running")) throw new FilePolicyError("Close terminals and stop development services before changing filesystem policy", 409);
|
|
1462
|
+
try {
|
|
1463
|
+
await manager.withIdleExecution(async () => {
|
|
1464
|
+
await pausePolicyBackground();
|
|
1465
|
+
previews.closeAll();
|
|
1466
|
+
sshPreviews.closeAll();
|
|
1467
|
+
await commit();
|
|
1468
|
+
});
|
|
1469
|
+
} finally {
|
|
1470
|
+
if (!fileAccess.hasRestrictions()) startPolicyBackground();
|
|
1471
|
+
}
|
|
1472
|
+
})));
|
|
1473
|
+
}
|
|
1474
|
+
throw new HttpError(405, "Use GET or PUT for filesystem policy");
|
|
1475
|
+
}
|
|
1476
|
+
if (parts[1] === "filtered-tasks") {
|
|
1477
|
+
await attempt(() => fileAccess.sharedGate(req));
|
|
1478
|
+
if (!filteredTasksReady) throw new HttpError(503, "Filtered task storage is unavailable; repair its private records before execution or policy changes");
|
|
1479
|
+
auth.claimable = false;
|
|
1480
|
+
if (req.method === "POST" && parts.length === 2) manager.noteActivity();
|
|
1481
|
+
if (await handleFilteredTasksApi(req, res, url, parts.slice(1), filteredTasks)) return;
|
|
1482
|
+
}
|
|
1483
|
+
if (parts[1] === "docs-help" && req.method === "POST") parseHelpRequest(await readJson(req, 64 * 1024, { timeoutMs: 1e4 }));
|
|
1484
|
+
if (parts[1] === "improve-prompt") {
|
|
1485
|
+
if (req.method !== "POST" || parts.length !== 2) throw new HttpError(404, "Unknown prompt improvement operation");
|
|
1486
|
+
await attempt(async () => assertImproveEnvironment());
|
|
1487
|
+
}
|
|
1488
|
+
await attempt(() => fileAccess.ownerGate(req, url, parts));
|
|
1489
|
+
if (parts[1] === "docs-help") {
|
|
1490
|
+
if (parts.length !== 2 || via !== "local") throw new HttpError(403, "Use the local owner app for documentation help");
|
|
1491
|
+
manager.noteActivity();
|
|
1492
|
+
return attempt(() => docsHelp.handle(req, res));
|
|
1493
|
+
}
|
|
1494
|
+
if (parts[1] === "ssh-runtime" && process.env.CODEFLOW_SSH_HOST || parts[1] === "team-host-runtime" && process.env.CODEFLOW_TEAM_HOST) {
|
|
1495
|
+
if (parts[2] === "preview" && parts.length === 3 && req.method === "POST") {
|
|
1496
|
+
if (fileAccess.hasRestrictions()) throw new HttpError(409, "Filesystem restrictions pause remote previews");
|
|
1497
|
+
const body = await readJson(req, 8192);
|
|
1498
|
+
const target = previewTarget(String(body.url ?? ""));
|
|
1499
|
+
if (!target || target.port === PORT || target.port === REMOTE_PORT) throw new HttpError(400, "Choose a remote development app's localhost HTTP address");
|
|
1500
|
+
return send(res, 200, await sshPreviews.show(String(body.url)));
|
|
1501
|
+
}
|
|
1502
|
+
if (parts[2] === "stop" && parts.length === 3 && req.method === "POST") {
|
|
1503
|
+
if (manager.list().some((run) => run.status === "running") || manager.busy.agents.running || manager.busy.checks.running || shells.list().some((shell) => shell.status === "running") || teamTerminals.active() || manager.devServers.list().some((service) => service.status === "running")) throw new HttpError(409, "Finish running work and close terminals and development servers before stopping the backend");
|
|
1504
|
+
await attempt(() => manager.withIdleExecution(async () => {
|
|
1505
|
+
await pausePolicyBackground();
|
|
1506
|
+
send(res, 200, { ok: true });
|
|
1507
|
+
void stop("SSH backend stopped from the app");
|
|
1508
|
+
}));
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
throw new HttpError(404, "Unknown SSH runtime operation");
|
|
1512
|
+
}
|
|
1513
|
+
if (parts[1] === "workspace-projects") {
|
|
1514
|
+
if (!sshProjects || via !== "local") throw new HttpError(403, "Connected projects require the local app");
|
|
1515
|
+
if (parts.length === 2 && req.method === "GET") return send(res, 200, { currentComputerId: "local", computers: await attempt(() => sshProjects.projectDirectory()) });
|
|
1516
|
+
throw new HttpError(404, "Unknown connected project operation");
|
|
1517
|
+
}
|
|
1518
|
+
if (parts[1] === "ssh-hosts") {
|
|
1519
|
+
if (!sshProjects || via !== "local") throw new HttpError(403, "Manage SSH connections from the local app");
|
|
1520
|
+
if (fileAccess.hasRestrictions()) throw new HttpError(409, "Filesystem restrictions pause SSH connection management");
|
|
1521
|
+
if (parts.length === 2 && req.method === "GET") return send(res, 200, await attempt(() => sshProjects.list()));
|
|
1522
|
+
if (parts.length === 2 && req.method === "POST") return send(res, 201, await attempt(async () => sshProjects.add(await readJson(req, 8192))));
|
|
1523
|
+
if (parts[3] === "team-server") {
|
|
1524
|
+
if (parts[4] === "status" && parts.length === 5 && req.method === "GET") return send(res, 200, await attempt(() => sshProjects.teamServerStatus(parts[2])));
|
|
1525
|
+
if (parts[4] === "installation" && parts.length === 5 && req.method === "DELETE") return send(res, 200, await attempt(() => sshProjects.removeTeamServer(parts[2])));
|
|
1526
|
+
if (parts[4] === "autostart" && parts.length === 5) {
|
|
1527
|
+
if (req.method === "GET") return send(res, 200, await attempt(() => sshProjects.teamAutostart(parts[2])));
|
|
1528
|
+
if (req.method === "POST") return send(res, 200, await attempt(async () => {
|
|
1529
|
+
const body = await readJson(req, 8192);
|
|
1530
|
+
if (body.action !== "enable" && body.action !== "disable") throw new HttpError(400, "Choose enable or disable for automatic startup");
|
|
1531
|
+
return sshProjects.teamAutostart(parts[2], body.action);
|
|
1532
|
+
}));
|
|
1533
|
+
throw new HttpError(405, "Use GET or POST for automatic startup");
|
|
1534
|
+
}
|
|
1535
|
+
if (parts.length === 4 && req.method === "POST") return send(res, 200, await attempt(async () => sshProjects.setupTeamServer(parts[2], await readJson(req, 8192))));
|
|
1536
|
+
if (parts.length === 4 && req.method === "DELETE") {
|
|
1537
|
+
await attempt(() => sshProjects.stopTeamServer(parts[2]));
|
|
1538
|
+
return send(res, 200, { ok: true });
|
|
1539
|
+
}
|
|
1540
|
+
if (parts[4] === "api") return attempt(() => sshProjects.forwardTeamServer(parts[2], req, res, url.pathname.slice(`/api/ssh-hosts/${parts[2]}/team-server`.length) + url.search, `http://${req.headers.host}`));
|
|
1541
|
+
throw new HttpError(404, "Unknown team server setup operation");
|
|
1542
|
+
}
|
|
1543
|
+
if (parts.length === 3 && req.method === "DELETE") {
|
|
1544
|
+
await attempt(() => sshProjects.remove(parts[2]));
|
|
1545
|
+
return send(res, 200, { ok: true });
|
|
1546
|
+
}
|
|
1547
|
+
if (parts.length === 4 && req.method === "POST") {
|
|
1548
|
+
if (parts[3] === "connect") return send(res, 200, await attempt(() => sshProjects.connect(parts[2])));
|
|
1549
|
+
if (parts[3] === "disconnect") {
|
|
1550
|
+
await attempt(() => sshProjects.disconnect(parts[2]));
|
|
1551
|
+
return send(res, 200, { ok: true });
|
|
1552
|
+
}
|
|
1553
|
+
if (parts[3] === "stop") {
|
|
1554
|
+
await attempt(() => sshProjects.stop(parts[2]));
|
|
1555
|
+
return send(res, 200, { ok: true });
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
throw new HttpError(404, "Unknown SSH connection operation");
|
|
1559
|
+
}
|
|
1560
|
+
if (parts[1] === "accounts") {
|
|
1561
|
+
if (!parts[2] && req.method === "GET") return send(res, 200, await attempt(async () => sharedAccounts.snapshot()));
|
|
1562
|
+
const body = await readJson(req);
|
|
1563
|
+
if (body.roles) {
|
|
1564
|
+
for (const id of Object.keys(body.roles)) if (!projects.get(id)) throw new HttpError(400, "Choose existing projects for shared roles");
|
|
1565
|
+
}
|
|
1566
|
+
if (!parts[2] && req.method === "POST") return send(res, 201, await attempt(() => sharedAccounts.create(body)));
|
|
1567
|
+
if (parts.length === 3 && req.method === "PUT") return send(res, 200, await attempt(() => sharedAccounts.update(parts[2], body)));
|
|
1568
|
+
if (parts.length === 4 && parts[3] === "revoke-sessions" && req.method === "POST") return send(res, 200, await attempt(() => sharedAccounts.revokeSessions(parts[2], body)));
|
|
1569
|
+
throw new HttpError(404, "Unknown shared account operation");
|
|
1570
|
+
}
|
|
1571
|
+
if (await codeHostsApi.handle(req, res, parts.slice(1))) return;
|
|
1572
|
+
if (parts[1] === "remote-workers") {
|
|
1573
|
+
if (parts.length === 2 && req.method === "GET") return send(res, 200, remoteWorkers.list());
|
|
1574
|
+
if (parts.length === 4 && parts[3] === "probe" && req.method === "POST") {
|
|
1575
|
+
const body = await readJson(req, 8192);
|
|
1576
|
+
if (typeof body.repoMapping !== "string") throw new HttpError(400, "Choose a registered repository mapping");
|
|
1577
|
+
return send(res, 200, await attempt(() => remoteWorkers.probe(parts[2], { repoMapping: body.repoMapping })));
|
|
1578
|
+
}
|
|
1579
|
+
throw new HttpError(404, "Unknown remote worker operation");
|
|
1580
|
+
}
|
|
1581
|
+
if (parts.join("/") === "api/extensions" && req.method === "GET") return send(res, 200, { api: { major: 1, minor: 0 }, extensions: extensions.list() });
|
|
1582
|
+
if (parts[1] === "acp-connections") {
|
|
1583
|
+
if (req.method === "GET" && parts.length === 2) return send(res, 200, acpConnections.snapshot());
|
|
1584
|
+
if (req.method === "POST" && parts[2] === "check" && parts.length === 3) {
|
|
1585
|
+
const body = await readJson(req);
|
|
1586
|
+
return attempt(async () => {
|
|
1587
|
+
const connection = validateAcpConnection(body.connection);
|
|
1588
|
+
if (acpChecks >= 2) throw new HttpError(429, "Two connection checks are already running. Try again shortly.");
|
|
1589
|
+
acpChecks++;
|
|
1590
|
+
try {
|
|
1591
|
+
return send(res, 200, await probeAcp(connection));
|
|
1592
|
+
} finally {
|
|
1593
|
+
acpChecks--;
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
if (req.method === "POST" && parts.length === 2) {
|
|
1598
|
+
const body = await readJson(req);
|
|
1599
|
+
return send(res, 200, await attempt(() => acpConnections.save(body.revision, body.connection)));
|
|
1600
|
+
}
|
|
1601
|
+
if (req.method === "DELETE" && parts[2] && parts.length === 3) return send(res, 200, await attempt(() => acpConnections.remove(Number(url.searchParams.get("revision")), parts[2])));
|
|
1602
|
+
throw new HttpError(404, "Unknown ACP connection operation");
|
|
1603
|
+
}
|
|
1604
|
+
if (parts[1] === "project-teams") return handleProjectTeams(projectTeams, req, res, parts, url);
|
|
1605
|
+
if (parts[1] === "coordinated-changes") return coordinatedApi.handle(req, res, parts);
|
|
1606
|
+
if (parts[1] === "team-terminals") return handleTeamTerminals(req, res, parts, teamTerminals, () => ownerActor(collab.ownerName), () => sharedMutation(req));
|
|
1607
|
+
if (parts[1] === "host-execution") return handleHostExecution(req, res, parts, hostExecution, ownerActor(collab.ownerName), () => sharedMutation(req));
|
|
1608
|
+
if (parts[1] === "team" || parts[1] === "team-link") {
|
|
1609
|
+
if (parts[1] === "team-link" && via !== "local") throw new HttpError(404, "Not found");
|
|
1610
|
+
return attempt(async () => {
|
|
1611
|
+
if (parts[1] === "team-link" && parts[2] === "connections" && parts.length === 3 && req.method === "GET") return send(res, 200, { connections: teamConnections.inventory() });
|
|
1612
|
+
if (parts[1] === "team-link" && parts[2] === "connect" && parts.length === 3 && req.method === "POST") {
|
|
1613
|
+
const body = await readJson(req, 16 * 1024, { timeoutMs: 1e4 });
|
|
1614
|
+
if (Object.keys(body).some((key) => !["url", "username", "password", "name"].includes(key))) throw new HttpError(400, "Unknown connection field");
|
|
1615
|
+
const entry2 = await teamConnections.connect({ url: body.url, username: body.username, password: body.password, name: body.name });
|
|
1616
|
+
return send(res, 201, { id: entry2.id, ...await linkInfo({ link: entry2.link, runner: entry2.runner, store: collab, manager, findPattern, agents: () => [] }) });
|
|
1617
|
+
}
|
|
1618
|
+
const selection = url.searchParams.get("teamConnection") ?? req.headers["x-codeflow-team"];
|
|
1619
|
+
url.searchParams.delete("teamConnection");
|
|
1620
|
+
if (selection !== void 0 && typeof selection !== "string") throw new HttpError(400, "Choose one team connection");
|
|
1621
|
+
const entry = teamConnections.get(selection);
|
|
1622
|
+
if (parts[1] === "team-link" && parts.length === 2 && req.method === "DELETE") {
|
|
1623
|
+
await teamConnections.disconnect(entry.id);
|
|
1624
|
+
return send(res, 200, { disconnected: true });
|
|
1625
|
+
}
|
|
1626
|
+
return teamConnections.use(entry.id, async () => {
|
|
1627
|
+
if (parts[1] === "team") return entry.link.linked ? entry.link.proxy(req, res, parts, url) : handleTeamApi(req, res, url, parts, { store: collab, actor: ownerActor(collab.ownerName), principal: "owner", mutation: () => {
|
|
1628
|
+
} });
|
|
1629
|
+
const installed = req.method === "POST" && !fileAccess.hasRestrictions() ? (await agentAvailability()).filter((agent) => agent.available).map((agent) => agent.id) : [];
|
|
1630
|
+
return handleTeamLinkApi(req, res, url, parts, { link: entry.link, runner: entry.runner, store: collab, manager, findPattern, agents: () => installed, hubName: collab.ownerName });
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (parts[1] === "runs" && parts.length === 6 && parts[3] === "remote-steps" && ["receipt", "cancel"].includes(parts[5]) && req.method === "POST") {
|
|
1635
|
+
return send(res, 200, redactExport(await attempt(() => manager.inspectRemoteStep(parts[2], parts[4], { cancel: parts[5] === "cancel" }))));
|
|
1636
|
+
}
|
|
1637
|
+
if (["readiness", "verification", "workspace", "diagnostics", "v1"].includes(parts[1] ?? "")) {
|
|
1638
|
+
const abort = new AbortController();
|
|
1639
|
+
res.once("close", () => abort.abort());
|
|
1640
|
+
const result = await attempt(() => handleWorkspaceApi(req, url, parts, manager, abort.signal));
|
|
1641
|
+
if (result) return send(res, result.status, result.body);
|
|
1642
|
+
}
|
|
1643
|
+
if (parts[1] === "integrations") {
|
|
1644
|
+
if (req.method === "GET" && !parts[2]) return send(res, 200, integrations.list());
|
|
1645
|
+
if (req.method === "GET" && parts[2] && parts[3] === "items") {
|
|
1646
|
+
const kind = url.searchParams.get("kind");
|
|
1647
|
+
if (kind !== "issue" && kind !== "pr") throw new HttpError(400, "Choose issues or review requests");
|
|
1648
|
+
return send(res, 200, await attempt(() => integrations.items(parts[2], kind, url.searchParams.get("cursor") ?? void 0)));
|
|
1649
|
+
}
|
|
1650
|
+
if (req.method === "POST" && parts[2] && parts[3] === "import") {
|
|
1651
|
+
const body = await readJson(req);
|
|
1652
|
+
if (!["issue", "pr"].includes(body.kind) || typeof body.key !== "string") throw new HttpError(400, "Choose a source item");
|
|
1653
|
+
return send(res, 201, await attempt(() => integrations.importSource(parts[2], body.kind, body.key)));
|
|
1654
|
+
}
|
|
1655
|
+
throw new HttpError(404, "Unknown external source operation");
|
|
1656
|
+
}
|
|
1657
|
+
if (parts[1] === "notifications") {
|
|
1658
|
+
if (!notificationsReady) throw new HttpError(503, "Notifications are disabled until their configuration and storage are repaired");
|
|
1659
|
+
if (req.method === "GET" && !parts[2]) return send(res, 200, notifications.list());
|
|
1660
|
+
if (req.method === "GET" && parts[2] === "deliveries") return send(res, 200, notifications.deliveries());
|
|
1661
|
+
if (req.method === "POST" && parts[2] === "deliveries" && parts[3] && parts[4] === "retry") return send(res, 202, await attempt(() => notifications.retry(parts[3])));
|
|
1662
|
+
if (req.method === "POST" && parts[2] && parts[3] === "test") return send(res, 202, await attempt(() => notifications.test(parts[2])));
|
|
1663
|
+
if (req.method === "POST" && parts[2] && !parts[3]) return send(res, 200, await attempt(async () => notifications.update(parts[2], await readJson(req))));
|
|
1664
|
+
throw new HttpError(404, "Unknown notification operation");
|
|
1665
|
+
}
|
|
1666
|
+
if (parts[1] === "approvals") {
|
|
1667
|
+
if (req.method === "GET") return send(res, 200, redactExport(approvals.list(url.searchParams.get("run") ?? void 0)));
|
|
1668
|
+
if (req.method === "POST" && parts[2]) {
|
|
1669
|
+
const body = await readJson(req);
|
|
1670
|
+
if (body.decision !== "approve" && body.decision !== "decline") throw new HttpError(400, "Choose approve or decline");
|
|
1671
|
+
const request = approvals.list().find((record) => record.id === parts[2]);
|
|
1672
|
+
const run = request ? manager.get(request.runId) : void 0;
|
|
1673
|
+
if (!request || !run || run.status !== "running" || !run.approvals.tools || !run.steps.some((step) => step.id === request.stepId && step.status === "running")) throw new HttpError(409, "This approval is no longer active");
|
|
1674
|
+
if (!approvals.respond(request.id, body.decision, { runId: run.id, stepId: request.stepId })) throw new HttpError(409, "This approval has expired or was already answered");
|
|
1675
|
+
manager.log(run.id, `You ${body.decision === "approve" ? "approved" : "declined"} one ${request.kind} action in ${request.stepId}`);
|
|
1676
|
+
return send(res, 200, { resolved: true });
|
|
1677
|
+
}
|
|
1678
|
+
throw new HttpError(404, "Unknown approval operation");
|
|
1679
|
+
}
|
|
1680
|
+
if (req.method === "GET" && parts[1] === "team-capabilities") return send(res, 200, await Promise.all(AGENT_IDS.map(adapterTeamCapabilities)));
|
|
1681
|
+
if (parts[1] === "team-rollout") {
|
|
1682
|
+
if (req.method === "GET") return send(res, 200, await teamEvaluationStore.rollout(autoTeamEnabled()));
|
|
1683
|
+
if (req.method === "POST") {
|
|
1684
|
+
const body = await readJson(req);
|
|
1685
|
+
await attempt(() => teamEvaluationStore.decide({ stage: body.stage, rationale: body.rationale, evaluationIds: body.evaluationIds }));
|
|
1686
|
+
return send(res, 200, await teamEvaluationStore.rollout(autoTeamEnabled()));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (req.method === "GET" && parts[1] === "team-evaluations") {
|
|
1690
|
+
const reports = await teamEvaluationStore.list();
|
|
1691
|
+
return send(res, 200, { reports, assessment: assessTeamRollout(reports), rollout: await teamEvaluationStore.rollout(autoTeamEnabled()) });
|
|
1692
|
+
}
|
|
1693
|
+
if (req.method === "GET" && parts[1] === "agents") return send(res, 200, fileAccess.hasRestrictions() ? Object.values(adapters).map((adapter) => ({ id: adapter.id, displayName: adapter.displayName, binary: adapter.binary, available: false })) : await agentAvailability());
|
|
1694
|
+
if (req.method === "GET" && parts[1] === "events") {
|
|
1695
|
+
const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : void 0;
|
|
1696
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1 || limit > 100)) throw new HttpError(400, "History page size must be between 1 and 100");
|
|
1697
|
+
return streamAllRuns(req, res, limit);
|
|
1698
|
+
}
|
|
1699
|
+
if (req.method === "GET" && parts[1] === "stream" && !parts[2]) return openHubStream(res, url);
|
|
1700
|
+
if (req.method === "POST" && parts[1] === "stream" && parts[2]) {
|
|
1701
|
+
const body = await readJson(req);
|
|
1702
|
+
if (!hub.update(parts[2], body)) throw new HttpError(404, "That stream has closed; open a new one");
|
|
1703
|
+
return send(res, 200, { ok: true });
|
|
1704
|
+
}
|
|
1705
|
+
if (parts[1] === "team-templates") {
|
|
1706
|
+
const project = url.searchParams.get("project") || void 0;
|
|
1707
|
+
if (project && !projects.list().some((p) => p.id === project)) throw new HttpError(404, "Project not found");
|
|
1708
|
+
if (req.method === "GET" && !parts[2]) return send(res, 200, await attempt(() => teamTemplateStore.list(project, url.searchParams.get("history") === "1")));
|
|
1709
|
+
if (req.method === "POST" && parts[2] === "migrate") return send(res, 200, await attempt(async () => teamTemplateStore.migrate(await readJson(req), project)));
|
|
1710
|
+
if (req.method === "POST" && !parts[2]) {
|
|
1711
|
+
const body = await readJson(req);
|
|
1712
|
+
const template = await attempt(async () => validateTeamTemplate(body.template));
|
|
1713
|
+
if (template.project && !projects.list().some((p) => p.id === template.project)) throw new HttpError(404, "Project not found");
|
|
1714
|
+
if (template.workflow && !findPattern(template.workflow)) throw new HttpError(400, "Unknown template workflow");
|
|
1715
|
+
return send(res, 201, await attempt(() => teamTemplateStore.save(template, body.expectedVersion)));
|
|
1716
|
+
}
|
|
1717
|
+
throw new HttpError(404, "Unknown team template operation");
|
|
1718
|
+
}
|
|
1719
|
+
if (req.method === "GET" && parts[1] === "patterns") return send(res, 200, patterns.map(describePattern));
|
|
1720
|
+
if (req.method === "GET" && parts[1] === "fs") {
|
|
1721
|
+
const listing = await listDirectories(url.searchParams.get("path") ?? void 0, url.searchParams.get("hidden") === "1").catch((err) => {
|
|
1722
|
+
const status = err.code === "ENOENT" ? 404 : err.code === "EACCES" || err.code === "EPERM" ? 403 : 400;
|
|
1723
|
+
throw new HttpError(status, err.code === "ENOENT" ? "Folder not found" : err.message);
|
|
1724
|
+
});
|
|
1725
|
+
return send(res, 200, listing);
|
|
1726
|
+
}
|
|
1727
|
+
if (req.method === "GET" && parts[1] === "branches") {
|
|
1728
|
+
const repo = await requestedRepo(url);
|
|
1729
|
+
return send(res, 200, { repo, worktreeRoot: worktreeRoot(repo), ...await listBranches(repo) });
|
|
1730
|
+
}
|
|
1731
|
+
if (req.method === "GET" && parts[1] === "tree") {
|
|
1732
|
+
const repo = await requestedFolder(url);
|
|
1733
|
+
const listing = await listTree(repo, url.searchParams.get("path") ?? "").catch((err) => {
|
|
1734
|
+
if (err instanceof FilePolicyError) throw new HttpError(err.status, err.message);
|
|
1735
|
+
throw new HttpError(err.code === "ENOENT" ? 404 : 400, err.code === "ENOENT" ? "Folder not found" : err.message);
|
|
1736
|
+
});
|
|
1737
|
+
return send(res, 200, listing);
|
|
1738
|
+
}
|
|
1739
|
+
if (req.method === "GET" && parts[1] === "doctor" && parts[2] === "dependencies") return send(res, 200, await discoverDependencies(manager.quota));
|
|
1740
|
+
if (req.method === "GET" && parts[1] === "doctor" && parts[2] === "git") return send(res, 200, await checkGit());
|
|
1741
|
+
if (req.method === "GET" && parts[1] === "doctor") {
|
|
1742
|
+
const health = await doctor(manager.quota);
|
|
1743
|
+
for (const entry of health) {
|
|
1744
|
+
if (entry.installed && entry.signedIn === false) manager.recordQuota(entry.id, "signed_out", {});
|
|
1745
|
+
else if (entry.signedIn === true && manager.quota[entry.id]?.status === "signed_out") manager.recordQuota(entry.id, "allowed", {});
|
|
1746
|
+
}
|
|
1747
|
+
return send(res, 200, health);
|
|
1748
|
+
}
|
|
1749
|
+
if (req.method === "GET" && parts[1] === "health") {
|
|
1750
|
+
return send(res, 200, { busy: manager.busy, activity: manager.inspectRepositoryActivity(), writes: writeHealth, performance: performanceMetrics.snapshot(), disk: writeQueueStats(), delivery: streamHealth, codeIntelligence: codeIntelligenceHealth, storage: manager.inspectStorage(), runs: manager.runningCount, interactiveTerminal: !fileAccess.hasRestrictions() && await shells.interactive(), streams: hub.stats });
|
|
1751
|
+
}
|
|
1752
|
+
if (req.method === "GET" && parts[1] === "experience" && parts.length === 2) {
|
|
1753
|
+
const list = runsIn(url.searchParams.get("project")).filter((run) => !url.searchParams.get("repo") || run.repo === url.searchParams.get("repo"));
|
|
1754
|
+
return send(res, 200, { metrics: outcomeMetrics(list), recommendations: workflowRecommendations(list, url.searchParams.get("repo") ?? "", taskIntent(url.searchParams.get("intent"))) });
|
|
1755
|
+
}
|
|
1756
|
+
if (req.method === "GET" && parts[1] === "stats") {
|
|
1757
|
+
const since = Number(url.searchParams.get("since")) || void 0;
|
|
1758
|
+
const repo = url.searchParams.get("repo") ? await requestedRepo(url) : void 0;
|
|
1759
|
+
return send(res, 200, computeStats(runsIn(url.searchParams.get("project")), { repo, since }));
|
|
1760
|
+
}
|
|
1761
|
+
if (req.method === "POST" && parts[1] === "compare") return send(res, 201, await startComparison(await readJson(req)));
|
|
1762
|
+
if (req.method === "GET" && parts[1] === "disk") {
|
|
1763
|
+
return send(res, 200, await diskUsage(runsIn(url.searchParams.get("project")), url.searchParams.get("repo") ? await requestedRepo(url) : void 0));
|
|
1764
|
+
}
|
|
1765
|
+
if (req.method === "POST" && parts[1] === "cleanup") {
|
|
1766
|
+
const body = await readJson(req);
|
|
1767
|
+
return send(res, 200, await cleanUp(runsIn(body.project), Math.max(0, Number(body.olderThanDays ?? 7)), typeof body.repo === "string" ? body.repo : void 0));
|
|
1768
|
+
}
|
|
1769
|
+
if (parts[1] === "maintenance" && parts[2] === "retention") {
|
|
1770
|
+
if (req.method === "GET" && !parts[3]) return send(res, 200, await attempt(() => retentionPolicy.get()));
|
|
1771
|
+
if (req.method === "POST" && !parts[3]) {
|
|
1772
|
+
const body = await readJson(req);
|
|
1773
|
+
return send(res, 200, await attempt(() => retentionPolicy.set({ days: body.days, keepRunIds: body.keepRunIds, keepMemoryIds: body.keepMemoryIds }, body.expectedRevision)));
|
|
1774
|
+
}
|
|
1775
|
+
if (req.method === "POST" && parts[3] === "preview") return send(res, 200, await attempt(() => previewRetention({ home: CODEFLOW_HOME })));
|
|
1776
|
+
throw new HttpError(404, "Retention deletion is an explicit offline CLI operation");
|
|
1777
|
+
}
|
|
1778
|
+
if (parts[1] === "deferred-runs") {
|
|
1779
|
+
const id = parts[2], action = parts[3];
|
|
1780
|
+
if (!id && req.method === "GET") return send(res, 200, await attempt(() => deferredRuns.list()));
|
|
1781
|
+
if (!id && req.method === "POST") {
|
|
1782
|
+
const body = await readJson(req);
|
|
1783
|
+
const raw = body.run;
|
|
1784
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "Choose a run configuration to defer");
|
|
1785
|
+
if (raw.teamRerun !== void 0 || raw.deferred !== void 0) throw new HttpError(400, "Server launch identities cannot be queued");
|
|
1786
|
+
if (raw.context?.attachments !== void 0 && (!Array.isArray(raw.context.attachments) || raw.context.attachments.length)) throw new HttpError(400, "Attachments cannot be deferred; remove them before queueing");
|
|
1787
|
+
const request = await attempt(async () => sanitizeDeferredRequest({ ...automationRun(raw), parentRunId: raw.parentRunId, branchPrefix: raw.branchPrefix }));
|
|
1788
|
+
const pattern = findPattern(request.pattern);
|
|
1789
|
+
if (!pattern) throw new HttpError(400, "Unknown pattern");
|
|
1790
|
+
request.repo = await attempt(() => repoRoot(request.repo));
|
|
1791
|
+
if (request.project && !projects.get(request.project)?.repos.includes(request.repo)) throw new HttpError(400, "Choose a repository belonging to this project");
|
|
1792
|
+
if (request.parentRunId && manager.get(request.parentRunId)?.repo !== request.repo) throw new HttpError(400, "Choose an existing parent run in the same repository");
|
|
1793
|
+
for (const spec of pattern.roles) if (!spec.optional && pattern.id !== "auto-team" && !request.roles[spec.id]?.length) throw new HttpError(400, `Pick an agent for "${spec.label}"`);
|
|
1794
|
+
validateLoop(request.loop);
|
|
1795
|
+
request.options = validateOptions(pattern, request.options);
|
|
1796
|
+
return send(res, 201, await attempt(() => deferredRuns.enqueue({ name: body.name, run: request, notBefore: body.notBefore, waitForProviders: body.waitForProviders, maxAttempts: body.maxAttempts, operationId: body.operationId })));
|
|
1797
|
+
}
|
|
1798
|
+
if (id && req.method === "POST") {
|
|
1799
|
+
const body = await readJson(req);
|
|
1800
|
+
if (action === "cancel") return send(res, 200, await attempt(() => deferredRuns.cancel(id, body.expectedRevision)));
|
|
1801
|
+
if (action === "retry") return send(res, 200, await attempt(() => deferredRuns.retry(id, body.expectedRevision, { acknowledgeUnknown: body.acknowledgeUnknown === true })));
|
|
1802
|
+
if (action === "attach") {
|
|
1803
|
+
const job = (await deferredRuns.list()).find((item) => item.id === id), run = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
|
|
1804
|
+
if (!job || !run || run.repo !== job.run.repo || job.run.project && run.project !== job.run.project || run.deferred?.jobId !== job.id || run.deferred.claimId !== job.claimId || run.deferred.attempt !== job.attempts) throw new HttpError(409, "This run does not belong to this exact deferred launch claim");
|
|
1805
|
+
await attempt(() => persistDeferredReceipt(run));
|
|
1806
|
+
return send(res, 200, await attempt(() => deferredRuns.attach(id, body.expectedRevision, run.id)));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
throw new HttpError(404, "Unknown deferred run operation");
|
|
1810
|
+
}
|
|
1811
|
+
if (parts[1] === "projects") {
|
|
1812
|
+
const id = parts[2];
|
|
1813
|
+
if (!id && req.method === "GET") return send(res, 200, projects.list());
|
|
1814
|
+
if (!id && req.method === "POST") {
|
|
1815
|
+
const body = await readJson(req);
|
|
1816
|
+
return send(res, 201, await attempt(() => projects.create({ name: body.name, repos: Array.isArray(body.repos) ? body.repos : [] })));
|
|
1817
|
+
}
|
|
1818
|
+
if (id && !projects.get(id)) throw new HttpError(404, "Project not found");
|
|
1819
|
+
if (id && parts[3] === "summary" && req.method === "GET") {
|
|
1820
|
+
const inventory = projects.list();
|
|
1821
|
+
return send(res, 200, manager.projectReport((run) => projectOf(run, inventory) === id && !fileAccess.restrictedRun(run)));
|
|
1822
|
+
}
|
|
1823
|
+
if (id && parts[3] === "repositories" && parts.length === 4 && req.method === "POST") {
|
|
1824
|
+
const body = await readJson(req);
|
|
1825
|
+
if (typeof body.repo !== "string" || !body.repo.startsWith("/")) throw new HttpError(400, "Choose a repository folder");
|
|
1826
|
+
return send(res, 200, await attempt(async () => (await projects.include(id, await repoRoot(body.repo))).project));
|
|
1827
|
+
}
|
|
1828
|
+
if (id && parts[3] === "conventions") {
|
|
1829
|
+
const project = projects.get(id);
|
|
1830
|
+
if (!parts[4] && req.method === "GET") return send(res, 200, await attempt(() => conventions.get(id)));
|
|
1831
|
+
if (!parts[4] && req.method === "PUT") return send(res, 200, await attempt(async () => conventions.save(id, await readJson(req))));
|
|
1832
|
+
if (parts[4] === "restore-preview" && req.method === "POST") {
|
|
1833
|
+
const body = await readJson(req);
|
|
1834
|
+
return send(res, 200, await attempt(() => conventions.previewRestore(id, body.version)));
|
|
1835
|
+
}
|
|
1836
|
+
if (parts[4] === "restore" && req.method === "POST") return send(res, 200, await attempt(async () => conventions.restore(id, await readJson(req))));
|
|
1837
|
+
if (parts[4] === "publication" && req.method === "GET") {
|
|
1838
|
+
const repo = url.searchParams.get("repo");
|
|
1839
|
+
if (!repo || !project.repos.includes(repo)) throw new HttpError(400, "Choose a repository belonging to this project");
|
|
1840
|
+
return send(res, 200, await attempt(() => conventions.inspectRepository(repo)));
|
|
1841
|
+
}
|
|
1842
|
+
if (parts[4] === "publish" && req.method === "POST") {
|
|
1843
|
+
const body = await readJson(req);
|
|
1844
|
+
if (typeof body.repo !== "string" || !project.repos.includes(body.repo)) throw new HttpError(400, "Choose a repository belonging to this project");
|
|
1845
|
+
return send(res, 200, await attempt(() => manager.withIdleRepository(body.repo, () => conventions.publish(id, body, { repos: project.repos, actor: "user", isIdle: () => manager.isRepositoryIdle(body.repo) }), { kind: "conventions-publication", owner: id })));
|
|
1846
|
+
}
|
|
1847
|
+
throw new HttpError(404, "Unknown conventions operation");
|
|
1848
|
+
}
|
|
1849
|
+
if (id && parts[3] === "memory") {
|
|
1850
|
+
const project = projects.get(id);
|
|
1851
|
+
const requested = url.searchParams.get("repo");
|
|
1852
|
+
if (requested && !project.repos.includes(requested)) throw new HttpError(400, "That repository is not in this project");
|
|
1853
|
+
const repos = requested ? [requested] : project.repos;
|
|
1854
|
+
if (parts[4] === "graph" && !parts[5] && req.method === "GET") {
|
|
1855
|
+
const repo = requested ?? (project.repos.length === 1 ? project.repos[0] : void 0);
|
|
1856
|
+
if (!repo) throw new HttpError(400, "Choose a repository in this project");
|
|
1857
|
+
return send(res, 200, await attempt(() => memoryStore.graph({ repo, projectId: id, scope: "project" }, { kind: "user", repo, projectId: id })));
|
|
1858
|
+
}
|
|
1859
|
+
if (parts[4] === "analysis") {
|
|
1860
|
+
const body = req.method === "POST" ? await readJson(req) : {};
|
|
1861
|
+
const repo = typeof body.repo === "string" ? body.repo : requested ?? (project.repos.length === 1 ? project.repos[0] : void 0);
|
|
1862
|
+
if (!repo || !project.repos.includes(repo)) throw new HttpError(400, "Choose a repository in this project");
|
|
1863
|
+
const entries = await memoryStore.list({ repo, projectId: id, scope: "project" }, { kind: "user", repo, projectId: id });
|
|
1864
|
+
const analysis = await attempt(() => analyzeProjectMemory({ repo, projectId: id, entries }));
|
|
1865
|
+
if (req.method === "GET" && !parts[5]) return send(res, 200, analysis);
|
|
1866
|
+
if (req.method === "POST" && parts[5] === "publish") {
|
|
1867
|
+
if (!Array.isArray(body.expectedSources) || JSON.stringify(body.expectedSources) !== JSON.stringify(analysis.onboarding.sourceRefs)) throw new HttpError(409, "The onboarding sources changed. Review the current summary before publishing");
|
|
1868
|
+
if (!analysis.onboarding.sourceRefs.length) throw new HttpError(400, "There is no current trusted user guidance to publish");
|
|
1869
|
+
return send(res, 201, await attempt(() => memoryStore.propose({ scope: "project", repo, projectId: id, title: analysis.onboarding.title, body: analysis.onboarding.body, tags: ["onboarding-summary"], derivedFrom: analysis.onboarding.sourceRefs, sourceRefs: analysis.onboarding.sourceRefs.map((ref) => `${ref.id}@${ref.revision}`) }, { kind: "user", repo, projectId: id }, { expectedSources: analysis.onboarding.sourceRefs })));
|
|
1870
|
+
}
|
|
1871
|
+
throw new HttpError(404, "Unknown memory analysis operation");
|
|
1872
|
+
}
|
|
1873
|
+
if (req.method === "GET" && !parts[4]) {
|
|
1874
|
+
const entries = (await Promise.all(repos.map((repo) => memoryStore.list({ repo, projectId: id, scope: "project" }, { kind: "user", repo, projectId: id })))).flat();
|
|
1875
|
+
return send(res, 200, entries.sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id)));
|
|
1876
|
+
}
|
|
1877
|
+
if (req.method === "POST" && parts[4] === "commands" && !parts[5]) {
|
|
1878
|
+
const body = await readJson(req);
|
|
1879
|
+
try {
|
|
1880
|
+
let entry;
|
|
1881
|
+
if (body.op === "create") {
|
|
1882
|
+
const repo = typeof body.repo === "string" ? body.repo : project.repos.length === 1 ? project.repos[0] : "";
|
|
1883
|
+
if (!project.repos.includes(repo)) throw new HttpError(400, "Choose a repository in this project");
|
|
1884
|
+
entry = await memoryStore.propose({
|
|
1885
|
+
scope: "project",
|
|
1886
|
+
repo,
|
|
1887
|
+
projectId: id,
|
|
1888
|
+
title: body.title,
|
|
1889
|
+
body: body.body,
|
|
1890
|
+
paths: body.paths,
|
|
1891
|
+
tags: body.tags,
|
|
1892
|
+
applicability: body.applicability
|
|
1893
|
+
}, { kind: "user", repo, projectId: id });
|
|
1894
|
+
} else {
|
|
1895
|
+
if (!["approve", "update", "delete", "feedback"].includes(body.op)) throw new HttpError(400, "Unknown memory command");
|
|
1896
|
+
if (typeof body.id !== "string" || !Number.isSafeInteger(body.expectedRevision) || body.expectedRevision < 1) throw new HttpError(400, "Memory commands need an entry ID and exact revision");
|
|
1897
|
+
const entries = (await Promise.all(project.repos.map((repo) => memoryStore.list({ repo, projectId: id, scope: "project" }, { kind: "user", repo, projectId: id })))).flat();
|
|
1898
|
+
const existing = entries.find((candidate) => candidate.id === body.id);
|
|
1899
|
+
if (!existing) throw new HttpError(404, "Memory entry not found in this project");
|
|
1900
|
+
const authority = { kind: "user", repo: existing.repo, projectId: id };
|
|
1901
|
+
if (body.op === "approve") entry = await memoryStore.accept(existing.id, body.expectedRevision, authority);
|
|
1902
|
+
else if (body.op === "delete") entry = await memoryStore.remove(existing.id, body.expectedRevision, authority);
|
|
1903
|
+
else if (body.op === "feedback") {
|
|
1904
|
+
if (body.runId !== void 0) {
|
|
1905
|
+
const source = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
|
|
1906
|
+
if (!source || source.repo !== existing.repo || source.project !== id || !source.memoryManifest?.included.some((ref) => ref.id === existing.id && ref.revision === body.expectedRevision)) throw new HttpError(400, "Feedback must refer to memory actually selected by this project's run");
|
|
1907
|
+
}
|
|
1908
|
+
entry = await memoryStore.recordFeedback(existing.id, body.expectedRevision, { rating: body.rating, note: body.note, runId: body.runId }, authority);
|
|
1909
|
+
} else {
|
|
1910
|
+
const patch = {};
|
|
1911
|
+
for (const key of ["title", "body", "paths", "tags", "applicability"]) if (key in body) patch[key] = body[key];
|
|
1912
|
+
entry = await memoryStore.revise(existing.id, body.expectedRevision, patch, authority, { accept: body.accept === true });
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return send(res, body.op === "create" ? 201 : 200, entry);
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
if (error instanceof HttpError) throw error;
|
|
1918
|
+
if (error instanceof MemoryConflictError) throw new HttpError(409, error.message);
|
|
1919
|
+
const message = error.message;
|
|
1920
|
+
if (/^Memory store cannot be read|^Memory storage limit/.test(message)) throw new HttpError(500, message);
|
|
1921
|
+
throw new HttpError(400, message);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
throw new HttpError(404, "Unknown project memory route");
|
|
1925
|
+
}
|
|
1926
|
+
if (id && req.method === "POST") {
|
|
1927
|
+
const body = await readJson(req);
|
|
1928
|
+
return send(res, 200, await attempt(() => manager.updateProject(id, { name: body.name, repos: Array.isArray(body.repos) ? body.repos : void 0 })));
|
|
1929
|
+
}
|
|
1930
|
+
if (id && req.method === "DELETE") {
|
|
1931
|
+
await manager.removeProject(id);
|
|
1932
|
+
return send(res, 200, { deleted: true });
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (req.method === "GET" && parts[1] === "templates") return send(res, 200, templates);
|
|
1936
|
+
if (parts[1] === "remote") {
|
|
1937
|
+
if (process.env.CODEFLOW_SSH_HOST && req.method !== "GET") throw new HttpError(403, "Use SSH to reach this backend; phone listeners are unavailable on SSH hosts");
|
|
1938
|
+
if (parts[2] === "tls" && parts.length === 3) {
|
|
1939
|
+
if (via !== "local") throw new HttpError(403, "Configure HTTPS from Codeflow on this computer");
|
|
1940
|
+
if (req.method !== "POST") throw new HttpError(405, "Method not allowed");
|
|
1941
|
+
if (tlsConfigured) throw new HttpError(409, "HTTPS is managed by environment variables. Remove those overrides and restart before managing certificates in Settings.");
|
|
1942
|
+
const body = await readJson(req, 192 * 1024);
|
|
1943
|
+
if (body.mode !== "tailscale" && body.mode !== "import") throw new HttpError(400, "Choose Tailscale or import certificate files");
|
|
1944
|
+
if (configuringTls) throw new HttpError(409, "Secure access setup is already in progress");
|
|
1945
|
+
configuringTls = true;
|
|
1946
|
+
try {
|
|
1947
|
+
const change = changingRemote.catch(() => void 0).then(async () => {
|
|
1948
|
+
let certificate;
|
|
1949
|
+
if (body.mode === "tailscale") {
|
|
1950
|
+
await refreshTailscale(true);
|
|
1951
|
+
if (remote.transport !== "tailscale" || tailscaleStatus.state !== "connected" || !tailscaleStatus.dnsName) throw new HttpError(400, "Choose Tailscale and connect this computer before setting up its certificate");
|
|
1952
|
+
certificate = await attempt(() => requestTailscaleCertificate(tailscaleStatus.dnsName));
|
|
1953
|
+
} else certificate = { source: "import", hostname: await attempt(async () => certificateHostname(body.hostname)), cert: body.cert, key: body.key };
|
|
1954
|
+
const details = await attempt(async () => validateRemoteCertificate(certificate));
|
|
1955
|
+
if (remote.transport === "tailscale" && tailscaleStatus.dnsName && details.hostname !== tailscaleStatus.dnsName) throw new HttpError(400, "Use a certificate for this computer's Tailscale DNS name");
|
|
1956
|
+
const next = changeRemote(remote, { rotate: true, control: false });
|
|
1957
|
+
await saveRemote(next);
|
|
1958
|
+
remote = next;
|
|
1959
|
+
await saveRemoteCertificate(certificate);
|
|
1960
|
+
phoneTls = { cert: certificate.cert, key: certificate.key };
|
|
1961
|
+
phoneProtocol = "https";
|
|
1962
|
+
savedCertificate = details;
|
|
1963
|
+
tlsLoadError = void 0;
|
|
1964
|
+
await syncLanServer(true);
|
|
1965
|
+
});
|
|
1966
|
+
changingRemote = change;
|
|
1967
|
+
await change;
|
|
1968
|
+
return send(res, 200, remoteInfo(via));
|
|
1969
|
+
} finally {
|
|
1970
|
+
configuringTls = false;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (parts[2] && parts[2] !== "devices") throw new HttpError(404, "Unknown remote access operation");
|
|
1974
|
+
if (parts[2] === "devices") {
|
|
1975
|
+
if (req.method === "GET") return send(res, 200, phoneSessions.list(remote.controlToken));
|
|
1976
|
+
if (req.method === "DELETE" && parts[3]) return send(res, 200, { revoked: await phoneSessions.revoke(parts[3], remote.controlToken) });
|
|
1977
|
+
throw new HttpError(404, "Unknown paired device operation");
|
|
1978
|
+
}
|
|
1979
|
+
if (req.method === "POST") {
|
|
1980
|
+
const body = await readJson(req);
|
|
1981
|
+
if (body.transport !== void 0 && body.transport !== "lan" && body.transport !== "tailscale") throw new HttpError(400, "Choose Wi-Fi or Tailscale for remote access");
|
|
1982
|
+
const before = await changeSavedRemote({ enabled: body.enabled, lan: body.lan, transport: body.transport, control: body.control, rotate: !!body.rotate }).catch((err) => {
|
|
1983
|
+
if (err instanceof HttpError) throw err;
|
|
1984
|
+
logError("Couldn't save the phone settings", err);
|
|
1985
|
+
throw new HttpError(500, `Couldn't save the phone settings: ${err.message}`);
|
|
1986
|
+
});
|
|
1987
|
+
const signOut = remote.controlToken !== before.controlToken || remote.token !== before.token || phoneControl(before) && !phoneControl();
|
|
1988
|
+
if (remote.transport === "tailscale") await refreshTailscale();
|
|
1989
|
+
if (via === "local") await syncLanServer(signOut);
|
|
1990
|
+
if (via === "lan") res.once("close", () => void syncLanServer(signOut));
|
|
1991
|
+
} else if (req.method === "GET") {
|
|
1992
|
+
await refreshTailscale(url.searchParams.has("refresh"));
|
|
1993
|
+
if (via === "local") await syncLanServer();
|
|
1994
|
+
else res.once("close", () => void syncLanServer());
|
|
1995
|
+
} else throw new HttpError(405, "Method not allowed");
|
|
1996
|
+
return send(res, 200, remoteInfo(via));
|
|
1997
|
+
}
|
|
1998
|
+
if (parts[1] === "automations") {
|
|
1999
|
+
if (parts[2] === "schedule-preview" && req.method === "POST") {
|
|
2000
|
+
const body = await readJson(req);
|
|
2001
|
+
const schedule = await attempt(async () => validateSchedule(body.schedule));
|
|
2002
|
+
if (!schedule) throw new HttpError(400, "Choose a schedule");
|
|
2003
|
+
return send(res, 200, { times: await attempt(async () => previewSchedule(schedule, body.after, body.count)) });
|
|
2004
|
+
}
|
|
2005
|
+
const id = parts[2];
|
|
2006
|
+
if (!id && req.method === "GET") return send(res, 200, automations.list().map(describeAutomation));
|
|
2007
|
+
if (!id && req.method === "POST") {
|
|
2008
|
+
const body = await readJson(req);
|
|
2009
|
+
const name = String(body.name ?? "").trim().slice(0, 80);
|
|
2010
|
+
if (!name) throw new HttpError(400, "Name the automation");
|
|
2011
|
+
const run = automationRun(body.run ?? {});
|
|
2012
|
+
await buildStartInput(run);
|
|
2013
|
+
const schedule = await attempt(async () => validateSchedule(body.schedule));
|
|
2014
|
+
if (!schedule && !body.webhook) throw new HttpError(400, "Give it a schedule, a webhook, or both");
|
|
2015
|
+
const trigger = await automations.create({ name, enabled: body.enabled !== false, run, schedule, webhook: !!body.webhook, isolate: !!body.isolate, image: typeof body.image === "string" ? body.image.trim() || void 0 : void 0 });
|
|
2016
|
+
return send(res, 201, describeAutomation(trigger));
|
|
2017
|
+
}
|
|
2018
|
+
if (id && !automations.get(id)) throw new HttpError(404, "Automation not found");
|
|
2019
|
+
if (id && parts[3] === "events" && req.method === "GET") return send(res, 200, redactExport(automations.events(id)));
|
|
2020
|
+
if (id && parts[3] === "events" && parts[4] && parts[5] === "retry" && req.method === "POST") return send(res, 200, await attempt(() => automations.retryEvent(id, parts[4])));
|
|
2021
|
+
if (id && parts[3] === "preview" && req.method === "POST") {
|
|
2022
|
+
const body = await readJson(req);
|
|
2023
|
+
return send(res, 200, redactExport(await attempt(() => automations.dryRun(id, Object.keys(body).length ? body : void 0))));
|
|
2024
|
+
}
|
|
2025
|
+
if (id && req.method === "DELETE") {
|
|
2026
|
+
await automations.remove(id);
|
|
2027
|
+
return send(res, 200, { deleted: true });
|
|
2028
|
+
}
|
|
2029
|
+
if (id && req.method === "POST" && parts[3] === "run") {
|
|
2030
|
+
const fired = await attempt(() => automations.fire(id, { via: "manual" }));
|
|
2031
|
+
if (fired.skipped) throw new HttpError(409, `Skipped: ${fired.skipped}`);
|
|
2032
|
+
return send(res, 201, { id: fired.run?.id ?? fired.runs?.[0]?.id, ids: fired.runs?.map((r) => r.id) });
|
|
2033
|
+
}
|
|
2034
|
+
if (id && req.method === "POST" && !parts[3]) {
|
|
2035
|
+
const body = await readJson(req);
|
|
2036
|
+
const patch = {};
|
|
2037
|
+
if (typeof body.name === "string" && body.name.trim()) patch.name = body.name.trim().slice(0, 80);
|
|
2038
|
+
if (typeof body.enabled === "boolean") patch.enabled = body.enabled;
|
|
2039
|
+
if (typeof body.webhook === "boolean") patch.webhook = body.webhook;
|
|
2040
|
+
if (typeof body.isolate === "boolean") patch.isolate = body.isolate;
|
|
2041
|
+
if (typeof body.image === "string") patch.image = body.image.trim() || void 0;
|
|
2042
|
+
if ("schedule" in body) patch.schedule = await attempt(async () => validateSchedule(body.schedule));
|
|
2043
|
+
if (body.run) {
|
|
2044
|
+
const run = automationRun(body.run);
|
|
2045
|
+
await buildStartInput(run);
|
|
2046
|
+
patch.run = run;
|
|
2047
|
+
}
|
|
2048
|
+
if (body.rotateToken) patch.rotateToken = true;
|
|
2049
|
+
return send(res, 200, describeAutomation(await automations.update(id, patch)));
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (req.method === "GET" && parts[1] === "github" && parts[2] === "sweep") {
|
|
2053
|
+
const repo = await requestedRepo(url);
|
|
2054
|
+
const kind = url.searchParams.get("kind");
|
|
2055
|
+
if (!SWEEP_KINDS.includes(kind)) throw new HttpError(400, "Unknown kind of sweep");
|
|
2056
|
+
return send(res, 200, await attempt(() => listSweep(repo, kind, manager.list())));
|
|
2057
|
+
}
|
|
2058
|
+
if (req.method === "POST" && parts[1] === "sweeps") return send(res, 201, await startSweep(await readJson(req)));
|
|
2059
|
+
if (req.method === "GET" && parts[1] === "github" && (parts[2] === "issues" || parts[2] === "prs")) {
|
|
2060
|
+
const repo = await requestedRepo(url);
|
|
2061
|
+
return send(res, 200, await attempt(() => parts[2] === "issues" ? listIssues(repo) : listPullRequests(repo)));
|
|
2062
|
+
}
|
|
2063
|
+
if (req.method === "GET" && parts[1] === "repo-config") {
|
|
2064
|
+
const repo = await requestedRepo(url);
|
|
2065
|
+
return send(res, 200, await readRepoConfig(repo));
|
|
2066
|
+
}
|
|
2067
|
+
if (req.method === "GET" && parts[1] === "check") {
|
|
2068
|
+
const repo = await requestedRepo(url);
|
|
2069
|
+
return send(res, 200, await detectCheckCommand(repo) ?? { command: null });
|
|
2070
|
+
}
|
|
2071
|
+
if (req.method === "GET" && parts[1] === "search") {
|
|
2072
|
+
const repo = await requestedFolder(url);
|
|
2073
|
+
return send(res, 200, await searchFiles(repo, url.searchParams.get("q") ?? ""));
|
|
2074
|
+
}
|
|
2075
|
+
if (req.method === "GET" && parts[1] === "file") {
|
|
2076
|
+
const repo = await requestedFolder(url);
|
|
2077
|
+
const content = await readRepoFile(repo, url.searchParams.get("path") ?? "").catch((err) => {
|
|
2078
|
+
if (err instanceof FilePolicyError) throw new HttpError(err.status, err.message);
|
|
2079
|
+
throw new HttpError(err.code === "ENOENT" ? 404 : 400, err.code === "ENOENT" ? "File not found" : err.message);
|
|
2080
|
+
});
|
|
2081
|
+
return send(res, 200, content);
|
|
2082
|
+
}
|
|
2083
|
+
if (parts[1] === "notes") {
|
|
2084
|
+
if (req.method === "GET" && parts[2] === "search") return send(res, 200, await attempt(() => notes.search(url.searchParams.get("q") ?? "")));
|
|
2085
|
+
if (req.method === "GET" && parts[2] === "all") return send(res, 200, await notes.keys());
|
|
2086
|
+
if (req.method === "GET" && parts[2] === "history") return send(res, 200, await notes.history(url.searchParams.get("key") ?? ""));
|
|
2087
|
+
if (req.method === "POST" && parts[2] === "restore") {
|
|
2088
|
+
const body = await readJson(req);
|
|
2089
|
+
try {
|
|
2090
|
+
return send(res, 200, await notes.restore(String(body.key ?? ""), body.revision, body.expectedRevision));
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
if (error instanceof NotesConflictError) throw new HttpError(409, error.message);
|
|
2093
|
+
throw error;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if (req.method === "GET") return send(res, 200, await notes.get(url.searchParams.get("key") ?? ""));
|
|
2097
|
+
if (req.method === "POST") {
|
|
2098
|
+
const body = await readJson(req);
|
|
2099
|
+
if (typeof body.text !== "string") throw new HttpError(400, "Send the note's text");
|
|
2100
|
+
try {
|
|
2101
|
+
return send(res, 200, await notes.set(String(body.key ?? ""), body.text, body.expectedRevision));
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
if (error instanceof NotesConflictError) return send(res, 409, { error: error.message, current: error.current });
|
|
2104
|
+
throw new HttpError(400, error.message);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (req.method === "GET" && parts[1] === "serve") return send(res, 200, manager.devServers.list());
|
|
2109
|
+
if (await handleJourneys(req, res, url, parts)) return;
|
|
2110
|
+
if (parts[1] === "preview" && req.method === "POST") {
|
|
2111
|
+
const body = await readJson(req);
|
|
2112
|
+
return send(res, 200, await attempt(() => previews.show(String(body.url ?? ""), req.headers.host)));
|
|
2113
|
+
}
|
|
2114
|
+
if (parts[1] === "snapshot" && req.method === "POST") {
|
|
2115
|
+
const body = await readJson(req);
|
|
2116
|
+
const localApp = (url2) => {
|
|
2117
|
+
const target = previewTarget(url2.href);
|
|
2118
|
+
return !!target && target.port !== PORT && target.port !== REMOTE_PORT;
|
|
2119
|
+
};
|
|
2120
|
+
try {
|
|
2121
|
+
return send(res, 200, await takeSnapshot({ url: String(body.url ?? ""), width: Number(body.width), height: body.height === void 0 ? void 0 : Number(body.height), full: body.full === true }, { localApp }));
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
if (err instanceof SnapshotError) throw new HttpError(err.status, err.message);
|
|
2124
|
+
throw err;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (parts[1] === "shell") {
|
|
2128
|
+
const id = parts[2];
|
|
2129
|
+
if (!id && req.method === "GET") return send(res, 200, shells.list());
|
|
2130
|
+
if (!id && req.method === "POST") {
|
|
2131
|
+
const body = await readJson(req);
|
|
2132
|
+
const cwd = String(body.cwd ?? "").trim();
|
|
2133
|
+
if (!cwd.startsWith("/")) throw new HttpError(400, "The folder must be an absolute path");
|
|
2134
|
+
if (!await stat(cwd).then((s) => s.isDirectory(), () => false)) throw new HttpError(404, "Folder not found");
|
|
2135
|
+
if (!await manager.worktreeAt(cwd).then(Boolean, () => false)) {
|
|
2136
|
+
await repoRoot(cwd).catch(() => {
|
|
2137
|
+
throw new HttpError(400, "The terminal opens in a git repository or one of its worktrees");
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
manager.noteActivity();
|
|
2141
|
+
try {
|
|
2142
|
+
return send(res, 201, await shells.open(cwd, { pty: body.pty === true, cols: body.cols, rows: body.rows }));
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
if (err instanceof ShellLimitError) throw new HttpError(409, err.message);
|
|
2145
|
+
throw err;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (id && !shells.get(id)) throw new HttpError(404, "That terminal is gone (code-flow restarted?)");
|
|
2149
|
+
if (id && req.method === "GET" && parts[3] === "events") return streamTopic(req, res, "shell", id);
|
|
2150
|
+
if (id && req.method === "GET") return send(res, 200, shells.get(id));
|
|
2151
|
+
if (id && req.method === "POST" && parts[3] === "input") {
|
|
2152
|
+
const body = await readJson(req);
|
|
2153
|
+
const keys = typeof body.binary === "string" ? body.binary : body.data;
|
|
2154
|
+
if (typeof keys === "string" && shells.get(id)?.interactive) {
|
|
2155
|
+
if (keys.length > 1e6) throw new HttpError(413, "That paste is too long");
|
|
2156
|
+
if (!shells.type(id, keys, typeof body.binary === "string" ? "latin1" : "utf8")) throw new HttpError(409, "That shell has exited; open a new one");
|
|
2157
|
+
return send(res, 200, shells.get(id));
|
|
2158
|
+
}
|
|
2159
|
+
if (typeof body.text !== "string" || body.text.length > 2e4) throw new HttpError(400, "Send a command");
|
|
2160
|
+
if (!shells.write(id, body.text)) throw new HttpError(409, "That shell has exited; open a new one");
|
|
2161
|
+
return send(res, 200, shells.get(id));
|
|
2162
|
+
}
|
|
2163
|
+
if (id && req.method === "POST" && parts[3] === "resize") {
|
|
2164
|
+
const body = await readJson(req);
|
|
2165
|
+
const [cols, rows] = [Number(body.cols), Number(body.rows)];
|
|
2166
|
+
if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 1 || rows < 1) throw new HttpError(400, "Send the size as whole numbers: cols and rows");
|
|
2167
|
+
if (!shells.get(id)?.interactive) throw new HttpError(409, "Only an interactive terminal has a size");
|
|
2168
|
+
if (!shells.resize(id, cols, rows)) throw new HttpError(409, "That shell has exited; open a new one");
|
|
2169
|
+
return send(res, 200, shells.get(id));
|
|
2170
|
+
}
|
|
2171
|
+
if (id && req.method === "DELETE") return send(res, 200, { closed: shells.close(id) });
|
|
2172
|
+
}
|
|
2173
|
+
if (req.method === "POST" && parts[1] === "improve-prompt" && parts.length === 2) return improveTask(await readJson(req, 256 * 1024), res);
|
|
2174
|
+
if (parts[1] === "model-catalog" && parts.length === 2 && ["GET", "POST"].includes(req.method ?? "")) {
|
|
2175
|
+
const body = req.method === "POST" ? await readJson(req) : { antigravityExecution: url.searchParams.get("antigravityExecution") ?? void 0 };
|
|
2176
|
+
const nativeAgy = antigravityExecution(body.antigravityExecution) === "native";
|
|
2177
|
+
if (Object.keys(body).some((k) => !["allowResearch", "offline", "antigravityExecution"].includes(k)) || [body.allowResearch, body.offline].some((v) => v !== void 0 && typeof v !== "boolean")) throw new HttpError(400, "Invalid catalog refresh options");
|
|
2178
|
+
const settings = await modelCatalogStore.settings(body.allowResearch);
|
|
2179
|
+
const providers = await Promise.all((await agentAvailability()).map(async (a) => ({
|
|
2180
|
+
provider: a.id,
|
|
2181
|
+
available: a.available,
|
|
2182
|
+
blockedReason: a.id === "agy" && nativeAgy ? void 0 : permissionProviderProblem(a.id, catalogPermissions("standard", false, false)) ?? (a.available ? await getAdapter(a.id).managedAvailability?.() : void 0),
|
|
2183
|
+
limitations: a.id === "agy" && nativeAgy ? [AGY_NATIVE_LIMITATION] : getAdapter(a.id).managedLimitations,
|
|
2184
|
+
catalog: await modelCatalogStore.get(a.id, { context: a.id === "agy" && !nativeAgy && process.platform === "darwin" ? { isolatedAgy: true } : void 0, force: req.method === "POST", offline: !a.available || body.offline })
|
|
2185
|
+
})));
|
|
2186
|
+
return send(res, 200, { providers, ...settings, offline: process.env.CODEFLOW_MODEL_NETWORK === "offline" });
|
|
2187
|
+
}
|
|
2188
|
+
if (req.method === "GET" && parts[1] === "models") {
|
|
2189
|
+
const agents = await agentAvailability();
|
|
2190
|
+
const entries = await Promise.all(
|
|
2191
|
+
agents.map(async (a) => {
|
|
2192
|
+
if (!a.available) return [a.id, { error: "not installed" }];
|
|
2193
|
+
return [a.id, await agentCatalog(a.id).catch((err) => ({ error: err.message }))];
|
|
2194
|
+
})
|
|
2195
|
+
);
|
|
2196
|
+
return send(res, 200, Object.fromEntries(entries));
|
|
2197
|
+
}
|
|
2198
|
+
if (parts[1] === "runs") {
|
|
2199
|
+
const runId = parts[2];
|
|
2200
|
+
if (!runId) {
|
|
2201
|
+
if (req.method === "GET") {
|
|
2202
|
+
if (!url.searchParams.has("limit")) return send(res, 200, fileAccess.list());
|
|
2203
|
+
const status = url.searchParams.get("status") ?? "all";
|
|
2204
|
+
const section = url.searchParams.get("section") ?? void 0;
|
|
2205
|
+
if (!HISTORY_STATUSES.includes(status)) throw new HttpError(400, "Invalid history status");
|
|
2206
|
+
if (section && !["waiting", "running", "results"].includes(section)) throw new HttpError(400, "Invalid project history section");
|
|
2207
|
+
const project = url.searchParams.get("project"), inventory = projects.list();
|
|
2208
|
+
return send(res, 200, await attempt(async () => fileAccess.page({
|
|
2209
|
+
limit: Number(url.searchParams.get("limit")),
|
|
2210
|
+
cursor: url.searchParams.get("cursor") ?? void 0,
|
|
2211
|
+
before: url.searchParams.has("before") ? Number(url.searchParams.get("before")) : void 0,
|
|
2212
|
+
query: url.searchParams.get("q") ?? void 0,
|
|
2213
|
+
status: status === "all" ? void 0 : status,
|
|
2214
|
+
section,
|
|
2215
|
+
pattern: url.searchParams.get("pattern") ?? void 0,
|
|
2216
|
+
agent: url.searchParams.get("agent") ?? void 0,
|
|
2217
|
+
repo: url.searchParams.get("repo") ?? void 0,
|
|
2218
|
+
predicate: project ? (run2) => (projectOf(run2, inventory) ?? "none") === project : void 0
|
|
2219
|
+
})));
|
|
2220
|
+
}
|
|
2221
|
+
if (req.method === "POST") {
|
|
2222
|
+
const body = await readJson(req);
|
|
2223
|
+
const run2 = await startFromBody(body);
|
|
2224
|
+
if (run2.project && body.remember === true && !body.parentRunId) {
|
|
2225
|
+
void projects.remember(run2.project, { pattern: run2.pattern, roles: requestedRoles(findPattern(run2.pattern), body.roles), roleSettings: run2.roleSettings, agentSettings: validateAgentSettings(body.agentSettings) });
|
|
2226
|
+
}
|
|
2227
|
+
return send(res, 201, { id: run2.id, project: run2.project });
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
const run = runId ? await manager.getAsync(runId) : void 0;
|
|
2231
|
+
if (!run) throw new HttpError(404, "Run not found");
|
|
2232
|
+
if (req.method === "GET" && parts[3] === "transcript" && parts.length === 4) {
|
|
2233
|
+
const abort = new AbortController();
|
|
2234
|
+
res.once("close", () => abort.abort());
|
|
2235
|
+
try {
|
|
2236
|
+
return send(res, 200, await transcriptPage(run.id, { before: url.searchParams.get("before") ?? void 0, step: url.searchParams.get("step") ?? void 0, signal: abort.signal }));
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
if (error instanceof TranscriptCursorError) throw new HttpError(409, error.message);
|
|
2239
|
+
throw error;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (parts[3] === "collaboration" && parts.length === 4) {
|
|
2243
|
+
if (req.method === "GET") return send(res, 200, await attempt(() => collaborationReviews.list(run.id)));
|
|
2244
|
+
if (req.method === "POST") {
|
|
2245
|
+
const body = await readJson(req, 16 * 1024, { timeoutMs: 1e4 });
|
|
2246
|
+
if (typeof body.branch !== "string" || !run.worktrees.some((tree) => tree.branch === body.branch)) throw new HttpError(400, "Choose an owned result branch");
|
|
2247
|
+
return send(res, 201, await attempt(() => collaborationReviews.add(run.id, { branch: body.branch, expectedHead: body.expectedHead, text: body.text }, { id: "owner", name: "Owner" }, async () => (await manager.reviewCoverage(run.id, body.branch)).head)));
|
|
2248
|
+
}
|
|
2249
|
+
throw new HttpError(405, "Use GET or POST for human collaboration comments");
|
|
2250
|
+
}
|
|
2251
|
+
if (parts[3] === "pr-threads" && parts.length === 4 && ["GET", "POST"].includes(req.method ?? "")) {
|
|
2252
|
+
const branch = url.searchParams.get("branch") ?? "", prUrl = branch ? run.prs[branch] : run.source?.kind === "pr" ? run.source.url : Object.values(run.prs).length === 1 ? Object.values(run.prs)[0] : void 0;
|
|
2253
|
+
if (!prUrl) throw new HttpError(400, "Choose a pull request stored on this run");
|
|
2254
|
+
const abort = new AbortController();
|
|
2255
|
+
res.once("close", () => abort.abort());
|
|
2256
|
+
try {
|
|
2257
|
+
if (req.method === "GET") return send(res, 200, await reviewThreads.list(run.repo, prUrl, abort.signal));
|
|
2258
|
+
const body = await readJson(req);
|
|
2259
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new HttpError(400, "Provide a review thread action object");
|
|
2260
|
+
if (body.action === "load") return send(res, 200, await reviewThreads.detail(run.repo, prUrl, { snapshotId: body.snapshotId, threadId: body.threadId }, abort.signal));
|
|
2261
|
+
if (body.action !== "resolve") throw new HttpError(400, "Choose load or an explicit resolve action");
|
|
2262
|
+
return send(res, 200, await reviewThreads.setResolved(run.repo, prUrl, body, abort.signal));
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
if (error instanceof HttpError) throw error;
|
|
2265
|
+
if (error instanceof ReviewThreadError) throw new HttpError(error.status, error.message);
|
|
2266
|
+
throw new HttpError(502, error.message);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (req.method === "GET" && parts.length === 3) return send(res, 200, run);
|
|
2270
|
+
if (req.method === "GET" && parts[3] === "evaluation") {
|
|
2271
|
+
const report = (await teamEvaluationStore.list()).find((item) => item.runId === run.id && item.at === run.endedAt);
|
|
2272
|
+
if (report) return send(res, 200, report);
|
|
2273
|
+
const journal = await loadJournal(run.id, { strict: run.pattern === "auto-team" });
|
|
2274
|
+
const memoryEntries = run.project && projects.get(run.project)?.repos.includes(run.repo) ? await memoryStore.list({ repo: run.repo, projectId: run.project, scope: "project" }, { kind: "user", repo: run.repo, projectId: run.project }) : [];
|
|
2275
|
+
return send(res, 200, buildTeamEvaluation(run, { replay: run.recovery, journal, memoryEntries }));
|
|
2276
|
+
}
|
|
2277
|
+
if (req.method === "GET" && parts[3] === "export") {
|
|
2278
|
+
return send(res, 200, redactExport({ version: 1, exportedAt: Date.now(), run, verificationMarkdown: run.verification ? renderVerificationReport(run.verification) : void 0, events: url.searchParams.get("transcript") === "1" ? await manager.events(run.id) : void 0 }));
|
|
2279
|
+
}
|
|
2280
|
+
if (parts[3] === "review-coverage") {
|
|
2281
|
+
if (req.method === "GET") return send(res, 200, await attempt(() => manager.reviewCoverage(run.id, url.searchParams.get("branch") ?? "")));
|
|
2282
|
+
if (req.method === "POST") {
|
|
2283
|
+
const body = await readJson(req);
|
|
2284
|
+
return send(res, 200, await attempt(() => manager.recordFileReview(run.id, body)));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (parts[3] === "outcome" && parts.length === 4 && req.method === "POST") {
|
|
2288
|
+
const body = await readJson(req);
|
|
2289
|
+
return send(res, 200, await attempt(() => manager.recordOutcome(run.id, body)));
|
|
2290
|
+
}
|
|
2291
|
+
if (parts[3] === "human-rework" && req.method === "POST") {
|
|
2292
|
+
const body = await readJson(req);
|
|
2293
|
+
return send(res, 201, await attempt(() => manager.recordHumanRework(run.id, body)));
|
|
2294
|
+
}
|
|
2295
|
+
if (parts[3] === "launch-profiles") {
|
|
2296
|
+
const body = req.method === "POST" ? await readJson(req) : {};
|
|
2297
|
+
const name = typeof body.worktree === "string" ? body.worktree : url.searchParams.get("worktree");
|
|
2298
|
+
const worktree = run.worktrees.find((tree) => tree.name === name);
|
|
2299
|
+
if (!worktree) throw new HttpError(400, "Choose a worktree owned by this run");
|
|
2300
|
+
const key = `${run.id}/${worktree.name}/launch`;
|
|
2301
|
+
if (req.method === "DELETE") return send(res, 200, { stopped: launchProfiles.stop(key) });
|
|
2302
|
+
const config = await readLaunchProfiles(worktree.path);
|
|
2303
|
+
if (req.method === "GET") return send(res, 200, redactExport({ ...config, session: launchProfiles.get(key) ?? null }));
|
|
2304
|
+
if (req.method === "POST") {
|
|
2305
|
+
const profile = config.profiles.find((profile2) => profile2.id === body.profile);
|
|
2306
|
+
if (!profile) throw new HttpError(400, config.error ?? "Choose a configured launch profile");
|
|
2307
|
+
const abort = new AbortController();
|
|
2308
|
+
res.once("close", () => abort.abort());
|
|
2309
|
+
try {
|
|
2310
|
+
manager.noteActivity();
|
|
2311
|
+
return send(res, 201, redactExport(await launchProfiles.start(key, worktree.path, profile, { signal: abort.signal, lockdown: run.lockdown, allowUntrusted: body.allowUntrusted === true })));
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (error instanceof LaunchProfileError) throw Object.assign(new HttpError(error.status, error.message), { confirm: error.confirm });
|
|
2314
|
+
throw new HttpError(400, error.message);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
throw new HttpError(404, "Unknown launch profile operation");
|
|
2318
|
+
}
|
|
2319
|
+
if (req.method === "GET" && parts[3] === "events") return await streamTopic(req, res, "run", run.id);
|
|
2320
|
+
if (req.method === "POST" && parts[3] === "revise-goal") {
|
|
2321
|
+
const body = await readJson(req);
|
|
2322
|
+
const revised = await attempt(() => manager.reviseGoal(run.id, { message: body.message, budget: body.budget === void 0 ? void 0 : { maxSteps: body.budget?.maxSteps, maxInvocations: body.budget?.maxInvocations, maxMinutes: body.budget?.maxMinutes } }));
|
|
2323
|
+
return send(res, 201, { id: revised.id });
|
|
2324
|
+
}
|
|
2325
|
+
if (req.method === "POST" && parts[3] === "rerun-tasks") {
|
|
2326
|
+
const body = await readJson(req);
|
|
2327
|
+
const child = await attempt(() => manager.rerunTeamTasks(run.id, body));
|
|
2328
|
+
return send(res, 201, { id: child.id });
|
|
2329
|
+
}
|
|
2330
|
+
if (req.method === "POST" && parts[3] === "pause") {
|
|
2331
|
+
const body = await readJson(req);
|
|
2332
|
+
return send(res, 200, await attempt(async () => manager.setPaused(run.id, body.paused !== false)));
|
|
2333
|
+
}
|
|
2334
|
+
if (req.method === "POST" && parts[3] === "task-priority" && parts.length === 4) {
|
|
2335
|
+
const body = await readJson(req);
|
|
2336
|
+
return send(res, 200, await attempt(async () => manager.setTaskPriority(run.id, body)));
|
|
2337
|
+
}
|
|
2338
|
+
if (req.method === "POST" && parts[3] === "priority") {
|
|
2339
|
+
const body = await readJson(req);
|
|
2340
|
+
return send(res, 200, await attempt(async () => manager.setPriority(run.id, body.priority)));
|
|
2341
|
+
}
|
|
2342
|
+
if (req.method === "POST" && parts[3] === "provider-chat" && parts.length === 4) {
|
|
2343
|
+
const body = await readJson(req, 64 * 1024);
|
|
2344
|
+
const { roles } = await validateRoles(findPattern("solo"), { agent: [body.agent] }, run.repo, run.loop.remoteWorker, run.trust);
|
|
2345
|
+
const next = await attempt(() => manager.startProviderChat(run.id, { agent: roles.agent[0], settings: body.settings, message: body.message }));
|
|
2346
|
+
return send(res, 201, { id: next.id });
|
|
2347
|
+
}
|
|
2348
|
+
if (req.method === "POST" && parts[3] === "fork") {
|
|
2349
|
+
const body = await readJson(req);
|
|
2350
|
+
const checkpoint = typeof body.commit === "string" ? body.commit : void 0;
|
|
2351
|
+
if (!checkpoint || !/^[a-f0-9]{40,64}$/i.test(checkpoint)) throw new HttpError(400, "Choose an exact checkpoint commit");
|
|
2352
|
+
const owned = await Promise.all(run.worktrees.map(async (tree) => (await git(["merge-base", "--is-ancestor", checkpoint, tree.branch], run.repo)).code === 0));
|
|
2353
|
+
if (!owned.some(Boolean)) throw new HttpError(400, "The checkpoint is not part of this run");
|
|
2354
|
+
const fork = await startFromBody({ repo: run.repo, project: run.project, pattern: run.pattern, task: typeof body.task === "string" ? body.task : run.task, baseRef: checkpoint, parentRunId: run.id, roles: run.roles, options: run.options, loop: run.loop, agentSettings: run.agentSettings, roleSettings: run.roleSettings, trust: run.trust, approvals: run.approvals });
|
|
2355
|
+
return send(res, 201, { id: fork.id });
|
|
2356
|
+
}
|
|
2357
|
+
if (req.method === "POST" && parts[3] === "cancel") return send(res, 200, { cancelled: manager.cancel(run.id) });
|
|
2358
|
+
if (req.method === "POST" && parts[3] === "resume") {
|
|
2359
|
+
await manager.resume(run.id).catch((err) => {
|
|
2360
|
+
throw new HttpError(400, err.message);
|
|
2361
|
+
});
|
|
2362
|
+
return send(res, 200, { resumed: true });
|
|
2363
|
+
}
|
|
2364
|
+
if (req.method === "POST" && parts[3] === "project") {
|
|
2365
|
+
const body = await readJson(req);
|
|
2366
|
+
if (typeof body.project !== "string" || !projects.get(body.project)) throw new HttpError(400, "Pick a project");
|
|
2367
|
+
return send(res, 200, await attempt(() => manager.setProject(run.id, body.project)));
|
|
2368
|
+
}
|
|
2369
|
+
if (req.method === "POST" && parts[3] === "seen") {
|
|
2370
|
+
manager.setSeen(run.id, true);
|
|
2371
|
+
return send(res, 200, { seen: true });
|
|
2372
|
+
}
|
|
2373
|
+
if (req.method === "DELETE" && parts.length === 3) {
|
|
2374
|
+
await attempt(() => manager.deleteRun(run.id));
|
|
2375
|
+
return send(res, 200, { deleted: true });
|
|
2376
|
+
}
|
|
2377
|
+
if (req.method === "POST" && parts[3] === "archive") {
|
|
2378
|
+
const body = await readJson(req);
|
|
2379
|
+
if (body.archived === false) {
|
|
2380
|
+
await attempt(async () => manager.setArchived(run.id, false));
|
|
2381
|
+
return send(res, 200, { archived: false });
|
|
2382
|
+
}
|
|
2383
|
+
const result = await attempt(() => manager.archive(run.id, { cleanup: body.cleanup !== false, deleteBranches: !!body.deleteBranches }));
|
|
2384
|
+
return send(res, 200, { archived: true, ...result });
|
|
2385
|
+
}
|
|
2386
|
+
if (req.method === "GET" && parts[3] === "diff") {
|
|
2387
|
+
const files = await attempt(() => manager.diff(run.id, url.searchParams.get("branch") ?? "", url.searchParams.get("last") === "1"));
|
|
2388
|
+
return send(res, 200, filterPolicyDiff(await fileAccess.forRepo(run.repo, run), files));
|
|
2389
|
+
}
|
|
2390
|
+
if (req.method === "POST" && parts[3] === "followups") {
|
|
2391
|
+
const body = await readJson(req);
|
|
2392
|
+
if (!isAgentId(body.agent)) throw new HttpError(400, "Pick an agent");
|
|
2393
|
+
const side = body.kind === "side" ? sideTurn(body.side) : void 0;
|
|
2394
|
+
await attempt(
|
|
2395
|
+
() => manager.followUp(run.id, {
|
|
2396
|
+
kind: side ? "side" : body.kind === "resolve-conflicts" ? "resolve-conflicts" : body.kind === "reply" ? "reply" : "comments",
|
|
2397
|
+
side,
|
|
2398
|
+
worktree: String(body.worktree ?? ""),
|
|
2399
|
+
agent: body.agent,
|
|
2400
|
+
settings: body.settings,
|
|
2401
|
+
trust: body.trust,
|
|
2402
|
+
tools: body.tools,
|
|
2403
|
+
message: typeof body.message === "string" ? body.message : void 0,
|
|
2404
|
+
mergeFrom: typeof body.mergeFrom === "string" ? body.mergeFrom : void 0,
|
|
2405
|
+
budget: body.budget === void 0 ? void 0 : { maxSteps: body.budget?.maxSteps, maxInvocations: body.budget?.maxInvocations, maxMinutes: body.budget?.maxMinutes },
|
|
2406
|
+
attachments: Array.isArray(body.attachments) ? body.attachments.filter((a) => a && typeof a.data === "string").map((a) => ({ name: String(a.name ?? "file"), data: a.data })) : void 0
|
|
2407
|
+
})
|
|
2408
|
+
);
|
|
2409
|
+
return send(res, 200, { ok: true });
|
|
2410
|
+
}
|
|
2411
|
+
if (parts[3] === "worktrees" && parts[4]) {
|
|
2412
|
+
const wt = run.worktrees.find((w) => w.name === parts[4]);
|
|
2413
|
+
if (!wt) throw new HttpError(404, "Unknown worktree");
|
|
2414
|
+
const key = `${run.id}/${wt.name}`;
|
|
2415
|
+
if (req.method === "POST" && parts[5] === "open") {
|
|
2416
|
+
const body = await readJson(req);
|
|
2417
|
+
const target = body.target === "editor" || body.target === "finder" ? body.target : "terminal";
|
|
2418
|
+
return send(res, 200, { openedWith: await attempt(() => openFolder(wt.path, target)) });
|
|
2419
|
+
}
|
|
2420
|
+
if (parts[5] === "serve" && parts[6] === "ready" && parts.length === 7 && req.method === "GET") return send(res, 200, { ready: await manager.devServers.ready(key), status: manager.devServers.get(key)?.status ?? "missing" });
|
|
2421
|
+
if (parts[5] === "serve") {
|
|
2422
|
+
if (req.method === "GET") return send(res, 200, manager.devServers.get(key) ?? null);
|
|
2423
|
+
if (req.method === "DELETE") return send(res, 200, { stopped: manager.devServers.stop(key) });
|
|
2424
|
+
if (req.method === "POST") {
|
|
2425
|
+
const body = await readJson(req);
|
|
2426
|
+
const command = typeof body.command === "string" && body.command.trim() || run.app?.command;
|
|
2427
|
+
if (!command) throw new HttpError(400, 'Say how to run the app (e.g. "npm run dev"), or add "run" to .codeflow/config.json');
|
|
2428
|
+
if (/[\r\n]/.test(command)) throw new HttpError(400, "The command must be a single line");
|
|
2429
|
+
if (run.lockdown && body.allowUntrusted !== true) {
|
|
2430
|
+
throw Object.assign(new HttpError(409, `${run.lockdown}. Run app starts that code on this machine, outside any sandbox or container.`), { confirm: "allowUntrusted" });
|
|
2431
|
+
}
|
|
2432
|
+
manager.noteActivity();
|
|
2433
|
+
return send(res, 200, await manager.devServers.start(key, wt.path, command, run.app?.port));
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (req.method === "POST" && parts[3] === "notes") {
|
|
2438
|
+
const body = await readJson(req);
|
|
2439
|
+
const note = await attempt(async () => typeof body.requestId === "string" ? manager.answerTeamQuestion(run.id, body.requestId, String(body.text ?? "")) : (run.team ? manager.addTeamNote.bind(manager) : manager.addNote.bind(manager))(run.id, String(body.text ?? ""), isAgentId(body.agent) ? body.agent : void 0, typeof body.memberId === "string" ? body.memberId : void 0));
|
|
2440
|
+
return send(res, 200, note);
|
|
2441
|
+
}
|
|
2442
|
+
if (req.method === "POST" && parts[3] === "answer") {
|
|
2443
|
+
const body = await readJson(req);
|
|
2444
|
+
const decision = body.decision;
|
|
2445
|
+
if (decision !== "approve" && decision !== "revise" && decision !== "reject") throw new HttpError(400, "Unknown decision");
|
|
2446
|
+
await attempt(async () => manager.answer(run.id, { decision, note: typeof body.note === "string" ? body.note : void 0 }));
|
|
2447
|
+
return send(res, 200, { ok: true });
|
|
2448
|
+
}
|
|
2449
|
+
if (req.method === "POST" && parts[3] === "team-plan") {
|
|
2450
|
+
const body = await readJson(req);
|
|
2451
|
+
return send(res, 200, { team: await attempt(() => manager.editTeamPlan(run.id, body)) });
|
|
2452
|
+
}
|
|
2453
|
+
if (req.method === "POST" && parts[3] === "merge") {
|
|
2454
|
+
const body = await readJson(req);
|
|
2455
|
+
const outcome = await attempt(
|
|
2456
|
+
() => manager.merge(run.id, { branch: String(body.branch ?? ""), into: String(body.into ?? ""), archiveAfter: !!body.archiveAfter, requireValidation: body.requireValidation === true })
|
|
2457
|
+
);
|
|
2458
|
+
return send(res, 200, outcome);
|
|
2459
|
+
}
|
|
2460
|
+
if (req.method === "POST" && parts[3] === "validate-merge") {
|
|
2461
|
+
const body = await readJson(req);
|
|
2462
|
+
const abort = new AbortController();
|
|
2463
|
+
res.once("close", () => abort.abort());
|
|
2464
|
+
try {
|
|
2465
|
+
return send(res, 200, await manager.validateMerge(run.id, { branch: String(body.branch ?? ""), into: String(body.into ?? ""), allowUntrusted: body.allowUntrusted === true }, abort.signal));
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
if (error instanceof UntrustedSourceError) throw Object.assign(new HttpError(409, error.message), { confirm: "allowUntrusted" });
|
|
2468
|
+
throw new HttpError(400, error.message);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (req.method === "GET" && parts[3] === "pr-merge") {
|
|
2472
|
+
return send(res, 200, await attempt(() => manager.prMergeInfo(run.id, url.searchParams.get("branch") ?? "")));
|
|
2473
|
+
}
|
|
2474
|
+
if (req.method === "POST" && parts[3] === "merge-pr") {
|
|
2475
|
+
const body = await readJson(req);
|
|
2476
|
+
if (!MERGE_METHODS.includes(body.method)) throw new HttpError(400, "Pick how to merge: merge, squash or rebase");
|
|
2477
|
+
const outcome = await attempt(() => manager.mergePr(run.id, { branch: String(body.branch ?? ""), method: body.method, archiveAfter: !!body.archiveAfter }));
|
|
2478
|
+
return send(res, 200, outcome);
|
|
2479
|
+
}
|
|
2480
|
+
if (req.method === "GET" && parts[3] === "pr-targets") {
|
|
2481
|
+
const targets = await attempt(async () => {
|
|
2482
|
+
const { branches } = await remoteBranches(run.repo);
|
|
2483
|
+
return { branches, initial: await manager.pullRequestTarget(run.id) };
|
|
2484
|
+
});
|
|
2485
|
+
return send(res, 200, targets);
|
|
2486
|
+
}
|
|
2487
|
+
if (req.method === "POST" && parts[3] === "pr") {
|
|
2488
|
+
const body = await readJson(req);
|
|
2489
|
+
const prUrl = await attempt(() => manager.pullRequest(run.id, { branch: String(body.branch ?? ""), into: String(body.into ?? "") }));
|
|
2490
|
+
return send(res, 200, { url: prUrl });
|
|
2491
|
+
}
|
|
2492
|
+
if (req.method === "POST" && parts[3] === "triage") {
|
|
2493
|
+
const body = await readJson(req);
|
|
2494
|
+
if (!["approve", "revise", "reject"].includes(body.decision)) throw new HttpError(400, "Unknown decision");
|
|
2495
|
+
await attempt(() => manager.triage(run.id, { decision: body.decision, note: typeof body.note === "string" ? body.note : void 0, openPr: !!body.openPr, pushPr: !!body.pushPr, budget: body.budget === void 0 ? void 0 : { maxSteps: body.budget?.maxSteps, maxInvocations: body.budget?.maxInvocations, maxMinutes: body.budget?.maxMinutes } }));
|
|
2496
|
+
return send(res, 200, { ok: true });
|
|
2497
|
+
}
|
|
2498
|
+
if (req.method === "POST" && parts[3] === "push-pr") {
|
|
2499
|
+
const body = await readJson(req);
|
|
2500
|
+
return send(res, 200, await attempt(() => manager.pushToPr(run.id, { branch: String(body.branch ?? "") })));
|
|
2501
|
+
}
|
|
2502
|
+
if (req.method === "POST" && parts[3] === "post-review") {
|
|
2503
|
+
return send(res, 200, { url: await attempt(() => manager.postReview(run.id)) });
|
|
2504
|
+
}
|
|
2505
|
+
if (req.method === "POST" && parts[3] === "watch") {
|
|
2506
|
+
const body = await readJson(req);
|
|
2507
|
+
const branch = String(body.branch ?? "");
|
|
2508
|
+
if (body.stop) {
|
|
2509
|
+
await attempt(async () => manager.unwatchPr(run.id, branch));
|
|
2510
|
+
return send(res, 200, { watching: false });
|
|
2511
|
+
}
|
|
2512
|
+
if (!isAgentId(body.agent)) throw new HttpError(400, "Pick the agent that fixes problems");
|
|
2513
|
+
const allow = Array.isArray(body.allow) ? body.allow.filter((a) => typeof a === "string").slice(0, 20) : [];
|
|
2514
|
+
const watch = await attempt(async () => manager.watchPr(run.id, { branch, agent: body.agent, maxFixes: Number(body.maxFixes) || void 0, allow }));
|
|
2515
|
+
void watcher.poll();
|
|
2516
|
+
return send(res, 200, watch);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
throw new HttpError(404, "Not found");
|
|
2520
|
+
}
|
|
2521
|
+
await manager.load();
|
|
2522
|
+
await projects.load(manager.list());
|
|
2523
|
+
await acpConnections.load().catch((error) => logError("ACP connections unavailable; repair their configuration and restart", error));
|
|
2524
|
+
await collab.load();
|
|
2525
|
+
await teamConnections.load();
|
|
2526
|
+
await fileAccess.all().catch((error) => logError("Filesystem policy is unavailable; protected operations and extension loading remain disabled until the private policy file is repaired", error));
|
|
2527
|
+
filteredTasksReady = await filteredTasks.load().then(() => true, (error) => {
|
|
2528
|
+
logError("Filtered tasks unavailable; policy activation remains blocked until task receipts are repaired", error);
|
|
2529
|
+
return false;
|
|
2530
|
+
});
|
|
2531
|
+
if (!fileAccess.hasRestrictions()) await extensions.load().catch((error) => logError("Extension configuration could not be loaded; built-in workflows remain available", error));
|
|
2532
|
+
else logWarn("Extensions deferred: custom filesystem policy cannot isolate trusted plugin initialization; clear the policy and restart to load extensions");
|
|
2533
|
+
await codeHostsApi.service.load();
|
|
2534
|
+
await automations.load();
|
|
2535
|
+
await projectTeams.load().catch((error) => {
|
|
2536
|
+
projectTeams.store.faults.set("storage", error.message);
|
|
2537
|
+
logError("Project teams disabled: their private storage cannot be read", error);
|
|
2538
|
+
});
|
|
2539
|
+
await deferredRuns.load();
|
|
2540
|
+
await coordinatedApi.service.load();
|
|
2541
|
+
await coordinatedApi.recover().catch((error) => logError("A coordinated launch requires manual recovery", error));
|
|
2542
|
+
await recoverDeferredRuns(deferredRuns, manager).catch((error) => logError("A deferred launch requires manual recovery", error));
|
|
2543
|
+
remote = await loadRemote();
|
|
2544
|
+
if (!tlsConfigured) {
|
|
2545
|
+
try {
|
|
2546
|
+
const certificate = await loadRemoteCertificate(void 0, true);
|
|
2547
|
+
if (certificate) {
|
|
2548
|
+
savedCertificate = validateRemoteCertificate(certificate, Date.now(), true);
|
|
2549
|
+
phoneTls = { cert: certificate.cert, key: certificate.key };
|
|
2550
|
+
phoneProtocol = "https";
|
|
2551
|
+
}
|
|
2552
|
+
} catch (error) {
|
|
2553
|
+
tlsLoadError = error.message;
|
|
2554
|
+
logWarn(tlsLoadError);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
await certificateRenewal.load().catch((error) => logWarn(error.message));
|
|
2558
|
+
await phoneSessions.load().catch((error) => logError("Phone pairing disabled: session storage cannot be read", error));
|
|
2559
|
+
if (notificationsReady) {
|
|
2560
|
+
projectTeams.store.subscribe(() => {
|
|
2561
|
+
for (const team of projectTeams.list()) void notifications.observeProjectTeam(team).catch((error) => logError("Project team notification could not be retained", error));
|
|
2562
|
+
});
|
|
2563
|
+
manager.subscribeAll((run) => {
|
|
2564
|
+
if (!run.deleted) void notifications.observe(run).catch((error) => logError("Notification event could not be retained", error));
|
|
2565
|
+
});
|
|
2566
|
+
approvals.subscribe(() => {
|
|
2567
|
+
for (const request of approvals.list()) {
|
|
2568
|
+
const run = request.status === "pending" ? manager.get(request.runId) : void 0;
|
|
2569
|
+
if (run) void notifications.observe(run, `tool-approval:${request.id}`).catch((error) => logError("Approval notification could not be retained", error));
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
if (!fileAccess.hasRestrictions()) startPolicyBackground();
|
|
2574
|
+
else await pausePolicyBackground();
|
|
2575
|
+
teamConnections.start();
|
|
2576
|
+
await teamTerminals.load().then(() => teamTerminals.start()).catch((error) => logError("Team terminals disabled: repair their access storage", error));
|
|
2577
|
+
await hostExecution.load().then(() => hostExecution.start()).catch((error) => logError("Host execution disabled: repair its queue storage", error));
|
|
2578
|
+
const teamPruneTimer = setInterval(() => {
|
|
2579
|
+
if (collab.available) void collab.prune().catch((error) => logError("Team board housekeeping failed", error));
|
|
2580
|
+
}, 60 * 6e4);
|
|
2581
|
+
teamPruneTimer.unref();
|
|
2582
|
+
if (!process.env.CODEFLOW_SSH_HOST) void (async () => {
|
|
2583
|
+
if (remote.transport === "tailscale") await refreshTailscale();
|
|
2584
|
+
await syncLanServer();
|
|
2585
|
+
await renewRemoteCertificate();
|
|
2586
|
+
})().catch((error) => logError("Remote access startup failed", error));
|
|
2587
|
+
const tailscaleTimer = setInterval(() => {
|
|
2588
|
+
if (!process.env.CODEFLOW_SSH_HOST && remote.enabled && remote.lan) {
|
|
2589
|
+
void (remote.transport === "tailscale" ? refreshTailscale() : Promise.resolve()).then(() => syncLanServer()).catch((err) => logError("Tailscale access refresh failed", err));
|
|
2590
|
+
}
|
|
2591
|
+
}, 1e4);
|
|
2592
|
+
tailscaleTimer.unref();
|
|
2593
|
+
const certificateRenewalTimer = setInterval(() => {
|
|
2594
|
+
void renewRemoteCertificate().catch((error) => logError("Certificate renewal failed", error));
|
|
2595
|
+
}, 6e4);
|
|
2596
|
+
certificateRenewalTimer.unref();
|
|
2597
|
+
function listener(via) {
|
|
2598
|
+
return (req, res) => {
|
|
2599
|
+
handle(req, res, via).catch((err) => {
|
|
2600
|
+
const status = err instanceof HttpError ? err.status : 500;
|
|
2601
|
+
if (status >= 500) logError(`${req.method} ${req.url?.split("?")[0]} failed`, err);
|
|
2602
|
+
if (res.headersSent) return res.end();
|
|
2603
|
+
send(res, status, { error: err.message, confirm: err.confirm, signIn: err.signIn });
|
|
2604
|
+
}).finally(() => fileAccess.end(req));
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
const server = createServer({ maxHeaderSize: MAX_HEADER_SIZE }, listener("local")).listen(PORT, HOST, async () => {
|
|
2608
|
+
const agents = fileAccess.hasRestrictions() ? [] : await trackPolicyPreparation(() => agentAvailability());
|
|
2609
|
+
const runs = manager.list();
|
|
2610
|
+
console.log(`code-flow running at http://localhost:${PORT} (${runs.length} runs loaded from ${CODEFLOW_HOME})`);
|
|
2611
|
+
console.log(` Open http://localhost:${PORT}/?token=${auth.token} (the first browser to open code-flow signs in without it)`);
|
|
2612
|
+
if (process.env.CODEFLOW_LAUNCHER === "1" && process.connected)
|
|
2613
|
+
process.send?.({ type: "codeflow-ready", url: `http://localhost:${PORT}/?token=${auth.token}` });
|
|
2614
|
+
for (const a of agents) console.log(` ${a.available ? "\u2713" : "\u2717"} ${a.displayName} (${a.binary})`);
|
|
2615
|
+
const gitHealth = await checkGit();
|
|
2616
|
+
if (!gitHealth.ok) {
|
|
2617
|
+
console.log(` \u2717 ${gitHealth.warning}`);
|
|
2618
|
+
logWarn(gitHealth.warning ?? "git check failed");
|
|
2619
|
+
}
|
|
2620
|
+
const interrupted = runs.filter((r) => r.status === "interrupted").length;
|
|
2621
|
+
if (interrupted) console.log(` ${interrupted} interrupted run(s) can be resumed from the UI`);
|
|
2622
|
+
});
|
|
2623
|
+
let listenAttempts = 0;
|
|
2624
|
+
server.on("error", (err) => {
|
|
2625
|
+
if (err.code === "EADDRINUSE" && ++listenAttempts <= 40) {
|
|
2626
|
+
if (listenAttempts === 1) console.log(`Port ${PORT} is busy; waiting for it to be released\u2026`);
|
|
2627
|
+
setTimeout(() => server.listen(PORT, HOST), 500);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
console.error(err.code === "EADDRINUSE" ? `Port ${PORT} is still in use. Is code-flow already running?` : err);
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
});
|
|
2633
|
+
async function startComparison(body) {
|
|
2634
|
+
const variants = Array.isArray(body.variants) ? body.variants.slice(0, 6) : [];
|
|
2635
|
+
if (variants.length < 2) throw new HttpError(400, "Add at least two setups to compare");
|
|
2636
|
+
const repeat = Math.min(3, Math.max(1, Math.round(Number(body.repeat) || 1)));
|
|
2637
|
+
if (variants.length * repeat > 9) throw new HttpError(400, "That's more than 9 runs; compare fewer setups or repeat less");
|
|
2638
|
+
const base = body.run && typeof body.run === "object" ? body.run : {};
|
|
2639
|
+
const inputs = await Promise.all(
|
|
2640
|
+
variants.map(async (v, i) => {
|
|
2641
|
+
const label = String(v.label ?? "").trim().slice(0, 40) || `Setup ${i + 1}`;
|
|
2642
|
+
return {
|
|
2643
|
+
label,
|
|
2644
|
+
input: await buildStartInput({ ...base, task: v.task ?? base.task, pattern: v.pattern ?? base.pattern, roles: v.roles ?? base.roles, options: v.options ?? base.options, roleSettings: v.roleSettings ?? base.roleSettings })
|
|
2645
|
+
};
|
|
2646
|
+
})
|
|
2647
|
+
);
|
|
2648
|
+
const group = { id: `g${Date.now().toString(36)}${randomBytes(2).toString("hex")}` };
|
|
2649
|
+
const started = [];
|
|
2650
|
+
for (let n = 1; n <= repeat; n++) {
|
|
2651
|
+
for (const { label, input } of inputs) {
|
|
2652
|
+
const run = await manager.start({ ...input, group: { id: group.id, label: repeat > 1 ? `${label} #${n}` : label } }).catch((err) => {
|
|
2653
|
+
throw new HttpError(400, `${label}: ${err.message}`);
|
|
2654
|
+
});
|
|
2655
|
+
started.push(run.id);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return { group: group.id, runs: started };
|
|
2659
|
+
}
|
|
2660
|
+
function runsIn(project) {
|
|
2661
|
+
const all = manager.list();
|
|
2662
|
+
if (typeof project !== "string" || !project) return all;
|
|
2663
|
+
const list = projects.list();
|
|
2664
|
+
return all.filter((r) => (projectOf(r, list) ?? "none") === project);
|
|
2665
|
+
}
|
|
2666
|
+
async function diskUsage(all, repo) {
|
|
2667
|
+
const runs = all.filter((r) => !repo || r.repo === repo);
|
|
2668
|
+
const sizes = await folderSizes(runs.map((r) => r.worktreeDir));
|
|
2669
|
+
const list = runs.filter((r) => sizes[r.worktreeDir] !== void 0).map((r) => ({ id: r.id, task: r.task.split("\n")[0].slice(0, 100), repo: r.repo, status: r.status, archived: r.archived, endedAt: r.endedAt, bytes: sizes[r.worktreeDir] })).sort((a, b) => b.bytes - a.bytes);
|
|
2670
|
+
return { total: list.reduce((n, r) => n + r.bytes, 0), runs: list };
|
|
2671
|
+
}
|
|
2672
|
+
async function cleanUp(all, days, repo) {
|
|
2673
|
+
const cutoff = Date.now() - days * 864e5;
|
|
2674
|
+
const done = [];
|
|
2675
|
+
let removed = 0;
|
|
2676
|
+
for (const r of all) {
|
|
2677
|
+
if (repo && r.repo !== repo) continue;
|
|
2678
|
+
if (r.status === "running" || !r.endedAt || r.endedAt > cutoff || !r.seen) continue;
|
|
2679
|
+
if (r.triage?.state === "pending" || Object.values(r.watches).some((w) => ["watching", "fixing", "green"].includes(w.state))) continue;
|
|
2680
|
+
const result = await removeWorktrees(r.repo, r.worktrees, false).catch((err) => ({ removed: [], deletedBranches: [], failed: [err.message] }));
|
|
2681
|
+
if (result.failed.length) manager.log(r.id, `Clean-up couldn't remove everything: ${result.failed.join("; ")}`, "warn");
|
|
2682
|
+
if (!result.removed.length) continue;
|
|
2683
|
+
removed += result.removed.length;
|
|
2684
|
+
done.push(r.id);
|
|
2685
|
+
if (!r.archived) manager.setArchived(r.id, true);
|
|
2686
|
+
manager.log(r.id, `Cleaned up: removed ${result.removed.length} worktree(s); branches kept`);
|
|
2687
|
+
}
|
|
2688
|
+
return { runs: done.length, worktrees: removed };
|
|
2689
|
+
}
|
|
2690
|
+
let stopping = false;
|
|
2691
|
+
async function stop(reason, code = 0) {
|
|
2692
|
+
if (stopping) process.exit(1);
|
|
2693
|
+
stopping = true;
|
|
2694
|
+
performanceMetrics.close();
|
|
2695
|
+
await closeCodeIntelligence();
|
|
2696
|
+
console.log(`
|
|
2697
|
+
Stopping code-flow (${reason}): interrupting running runs so they can be resumed\u2026`);
|
|
2698
|
+
logInfo(`Stopping: ${reason}`);
|
|
2699
|
+
let automationsSaved = Promise.resolve();
|
|
2700
|
+
try {
|
|
2701
|
+
server.close();
|
|
2702
|
+
remoteStopping = true;
|
|
2703
|
+
clearInterval(tailscaleTimer);
|
|
2704
|
+
clearInterval(certificateRenewalTimer);
|
|
2705
|
+
clearInterval(teamPruneTimer);
|
|
2706
|
+
lanServer?.close();
|
|
2707
|
+
lanServer?.closeAllConnections();
|
|
2708
|
+
closeLanConnections();
|
|
2709
|
+
watcher.stop();
|
|
2710
|
+
approvals.close();
|
|
2711
|
+
launchProfiles.stopAll();
|
|
2712
|
+
shells.closeAll();
|
|
2713
|
+
previews.closeAll();
|
|
2714
|
+
sshPreviews.closeAll();
|
|
2715
|
+
automationsSaved = automations.stop();
|
|
2716
|
+
} catch {
|
|
2717
|
+
}
|
|
2718
|
+
await teamTerminals.close().catch((err) => logError("Team terminal shutdown failed", err));
|
|
2719
|
+
await sshProjects?.close();
|
|
2720
|
+
await Promise.race([teamConnections.close(), new Promise((resolve) => setTimeout(resolve, 5e3).unref())]).catch((err) => logError("Team runner shutdown failed", err));
|
|
2721
|
+
await hostExecution.close().catch((err) => logError("Host execution shutdown failed", err));
|
|
2722
|
+
await collab.close().catch((err) => logError("Team board shutdown failed", err));
|
|
2723
|
+
await projectTeams.stop().catch((err) => logError("Project team shutdown failed", err));
|
|
2724
|
+
await coordinatedApi.service.stop().catch((err) => logError("Coordinated changes shutdown failed", err));
|
|
2725
|
+
await deferredRuns.stop().catch((err) => logError("Deferred queue shutdown failed", err));
|
|
2726
|
+
docsHelp.close();
|
|
2727
|
+
for (const controller of improvingControllers) controller.abort();
|
|
2728
|
+
await filteredTasks.close().catch((err) => logError("Filtered task shutdown failed; inspect its durable unknown receipt before retrying", err));
|
|
2729
|
+
await manager.shutdown().catch((err) => logError("Shutdown failed", err));
|
|
2730
|
+
await teamToolBridge.close().catch(() => void 0);
|
|
2731
|
+
await automationsSaved;
|
|
2732
|
+
await notifications.stop();
|
|
2733
|
+
process.exit(code);
|
|
2734
|
+
}
|
|
2735
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
2736
|
+
process.on(signal, () => void stop(signal));
|
|
2737
|
+
}
|
|
2738
|
+
if (process.env.CODEFLOW_LAUNCHER === "1") process.on("disconnect", () => void stop("launcher disconnected"));
|