@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3
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 +141 -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/changes-62ZIEQSN.js +1 -0
- package/public/assets/chunk-2PZVOKID.js +1 -0
- package/public/assets/chunk-4D5IENEB.js +1 -0
- package/public/assets/chunk-5SARP5MU.js +1 -0
- package/public/assets/chunk-63YLZ3PR.js +1 -0
- package/public/assets/chunk-7FUHKF2M.js +1 -0
- package/public/assets/chunk-APQEVJ2K.js +1 -0
- package/public/assets/chunk-CGOZQMEZ.js +2 -0
- package/public/assets/chunk-J4VYS3KV.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-SIHDWM2Y.js +154 -0
- package/public/assets/chunk-TEJVYIKC.js +1 -0
- package/public/assets/code-browser-VB2HGZSG.js +4 -0
- package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
- package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
- package/public/assets/join-project-6RTFHNCA.js +1 -0
- package/public/assets/main-65EW73SB.js +1 -0
- package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
- package/public/assets/settings-UB5Q4AQM.js +9 -0
- package/public/assets/styles-18792BB4.css +32 -0
- package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
- package/public/assets/terminal-IWAIM4DE.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 +148 -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 +143 -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 +88 -0
- package/public/docs/installation.html +80 -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 +129 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +65 -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 +412 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +131 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/computer-use.swift +64 -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/agent-tools.js +28 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +242 -0
- package/src/adapters/agy-tools.js +45 -0
- package/src/adapters/agy.js +326 -0
- package/src/adapters/claude-stream.js +58 -0
- package/src/adapters/claude.js +282 -0
- package/src/adapters/codex-app-server.js +555 -0
- package/src/adapters/codex.js +311 -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 +433 -0
- package/src/adapters/team-tools.js +103 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-browser.js +688 -0
- package/src/agent-computer.js +88 -0
- package/src/agent-container.js +155 -0
- package/src/agent-team-definitions.js +131 -0
- package/src/agent-tool-client.mjs +79 -0
- package/src/agent-tools.js +296 -0
- package/src/approvals.js +134 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/browser-files.js +293 -0
- package/src/browser-ownership.js +150 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-family.js +25 -0
- package/src/chat-handoff.js +87 -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/documentation-changes.js +20 -0
- package/src/engine.js +5258 -0
- package/src/env.js +44 -0
- package/src/execution-budget.js +8 -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/frame-policy.js +66 -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/local-app-url.js +19 -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 +239 -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 +230 -0
- package/src/patterns/auto-team.js +816 -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 +21 -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 +46 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/task-review.js +45 -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 +392 -0
- package/src/preview.js +230 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-budget.js +22 -0
- package/src/project-team-model.js +225 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +234 -0
- package/src/project-team-usage.js +24 -0
- package/src/project-teams-api.js +43 -0
- package/src/project-teams.js +644 -0
- package/src/projects.js +290 -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 +725 -0
- package/src/repo-tree.js +106 -0
- package/src/repository-identity.js +13 -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 +111 -0
- package/src/run-projection.js +77 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +56 -0
- package/src/serve.js +138 -0
- package/src/server.js +2871 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +381 -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 +133 -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/task-team.js +164 -0
- package/src/team-evaluation.js +243 -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-steering.js +21 -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 +235 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
package/src/preview.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createServer, request } from "node:http";
|
|
4
|
+
import { connect } from "node:net";
|
|
5
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
6
|
+
import { logWarn } from "./log.js";
|
|
7
|
+
const LOCAL = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
8
|
+
const MAX_PREVIEWS = 20;
|
|
9
|
+
const OVERLAY_PATH = "/__codeflow/overlay.js";
|
|
10
|
+
const overlaySource = readFileSync(new URL("./preview-overlay.js", import.meta.url), "utf8");
|
|
11
|
+
const DROPPED = /* @__PURE__ */ new Set(["content-security-policy-report-only", "x-frame-options", "cross-origin-opener-policy", "cross-origin-embedder-policy"]);
|
|
12
|
+
const OWN_COOKIE = /^codeflow_/;
|
|
13
|
+
const DECODE = { gzip: gunzipSync, "x-gzip": gunzipSync, deflate: inflateSync, br: brotliDecompressSync };
|
|
14
|
+
function previewTarget(address) {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(address);
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== "http:" || !LOCAL.has(url.hostname)) return void 0;
|
|
22
|
+
const port = Number(url.port || 80);
|
|
23
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return void 0;
|
|
24
|
+
return { hostname: url.hostname, port, path: `${url.pathname}${url.search}${url.hash}` };
|
|
25
|
+
}
|
|
26
|
+
class Previews {
|
|
27
|
+
/** The origins code-flow's own page can have: the overlay takes commands only from them, and only they may frame a preview. */
|
|
28
|
+
constructor(parents, ownPorts, remoteConnect, overlay = true) {
|
|
29
|
+
this.parents = parents;
|
|
30
|
+
this.ownPorts = ownPorts;
|
|
31
|
+
this.remoteConnect = remoteConnect;
|
|
32
|
+
this.overlay = overlay;
|
|
33
|
+
}
|
|
34
|
+
open = /* @__PURE__ */ new Map();
|
|
35
|
+
async ready(address) {
|
|
36
|
+
const target = previewTarget(address);
|
|
37
|
+
if (!target || this.ownPorts().includes(target.port)) throw new Error("Choose a local HTTP app other than code-flow");
|
|
38
|
+
if (this.remoteConnect) throw new Error("Local readiness checks are unavailable for a forwarded preview");
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`http://${target.hostname}:${target.port}/`, { method: "HEAD", redirect: "manual", signal: AbortSignal.timeout(2e3) });
|
|
41
|
+
return response.status >= 200 && response.status < 500;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The preview for a local app: its address on the proxy. The same app keeps its port. `host`
|
|
48
|
+
* is the Host code-flow's page was asked for with; the preview uses the same host name.
|
|
49
|
+
*/
|
|
50
|
+
async show(address, host = "") {
|
|
51
|
+
const target = previewTarget(address);
|
|
52
|
+
if (!target) throw new Error("The preview shows apps on this computer: an http://localhost:<port> address");
|
|
53
|
+
if (this.ownPorts().includes(target.port)) throw new Error("That's code-flow itself");
|
|
54
|
+
const key = `${target.hostname}:${target.port}`;
|
|
55
|
+
let entry = this.open.get(key);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
if (this.open.size >= MAX_PREVIEWS) this.closeOldest();
|
|
58
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
59
|
+
const server = this.proxy(target, sockets);
|
|
60
|
+
const port = await new Promise((resolve, reject) => {
|
|
61
|
+
server.once("error", reject);
|
|
62
|
+
server.listen(0, "127.0.0.1", () => resolve(server.address().port));
|
|
63
|
+
});
|
|
64
|
+
entry = { port, server, usedAt: Date.now(), sockets };
|
|
65
|
+
this.open.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
entry.usedAt = Date.now();
|
|
68
|
+
const hostname = host.split(":")[0] === "localhost" ? "localhost" : "127.0.0.1";
|
|
69
|
+
const origin = `http://${hostname}:${entry.port}`;
|
|
70
|
+
return { url: `${origin}${target.path}`, origin };
|
|
71
|
+
}
|
|
72
|
+
closeAll() {
|
|
73
|
+
for (const preview of this.open.values()) stop(preview);
|
|
74
|
+
this.open.clear();
|
|
75
|
+
}
|
|
76
|
+
closeOldest() {
|
|
77
|
+
const [key, oldest] = [...this.open.entries()].sort((a, b) => a[1].usedAt - b[1].usedAt)[0];
|
|
78
|
+
stop(oldest);
|
|
79
|
+
this.open.delete(key);
|
|
80
|
+
}
|
|
81
|
+
proxy(target, sockets) {
|
|
82
|
+
const upstreamHost = target.hostname.replace(/^\[|\]$/g, "");
|
|
83
|
+
const targetOrigin = `http://${target.hostname}:${target.port}`;
|
|
84
|
+
const server = createServer((req, res) => {
|
|
85
|
+
const own = ownHost(req, server);
|
|
86
|
+
if (!own) return refuse(res, 403, "Bad Host header");
|
|
87
|
+
if (!ownOrigin(req, server)) return refuse(res, 403, "Requests from other sites don't reach the app through its preview");
|
|
88
|
+
if (this.overlay && req.url?.startsWith(OVERLAY_PATH)) {
|
|
89
|
+
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" });
|
|
90
|
+
return res.end(overlaySource.replace("__PARENTS__", JSON.stringify(this.parents())));
|
|
91
|
+
}
|
|
92
|
+
const headers = forwardHeaders(req, target, targetOrigin, own);
|
|
93
|
+
headers["accept-encoding"] = "identity";
|
|
94
|
+
const upstream = request({ host: upstreamHost, port: target.port, method: req.method, path: req.url, headers, ...this.remoteConnect ? { createConnection: () => this.remoteConnect(target) } : {} }, (up) => {
|
|
95
|
+
up.on("error", () => res.destroy());
|
|
96
|
+
const nonce = randomBytes(16).toString("base64");
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [name, value] of Object.entries(up.headers)) {
|
|
99
|
+
if (value === void 0 || DROPPED.has(name)) continue;
|
|
100
|
+
if (name === "location" && typeof value === "string") out[name] = value.replace(targetOrigin, `http://${own}`);
|
|
101
|
+
else if (name === "set-cookie") {
|
|
102
|
+
const cookies = [value].flat().filter((c) => !OWN_COOKIE.test(c.trimStart()));
|
|
103
|
+
if (cookies.length) out[name] = cookies;
|
|
104
|
+
} else out[name] = value;
|
|
105
|
+
}
|
|
106
|
+
const policy = framedPolicy(up.headers["content-security-policy"], this.parents(), nonce);
|
|
107
|
+
out["content-security-policy"] = policy;
|
|
108
|
+
const encoding = String(up.headers["content-encoding"] ?? "identity").trim().toLowerCase();
|
|
109
|
+
const decode = encoding === "identity" ? (body) => body : DECODE[encoding];
|
|
110
|
+
const html = String(up.headers["content-type"] ?? "").includes("text/html") && req.method !== "HEAD";
|
|
111
|
+
if (!html || !decode || !this.overlay) {
|
|
112
|
+
res.writeHead(up.statusCode ?? 502, out);
|
|
113
|
+
up.pipe(res);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const chunks = [];
|
|
117
|
+
up.on("data", (c) => chunks.push(c));
|
|
118
|
+
up.on("end", () => {
|
|
119
|
+
const body = Buffer.concat(chunks);
|
|
120
|
+
let page;
|
|
121
|
+
try {
|
|
122
|
+
page = injectOverlay(decode(body).toString("utf8"), policy.includes(`'nonce-${nonce}'`) ? nonce : void 0);
|
|
123
|
+
} catch {
|
|
124
|
+
res.writeHead(up.statusCode ?? 502, out);
|
|
125
|
+
return res.end(body);
|
|
126
|
+
}
|
|
127
|
+
delete out["content-length"];
|
|
128
|
+
delete out["content-encoding"];
|
|
129
|
+
delete out["etag"];
|
|
130
|
+
res.writeHead(up.statusCode ?? 502, { ...out, "cache-control": "no-store" });
|
|
131
|
+
res.end(page);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
upstream.on("error", (err) => refuse(res, 502, `The app at ${targetOrigin} didn't answer (${err.code ?? err.message}). Is it running?`));
|
|
135
|
+
res.on("close", () => {
|
|
136
|
+
if (!res.writableFinished) upstream.destroy();
|
|
137
|
+
});
|
|
138
|
+
req.pipe(upstream);
|
|
139
|
+
});
|
|
140
|
+
server.on("upgrade", (req, socket, head) => {
|
|
141
|
+
const own = ownHost(req, server);
|
|
142
|
+
if (!own || !ownOrigin(req, server)) return socket.destroy();
|
|
143
|
+
const up = this.remoteConnect ? this.remoteConnect(target) : connect(target.port, upstreamHost);
|
|
144
|
+
const connected = () => {
|
|
145
|
+
const headers = forwardHeaders(req, target, targetOrigin, own);
|
|
146
|
+
const lines = [`${req.method} ${req.url} HTTP/1.1`, ...Object.entries(headers).flatMap(([k, v]) => Array.isArray(v) ? v.map((x) => `${k}: ${x}`) : v === void 0 ? [] : [`${k}: ${v}`])];
|
|
147
|
+
up.write(`${lines.join("\r\n")}\r
|
|
148
|
+
\r
|
|
149
|
+
`);
|
|
150
|
+
if (head.length) up.write(head);
|
|
151
|
+
socket.pipe(up).pipe(socket);
|
|
152
|
+
};
|
|
153
|
+
if (this.remoteConnect) queueMicrotask(connected);
|
|
154
|
+
else up.once("connect", connected);
|
|
155
|
+
up.on("error", () => socket.destroy());
|
|
156
|
+
socket.on("error", () => up.destroy());
|
|
157
|
+
for (const side of [socket, up]) {
|
|
158
|
+
sockets.add(side);
|
|
159
|
+
side.on("close", () => sockets.delete(side));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
server.on("clientError", (_err, socket) => socket.destroy());
|
|
163
|
+
server.on("error", (err) => logWarn(`Preview of ${targetOrigin}: ${err.message}`));
|
|
164
|
+
return server;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function stop(preview) {
|
|
168
|
+
preview.server.close();
|
|
169
|
+
preview.server.closeAllConnections();
|
|
170
|
+
for (const socket of preview.sockets) socket.destroy();
|
|
171
|
+
}
|
|
172
|
+
function ownHost(req, server) {
|
|
173
|
+
const port = server.address()?.port;
|
|
174
|
+
const host = req.headers.host ?? "";
|
|
175
|
+
return host === `127.0.0.1:${port}` || host === `localhost:${port}` ? host : void 0;
|
|
176
|
+
}
|
|
177
|
+
function ownOrigin(req, server) {
|
|
178
|
+
const origin = req.headers.origin;
|
|
179
|
+
if (origin === void 0 || origin === "null") return true;
|
|
180
|
+
const port = server.address()?.port;
|
|
181
|
+
return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
|
|
182
|
+
}
|
|
183
|
+
function forwardHeaders(req, target, targetOrigin, own) {
|
|
184
|
+
const headers = { ...req.headers, host: `${target.hostname}:${target.port}` };
|
|
185
|
+
if (typeof headers.origin === "string" && headers.origin !== "null") headers.origin = targetOrigin;
|
|
186
|
+
if (typeof headers.referer === "string") headers.referer = headers.referer.replace(`http://${own}`, targetOrigin);
|
|
187
|
+
const cookies = String(req.headers.cookie ?? "").split(";").map((c) => c.trim()).filter((c) => c && !OWN_COOKIE.test(c));
|
|
188
|
+
if (cookies.length) headers.cookie = cookies.join("; ");
|
|
189
|
+
else delete headers.cookie;
|
|
190
|
+
return headers;
|
|
191
|
+
}
|
|
192
|
+
function framedPolicy(appPolicy, parents, nonce) {
|
|
193
|
+
const policies = [appPolicy ?? []].flat().flatMap((header) => header.split(",")).map((policy) => framedDirectives(policy, nonce)).filter(Boolean);
|
|
194
|
+
return [...policies, `frame-ancestors 'self' ${parents.join(" ")}`].join(", ");
|
|
195
|
+
}
|
|
196
|
+
function framedDirectives(policy, nonce) {
|
|
197
|
+
const directives = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const directive of policy.split(";")) {
|
|
199
|
+
const [name = "", ...sources] = directive.trim().split(/\s+/);
|
|
200
|
+
if (name && !directives.has(name.toLowerCase())) directives.set(name.toLowerCase(), sources);
|
|
201
|
+
}
|
|
202
|
+
directives.delete("frame-ancestors");
|
|
203
|
+
const fallback = directives.get("default-src");
|
|
204
|
+
const script = directives.has("script-src-elem") ? "script-src-elem" : "script-src";
|
|
205
|
+
const scripts = directives.get(script) ?? fallback;
|
|
206
|
+
if (scripts) {
|
|
207
|
+
const inline = scripts.some((s) => s.toLowerCase() === "'unsafe-inline'") && !scripts.some((s) => /^'(nonce-|sha(256|384|512)-|strict-dynamic')/i.test(s));
|
|
208
|
+
directives.set(script, [...scripts, inline ? "'self'" : `'nonce-${nonce}'`]);
|
|
209
|
+
}
|
|
210
|
+
const images = directives.get("img-src") ?? fallback;
|
|
211
|
+
if (images) directives.set("img-src", [...images, "data:"]);
|
|
212
|
+
return [...directives].map(([name, sources]) => [name, ...sources].join(" ")).join("; ");
|
|
213
|
+
}
|
|
214
|
+
const LEAD = /^\uFEFF?(?:\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<!doctype[^>]*>|<html(?:[\s/][^>]*)?>|<head(?:[\s/][^>]*)?>)*/i;
|
|
215
|
+
function injectOverlay(page, nonce) {
|
|
216
|
+
const tag = `<script src="${OVERLAY_PATH}"${nonce ? ` nonce="${nonce}"` : ""}></script>`;
|
|
217
|
+
const at = LEAD.exec(page)?.[0].length ?? 0;
|
|
218
|
+
return page.slice(0, at) + tag + page.slice(at);
|
|
219
|
+
}
|
|
220
|
+
function refuse(res, status, message) {
|
|
221
|
+
if (res.headersSent || res.destroyed) return res.destroy();
|
|
222
|
+
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
223
|
+
res.end(message);
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
Previews,
|
|
227
|
+
framedPolicy,
|
|
228
|
+
injectOverlay,
|
|
229
|
+
previewTarget
|
|
230
|
+
};
|
package/src/privacy.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { privateEnvironmentValues } from "./env.js";
|
|
2
|
+
function redactText(text, secrets = environmentSecrets()) {
|
|
3
|
+
let result = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[REDACTED]@").replace(/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]").replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g, "[REDACTED TOKEN]").replace(/(\bBearer\s+)[A-Za-z0-9._~+\/-]{12,}=*/gi, "$1[REDACTED]").replace(/([?&](?:token|key|secret|password|signature)=)[^&#\s]+/gi, "$1[REDACTED]").replace(/((?:api[_-]?key|access[_-]?token|password|client[_-]?secret)\s*[=:]\s*["']?)[^\s"',;}]+/gi, "$1[REDACTED]");
|
|
4
|
+
for (const secret of secrets) if (secret.length >= 8) result = result.split(secret).join("[REDACTED]");
|
|
5
|
+
return result;
|
|
6
|
+
}
|
|
7
|
+
function environmentSecrets() {
|
|
8
|
+
return [...privateEnvironmentValues(), ...Object.entries(process.env).filter(([key, value]) => /(?:TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY)$/.test(key) && value && value.length >= 8).map(([, value]) => value)];
|
|
9
|
+
}
|
|
10
|
+
function redactExport(value) {
|
|
11
|
+
const secrets = environmentSecrets();
|
|
12
|
+
const visit = (item, key = "") => {
|
|
13
|
+
if (typeof item === "string") return /^(?:token|controlToken|authorization|cookie|password|secret|apiKey|capability)$/i.test(key) ? "[REDACTED]" : redactText(item, secrets);
|
|
14
|
+
if (Array.isArray(item)) return item.map((entry) => visit(entry));
|
|
15
|
+
if (item && typeof item === "object") return Object.fromEntries(Object.entries(item).map(([name, entry]) => [name, visit(entry, name)]));
|
|
16
|
+
return item;
|
|
17
|
+
};
|
|
18
|
+
return visit(value);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
redactExport,
|
|
22
|
+
redactText
|
|
23
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { reviewReason } from "./review-inbox.js";
|
|
2
|
+
import { encodeRunCursor } from "./run-history.js";
|
|
3
|
+
import { matchesHistoryFilter, runPassed } from "./run-status.js";
|
|
4
|
+
function projectReport(runs, now = Date.now()) {
|
|
5
|
+
const week = runs.filter((run) => run.createdAt > now - 7 * 864e5);
|
|
6
|
+
const archived = runs.filter((run) => run.archived).length;
|
|
7
|
+
const section = (name, limit) => {
|
|
8
|
+
const matches = runs.filter((run) => matchesHistoryFilter(run, { section: name }));
|
|
9
|
+
const selected = matches.slice(0, limit);
|
|
10
|
+
return { runs: structuredClone(selected), total: matches.length, nextCursor: matches.length > limit ? encodeRunCursor(selected.at(-1)) : void 0 };
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
live: runs.length - archived,
|
|
14
|
+
archived,
|
|
15
|
+
pending: runs.filter((run) => reviewReason(run) !== void 0).length,
|
|
16
|
+
week: { runs: week.length, passed: week.filter(runPassed).length, costUsd: week.reduce((sum, run) => sum + (run.usage?.costUsd ?? 0), 0) },
|
|
17
|
+
sections: { waiting: section("waiting", 100), running: section("running", 100), results: section("results", 8) }
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
projectReport
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function projectTeamUsage(team, now = Date.now()) {
|
|
2
|
+
const week = 7 * 24 * 60 * 6e4;
|
|
3
|
+
return team.cycles.flatMap((c) => c.launches).reduce((total, launch) => {
|
|
4
|
+
const usage = launch.usage;
|
|
5
|
+
if (usage ? usage.endedAt <= now - week : launch.state === "settled" && launch.at <= now - week) return total;
|
|
6
|
+
total.calls += usage?.calls ?? (launch.state === "settled" ? launch.usedCalls ?? launch.calls : launch.calls);
|
|
7
|
+
total.minutes += usage ? Math.ceil(usage.durationMs / 6e4) : launch.minutes;
|
|
8
|
+
if (!usage) {
|
|
9
|
+
total.reservedCalls += launch.calls;
|
|
10
|
+
total.reservedMinutes += launch.minutes;
|
|
11
|
+
}
|
|
12
|
+
return total;
|
|
13
|
+
}, { calls: 0, minutes: 0, reservedCalls: 0, reservedMinutes: 0 });
|
|
14
|
+
}
|
|
15
|
+
function allowance(team, now = Date.now()) {
|
|
16
|
+
const used = projectTeamUsage(team, now);
|
|
17
|
+
return { calls: team.mission.budget.weeklyCalls - used.calls, minutes: team.mission.budget.weeklyMinutes - used.minutes };
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
allowance,
|
|
21
|
+
projectTeamUsage
|
|
22
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { sensitiveContextPath } from "./context-inspection.js";
|
|
2
|
+
import { redactText } from "./privacy.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { validateAgentSettings } from "./adapters/index.js";
|
|
6
|
+
import { validateSchedule } from "./automation.js";
|
|
7
|
+
import { validateVerificationProfile } from "./verification.js";
|
|
8
|
+
import { providerEligibility } from "./provider-eligibility.js";
|
|
9
|
+
const projectTeamsEnabled = () => process.env.CODEFLOW_PROJECT_TEAMS_ENABLED === "true";
|
|
10
|
+
const WEEK = 7 * 24 * 60 * 6e4;
|
|
11
|
+
const PT_LIMITS = { bytes: 16 * 1024 * 1024, cycles: 100, opportunities: 200, decisions: 2e3, operations: 2e3, open: 12, pending: 2 };
|
|
12
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item)).digest("hex");
|
|
13
|
+
const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
function closed(v, keys, label) {
|
|
15
|
+
if (!object(v) || Object.keys(v).some((key) => !keys.includes(key))) throw new Error(`Invalid ${label} fields`);
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
function text(v, max, label) {
|
|
19
|
+
if (typeof v !== "string" || !v.trim() || v.length > max || v.includes("\0")) throw new Error(`Invalid ${label}`);
|
|
20
|
+
return v.trim();
|
|
21
|
+
}
|
|
22
|
+
function integer(v, min, max, label) {
|
|
23
|
+
if (!Number.isSafeInteger(v) || Number(v) < min || Number(v) > max) throw new Error(`Invalid ${label}: use ${min}\u2013${max}`);
|
|
24
|
+
return Number(v);
|
|
25
|
+
}
|
|
26
|
+
function strings(v, max, label) {
|
|
27
|
+
if (!Array.isArray(v) || v.length > max) throw new Error(`Invalid ${label}`);
|
|
28
|
+
return [...new Set(v.map((s) => text(s, 1e3, label)))];
|
|
29
|
+
}
|
|
30
|
+
const safePath = (p) => p === "*" || p.length <= 500 && !p.startsWith("-") && !p.includes("\\") && !p.includes(":") && !/[\x00-\x1f]/.test(p) && !p.split("/").some((part) => !part || part === "." || part === ".." || part === ".git" || part.includes("*"));
|
|
31
|
+
const containsPath = (scope, path) => scope === "*" || path === scope || path.startsWith(`${scope}/`);
|
|
32
|
+
const CATEGORIES = ["bug", "usability", "performance", "tests", "docs", "experiment"];
|
|
33
|
+
function missionSchedule(value) {
|
|
34
|
+
const schedule = validateSchedule(value);
|
|
35
|
+
if (!schedule) throw new Error("Choose a schedule");
|
|
36
|
+
if ("at" in schedule && !schedule.timezone) throw new Error("Choose an explicit schedule time zone");
|
|
37
|
+
return schedule;
|
|
38
|
+
}
|
|
39
|
+
function missionPolicy(mission) {
|
|
40
|
+
const { schedule: _schedule, ...policy } = mission;
|
|
41
|
+
return policy;
|
|
42
|
+
}
|
|
43
|
+
function teamConfiguration(value) {
|
|
44
|
+
return { coordinator: value.coordinator, pool: value.pool, agentSettings: value.agentSettings, ...value.roleSettings && Object.keys(value.roleSettings).length ? { roleSettings: value.roleSettings } : {} };
|
|
45
|
+
}
|
|
46
|
+
function validateTeamConfiguration(value) {
|
|
47
|
+
const r = closed(value, ["coordinator", "pool", "agentSettings", "roleSettings"], "agent team configuration");
|
|
48
|
+
const coordinator = text(r.coordinator, 100, "coordinator"), pool = strings(r.pool, 8, "provider pool");
|
|
49
|
+
if (!pool.length) throw new Error("Select a worker and reviewer provider pool");
|
|
50
|
+
for (const id of [coordinator, ...pool]) {
|
|
51
|
+
const reason = providerEligibility(id, { trust: "standard", unattended: true, projectTeam: true });
|
|
52
|
+
if (reason) throw new Error(reason);
|
|
53
|
+
}
|
|
54
|
+
const roleSettings = {};
|
|
55
|
+
for (const [role, settings] of Object.entries(closed(r.roleSettings ?? {}, ["coordinator", "pool"], "team role settings"))) {
|
|
56
|
+
const valid = validateAgentSettings(settings);
|
|
57
|
+
if (Object.keys(valid).some((id) => !(role === "coordinator" ? [coordinator] : pool).includes(id))) throw new Error("Role settings must use the selected providers");
|
|
58
|
+
if (Object.keys(valid).length) roleSettings[role] = valid;
|
|
59
|
+
}
|
|
60
|
+
return { coordinator, pool, agentSettings: validateAgentSettings(r.agentSettings ?? {}), ...Object.keys(roleSettings).length ? { roleSettings } : {} };
|
|
61
|
+
}
|
|
62
|
+
function validateTeamSnapshot(value) {
|
|
63
|
+
const r = closed(value, ["id", "revision", "name", "configuration"], "agent team snapshot");
|
|
64
|
+
if (!/^at-[a-f0-9]{24}$/.test(r.id) || !Number.isSafeInteger(r.revision) || r.revision < 1) throw new Error("Invalid agent team version");
|
|
65
|
+
return { id: r.id, revision: r.revision, name: text(r.name, 80, "team name"), configuration: validateTeamConfiguration(r.configuration) };
|
|
66
|
+
}
|
|
67
|
+
function validateMission(value) {
|
|
68
|
+
const r = closed(value, ["mission", "users", "priorities", "exclusions", "baseBranch", "mode", "categories", "paths", "excludedPaths", "coordinator", "pool", "agentSettings", "roleSettings", "trust", "check", "setup", "verification", "schedule", "maxFiles", "maxLines", "rescanDays", "budget"], "project mission");
|
|
69
|
+
const paths = strings(r.paths ?? ["*"], 32, "included paths"), excludedPaths = strings(r.excludedPaths ?? [".codeflow", ".github/workflows"], 32, "excluded paths");
|
|
70
|
+
if (!paths.length || [...paths, ...excludedPaths].some((p) => !safePath(p))) throw new Error("Choose safe repository-relative paths");
|
|
71
|
+
const coordinator = text(r.coordinator, 100, "coordinator"), pool = strings(r.pool, 8, "provider pool");
|
|
72
|
+
if (!pool.length) throw new Error("Select a worker and reviewer provider pool");
|
|
73
|
+
const trust = r.trust ?? "standard";
|
|
74
|
+
if (!["safe", "standard", "trusted"].includes(trust)) throw new Error("Project teams cannot use Full access");
|
|
75
|
+
for (const id of [coordinator, ...pool]) {
|
|
76
|
+
const reason = providerEligibility(id, { trust, unattended: true, projectTeam: true });
|
|
77
|
+
if (reason) throw new Error(reason);
|
|
78
|
+
}
|
|
79
|
+
const mode = r.mode ?? "build-within-scope";
|
|
80
|
+
if (!["propose", "build-within-scope"].includes(mode)) throw new Error("Choose proposals only or build within scope");
|
|
81
|
+
const categories = strings(r.categories ?? ["bug", "usability", "performance", "tests", "docs"], 6, "categories");
|
|
82
|
+
if (!categories.length || categories.some((c) => !CATEGORIES.includes(c))) throw new Error("Choose supported improvement categories");
|
|
83
|
+
const branch = text(r.baseBranch, 200, "base branch");
|
|
84
|
+
if (branch === "HEAD" || branch.startsWith("-") || /[\s~^:?*\[\\]/.test(branch) || branch.includes("..") || branch.includes("@{")) throw new Error("Choose an explicit named base branch");
|
|
85
|
+
const schedule = missionSchedule(r.schedule);
|
|
86
|
+
const check = typeof r.check === "string" ? r.check.trim() : "", setup = typeof r.setup === "string" ? r.setup.trim() : "";
|
|
87
|
+
if (check.length > 4e3 || setup.length > 4e3 || `${check}${setup}`.includes("\0")) throw new Error("Invalid verification commands");
|
|
88
|
+
const verification = r.verification ? validateVerificationProfile(r.verification) : check ? validateVerificationProfile({ name: "Project team checks", checks: [{ id: "primary", kind: "custom", command: check, required: true }] }) : void 0;
|
|
89
|
+
if (mode === "build-within-scope" && !verification?.checks.some((c) => c.required)) throw new Error("Automatic implementation requires a required verification check");
|
|
90
|
+
const b = closed(r.budget ?? {}, ["discoveryCalls", "discoveryMinutes", "buildCalls", "buildMinutes", "weeklyCalls", "weeklyMinutes"], "budget");
|
|
91
|
+
const budget = { discoveryCalls: integer(b.discoveryCalls ?? 8, 8, 40, "discovery calls"), discoveryMinutes: integer(b.discoveryMinutes ?? 15, 1, 60, "discovery minutes"), buildCalls: integer(b.buildCalls ?? 16, 4, 80, "build calls"), buildMinutes: integer(b.buildMinutes ?? 45, 1, 120, "build minutes"), weeklyCalls: integer(b.weeklyCalls ?? 48, 4, 500, "weekly calls"), weeklyMinutes: integer(b.weeklyMinutes ?? 120, 1, 1440, "weekly minutes") };
|
|
92
|
+
return { mission: text(r.mission, 6e3, "mission"), users: text(r.users, 2e3, "intended users"), priorities: strings(r.priorities ?? [], 12, "priorities"), exclusions: strings(r.exclusions ?? [], 24, "exclusions"), baseBranch: branch, mode, categories, paths, excludedPaths, ...validateTeamConfiguration({ coordinator, pool, agentSettings: r.agentSettings, roleSettings: r.roleSettings }), trust, check, setup, verification, schedule, maxFiles: integer(r.maxFiles ?? 8, 1, 100, "changed files"), maxLines: integer(r.maxLines ?? 400, 1, 1e4, "changed lines"), rescanDays: integer(r.rescanDays ?? 28, 1, 90, "rescan days"), budget };
|
|
93
|
+
}
|
|
94
|
+
const stringSchema = { type: "string" };
|
|
95
|
+
const stringsSchema = { type: "array", items: stringSchema };
|
|
96
|
+
const obj = (properties) => ({ type: "object", additionalProperties: false, properties, required: Object.keys(properties) });
|
|
97
|
+
const DISCOVERY_REPORT_SCHEMA = obj({ version: { type: "integer", enum: [1] }, summary: stringSchema, candidates: { type: "array", items: obj({ key: stringSchema, title: stringSchema, category: { type: "string", enum: [...CATEGORIES] }, problem: stringSchema, users: stringSchema, proposal: stringSchema, benefit: stringSchema, evidence: { type: "array", items: obj({ path: stringSchema, quote: stringSchema }) }, paths: stringsSchema, acceptance: stringsSchema, kind: { type: "string", enum: ["observed", "hypothesis"] }, risk: { type: "string", enum: ["low", "medium", "high"] }, alignment: { type: "integer" }, impact: { type: "integer" }, confidence: { type: "integer" }, effort: { type: "integer" }, rationale: stringSchema }) } });
|
|
98
|
+
function validateReport(value) {
|
|
99
|
+
if (redactText(JSON.stringify(value)) !== JSON.stringify(value)) throw new Error("Discovery report contains sensitive text; return evidence without credentials");
|
|
100
|
+
const r = closed(value, ["version", "summary", "candidates"], "discovery report");
|
|
101
|
+
if (r.version !== 1 || !Array.isArray(r.candidates) || r.candidates.length > 3) throw new Error("A discovery report contains at most three opportunities");
|
|
102
|
+
const candidates = r.candidates.map((value2) => validateCandidate(value2));
|
|
103
|
+
if (new Set(candidates.map((c) => c.key)).size !== candidates.length) throw new Error("Duplicate problem keys in discovery report");
|
|
104
|
+
return { version: 1, candidates, summary: text(r.summary, 4e3, "discovery summary") };
|
|
105
|
+
}
|
|
106
|
+
function validateCandidate(value, ownerAssigned = false) {
|
|
107
|
+
const c = closed(value, Object.keys(DISCOVERY_REPORT_SCHEMA.properties.candidates.items.properties), "opportunity");
|
|
108
|
+
if (!CATEGORIES.includes(c.category) || !["observed", "hypothesis"].includes(c.kind) || !["low", "medium", "high"].includes(c.risk)) throw new Error("Invalid opportunity category or evidence classification");
|
|
109
|
+
if (!Array.isArray(c.evidence) || !ownerAssigned && !c.evidence.length || c.evidence.length > 8) throw new Error("Provide one to eight code evidence references");
|
|
110
|
+
const evidence = c.evidence.map((v) => {
|
|
111
|
+
const e = closed(v, ["path", "quote"], "evidence");
|
|
112
|
+
const path = text(e.path, 500, "evidence path");
|
|
113
|
+
if (!safePath(path) || path === "*" || sensitiveContextPath(path)) throw new Error("Invalid evidence path");
|
|
114
|
+
return { path, quote: text(e.quote, 2e3, "evidence quote") };
|
|
115
|
+
});
|
|
116
|
+
const paths = strings(c.paths, ownerAssigned ? 32 : 16, "opportunity paths"), acceptance = strings(c.acceptance, 12, "acceptance criteria");
|
|
117
|
+
if (!paths.length || paths.some((p) => !safePath(p)) || !acceptance.length) throw new Error("Opportunities need paths and acceptance criteria");
|
|
118
|
+
return { key: text(c.key, 150, "problem key").toLowerCase(), title: text(c.title, 180, "title"), category: c.category, problem: text(c.problem, 3e3, "problem"), users: text(c.users, 1e3, "users"), proposal: text(c.proposal, 3e3, "proposal"), benefit: text(c.benefit, 1500, "benefit"), evidence, paths, acceptance, kind: c.kind, risk: c.risk, alignment: integer(c.alignment, 0, 3, "alignment"), impact: integer(c.impact, 0, 3, "impact"), confidence: integer(c.confidence, 0, 3, "confidence"), effort: integer(c.effort, 1, 3, "effort"), rationale: text(c.rationale, 1500, "rationale") };
|
|
119
|
+
}
|
|
120
|
+
const ACTIVE_CYCLES = ["requested", "discovering", "selecting", "building", "unknown"];
|
|
121
|
+
import { allowance } from "./project-team-budget.js";
|
|
122
|
+
function pathPermitted(scope, path) {
|
|
123
|
+
return safePath(path) && !sensitiveContextPath(path) && scope.paths.some((p) => containsPath(p, path)) && !scope.excludedPaths.some((p) => containsPath(p, path));
|
|
124
|
+
}
|
|
125
|
+
function candidateProblem(c, mission) {
|
|
126
|
+
if (c.origin !== "owner" && !c.evidenceVerified) return "Evidence could not be verified at the pinned commit";
|
|
127
|
+
if (!mission.categories.includes(c.category)) return "Outside allowed categories";
|
|
128
|
+
if (c.risk !== "low" || c.confidence < 2 || c.alignment < 2) return "Needs an owner decision about risk, evidence or mission alignment";
|
|
129
|
+
if (c.kind === "hypothesis" && c.category !== "experiment") return "Product benefit needs evidence before automatic implementation";
|
|
130
|
+
if (c.paths.some((p) => !pathPermitted(mission, p))) return "Outside allowed paths";
|
|
131
|
+
if (!mission.verification?.checks.some((c2) => c2.required)) return "Configure a required verification check";
|
|
132
|
+
}
|
|
133
|
+
const score = (c) => c.alignment * 4 + c.impact * 3 + c.confidence * 2 - c.effort * 2;
|
|
134
|
+
function validateTeamDocument(v) {
|
|
135
|
+
if (!object(v) || v.version !== 1 || !/^pt-[a-f0-9]{24}$/.test(v.id) || !isAbsolute(v.repo) || !Number.isSafeInteger(v.revision) || v.revision < 1 || !["draft", "active", "paused", "archived"].includes(v.status)) throw new Error("Invalid standing team document");
|
|
136
|
+
validateMission(v.mission);
|
|
137
|
+
for (const key of ["missions", "cycles", "opportunities", "decisions", "operations"]) if (!Array.isArray(v[key])) throw new Error(`Invalid team ${key}`);
|
|
138
|
+
if (v.cycles.length > PT_LIMITS.cycles || v.opportunities.length > PT_LIMITS.opportunities || v.decisions.length > PT_LIMITS.decisions || v.operations.length > PT_LIMITS.operations) throw new Error("Standing team history is full; archive this team and retain its decisions before creating another");
|
|
139
|
+
const validTime = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
140
|
+
const validHash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
141
|
+
const commit = (value) => typeof value === "string" && /^[a-f0-9]{40,64}$/.test(value);
|
|
142
|
+
const unique = (items, field) => new Set(items.map((item) => item[field])).size === items.length;
|
|
143
|
+
if (v.repositoryId !== void 0 && !validHash(v.repositoryId)) throw new Error("Invalid repository identity");
|
|
144
|
+
if (v.schedule !== void 0) missionSchedule(v.schedule);
|
|
145
|
+
if (v.scheduleEnabled !== void 0 && typeof v.scheduleEnabled !== "boolean") throw new Error("Invalid scheduling setting");
|
|
146
|
+
if (v.definition && digest(validateTeamSnapshot(v.definition).configuration) !== digest(teamConfiguration(v.mission))) throw new Error("Assignment differs from its pinned team configuration");
|
|
147
|
+
if (typeof v.projectId !== "string" || !v.projectId || typeof v.name !== "string" || !v.name || !validTime(v.createdAt) || !validTime(v.updatedAt) || !Number.isSafeInteger(v.missionRevision) || v.missionRevision < 1 || !unique(v.missions, "revision") || !unique(v.cycles, "id") || !unique(v.opportunities, "id") || !unique(v.operations, "id")) throw new Error("Invalid standing team identity or history");
|
|
148
|
+
for (const m of v.missions) {
|
|
149
|
+
if (!object(m) || !Number.isSafeInteger(m.revision) || m.revision < 1) throw new Error("Invalid mission revision");
|
|
150
|
+
validateMission(m.value);
|
|
151
|
+
}
|
|
152
|
+
if (!v.missions.some((m) => m.revision === v.missionRevision && digest(m.value) === digest(v.mission))) throw new Error("Current mission differs from its revision history");
|
|
153
|
+
for (const c of v.cycles) {
|
|
154
|
+
if (!object(c) || !/^pc-[a-f0-9]{24}$/.test(c.id) || !commit(c.baseCommit) || !validHash(c.sourceDigest) || !validHash(c.fingerprint) || !validTime(c.at) || c.endedAt !== void 0 && !validTime(c.endedAt) || typeof c.context !== "string" || c.context.length > 2e5 || !Array.isArray(c.launches) || c.launches.length > 2 || !unique(c.launches, "claimId") || !unique(c.launches, "phase") || ![...ACTIVE_CYCLES, "completed", "no-op", "skipped", "blocked", "failed", "cancelled"].includes(c.status)) throw new Error("Invalid cycle accounting");
|
|
155
|
+
if (!v.missions.some((m) => m.revision === c.missionRevision && digest(m.value) === digest(c.mission))) throw new Error("Cycle mission is not an immutable saved revision");
|
|
156
|
+
if (c.definition && digest(validateTeamSnapshot(c.definition).configuration) !== digest(teamConfiguration(c.mission))) throw new Error("Cycle differs from its pinned team configuration");
|
|
157
|
+
for (const l of c.launches) if (!object(l) || !/^claim-[a-f0-9]{24}$/.test(l.claimId) || !["discovery", "implementation"].includes(l.phase) || !Number.isSafeInteger(l.attempt) || l.attempt < 1 || l.attempt > 2 || !validHash(l.inputHash) || !Number.isSafeInteger(l.calls) || l.calls < 4 || l.calls > 80 || !Number.isSafeInteger(l.minutes) || l.minutes < 1 || l.minutes > 120 || !validTime(l.at) || !["claimed", "running", "settled", "unknown"].includes(l.state) || l.usedCalls !== void 0 && (!Number.isSafeInteger(l.usedCalls) || l.usedCalls < 0 || l.usedCalls > l.calls) || l.runId !== void 0 && (typeof l.runId !== "string" || !/^[\w.-]{1,120}$/.test(l.runId)) || ["running", "settled"].includes(l.state) && !l.runId) throw new Error("Invalid launch reservation or usage");
|
|
158
|
+
for (const l of c.launches) if (l.usage) {
|
|
159
|
+
const u = closed(l.usage, ["version", "calls", "durationMs", "endedAt", "recordedAt", "digest"], "usage receipt");
|
|
160
|
+
if (u.version !== 1 || !l.runId || !Number.isSafeInteger(u.calls) || u.calls < 0 || u.calls > l.calls || !validTime(u.durationMs) || u.durationMs > l.minutes * 6e4 || !validTime(u.endedAt) || u.endedAt < l.at || !validTime(u.recordedAt) || !validHash(u.digest)) throw new Error("Invalid terminal usage receipt");
|
|
161
|
+
}
|
|
162
|
+
if (c.report) {
|
|
163
|
+
validateReport(c.report.report);
|
|
164
|
+
if (digest(c.report.report) !== c.report.digest || !commit(c.report.head)) throw new Error("Invalid report receipt");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const o of v.opportunities) {
|
|
168
|
+
if (!object(o) || !/^po-[a-f0-9]{24}$/.test(o.id) || !Number.isSafeInteger(o.revision) || o.revision < 1 || !commit(o.baseCommit) || !validTime(o.createdAt) || typeof o.evidenceVerified !== "boolean" || !v.cycles.some((c) => c.id === o.cycleId) || !["proposed", "selected", "implementing", "ready", "accepted", "shipped", "deferred", "rejected", "blocked", "expired"].includes(o.status)) throw new Error("Invalid opportunity history");
|
|
169
|
+
if (o.origin !== void 0 && o.origin !== "owner") throw new Error("Invalid task origin");
|
|
170
|
+
const candidate = Object.fromEntries(Object.keys(DISCOVERY_REPORT_SCHEMA.properties.candidates.items.properties).map((k) => [k, o[k]]));
|
|
171
|
+
if (o.origin === "owner") validateCandidate(candidate, true);
|
|
172
|
+
else validateReport({ version: 1, summary: "stored", candidates: [candidate] });
|
|
173
|
+
if (o.result && (!commit(o.result.head) || !validHash(o.result.reportHash) || typeof o.result.runId !== "string" || typeof o.result.branch !== "string")) throw new Error("Invalid result identity");
|
|
174
|
+
if (["ready", "accepted", "shipped"].includes(o.status) && !o.result) throw new Error("Result evidence is missing");
|
|
175
|
+
if (o.status === "deferred" && !validTime(o.deferUntil)) throw new Error("Invalid defer date");
|
|
176
|
+
}
|
|
177
|
+
for (const d of v.decisions) if (!object(d) || typeof d.id !== "string" || typeof d.note !== "string" || d.note.length > 4e3 || d.actor !== "owner" || !["approve", "reject", "defer", "build", "revise"].includes(d.action) || !Number.isSafeInteger(d.revision) || !validTime(d.at) || !v.opportunities.some((o) => o.id === d.opportunityId)) throw new Error("Invalid owner decision history");
|
|
178
|
+
for (const o of v.operations) if (!object(o) || typeof o.id !== "string" || !o.id || o.id.length > 150 || !validHash(o.hash) || !validTime(o.at)) throw new Error("Invalid operation history");
|
|
179
|
+
return v;
|
|
180
|
+
}
|
|
181
|
+
function disableRestoredProjectTeam(value) {
|
|
182
|
+
const team = structuredClone(validateTeamDocument(value));
|
|
183
|
+
if (team.status !== "archived") team.status = "paused";
|
|
184
|
+
team.revision++;
|
|
185
|
+
team.updatedAt = Date.now();
|
|
186
|
+
for (const opportunity of team.opportunities) if (["ready", "accepted", "selected", "implementing"].includes(opportunity.status)) {
|
|
187
|
+
opportunity.status = "blocked";
|
|
188
|
+
opportunity.reason = "Restored result requires a new verified run before approval";
|
|
189
|
+
opportunity.revision++;
|
|
190
|
+
}
|
|
191
|
+
team.error = "Restored from backup. Inspect repository membership, launch receipts and budgets before activating.";
|
|
192
|
+
for (const cycle of team.cycles) if (ACTIVE_CYCLES.includes(cycle.status)) {
|
|
193
|
+
cycle.status = "cancelled";
|
|
194
|
+
cycle.endedAt = Date.now();
|
|
195
|
+
cycle.reason = "Cancelled during restore; unresolved launch reservations retained";
|
|
196
|
+
}
|
|
197
|
+
return team;
|
|
198
|
+
}
|
|
199
|
+
export {
|
|
200
|
+
ACTIVE_CYCLES,
|
|
201
|
+
CATEGORIES,
|
|
202
|
+
DISCOVERY_REPORT_SCHEMA,
|
|
203
|
+
PT_LIMITS,
|
|
204
|
+
WEEK,
|
|
205
|
+
allowance,
|
|
206
|
+
candidateProblem,
|
|
207
|
+
closed,
|
|
208
|
+
containsPath,
|
|
209
|
+
digest,
|
|
210
|
+
disableRestoredProjectTeam,
|
|
211
|
+
missionPolicy,
|
|
212
|
+
missionSchedule,
|
|
213
|
+
object,
|
|
214
|
+
pathPermitted,
|
|
215
|
+
projectTeamsEnabled,
|
|
216
|
+
safePath,
|
|
217
|
+
score,
|
|
218
|
+
teamConfiguration,
|
|
219
|
+
text,
|
|
220
|
+
validateMission,
|
|
221
|
+
validateReport,
|
|
222
|
+
validateTeamConfiguration,
|
|
223
|
+
validateTeamDocument,
|
|
224
|
+
validateTeamSnapshot
|
|
225
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { git } from "./git.js";
|
|
4
|
+
import { digest, pathPermitted } from "./project-team-model.js";
|
|
5
|
+
function managedPlanProblem(plan, contract) {
|
|
6
|
+
if (!contract) return;
|
|
7
|
+
if (contract.phase === "discovery" && plan.tasks.some((t) => t.kind !== "investigation" || t.owner === "coordinator")) return "Project discovery permits investigation tasks only";
|
|
8
|
+
if (contract.phase === "implementation" && plan.tasks.some((t) => t.kind === "implementation" && t.paths.some((p) => !pathPermitted(contract.scope, p)))) return "Task ownership exceeds the saved project-team scope";
|
|
9
|
+
}
|
|
10
|
+
const managedRevisionProblem = (revision, contract) => managedPlanProblem({ tasks: revision.addTasks }, contract);
|
|
11
|
+
async function assertProjectTeamChanges(at, run) {
|
|
12
|
+
const c = run.projectTeam;
|
|
13
|
+
if (!c) return;
|
|
14
|
+
if (digest(run.loop.verification ?? null) !== c.verificationHash) throw new Error("The managed verification profile changed");
|
|
15
|
+
const commands = [["diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--name-only", "-z", run.baseRef, "--"], ["ls-files", "--others", "--exclude-standard", "-z"], ["diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--numstat", run.baseRef, "--"]];
|
|
16
|
+
const results = await Promise.all(commands.map((args) => git(args, at)));
|
|
17
|
+
if (results.some((r) => r.code !== 0)) throw new Error("Cannot establish the managed result's changed files");
|
|
18
|
+
const untracked = results[1].stdout.split("\0").filter(Boolean);
|
|
19
|
+
const files = [...new Set([...results[0].stdout.split("\0"), ...untracked].filter(Boolean))];
|
|
20
|
+
if (c.phase === "discovery" && files.length) throw new Error("Discovery produced source changes");
|
|
21
|
+
if (files.some((p) => !pathPermitted(c.scope, p))) throw new Error(`Changes outside the project-team scope: ${files.filter((p) => !pathPermitted(c.scope, p)).join(", ")}`);
|
|
22
|
+
if (files.length > c.scope.maxFiles) throw new Error("Change exceeds the saved file limit");
|
|
23
|
+
let lines = 0;
|
|
24
|
+
for (const line of results[2].stdout.split("\n").filter(Boolean)) {
|
|
25
|
+
const [a, d] = line.split(" ");
|
|
26
|
+
if (!/^\d+$/.test(a ?? "") || !/^\d+$/.test(d ?? "")) throw new Error("Binary changes require an owner-directed task");
|
|
27
|
+
lines += Number(a) + Number(d);
|
|
28
|
+
}
|
|
29
|
+
for (const path of untracked) {
|
|
30
|
+
const file = join(at, path), stat = await lstat(file);
|
|
31
|
+
if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error("Untracked file cannot be admitted within the project-team limits");
|
|
32
|
+
const bytes = await readFile(file);
|
|
33
|
+
if (bytes.includes(0)) throw new Error("Binary changes require an owner-directed task");
|
|
34
|
+
lines += bytes.toString().split("\n").length;
|
|
35
|
+
}
|
|
36
|
+
if (lines > c.scope.maxLines) throw new Error("Change exceeds the saved line limit");
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
assertProjectTeamChanges,
|
|
40
|
+
managedPlanProblem,
|
|
41
|
+
managedRevisionProblem
|
|
42
|
+
};
|