@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
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { resolveRunPermissions } from "../run-permissions.js";
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { getAdapter } from "../adapters/index.js";
|
|
7
|
+
import { validateAgentContainer } from "../agent-container.js";
|
|
8
|
+
import { ensureCodeflowExcludes, git, resolveCommit } from "../git.js";
|
|
9
|
+
import { redactExport } from "../privacy.js";
|
|
10
|
+
import { verificationIntegrity } from "../verification-integrity.js";
|
|
11
|
+
import { registerPrivateEnvNames } from "../env.js";
|
|
12
|
+
import { assertTransferTree, boundedBundle, disabledCheckoutFilters, gitChecked, sha256, writeContext } from "./bundles.js";
|
|
13
|
+
import { CAPABILITY, MAX_EVENTS_BYTES, REQUEST_ID, RemoteWorkerError, validateWorkerRequest } from "./protocol.js";
|
|
14
|
+
import { durableWorkerDirectory } from "./durability.js";
|
|
15
|
+
const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
const hashCapability = (value) => createHash("sha256").update(value).digest();
|
|
17
|
+
async function loadWorkerConfig(path) {
|
|
18
|
+
registerPrivateEnvNames(["CODEFLOW_WORKER_CONFIG_FILE", "CODEFLOW_REMOTE_WORKERS_FILE", "CODEFLOW_REMOTE_WORKERS_JSON"]);
|
|
19
|
+
if (!isAbsolute(path)) throw new RemoteWorkerError("Worker configuration path must be absolute");
|
|
20
|
+
const info = await lstat(path);
|
|
21
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 128 * 1024) throw new RemoteWorkerError("Worker configuration must be a regular file under 128 KiB");
|
|
22
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
23
|
+
if (!object(raw) || raw.version !== 1 || typeof raw.root !== "string" || !isAbsolute(raw.root) || !object(raw.repos) || Object.keys(raw.repos).length > 20 || !Array.isArray(raw.providers) || !raw.providers.length || raw.providers.some((id) => !["claude", "codex", "agy"].includes(id))) throw new RemoteWorkerError("Invalid explicitly provisioned worker configuration");
|
|
24
|
+
const repos = /* @__PURE__ */ Object.create(null);
|
|
25
|
+
for (const [id, path2] of Object.entries(raw.repos)) {
|
|
26
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(id) || typeof path2 !== "string" || !isAbsolute(path2) || !(await lstat(path2)).isDirectory()) throw new RemoteWorkerError("Worker repository mappings need regular, existing absolute Git directories");
|
|
27
|
+
repos[id] = await realpath(path2);
|
|
28
|
+
await resolveCommit(repos[id]);
|
|
29
|
+
}
|
|
30
|
+
const allowedTrust = raw.allowedTrust ?? ["safe", "standard"];
|
|
31
|
+
if (!Array.isArray(allowedTrust) || !allowedTrust.length || allowedTrust.some((value) => !["safe", "standard", "trusted", "full"].includes(value))) throw new RemoteWorkerError("Invalid worker permission policy");
|
|
32
|
+
if (raw.containers !== void 0 && (!object(raw.containers) || Object.keys(raw.containers).length > 20)) throw new RemoteWorkerError("Worker containers must be at most 20 explicit named profiles");
|
|
33
|
+
const containers = /* @__PURE__ */ Object.create(null);
|
|
34
|
+
for (const [id, value] of Object.entries(raw.containers ?? {})) {
|
|
35
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(id)) throw new RemoteWorkerError("Invalid worker container profile ID");
|
|
36
|
+
containers[id] = validateAgentContainer(value);
|
|
37
|
+
}
|
|
38
|
+
await durableWorkerDirectory(raw.root);
|
|
39
|
+
if (!(await lstat(raw.root)).isDirectory()) throw new RemoteWorkerError("Worker root must be a regular directory");
|
|
40
|
+
return { version: 1, root: await realpath(raw.root), repos, providers: [...new Set(raw.providers)], allowedTrust, containers };
|
|
41
|
+
}
|
|
42
|
+
class ProvisionedWorker {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
controllers = /* @__PURE__ */ new Map();
|
|
47
|
+
async providerAvailable(id, profile) {
|
|
48
|
+
if (!profile) return getAdapter(id).available();
|
|
49
|
+
if (!Object.hasOwn(this.config.containers, profile)) throw new RemoteWorkerError("Choose an explicit worker-local container profile");
|
|
50
|
+
const container = this.config.containers[profile], name = `codeflow-probe-${randomBytes(8).toString("hex")}`;
|
|
51
|
+
const invoke = (args, timeout) => new Promise((done) => execFile("docker", args, { timeout, killSignal: "SIGKILL", maxBuffer: 64 * 1024 }, (error) => done(!error)));
|
|
52
|
+
try {
|
|
53
|
+
return await invoke(["run", "--rm", "--pull=never", "--name", name, "--network=none", "--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--entrypoint", getAdapter(id).binary, container.image, "--version"], 1e4);
|
|
54
|
+
} finally {
|
|
55
|
+
await invoke(["rm", "-f", name], 5e3);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async capabilities(containerProfile, discoverModels = false) {
|
|
59
|
+
if (containerProfile !== void 0 && (!/^[a-z][a-z0-9-]{0,63}$/.test(containerProfile) || !Object.hasOwn(this.config.containers, containerProfile))) throw new RemoteWorkerError("Choose an explicit worker-local container profile");
|
|
60
|
+
return { protocol: 1, providers: await Promise.all(this.config.providers.map(async (id) => ({ id, available: await this.providerAvailable(id, containerProfile), modelCatalog: discoverModels ? await getAdapter(id).discoverModels?.({ cwd: this.config.root, agentContainer: containerProfile ? this.config.containers[containerProfile] : void 0 }).catch(() => void 0) : void 0 }))), repoMappings: Object.keys(this.config.repos), containerProfiles: Object.keys(this.config.containers), resumeSessions: false, liveTeamTools: false, interactiveApprovals: false, stagedContext: true, maximumBundleBytes: 64 * 1024 * 1024, maximumContextBytes: 16 * 1024 * 1024 };
|
|
61
|
+
}
|
|
62
|
+
repo(id) {
|
|
63
|
+
const path = Object.hasOwn(this.config.repos, id) ? this.config.repos[id] : void 0;
|
|
64
|
+
if (!path) throw new RemoteWorkerError("Choose an explicitly provisioned worker repository mapping");
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
directory(id) {
|
|
68
|
+
if (!REQUEST_ID.test(id)) throw new RemoteWorkerError("Invalid worker request identity");
|
|
69
|
+
return join(this.config.root, "requests", id);
|
|
70
|
+
}
|
|
71
|
+
async save(receipt) {
|
|
72
|
+
const folder = this.directory(receipt.requestId), temp = join(folder, `receipt-${randomBytes(8).toString("hex")}.tmp`);
|
|
73
|
+
const handle = await open(temp, "wx", 384);
|
|
74
|
+
try {
|
|
75
|
+
await handle.writeFile(JSON.stringify(receipt));
|
|
76
|
+
await handle.sync();
|
|
77
|
+
} finally {
|
|
78
|
+
await handle.close();
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await rename(temp, join(folder, "receipt.json"));
|
|
82
|
+
await this.syncDirectory(folder);
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(temp, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async syncDirectory(path) {
|
|
88
|
+
const handle = await open(path, "r");
|
|
89
|
+
try {
|
|
90
|
+
await handle.sync();
|
|
91
|
+
} finally {
|
|
92
|
+
await handle.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async acquireSlot(id) {
|
|
96
|
+
const root = join(this.config.root, "slots");
|
|
97
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
98
|
+
for (let slot = 0; slot < 4; slot++) {
|
|
99
|
+
const file = join(root, String(slot));
|
|
100
|
+
let handle;
|
|
101
|
+
try {
|
|
102
|
+
handle = await open(file, "wx", 384);
|
|
103
|
+
await handle.writeFile(JSON.stringify({ id, pid: process.pid }));
|
|
104
|
+
await handle.sync();
|
|
105
|
+
return async () => {
|
|
106
|
+
await rm(file, { force: true });
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code !== "EEXIST") throw error;
|
|
110
|
+
} finally {
|
|
111
|
+
await handle?.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new RemoteWorkerError("Worker has four active or unresolved execution slots. Inspect stale worker processes before removing their slot files.");
|
|
115
|
+
}
|
|
116
|
+
publicReceipt(receipt) {
|
|
117
|
+
const { capabilityHash: _hash, pid: _pid, ...value } = receipt;
|
|
118
|
+
return redactExport(value);
|
|
119
|
+
}
|
|
120
|
+
async receipt(id, capability) {
|
|
121
|
+
if (!CAPABILITY.test(capability)) throw new RemoteWorkerError("Invalid worker request capability");
|
|
122
|
+
const file = join(this.directory(id), "receipt.json"), info = await lstat(file).catch(() => void 0);
|
|
123
|
+
if (!info) return { version: 1, requestId: id, state: "unknown", reason: "No durable receipt is available. Do not automatically repeat this execution." };
|
|
124
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 2 * 1024 * 1024) throw new RemoteWorkerError("Worker receipt is damaged");
|
|
125
|
+
const saved = JSON.parse(await readFile(file, "utf8"));
|
|
126
|
+
if (saved.version !== 1 || saved.requestId !== id || !/^[a-f0-9]{64}$/.test(saved.capabilityHash) || !timingSafeEqual(Buffer.from(saved.capabilityHash, "hex"), hashCapability(capability))) throw new RemoteWorkerError("This capability does not own the worker receipt");
|
|
127
|
+
const value = this.publicReceipt(saved);
|
|
128
|
+
if (["accepted", "running"].includes(value.state) && Date.now() - (value.heartbeatAt ?? value.updatedAt ?? 0) > 15e3) {
|
|
129
|
+
value.state = "unknown";
|
|
130
|
+
value.reason = "Worker heartbeat expired; the execution outcome is ambiguous. Inspect or cancel it before explicitly retrying.";
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
async cancel(id, capability) {
|
|
135
|
+
const receipt = await this.receipt(id, capability);
|
|
136
|
+
if (["completed", "failed", "cancelled"].includes(receipt.state)) return receipt;
|
|
137
|
+
if (!receipt.createdAt) return { ...receipt, reason: "No execution receipt was found; nothing was cancelled. Do not automatically repeat an ambiguous request." };
|
|
138
|
+
await writeFile(join(this.directory(id), "cancel"), "cancel\n", { flag: "w", mode: 384 });
|
|
139
|
+
this.controllers.get(id)?.abort();
|
|
140
|
+
return receipt;
|
|
141
|
+
}
|
|
142
|
+
async capacity() {
|
|
143
|
+
const root = join(this.config.root, "requests");
|
|
144
|
+
await durableWorkerDirectory(root);
|
|
145
|
+
const entries = await readdir(root);
|
|
146
|
+
if (entries.length >= 100) throw new RemoteWorkerError("Worker keeps at most 100 request receipts. Archive inspected terminal request directories before accepting more work.");
|
|
147
|
+
}
|
|
148
|
+
/** The exclusive receipt lock is acquired before any provider launches and is never reused. */
|
|
149
|
+
async execute(raw, source, context, emit, heartbeat, signal) {
|
|
150
|
+
const request = validateWorkerRequest(raw), repo = this.repo(request.repoMapping);
|
|
151
|
+
const fingerprint = sha256(JSON.stringify({ ...request, capability: void 0, source: void 0 }));
|
|
152
|
+
if (!this.config.providers.includes(request.agent) || !await this.providerAvailable(request.agent, request.containerProfile)) throw new RemoteWorkerError("The selected official CLI is unavailable in this worker execution profile");
|
|
153
|
+
if (!this.config.allowedTrust.includes(request.trust)) throw new RemoteWorkerError("The requested permission level is outside this worker's explicit policy");
|
|
154
|
+
if (request.containerProfile && !Object.hasOwn(this.config.containers, request.containerProfile)) throw new RemoteWorkerError("Choose an explicit worker-local container profile; host credential volumes are never forwarded");
|
|
155
|
+
await this.capacity();
|
|
156
|
+
const folder = this.directory(request.id);
|
|
157
|
+
await mkdir(folder, { recursive: true, mode: 448 });
|
|
158
|
+
let lock;
|
|
159
|
+
try {
|
|
160
|
+
lock = await open(join(folder, "execution.lock"), "wx", 384);
|
|
161
|
+
await lock.writeFile(request.id);
|
|
162
|
+
await lock.sync();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.code !== "EEXIST") throw error;
|
|
165
|
+
const receipt = await this.receipt(request.id, request.capability);
|
|
166
|
+
if (receipt.fingerprint && receipt.fingerprint !== fingerprint) throw new RemoteWorkerError("This request identity belongs to a different scoped execution");
|
|
167
|
+
return { receipt, result: receipt.result ?? { ok: false, finalText: "", error: "This request was already accepted; inspect its receipt instead of duplicating it", errorKind: "other" } };
|
|
168
|
+
} finally {
|
|
169
|
+
await lock?.close();
|
|
170
|
+
}
|
|
171
|
+
const saved = { version: 1, requestId: request.id, state: "accepted", fingerprint, capabilityHash: hashCapability(request.capability).toString("hex"), createdAt: Date.now(), updatedAt: Date.now(), heartbeatAt: Date.now(), pid: process.pid };
|
|
172
|
+
await this.syncDirectory(folder);
|
|
173
|
+
await this.syncDirectory(join(this.config.root, "requests"));
|
|
174
|
+
await this.save(saved);
|
|
175
|
+
heartbeat(this.publicReceipt(saved));
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
this.controllers.set(request.id, controller);
|
|
178
|
+
const abort = () => controller.abort();
|
|
179
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
180
|
+
if (signal?.aborted) abort();
|
|
181
|
+
const timeout = setTimeout(abort, request.timeoutMs);
|
|
182
|
+
timeout.unref();
|
|
183
|
+
let ticking = false, eventsBytes = 0, artifact;
|
|
184
|
+
const tick = setInterval(() => {
|
|
185
|
+
if (ticking) return;
|
|
186
|
+
ticking = true;
|
|
187
|
+
void (async () => {
|
|
188
|
+
if (await lstat(join(folder, "cancel")).then(() => true, () => false)) controller.abort();
|
|
189
|
+
saved.heartbeatAt = Date.now();
|
|
190
|
+
saved.updatedAt = Date.now();
|
|
191
|
+
await this.save(saved);
|
|
192
|
+
heartbeat(this.publicReceipt(saved));
|
|
193
|
+
})().catch(abort).finally(() => {
|
|
194
|
+
ticking = false;
|
|
195
|
+
});
|
|
196
|
+
}, 2e3);
|
|
197
|
+
tick.unref();
|
|
198
|
+
const checkout = join(folder, "checkout"), importRef = `refs/codeflow/imports/${request.id}`;
|
|
199
|
+
let imported = false, attached = false;
|
|
200
|
+
let releaseSlot;
|
|
201
|
+
let result = { ok: false, finalText: "", error: "Remote execution did not complete", errorKind: "other" };
|
|
202
|
+
try {
|
|
203
|
+
releaseSlot = await this.acquireSlot(request.id);
|
|
204
|
+
if (request.source) {
|
|
205
|
+
if (!source || source.length !== request.source.size || sha256(source) !== request.source.sha256) throw new RemoteWorkerError("Uploaded source bundle failed its manifest");
|
|
206
|
+
const file = join(folder, "source.bundle");
|
|
207
|
+
await writeFile(file, source, { flag: "wx", mode: 384 });
|
|
208
|
+
if (await gitChecked(repo, ["bundle", "list-heads", file]) !== `${request.baseCommit} ${request.source.ref}`) throw new RemoteWorkerError("Source bundle has unexpected references");
|
|
209
|
+
await gitChecked(repo, ["bundle", "verify", file]);
|
|
210
|
+
if (!(await git(["show-ref", "--verify", "--quiet", importRef], repo)).code) throw new RemoteWorkerError("Worker import identity is already claimed");
|
|
211
|
+
await gitChecked(repo, ["fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", file, `${request.source.ref}:${importRef}`]);
|
|
212
|
+
imported = true;
|
|
213
|
+
}
|
|
214
|
+
if (await resolveCommit(repo, request.baseCommit) !== request.baseCommit) throw new RemoteWorkerError("The provisioned mapping lacks the exact requested commit");
|
|
215
|
+
await assertTransferTree(repo, request.baseCommit);
|
|
216
|
+
const filters = await disabledCheckoutFilters(repo);
|
|
217
|
+
await ensureCodeflowExcludes(repo);
|
|
218
|
+
await gitChecked(repo, [...filters, "worktree", "add", "--detach", checkout, request.baseCommit]);
|
|
219
|
+
attached = true;
|
|
220
|
+
for (const file of request.context) {
|
|
221
|
+
const data = context.get(file.path);
|
|
222
|
+
if (!data || data.length !== file.size || sha256(data) !== file.sha256) throw new RemoteWorkerError("Uploaded context failed its manifest");
|
|
223
|
+
await writeContext(checkout, file.path, data);
|
|
224
|
+
}
|
|
225
|
+
const integrity = await verificationIntegrity(checkout, controller.signal);
|
|
226
|
+
saved.state = "running";
|
|
227
|
+
saved.updatedAt = Date.now();
|
|
228
|
+
await this.save(saved);
|
|
229
|
+
heartbeat(this.publicReceipt(saved));
|
|
230
|
+
if (controller.signal.aborted || await lstat(join(folder, "cancel")).then(() => true, () => false)) {
|
|
231
|
+
controller.abort();
|
|
232
|
+
throw new RemoteWorkerError("Remote execution was cancelled before invoking its provider");
|
|
233
|
+
}
|
|
234
|
+
const permissions = request.managedPermissions ? resolveRunPermissions({ policy: request.managedPermissions, provider: request.agent, mode: request.mode, currentTrust: request.trust, currentTools: request.managedPermissions.toolApprovals, unattended: true, container: !!request.containerProfile }) : void 0;
|
|
235
|
+
result = await getAdapter(request.agent).run({ permissions, cwd: checkout, prompt: request.prompt, mode: request.mode, model: request.model, effort: request.effort, trust: permissions?.trust ?? request.trust, interactiveApprovals: permissions?.toolApprovals ?? false, timeoutMs: request.timeoutMs, idleTimeoutMs: request.idleTimeoutMs, signal: controller.signal, jsonSchema: request.jsonSchema, questionProtocol: request.questionProtocol, ...request.containerProfile ? { agentContainer: this.config.containers[request.containerProfile] } : {} }, (event) => {
|
|
236
|
+
if (event.kind === "session") return;
|
|
237
|
+
const data = redactExport(event), bytes = Buffer.byteLength(JSON.stringify(data));
|
|
238
|
+
eventsBytes += bytes;
|
|
239
|
+
if (bytes > 256 * 1024 || eventsBytes > MAX_EVENTS_BYTES) {
|
|
240
|
+
controller.abort();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
emit(data);
|
|
244
|
+
});
|
|
245
|
+
result = redactExport({ ...result, sessionId: void 0, finalText: result.finalText.slice(0, 512 * 1024) });
|
|
246
|
+
if (JSON.stringify(result).length > 1024 * 1024) result = { ok: false, finalText: "", error: "Worker result exceeded its bounded response", errorKind: "other" };
|
|
247
|
+
if (request.mode === "read") {
|
|
248
|
+
await integrity.assert("read-only agent turn");
|
|
249
|
+
if ((await git(["status", "--porcelain", "--untracked-files=all"], checkout)).stdout.trim()) throw new RemoteWorkerError("The read-only worker turn modified its checkout; its changes were discarded");
|
|
250
|
+
} else {
|
|
251
|
+
if (await resolveCommit(checkout) !== request.baseCommit) throw new RemoteWorkerError("The agent changed checkout history instead of returning work for its scoped commit");
|
|
252
|
+
await gitChecked(checkout, [...await disabledCheckoutFilters(checkout), "add", "--all", "--", "."]);
|
|
253
|
+
const changed = await git(["diff", "--cached", "--quiet", "--exit-code"], checkout);
|
|
254
|
+
if (changed.code > 1) throw new RemoteWorkerError("Cannot inspect remote staged changes");
|
|
255
|
+
if (changed.code === 1) {
|
|
256
|
+
await assertTransferTree(checkout, await gitChecked(checkout, ["write-tree"]));
|
|
257
|
+
await gitChecked(checkout, [...await disabledCheckoutFilters(checkout), "-c", "user.name=code-flow worker", "-c", "user.email=worker@code-flow.invalid", "commit", "-m", `Remote request ${request.id}`]);
|
|
258
|
+
const head = await resolveCommit(checkout);
|
|
259
|
+
await assertTransferTree(checkout, head);
|
|
260
|
+
const ref = `refs/codeflow/results/${request.id}`, file = join(folder, "result.bundle");
|
|
261
|
+
await gitChecked(checkout, ["update-ref", ref, head, "0".repeat(head.length)]);
|
|
262
|
+
let data;
|
|
263
|
+
try {
|
|
264
|
+
data = await boundedBundle(checkout, file, [ref, `^${request.baseCommit}`]);
|
|
265
|
+
} finally {
|
|
266
|
+
await gitChecked(checkout, ["update-ref", "-d", ref, head]);
|
|
267
|
+
}
|
|
268
|
+
artifact = { requestId: request.id, baseCommit: request.baseCommit, head, size: data.length, sha256: sha256(data), ref, file };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (controller.signal.aborted) result = { ok: false, finalText: result.finalText, error: "Remote execution was cancelled or exceeded its deadline", errorKind: "cancelled" };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
result = { ok: false, finalText: result.finalText, error: redactExport(error.message).slice(0, 2e3), errorKind: controller.signal.aborted ? "cancelled" : "other" };
|
|
274
|
+
} finally {
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
clearInterval(tick);
|
|
277
|
+
signal?.removeEventListener("abort", abort);
|
|
278
|
+
this.controllers.delete(request.id);
|
|
279
|
+
while (ticking) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
280
|
+
if (attached) await gitChecked(repo, ["worktree", "remove", "--force", checkout]).catch(() => void 0);
|
|
281
|
+
if (imported) await gitChecked(repo, ["update-ref", "-d", importRef, request.baseCommit]).catch(() => void 0);
|
|
282
|
+
await rm(checkout, { recursive: true, force: true });
|
|
283
|
+
await releaseSlot?.();
|
|
284
|
+
}
|
|
285
|
+
if (controller.signal.aborted) {
|
|
286
|
+
artifact = void 0;
|
|
287
|
+
await rm(join(folder, "result.bundle"), { force: true });
|
|
288
|
+
}
|
|
289
|
+
saved.state = controller.signal.aborted ? "cancelled" : result.ok ? "completed" : "failed";
|
|
290
|
+
saved.result = result;
|
|
291
|
+
saved.updatedAt = Date.now();
|
|
292
|
+
saved.heartbeatAt = Date.now();
|
|
293
|
+
if (artifact) {
|
|
294
|
+
const { file: _file, ...manifest } = artifact;
|
|
295
|
+
saved.artifact = manifest;
|
|
296
|
+
}
|
|
297
|
+
await this.save(saved);
|
|
298
|
+
return { receipt: this.publicReceipt(saved), result, artifact };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export {
|
|
302
|
+
ProvisionedWorker,
|
|
303
|
+
loadWorkerConfig
|
|
304
|
+
};
|
package/src/remote.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { networkInterfaces } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { cantRunMessage } from "./checks.js";
|
|
6
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
7
|
+
import { logError } from "./log.js";
|
|
8
|
+
import { projectOf } from "./projects.js";
|
|
9
|
+
const FILE = join(CODEFLOW_HOME, "remote.json");
|
|
10
|
+
const validToken = (t) => typeof t === "string" && /^[0-9a-f]{32,}$/.test(t);
|
|
11
|
+
const fresh = () => ({ enabled: false, token: newToken(), controlToken: newToken(), lan: false, control: false });
|
|
12
|
+
async function loadRemote(file = FILE) {
|
|
13
|
+
unreadable.delete(file);
|
|
14
|
+
let text;
|
|
15
|
+
try {
|
|
16
|
+
text = await readFile(file, "utf8");
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code !== "ENOENT") {
|
|
19
|
+
logError(`Couldn't read ${file}; phone access is off until it can be read`, err);
|
|
20
|
+
unreadable.set(file, err.message);
|
|
21
|
+
}
|
|
22
|
+
return fresh();
|
|
23
|
+
}
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
raw = void 0;
|
|
29
|
+
}
|
|
30
|
+
if (!raw || typeof raw !== "object" || !validToken(raw.token)) {
|
|
31
|
+
const aside = `${file}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
32
|
+
await rename(file, aside).then(
|
|
33
|
+
() => logError(`${file} couldn't be read: moved it to ${aside}; phone access is off`),
|
|
34
|
+
(err) => logError(`${file} couldn't be read, nor moved aside; phone access is off`, err)
|
|
35
|
+
);
|
|
36
|
+
return fresh();
|
|
37
|
+
}
|
|
38
|
+
const settings = {
|
|
39
|
+
enabled: raw.enabled === true,
|
|
40
|
+
token: raw.token,
|
|
41
|
+
// Saved before full control had a secret of its own: phones sign in again, once.
|
|
42
|
+
controlToken: validToken(raw.controlToken) && raw.controlToken !== raw.token ? raw.controlToken : newToken(),
|
|
43
|
+
// Unknown transports must not silently become a listener on every LAN interface.
|
|
44
|
+
lan: raw.lan === true && (raw.transport === void 0 || raw.transport === "lan" || raw.transport === "tailscale"),
|
|
45
|
+
transport: raw.transport === "tailscale" ? "tailscale" : "lan",
|
|
46
|
+
control: raw.control === true,
|
|
47
|
+
...Number.isSafeInteger(raw.pairingUntil) && raw.pairingUntil > 0 ? { pairingUntil: raw.pairingUntil } : {}
|
|
48
|
+
};
|
|
49
|
+
if (settings.controlToken !== raw.controlToken) {
|
|
50
|
+
await saveRemote(settings, file).catch((err) => logError(`Couldn't save ${file}; the full-control link changes at the next start`, err));
|
|
51
|
+
}
|
|
52
|
+
return settings;
|
|
53
|
+
}
|
|
54
|
+
const unreadable = /* @__PURE__ */ new Map();
|
|
55
|
+
async function saveRemote(settings, file = FILE) {
|
|
56
|
+
const why = unreadable.get(file);
|
|
57
|
+
if (why) throw new Error(`Phone settings can't be changed: ${file} couldn't be read when code-flow started (${why}). Fix its permissions, then restart code-flow.`);
|
|
58
|
+
await mkdir(dirname(file), { recursive: true });
|
|
59
|
+
const tmp = `${file}.tmp`;
|
|
60
|
+
await writeFile(tmp, JSON.stringify(settings, null, 2), { mode: 384 });
|
|
61
|
+
await rename(tmp, file);
|
|
62
|
+
}
|
|
63
|
+
function changeRemote(before, patch) {
|
|
64
|
+
const next = { ...before };
|
|
65
|
+
if (typeof patch.enabled === "boolean") next.enabled = patch.enabled;
|
|
66
|
+
if (typeof patch.lan === "boolean") next.lan = patch.lan;
|
|
67
|
+
if (patch.transport === "lan" || patch.transport === "tailscale") next.transport = patch.transport;
|
|
68
|
+
if (typeof patch.control === "boolean") next.control = patch.control;
|
|
69
|
+
const turnedOff = (key) => before[key] && !next[key];
|
|
70
|
+
const changedTransport = (before.transport ?? "lan") !== (next.transport ?? "lan");
|
|
71
|
+
if (patch.rotate || turnedOff("enabled") || turnedOff("lan") || changedTransport) {
|
|
72
|
+
next.token = newToken();
|
|
73
|
+
next.controlToken = newToken();
|
|
74
|
+
} else if (turnedOff("control")) {
|
|
75
|
+
next.controlToken = newToken();
|
|
76
|
+
}
|
|
77
|
+
const wasControl = before.enabled && before.lan && before.control;
|
|
78
|
+
const isControl = next.enabled && next.lan && next.control;
|
|
79
|
+
if (isControl && (patch.rotate || changedTransport || !wasControl)) next.pairingUntil = Date.now() + 10 * 6e4;
|
|
80
|
+
else if (!isControl) delete next.pairingUntil;
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
83
|
+
function newToken() {
|
|
84
|
+
return randomBytes(24).toString("hex");
|
|
85
|
+
}
|
|
86
|
+
function tokenMatches(given, token) {
|
|
87
|
+
const a = Buffer.from(given);
|
|
88
|
+
const b = Buffer.from(token);
|
|
89
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
90
|
+
}
|
|
91
|
+
function lanAddresses() {
|
|
92
|
+
return Object.values(networkInterfaces()).flat().filter((i) => i && i.family === "IPv4" && !i.internal).map((i) => i.address);
|
|
93
|
+
}
|
|
94
|
+
function isOwnAddress(address) {
|
|
95
|
+
if (!address) return true;
|
|
96
|
+
const plain = address.replace(/^::ffff:/i, "").replace(/%.*$/, "").toLowerCase();
|
|
97
|
+
if (plain === "::1" || plain === "::" || plain === "0.0.0.0" || plain.startsWith("127.")) return true;
|
|
98
|
+
return Object.values(networkInterfaces()).flat().some((i) => !!i && i.address.replace(/%.*$/, "").toLowerCase() === plain);
|
|
99
|
+
}
|
|
100
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
101
|
+
const STATUS = {
|
|
102
|
+
running: "Running",
|
|
103
|
+
done: "Done",
|
|
104
|
+
failed: "Failed",
|
|
105
|
+
cancelled: "Cancelled",
|
|
106
|
+
stopped: "Stopped at limit",
|
|
107
|
+
interrupted: "Interrupted"
|
|
108
|
+
};
|
|
109
|
+
function badge(run) {
|
|
110
|
+
if (run.awaiting || run.status === "running" && run.team?.awaitingRequestId) return `<span class="b warn">Waiting for you</span>`;
|
|
111
|
+
if (run.team?.status === "blocked") return `<span class="b warn">Team needs attention</span>`;
|
|
112
|
+
if (run.triage?.state === "pending") return `<span class="b warn">In your inbox</span>`;
|
|
113
|
+
const watch = Object.values(run.watches ?? {}).find((w) => w.state === "needs-you");
|
|
114
|
+
if (watch) return `<span class="b warn">PR needs you</span>`;
|
|
115
|
+
const failing = run.status === "done" && run.verdict === "failing";
|
|
116
|
+
if (run.status === "done" && !failing && run.checkCantRun) return `<span class="b warn">Done \xB7 check can't run</span>`;
|
|
117
|
+
const cls = run.status === "running" ? "run" : run.status === "done" && !failing ? "ok" : "bad";
|
|
118
|
+
return `<span class="b ${cls}">${esc(failing ? "Done \xB7 check failing" : STATUS[run.status] ?? run.status)}</span>`;
|
|
119
|
+
}
|
|
120
|
+
const ago = (ts) => {
|
|
121
|
+
const s = Math.round((Date.now() - ts) / 1e3);
|
|
122
|
+
return s < 60 ? "just now" : s < 3600 ? `${Math.floor(s / 60)}m ago` : s < 86400 ? `${Math.floor(s / 3600)}h ago` : `${Math.floor(s / 86400)}d ago`;
|
|
123
|
+
};
|
|
124
|
+
function page(title, body, refresh = true) {
|
|
125
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
126
|
+
${refresh ? '<meta http-equiv="refresh" content="15">' : ""}<meta name="referrer" content="no-referrer"><meta name="robots" content="noindex">
|
|
127
|
+
<title>${esc(title)}</title><style>
|
|
128
|
+
:root{--bg:#f8f7f3;--raised:#fdfcf9;--sunken:#f0efe9;--text:#292a26;--muted:#65675e;--border:#e4e2da;--border-strong:#d4d2c7;--link:#2d6a4b;--mint:#36b98a;--ok:#2d6a4b;--ok-soft:#e5f4ea;--warn:#87662e;--warn-soft:#f6efdd;--bad:#a44a36;--bad-soft:#f7e9e4;--shadow:0 1px 2px rgba(34,35,24,.05);color-scheme:light}
|
|
129
|
+
@media (prefers-color-scheme:dark){:root{--bg:#161713;--raised:#1e1f1a;--sunken:#121310;--text:#ecebe4;--muted:#9c9d92;--border:#2e3029;--border-strong:#3d4036;--link:#8fe3c4;--mint:#4fd0a0;--ok:#8fe3c4;--ok-soft:#19362a;--warn:#eac77f;--warn-soft:#3a2f18;--bad:#f0a08d;--bad-soft:#3d201a;--shadow:0 1px 2px rgba(0,0,0,.3);color-scheme:dark}}
|
|
130
|
+
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
131
|
+
main{max-width:720px;margin:0 auto;padding:16px 16px calc(32px + env(safe-area-inset-bottom))}
|
|
132
|
+
h1{font:400 23px/1.2 Georgia,"Times New Roman",serif;letter-spacing:-.5px;margin:8px 0 12px;overflow-wrap:anywhere}h2,h3{font-size:12px;font-weight:500;color:var(--muted);margin:24px 0 8px}
|
|
133
|
+
h2.project{font:400 20px/1.25 Georgia,"Times New Roman",serif;letter-spacing:-.3px;color:var(--text);margin:32px 0 0;padding-top:20px;border-top:1px solid var(--border)}
|
|
134
|
+
section:first-of-type h2.project{margin-top:20px;padding-top:0;border-top:0}h2.project+h3{margin-top:12px}
|
|
135
|
+
a{color:var(--link);text-decoration:none;font-weight:500}a:hover{text-decoration:underline}
|
|
136
|
+
.card{display:block;background:var(--raised);border:1px solid var(--border);border-radius:12px;padding:12px 16px;margin:8px 0;color:var(--text);font-weight:400;box-shadow:var(--shadow)}
|
|
137
|
+
.card:hover{border-color:var(--border-strong);text-decoration:none}
|
|
138
|
+
.t{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.m{font-size:12px;color:var(--muted)}.card .m{margin-top:4px}
|
|
139
|
+
.b{display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 8px;border-radius:999px;font-size:12px;font-weight:500;line-height:1;vertical-align:1px;background:var(--sunken);color:var(--text)}
|
|
140
|
+
.b.ok,.b.run{background:var(--ok-soft);color:var(--ok)}.b.warn{background:var(--warn-soft);color:var(--warn)}.b.bad{background:var(--bad-soft);color:var(--bad)}
|
|
141
|
+
.b.run:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--mint)}
|
|
142
|
+
pre{white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--sunken);border:1px solid var(--border);border-radius:8px;padding:10px 12px;margin:8px 0}
|
|
143
|
+
ul,ol{padding-left:20px}li{margin:6px 0}li b{font-weight:600}.foot{font-size:12px;color:var(--muted);margin-top:32px}
|
|
144
|
+
@media (min-width:640px){h1{font-size:26px}}
|
|
145
|
+
</style></head><body><main>${body}<p class="foot">Read-only view of code-flow \xB7 refreshes every 15 s</p></main></body></html>`;
|
|
146
|
+
}
|
|
147
|
+
function renderRemote(manager, projects, base, path) {
|
|
148
|
+
if (path === "" || path === "/") {
|
|
149
|
+
const runs = manager.list().filter((r) => !r.archived);
|
|
150
|
+
const needs = runs.filter((r) => r.awaiting || r.status === "running" && r.team?.awaitingRequestId || r.team?.status === "blocked" || r.triage?.state === "pending" || Object.values(r.watches ?? {}).some((w) => w.state === "needs-you") || r.status !== "running" && !r.seen);
|
|
151
|
+
const running = runs.filter((r) => r.status === "running" && !needs.includes(r));
|
|
152
|
+
const recent = runs.filter((r) => !needs.includes(r) && !running.includes(r)).slice(0, 15);
|
|
153
|
+
const card = (r) => `<a class="card" href="${base}/run/${esc(r.id)}"><div class="t">${esc(r.task.split("\n")[0])}</div><div class="m">${badge(r)} \xB7 ${esc(r.pattern)} \xB7 ${esc(r.repo.split("/").pop())}${r.trigger ? ` \xB7 \u23F1 ${esc(r.trigger.name)}` : ""} \xB7 ${ago(r.createdAt)}</div></a>`;
|
|
154
|
+
const group = (tag, title, list) => list.length ? `<${tag}>${title} (${list.length})</${tag}>${list.map(card).join("")}` : "";
|
|
155
|
+
const groups = (tag, keep = () => true) => group(tag, "Needs you", needs.filter(keep)) + group(tag, "Running", running.filter(keep)) + group(tag, "Recent", recent.filter(keep));
|
|
156
|
+
const ids = [...new Set([...needs, ...running, ...recent].map((r) => projectOf(r, projects)))].sort((a, b) => Number(a === void 0) - Number(b === void 0));
|
|
157
|
+
const name = (id) => projects.find((p) => p.id === id)?.name ?? "No project";
|
|
158
|
+
const body = ids.length === 1 && ids[0] === void 0 ? groups("h2") : ids.map((id) => `<section><h2 class="project">${esc(name(id))}</h2>${groups("h3", (r) => projectOf(r, projects) === id)}</section>`).join("");
|
|
159
|
+
return page("code-flow", `<h1>code-flow</h1>${body}${runs.length ? "" : '<p class="m">No runs yet.</p>'}`);
|
|
160
|
+
}
|
|
161
|
+
const match = path.match(/^\/run\/([\w-]+)$/);
|
|
162
|
+
const run = match ? manager.get(match[1]) : void 0;
|
|
163
|
+
if (!run) return null;
|
|
164
|
+
return page(run.task.split("\n")[0].slice(0, 60), renderRun(run, base, projects.find((p) => p.id === projectOf(run, projects))), run.status === "running");
|
|
165
|
+
}
|
|
166
|
+
function renderRun(run, base, project) {
|
|
167
|
+
const steps = run.steps.map((s) => `<li>${s.status === "done" ? "\u2713" : s.status === "running" ? "\u2026" : s.status === "failed" ? "\u2715" : "\u25A0"} ${esc(s.title)} <span class="m">${esc(s.agent ?? s.kind)}${s.endedAt ? ` \xB7 ${Math.round((s.endedAt - s.startedAt) / 1e3)}s` : ""}</span></li>`).join("");
|
|
168
|
+
const review = run.review ? `<h2>Review</h2><p>${esc(run.review.verdict === "approve" ? "Approve" : "Changes requested")}${run.review.summary ? ` \u2014 ${esc(run.review.summary)}` : ""}</p><ol>${run.review.findings.map((f) => `<li><b>${esc(f.severity)}</b> ${esc(f.file)} \u2014 ${esc(f.problem)} <span class="m">(${esc(f.raisedBy.join(", "))})</span></li>`).join("")}</ol>` : "";
|
|
169
|
+
const watches = Object.values(run.watches ?? {}).map((w) => `<li>${esc(w.pr)} \u2014 <b>${esc(w.state)}</b>: ${esc(w.detail)}</li>`).join("");
|
|
170
|
+
const artifacts = run.artifacts.map((a) => `<li><b>${esc(a.label)}</b> <span class="m">${esc(a.branch)}</span>${a.stat ? `<pre>${esc(a.stat)}</pre>` : ""}</li>`).join("");
|
|
171
|
+
const team = run.team;
|
|
172
|
+
const memberName = (id) => id === "user" ? "You" : team?.members.find((member) => member.id === id)?.name ?? id;
|
|
173
|
+
const teamView = team ? [
|
|
174
|
+
`<h2>Team and workflow</h2><p>${esc(team.status)} \xB7 revision ${team.revision} \xB7 ${team.metrics.invocations} lifetime calls (${team.metrics.coordinatorInvocations} coordinating)</p>`,
|
|
175
|
+
`<ul>${team.tasks.map((task) => `<li><b>${esc(task.title)}</b> \xB7 ${esc(memberName(task.owner))} \xB7 ${esc(task.status ?? "planned")}${task.dependsOn.length ? `<br><span class="m">Needs ${task.dependsOn.map((id) => esc(team.tasks.find((item) => item.id === id)?.title ?? id)).join(", ")}</span>` : ""}</li>`).join("")}</ul>`,
|
|
176
|
+
team.messages.some((message) => message.kind === "question" && message.status === "open") ? `<h2>Open questions</h2><ul>${team.messages.filter((message) => message.kind === "question" && message.status === "open").map((message) => `<li><b>${esc(memberName(message.sender))} \u2192 ${esc(memberName(message.recipient))}</b>: ${esc(message.body)}${message.recipient === "user" ? `<br><span class="m">Open the full-control app or code-flow on your computer to answer.</span>` : ""}${message.deadlineAt ? `<br><span class="m">Deadline ${esc(new Date(message.deadlineAt).toLocaleString())}</span>` : ""}</li>`).join("")}</ul>` : "",
|
|
177
|
+
team.gaps.length ? `<h2>Unresolved work</h2><ul>${team.gaps.map((gap) => `<li>${esc(gap)}</li>`).join("")}</ul>` : "",
|
|
178
|
+
team.verification ? `<p class="m">Verification: ${esc(team.verification.check)} \xB7 independent review ${team.verification.reviewed ? team.verification.approved ? "approved" : "changes requested" : "pending"} \xB7 ${esc(team.verification.head.slice(0, 12))}</p>` : ""
|
|
179
|
+
].join("") : "";
|
|
180
|
+
return [
|
|
181
|
+
`<p><a href="${base}/">\u2190 All runs</a></p>`,
|
|
182
|
+
`<h1>${esc(run.task.split("\n")[0])}</h1>`,
|
|
183
|
+
`<p class="m">${badge(run)} \xB7 ${esc(run.pattern)} \xB7 ${project && project.name !== run.repo.split("/").pop() ? `${esc(project.name)} \xB7 ` : ""}${esc(run.repo.split("/").pop())} \xB7 started ${ago(run.createdAt)}${run.trigger ? ` \xB7 by ${run.trigger.via === "sweep" ? "sweep" : "automation"} \u201C${esc(run.trigger.name)}\u201D` : ""}</p>`,
|
|
184
|
+
run.awaiting ? `<h2>Waiting for you</h2><p>${esc(run.awaiting.title)} \u2014 open code-flow on your computer to answer.</p><pre>${esc(run.awaiting.content.slice(0, 4e3))}</pre>` : "",
|
|
185
|
+
run.triage?.state === "pending" ? "<h2>In your inbox</h2><p>Approve, revise or reject it in code-flow on your computer.</p>" : "",
|
|
186
|
+
run.error ? `<h2>Error</h2><pre>${esc(run.error)}</pre>` : "",
|
|
187
|
+
run.checkCantRun ? `<h2>Check</h2><p>${esc(cantRunMessage(run.checkCantRun.reason))}.</p>` : "",
|
|
188
|
+
teamView,
|
|
189
|
+
review || (run.summary ? `<h2>Summary</h2><pre>${esc(run.summary.slice(0, 8e3))}</pre>` : ""),
|
|
190
|
+
artifacts ? `<h2>Results</h2><ul>${artifacts}</ul>` : "",
|
|
191
|
+
watches ? `<h2>Pull requests</h2><ul>${watches}</ul>` : "",
|
|
192
|
+
steps ? `<h2>Steps</h2><ul>${steps}</ul>` : ""
|
|
193
|
+
].join("");
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
changeRemote,
|
|
197
|
+
isOwnAddress,
|
|
198
|
+
lanAddresses,
|
|
199
|
+
loadRemote,
|
|
200
|
+
newToken,
|
|
201
|
+
renderRemote,
|
|
202
|
+
saveRemote,
|
|
203
|
+
tokenMatches
|
|
204
|
+
};
|