@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,141 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { git, repoRoot } from "./git.js";
|
|
5
|
+
import { readEditableFile } from "./workspace.js";
|
|
6
|
+
import { assertContextPathAllowed } from "./context-inspection.js";
|
|
7
|
+
import { Slots } from "./slots.js";
|
|
8
|
+
const slots = new Slots(1);
|
|
9
|
+
const CODE_INTELLIGENCE_LIMITS = { files: 500, bytes: 8 * 1024 * 1024, hits: 200, timeoutMs: 1e4, memoryMb: 192 };
|
|
10
|
+
const pool = /* @__PURE__ */ new Map();
|
|
11
|
+
const codeIntelligenceHealth = { requests: 0, reused: 0, failures: 0 };
|
|
12
|
+
async function closeCodeIntelligence() {
|
|
13
|
+
const entries = [...pool.values()];
|
|
14
|
+
pool.clear();
|
|
15
|
+
await Promise.all(entries.map(async (entry) => {
|
|
16
|
+
clearTimeout(entry.idle);
|
|
17
|
+
await entry.worker.terminate();
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
async function discard(root, entry) {
|
|
21
|
+
clearTimeout(entry.idle);
|
|
22
|
+
if (pool.get(root) === entry) pool.delete(root);
|
|
23
|
+
await entry.worker.terminate();
|
|
24
|
+
}
|
|
25
|
+
async function workerFor(root) {
|
|
26
|
+
const cached = pool.get(root);
|
|
27
|
+
if (cached) {
|
|
28
|
+
codeIntelligenceHealth.reused++;
|
|
29
|
+
clearTimeout(cached.idle);
|
|
30
|
+
pool.delete(root);
|
|
31
|
+
pool.set(root, cached);
|
|
32
|
+
cached.worker.ref();
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
if (pool.size >= 2) {
|
|
36
|
+
const [key, oldest] = pool.entries().next().value;
|
|
37
|
+
await discard(key, oldest);
|
|
38
|
+
}
|
|
39
|
+
const worker = new Worker(new URL("./code-intelligence-worker.mjs", import.meta.url), { workerData: { root }, resourceLimits: { maxOldGenerationSizeMb: CODE_INTELLIGENCE_LIMITS.memoryMb } });
|
|
40
|
+
const entry = { worker, sources: /* @__PURE__ */ new Map() };
|
|
41
|
+
pool.set(root, entry);
|
|
42
|
+
worker.on("error", () => {
|
|
43
|
+
codeIntelligenceHealth.failures++;
|
|
44
|
+
if (pool.get(root) === entry) pool.delete(root);
|
|
45
|
+
});
|
|
46
|
+
worker.once("exit", () => {
|
|
47
|
+
clearTimeout(entry.idle);
|
|
48
|
+
if (pool.get(root) === entry) pool.delete(root);
|
|
49
|
+
});
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
async function inspect(repo, request, signal) {
|
|
53
|
+
const release = await slots.acquire(signal);
|
|
54
|
+
let entry, root = "", succeeded = false;
|
|
55
|
+
codeIntelligenceHealth.requests++;
|
|
56
|
+
try {
|
|
57
|
+
root = await realpath(await repoRoot(repo));
|
|
58
|
+
const listed = await git(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], root, { signal, timeoutMs: 1e4 });
|
|
59
|
+
if (listed.code) throw new Error("Cannot list sources for the language service");
|
|
60
|
+
const candidates = [...new Set(listed.stdout.split("\0").filter((path) => /\.[cm]?[jt]sx?$/.test(path) && !/(^|\/)(?:node_modules|vendor|dist|build|coverage|\.git)(\/|$)/.test(path)))].sort();
|
|
61
|
+
const sources = [];
|
|
62
|
+
let bytes = 0, truncated = false;
|
|
63
|
+
for (const path of candidates) {
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
if (sources.length >= CODE_INTELLIGENCE_LIMITS.files) {
|
|
66
|
+
truncated = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
assertContextPathAllowed(path);
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const file = await readEditableFile(root, path).catch(() => void 0);
|
|
75
|
+
if (!file) continue;
|
|
76
|
+
if (bytes + file.bytes > CODE_INTELLIGENCE_LIMITS.bytes) {
|
|
77
|
+
truncated = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
bytes += file.bytes;
|
|
81
|
+
sources.push({ absolute: join(root, path), text: file.text });
|
|
82
|
+
}
|
|
83
|
+
const operation = request.kind === "references" ? { ...request, file: join(root, assertContextPathAllowed(request.file)) } : request;
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
entry = await workerFor(root);
|
|
86
|
+
const current = new Map(sources.map((file) => [file.absolute, file.text]));
|
|
87
|
+
const changed = sources.filter((file) => entry.sources.get(file.absolute) !== file.text);
|
|
88
|
+
const removed = [...entry.sources.keys()].filter((name) => !current.has(name));
|
|
89
|
+
const worker = entry.worker;
|
|
90
|
+
const result = await new Promise((accept, reject) => {
|
|
91
|
+
const finish = (error, result2) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
signal?.removeEventListener("abort", abort);
|
|
94
|
+
worker.off("message", message);
|
|
95
|
+
worker.off("error", fail);
|
|
96
|
+
worker.off("exit", exited);
|
|
97
|
+
error ? reject(error) : accept(result2);
|
|
98
|
+
};
|
|
99
|
+
const abort = () => finish(new Error("Language service inspection cancelled"));
|
|
100
|
+
const fail = (error) => finish(error);
|
|
101
|
+
const exited = () => finish(new Error("Language service exited before returning its bounded result"));
|
|
102
|
+
const message = (value) => value.error ? finish(new Error(value.error)) : finish(void 0, value);
|
|
103
|
+
const timer = setTimeout(() => finish(new Error("Language service inspection exceeded its time limit; narrow the repository")), CODE_INTELLIGENCE_LIMITS.timeoutMs);
|
|
104
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
105
|
+
worker.once("message", message);
|
|
106
|
+
worker.once("error", fail);
|
|
107
|
+
worker.once("exit", exited);
|
|
108
|
+
if (signal?.aborted) abort();
|
|
109
|
+
else worker.postMessage({ sources: changed, removed, request: operation });
|
|
110
|
+
});
|
|
111
|
+
entry.sources = current;
|
|
112
|
+
succeeded = true;
|
|
113
|
+
return { ...result, scanned: sources.length, truncated: truncated || result.truncated };
|
|
114
|
+
} finally {
|
|
115
|
+
if (entry) {
|
|
116
|
+
if (!succeeded) await discard(root, entry);
|
|
117
|
+
else {
|
|
118
|
+
const cached = entry;
|
|
119
|
+
entry.worker.unref();
|
|
120
|
+
entry.idle = setTimeout(() => void discard(root, cached), 6e4);
|
|
121
|
+
entry.idle.unref();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
release();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function searchSemanticSymbols(repo, query, signal) {
|
|
128
|
+
if (typeof query !== "string" || !query.trim() || query.length > 300) throw new Error("Enter a symbol query of 1\u2013300 characters");
|
|
129
|
+
return inspect(repo, { kind: "symbols", query: query.trim() }, signal);
|
|
130
|
+
}
|
|
131
|
+
function findCodeReferences(repo, input, signal) {
|
|
132
|
+
if (!Number.isInteger(input.line) || input.line < 1 || !Number.isInteger(input.column) || input.column < 1) throw new Error("Reference lookup needs a one-based line and column");
|
|
133
|
+
return inspect(repo, { kind: "references", file: input.path, line: input.line, column: input.column }, signal);
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
CODE_INTELLIGENCE_LIMITS,
|
|
137
|
+
closeCodeIntelligence,
|
|
138
|
+
codeIntelligenceHealth,
|
|
139
|
+
findCodeReferences,
|
|
140
|
+
searchSemanticSymbols
|
|
141
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { HttpError, readJson, readLoginJson, send } from "../http.js";
|
|
5
|
+
import { SharedAccountError } from "../shared-accounts.js";
|
|
6
|
+
import { ATTEMPT_ID, CHECKPOINT_ID, CollabError, MAX, PATH_ID, RUNNER_TOKEN, SHA256_HEX, actorRole, roleAtLeast, validateText } from "./model.js";
|
|
7
|
+
const looksLikeRunnerToken = (bearer) => typeof bearer === "string" && RUNNER_TOKEN.test(bearer);
|
|
8
|
+
const isTeamApiPath = (parts) => parts[0] === "api" && parts[1] === "team";
|
|
9
|
+
function runnerAllowed(route) {
|
|
10
|
+
return route !== void 0 && route.principals.includes("runner");
|
|
11
|
+
}
|
|
12
|
+
const KIB = 1024;
|
|
13
|
+
const TEAM_BODY_LIMIT = { small: 16 * KIB, task: 64 * KIB, progress: 192 * KIB, manifest: 1280 * KIB, connect: 8 * KIB };
|
|
14
|
+
const ALL = ["owner", "browser", "runner"], EXECUTING = ["owner", "runner"], OWNER_ONLY = ["owner"], NOBODY = [];
|
|
15
|
+
const ID = Symbol("id");
|
|
16
|
+
const r = (method, path, op, principals = ALL, bodyLimit) => bodyLimit === void 0 ? { method, path, op, principals } : { method, path, op, principals, bodyLimit };
|
|
17
|
+
const ROUTES = [
|
|
18
|
+
r("POST", ["runners", "connect"], "connect", NOBODY, TEAM_BODY_LIMIT.connect),
|
|
19
|
+
r("GET", ["me"], "me"),
|
|
20
|
+
r("GET", ["board"], "board"),
|
|
21
|
+
r("GET", ["events"], "events"),
|
|
22
|
+
r("GET", ["tasks", ID], "task"),
|
|
23
|
+
r("POST", ["tasks"], "task-create", ALL, TEAM_BODY_LIMIT.task),
|
|
24
|
+
r("PUT", ["tasks", ID], "task-update", ALL, TEAM_BODY_LIMIT.task),
|
|
25
|
+
r("DELETE", ["tasks", ID], "task-delete", ALL, TEAM_BODY_LIMIT.small),
|
|
26
|
+
r("POST", ["tasks", ID, "comments"], "comment", ALL, TEAM_BODY_LIMIT.small),
|
|
27
|
+
r("POST", ["tasks", ID, "decisions"], "decision", ALL, TEAM_BODY_LIMIT.small),
|
|
28
|
+
r("POST", ["tasks", ID, "review-request"], "review-request", ALL, TEAM_BODY_LIMIT.small),
|
|
29
|
+
r("POST", ["tasks", ID, "reviews"], "review", ALL, TEAM_BODY_LIMIT.small),
|
|
30
|
+
r("POST", ["tasks", ID, "instructions"], "instruction-propose", ALL, TEAM_BODY_LIMIT.small),
|
|
31
|
+
r("POST", ["tasks", ID, "instructions", ID], "instruction-decide", ALL, TEAM_BODY_LIMIT.small),
|
|
32
|
+
r("POST", ["tasks", ID, "attempts"], "attempt-claim", EXECUTING, TEAM_BODY_LIMIT.small),
|
|
33
|
+
r("POST", ["tasks", ID, "handoff"], "handoff-offer", ALL, TEAM_BODY_LIMIT.small),
|
|
34
|
+
r("POST", ["tasks", ID, "handoff", ID], "handoff-respond", ALL, TEAM_BODY_LIMIT.small),
|
|
35
|
+
r("POST", ["attempts", ID, "progress"], "attempt-progress", EXECUTING, TEAM_BODY_LIMIT.progress),
|
|
36
|
+
r("POST", ["attempts", ID, "heartbeat"], "attempt-heartbeat", EXECUTING),
|
|
37
|
+
r("POST", ["attempts", ID, "finish"], "attempt-finish", EXECUTING, TEAM_BODY_LIMIT.small),
|
|
38
|
+
r("POST", ["attempts", ID, "checkpoints"], "checkpoint-add", EXECUTING, TEAM_BODY_LIMIT.manifest),
|
|
39
|
+
r("POST", ["attempts", ID, "recover"], "attempt-recover", ALL, TEAM_BODY_LIMIT.small),
|
|
40
|
+
r("GET", ["checkpoints", ID], "checkpoint"),
|
|
41
|
+
r("DELETE", ["checkpoints", ID], "checkpoint-delete"),
|
|
42
|
+
r("PUT", ["checkpoints", ID, "bundle"], "bundle-upload", EXECUTING, MAX.bundle),
|
|
43
|
+
r("GET", ["checkpoints", ID, "bundle"], "bundle-download"),
|
|
44
|
+
r("GET", ["inbox"], "inbox"),
|
|
45
|
+
r("POST", ["inbox", "read"], "inbox-read", ALL, TEAM_BODY_LIMIT.small),
|
|
46
|
+
r("GET", ["runners"], "runners"),
|
|
47
|
+
r("DELETE", ["runners", ID], "runner-revoke"),
|
|
48
|
+
r("PUT", ["projects", ID, "repositories"], "repositories", ALL, TEAM_BODY_LIMIT.task),
|
|
49
|
+
r("PUT", ["profile"], "profile", OWNER_ONLY, TEAM_BODY_LIMIT.small)
|
|
50
|
+
];
|
|
51
|
+
const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
52
|
+
const idOk = (part) => part !== void 0 && PATH_ID.test(part) && !UNSAFE_KEYS.includes(part);
|
|
53
|
+
function teamRoutePolicy(method, parts) {
|
|
54
|
+
if (!isTeamApiPath(parts) || parts.length > 6) return void 0;
|
|
55
|
+
const rest = parts.slice(2);
|
|
56
|
+
for (const spec of ROUTES) {
|
|
57
|
+
if (spec.method !== method || spec.path.length !== rest.length || !spec.path.every((segment, index) => segment === ID ? idOk(rest[index]) : segment === rest[index])) continue;
|
|
58
|
+
const route = { op: spec.op, principals: [...spec.principals] };
|
|
59
|
+
if (spec.bodyLimit !== void 0) route.bodyLimit = spec.bodyLimit;
|
|
60
|
+
return route;
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
function denied(route) {
|
|
65
|
+
if (!route.principals.length) return new HttpError(403, "Connect a computer without a session or runner credential: POST /api/team/runners/connect with { username, password, name }");
|
|
66
|
+
if (!route.principals.includes("runner")) return new HttpError(403, "Only the hub owner may do this, from the team server itself");
|
|
67
|
+
return new HttpError(403, "Only the computer executing an attempt may start it or report on it; run team tasks from your own code-flow (Settings \u2192 Team \u2192 Connect this computer)");
|
|
68
|
+
}
|
|
69
|
+
const checkpointIdOf = (value) => {
|
|
70
|
+
if (typeof value !== "string" || !CHECKPOINT_ID.test(value)) throw new CollabError("Send the checkpoint id as 32 lowercase hexadecimal characters");
|
|
71
|
+
return value;
|
|
72
|
+
};
|
|
73
|
+
const attemptIdOf = (value) => {
|
|
74
|
+
if (typeof value !== "string" || !ATTEMPT_ID.test(value)) throw new CollabError("Send the attempt id as 32 lowercase hexadecimal characters");
|
|
75
|
+
return value;
|
|
76
|
+
};
|
|
77
|
+
async function handleTeamApi(req, res, url, parts, host) {
|
|
78
|
+
const method = req.method ?? "GET", route = teamRoutePolicy(method, parts);
|
|
79
|
+
if (!route) throw new HttpError(404, "Unknown team operation");
|
|
80
|
+
if (!route.principals.includes(host.principal)) throw denied(route);
|
|
81
|
+
if (method !== "GET") host.mutation(req);
|
|
82
|
+
try {
|
|
83
|
+
await dispatch(req, res, url, parts, host, route);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof CollabError && !res.headersSent) return send(res, error.status, { error: error.message });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function dispatch(req, res, url, parts, host, route) {
|
|
90
|
+
const { store, actor } = host, id = parts[3] ?? "", sub = parts[5] ?? "", body = () => readJson(req, route.bodyLimit ?? TEAM_BODY_LIMIT.small, { timeoutMs: 1e4 });
|
|
91
|
+
switch (route.op) {
|
|
92
|
+
case "connect":
|
|
93
|
+
throw denied(route);
|
|
94
|
+
case "me": {
|
|
95
|
+
const { runners, ...me } = (await store.board(actor)).me;
|
|
96
|
+
return send(res, 200, { me, runners, linked: false });
|
|
97
|
+
}
|
|
98
|
+
case "board": {
|
|
99
|
+
const project = url.searchParams.get("project");
|
|
100
|
+
if (project) {
|
|
101
|
+
if (!idOk(project)) throw new CollabError("Project not found", 404);
|
|
102
|
+
return send(res, 200, await store.board(actor, project));
|
|
103
|
+
}
|
|
104
|
+
return send(res, 200, await store.board(actor));
|
|
105
|
+
}
|
|
106
|
+
case "events":
|
|
107
|
+
return openTeamEvents(res, url, host);
|
|
108
|
+
case "task":
|
|
109
|
+
return send(res, 200, await store.task(actor, id));
|
|
110
|
+
case "task-create":
|
|
111
|
+
return send(res, 201, await store.createTask(actor, await body()));
|
|
112
|
+
case "task-update": {
|
|
113
|
+
const patch = await body();
|
|
114
|
+
return send(res, 200, await store.updateTask(actor, id, patch, patch.revision));
|
|
115
|
+
}
|
|
116
|
+
case "task-delete":
|
|
117
|
+
return send(res, 200, await store.deleteTask(actor, id, (await body()).revision));
|
|
118
|
+
case "comment":
|
|
119
|
+
return send(res, 200, await store.comment(actor, id, (await body()).text));
|
|
120
|
+
case "decision":
|
|
121
|
+
return send(res, 200, await store.decide(actor, id, (await body()).text));
|
|
122
|
+
case "review-request": {
|
|
123
|
+
const input = await body();
|
|
124
|
+
return send(res, 200, await store.requestReview(actor, id, input.reviewer, input.revision));
|
|
125
|
+
}
|
|
126
|
+
case "review": {
|
|
127
|
+
const input = await body(), review = { checkpointId: checkpointIdOf(input.checkpointId), verdict: input.verdict };
|
|
128
|
+
if (input.text !== void 0 && input.text !== null) review.text = input.text;
|
|
129
|
+
return send(res, 200, await store.review(actor, id, review, input.revision));
|
|
130
|
+
}
|
|
131
|
+
case "instruction-propose":
|
|
132
|
+
return send(res, 200, await store.proposeInstruction(actor, id, (await body()).text));
|
|
133
|
+
case "instruction-decide": {
|
|
134
|
+
const input = await body();
|
|
135
|
+
if (input.action === "delivered") return send(res, 200, await store.markInstructionDelivered(actor, id, sub, attemptIdOf(input.attemptId)));
|
|
136
|
+
if (input.action !== "approve" && input.action !== "decline") throw new CollabError('Choose an action: "approve", "decline" or "delivered"');
|
|
137
|
+
return send(res, 200, await store.decideInstruction(actor, id, sub, input.action));
|
|
138
|
+
}
|
|
139
|
+
case "attempt-claim":
|
|
140
|
+
return send(res, 201, await store.claimAttempt(actor, id, await body()));
|
|
141
|
+
case "handoff-offer": {
|
|
142
|
+
const input = await body(), handoff = { to: input.to, checkpointId: input.checkpointId };
|
|
143
|
+
if (input.note !== void 0 && input.note !== null) handoff.note = input.note;
|
|
144
|
+
return send(res, 200, await store.offerHandoff(actor, id, handoff, input.revision));
|
|
145
|
+
}
|
|
146
|
+
case "handoff-respond": {
|
|
147
|
+
const input = await body();
|
|
148
|
+
return send(res, 200, await store.respondHandoff(actor, id, sub, input.action, input.revision));
|
|
149
|
+
}
|
|
150
|
+
case "attempt-progress":
|
|
151
|
+
return send(res, 200, await store.attemptProgress(actor, id, await body()));
|
|
152
|
+
case "attempt-heartbeat":
|
|
153
|
+
return send(res, 200, await store.heartbeat(actor, id));
|
|
154
|
+
case "attempt-finish":
|
|
155
|
+
return send(res, 200, await store.finishAttempt(actor, id, await body()));
|
|
156
|
+
case "attempt-recover":
|
|
157
|
+
return send(res, 200, await store.recoverAttempt(actor, id, await body()));
|
|
158
|
+
case "checkpoint-add":
|
|
159
|
+
return send(res, 201, await store.addCheckpoint(actor, id, await body()));
|
|
160
|
+
case "checkpoint":
|
|
161
|
+
return send(res, 200, await checkpointById(store, actor, id));
|
|
162
|
+
case "checkpoint-delete":
|
|
163
|
+
await store.deleteCheckpoint(actor, id);
|
|
164
|
+
return send(res, 200, { deleted: true });
|
|
165
|
+
case "bundle-upload":
|
|
166
|
+
return uploadBundle(req, res, store, actor, id);
|
|
167
|
+
case "bundle-download":
|
|
168
|
+
return downloadBundle(res, store, actor, id);
|
|
169
|
+
case "inbox":
|
|
170
|
+
return send(res, 200, await store.inbox(actor, url.searchParams.get("after") ?? void 0));
|
|
171
|
+
case "inbox-read": {
|
|
172
|
+
const input = await body();
|
|
173
|
+
return send(res, 200, await store.markRead(actor, input.all === true ? "all" : input.ids));
|
|
174
|
+
}
|
|
175
|
+
case "runners":
|
|
176
|
+
return send(res, 200, await store.runners(actor));
|
|
177
|
+
case "runner-revoke":
|
|
178
|
+
await store.revokeRunner(actor, id);
|
|
179
|
+
return send(res, 200, { revoked: true });
|
|
180
|
+
case "repositories": {
|
|
181
|
+
const input = await body();
|
|
182
|
+
return send(res, 200, await store.setRepositories(actor, id, input.repositories, input.revision));
|
|
183
|
+
}
|
|
184
|
+
case "profile":
|
|
185
|
+
return send(res, 200, await store.setOwnerName(actor, (await body()).displayName));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function checkpointById(store, actor, id) {
|
|
189
|
+
if (CHECKPOINT_ID.test(id)) {
|
|
190
|
+
const board = await store.board(actor), order = board.tasks.filter((task) => task.checkpoint?.id === id).map((task) => task.id);
|
|
191
|
+
for (const task of board.tasks) if (!order.includes(task.id)) order.push(task.id);
|
|
192
|
+
for (const taskId of order) {
|
|
193
|
+
const found = (await store.task(actor, taskId)).checkpoints.find((checkpoint) => checkpoint.id === id);
|
|
194
|
+
if (found) return found;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new CollabError("Checkpoint not found", 404);
|
|
198
|
+
}
|
|
199
|
+
async function uploadBundle(req, res, store, actor, id) {
|
|
200
|
+
const declared = req.headers["content-length"], size = declared !== void 0 && /^\d{1,12}$/.test(declared) ? Number(declared) : void 0;
|
|
201
|
+
const refuse = (status, message) => {
|
|
202
|
+
send(res, status, { error: message });
|
|
203
|
+
if (req.complete || req.destroyed) return;
|
|
204
|
+
if (size !== void 0 && size <= MAX.bundle) req.resume();
|
|
205
|
+
else res.once("finish", () => req.destroy());
|
|
206
|
+
};
|
|
207
|
+
if (!/^application\/octet-stream(?:\s*;|$)/i.test(String(req.headers["content-type"] ?? ""))) return refuse(415, "Upload the checkpoint bundle as application/octet-stream");
|
|
208
|
+
if (size === void 0) return refuse(411, "Send content-length with the checkpoint bundle");
|
|
209
|
+
if (size > MAX.bundle) return refuse(413, `Checkpoint bundles are limited to ${MAX.bundle / 1024 / 1024} MiB`);
|
|
210
|
+
const sha256 = String(req.headers["x-codeflow-sha256"] ?? "").toLowerCase();
|
|
211
|
+
if (!SHA256_HEX.test(sha256)) return refuse(400, "Send the bundle's SHA-256 digest as 64 hexadecimal characters in the x-codeflow-sha256 header");
|
|
212
|
+
try {
|
|
213
|
+
const checkpoint = await store.storeBundle(actor, id, req, { size, sha256 });
|
|
214
|
+
send(res, 200, { checkpoint });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (!(error instanceof CollabError)) throw error;
|
|
217
|
+
refuse(error.status, error.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function downloadBundle(res, store, actor, id) {
|
|
221
|
+
const { path, checkpoint } = await store.bundlePath(actor, id), missing = () => new CollabError("The checkpoint bundle is missing from the hub's storage; create a new checkpoint", 404);
|
|
222
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(() => {
|
|
223
|
+
throw missing();
|
|
224
|
+
});
|
|
225
|
+
const info = await handle.stat().catch(() => void 0);
|
|
226
|
+
if (!info?.isFile() || info.size !== checkpoint.size) {
|
|
227
|
+
await handle.close().catch(() => void 0);
|
|
228
|
+
throw missing();
|
|
229
|
+
}
|
|
230
|
+
res.writeHead(200, { "content-type": "application/octet-stream", "content-length": String(checkpoint.size), "x-codeflow-sha256": checkpoint.sha256, "cache-control": "no-store", "content-disposition": `attachment; filename="${checkpoint.id}.bundle"` });
|
|
231
|
+
try {
|
|
232
|
+
await pipeline(handle.createReadStream(), res);
|
|
233
|
+
} catch {
|
|
234
|
+
res.destroy();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const teamStreams = /* @__PURE__ */ new Map();
|
|
238
|
+
let totalTeamStreams = 0;
|
|
239
|
+
const STREAM_BYTES = 2 * 1024 * 1024, PING_MS = 15e3, STREAMS_PER_USER = 4, STREAMS_TOTAL = 128, CURSOR_MAX = 100;
|
|
240
|
+
function teamStream(res, userId) {
|
|
241
|
+
const count = teamStreams.get(userId) ?? 0;
|
|
242
|
+
if (count >= STREAMS_PER_USER || totalTeamStreams >= STREAMS_TOTAL) throw new HttpError(429, "Too many team event streams; close another tab and retry");
|
|
243
|
+
teamStreams.set(userId, count + 1);
|
|
244
|
+
totalTeamStreams++;
|
|
245
|
+
let closed = false;
|
|
246
|
+
const disposers = [];
|
|
247
|
+
const close = () => {
|
|
248
|
+
if (closed) return;
|
|
249
|
+
closed = true;
|
|
250
|
+
const remaining = (teamStreams.get(userId) ?? 1) - 1;
|
|
251
|
+
if (remaining) teamStreams.set(userId, remaining);
|
|
252
|
+
else teamStreams.delete(userId);
|
|
253
|
+
totalTeamStreams--;
|
|
254
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
255
|
+
res.destroy();
|
|
256
|
+
};
|
|
257
|
+
const write = (frame) => {
|
|
258
|
+
if (closed) return;
|
|
259
|
+
const bytes = Buffer.byteLength(frame);
|
|
260
|
+
if (bytes > STREAM_BYTES || res.writableLength + bytes > STREAM_BYTES) return close();
|
|
261
|
+
res.write(frame);
|
|
262
|
+
};
|
|
263
|
+
res.on("close", close);
|
|
264
|
+
if (res.destroyed) close();
|
|
265
|
+
if (!closed) res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store", connection: "keep-alive", "x-accel-buffering": "no" });
|
|
266
|
+
return { close, add: (dispose) => {
|
|
267
|
+
if (closed) dispose();
|
|
268
|
+
else disposers.push(dispose);
|
|
269
|
+
}, emit: (frame) => write(`data: ${JSON.stringify(frame)}
|
|
270
|
+
|
|
271
|
+
`), ping: () => write(": connected\n\n") };
|
|
272
|
+
}
|
|
273
|
+
const rolesKey = (actor) => actor.kind === "owner" ? "owner" : Object.entries(actor.roles).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([project, role]) => `${project}=${role}`).join(",");
|
|
274
|
+
const seesAllRunners = (actor) => actor.kind === "owner";
|
|
275
|
+
async function openTeamEvents(res, url, host) {
|
|
276
|
+
const after = url.searchParams.get("after") ?? void 0;
|
|
277
|
+
if (after !== void 0 && after.length > CURSOR_MAX) throw new HttpError(400, "Invalid team event cursor");
|
|
278
|
+
const { store } = host;
|
|
279
|
+
let actor = host.actor;
|
|
280
|
+
const { unread } = await store.inbox(actor);
|
|
281
|
+
const stream = teamStream(res, actor.userId);
|
|
282
|
+
let roles = rolesKey(actor), last = 0, counting = false;
|
|
283
|
+
const refresh = () => {
|
|
284
|
+
if (!host.reauthorize) return;
|
|
285
|
+
const next = host.reauthorize();
|
|
286
|
+
if (next.userId !== actor.userId) throw new HttpError(401, "Team session changed");
|
|
287
|
+
actor = next;
|
|
288
|
+
const key = rolesKey(next);
|
|
289
|
+
if (key !== roles) {
|
|
290
|
+
roles = key;
|
|
291
|
+
stream.emit({ type: "resync" });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const inboxFrame = () => {
|
|
295
|
+
if (counting) return;
|
|
296
|
+
counting = true;
|
|
297
|
+
store.inbox(actor).then(({ unread: unread2 }) => {
|
|
298
|
+
counting = false;
|
|
299
|
+
stream.emit({ type: "inbox", seq: last, unread: unread2 });
|
|
300
|
+
}, () => stream.close());
|
|
301
|
+
};
|
|
302
|
+
const deliver = (event) => {
|
|
303
|
+
if (event.seq <= last) return;
|
|
304
|
+
last = event.seq;
|
|
305
|
+
if (event.kind === "inbox") {
|
|
306
|
+
if (event.userIds?.includes(actor.userId)) inboxFrame();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (event.kind === "runners") {
|
|
310
|
+
if (!event.userIds || event.userIds.includes(actor.userId) || seesAllRunners(actor)) stream.emit({ type: "change", seq: event.seq, kind: "runners" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (event.projectId !== void 0 ? actorRole(actor, event.projectId) === void 0 : event.kind !== "board") return;
|
|
314
|
+
const frame = { type: "change", seq: event.seq, kind: event.kind };
|
|
315
|
+
if (event.taskId !== void 0) frame.taskId = event.taskId;
|
|
316
|
+
if (event.projectId !== void 0) frame.projectId = event.projectId;
|
|
317
|
+
stream.emit(frame);
|
|
318
|
+
};
|
|
319
|
+
stream.add(store.onChange((event) => {
|
|
320
|
+
try {
|
|
321
|
+
refresh();
|
|
322
|
+
deliver(event);
|
|
323
|
+
} catch {
|
|
324
|
+
stream.close();
|
|
325
|
+
}
|
|
326
|
+
}));
|
|
327
|
+
const timer = setInterval(() => {
|
|
328
|
+
try {
|
|
329
|
+
refresh();
|
|
330
|
+
stream.ping();
|
|
331
|
+
} catch {
|
|
332
|
+
stream.close();
|
|
333
|
+
}
|
|
334
|
+
}, PING_MS);
|
|
335
|
+
timer.unref();
|
|
336
|
+
stream.add(() => clearInterval(timer));
|
|
337
|
+
const page = store.eventsSince(after);
|
|
338
|
+
stream.emit({ type: "hello", epoch: page.epoch, seq: page.seq, unread });
|
|
339
|
+
if (page.resync) stream.emit({ type: "resync" });
|
|
340
|
+
for (const event of page.events) deliver(event);
|
|
341
|
+
last = Math.max(last, page.seq);
|
|
342
|
+
}
|
|
343
|
+
async function handleRunnerConnect(req, res, host) {
|
|
344
|
+
const body = await readLoginJson(req);
|
|
345
|
+
try {
|
|
346
|
+
const name = validateText(body.name, MAX.runnerName, "computer name", { required: true, singleLine: true });
|
|
347
|
+
const user = await host.accounts.verifyCredentials({ username: body.username, password: body.password, ...host.transport });
|
|
348
|
+
if (!Object.values(user.roles).some((role) => roleAtLeast(role, "contributor"))) throw new CollabError("Ask the team server's owner for a contributor role before connecting a computer", 403);
|
|
349
|
+
const { runner, token } = await host.store.registerRunner(user.id, name, host.transport.address);
|
|
350
|
+
send(res, 201, { runner, token, user: { userId: user.id, username: user.username, displayName: user.displayName }, hub: { name: host.ownerName ?? host.store.ownerName } });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof CollabError || error instanceof SharedAccountError) return send(res, error.status, { error: error.message });
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export {
|
|
357
|
+
RUNNER_TOKEN,
|
|
358
|
+
TEAM_BODY_LIMIT,
|
|
359
|
+
handleRunnerConnect,
|
|
360
|
+
handleTeamApi,
|
|
361
|
+
isTeamApiPath,
|
|
362
|
+
looksLikeRunnerToken,
|
|
363
|
+
runnerAllowed,
|
|
364
|
+
teamRoutePolicy
|
|
365
|
+
};
|