@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,418 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { uptime } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CODEFLOW_HOME, timerMs } from "../config.js";
|
|
6
|
+
import { childEnv } from "../env.js";
|
|
7
|
+
import { agentContainerLaunch, isAgentContainerName, stopAgentContainer, stopAgentContainerSync } from "../agent-container.js";
|
|
8
|
+
const DEFAULT_TIMEOUT = 30 * 6e4;
|
|
9
|
+
const DRAIN_MS = 3e3;
|
|
10
|
+
const RESULT_GRACE_MS = 1e4;
|
|
11
|
+
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const liveGroups = /* @__PURE__ */ new Map();
|
|
13
|
+
const dyingGroups = /* @__PURE__ */ new Map();
|
|
14
|
+
const GROUPS_FILE = join(CODEFLOW_HOME, "processes.json");
|
|
15
|
+
process.once("exit", () => {
|
|
16
|
+
for (const record of [...liveGroups.values(), ...dyingGroups.values()]) if (record.containerName) stopAgentContainerSync(record.containerName);
|
|
17
|
+
for (const pid of [...liveGroups.keys(), ...dyingGroups.keys()]) signalGroup(pid, "SIGKILL");
|
|
18
|
+
liveGroups.clear();
|
|
19
|
+
dyingGroups.clear();
|
|
20
|
+
persistGroups();
|
|
21
|
+
});
|
|
22
|
+
function bootTime() {
|
|
23
|
+
return Date.now() - uptime() * 1e3;
|
|
24
|
+
}
|
|
25
|
+
function persistGroups() {
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(CODEFLOW_HOME, { recursive: true });
|
|
28
|
+
const groups = [...new Map([...dyingGroups, ...liveGroups]).values()];
|
|
29
|
+
const tmp = `${GROUPS_FILE}.${process.pid}.tmp`;
|
|
30
|
+
writeFileSync(tmp, JSON.stringify({ server: process.pid, boot: Math.round(bootTime()), groups }));
|
|
31
|
+
renameSync(tmp, GROUPS_FILE);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function trackGroup(pid, containerName) {
|
|
36
|
+
if (!pid) return;
|
|
37
|
+
liveGroups.set(pid, { pid, startedAt: Date.now(), ...containerName ? { containerName } : {} });
|
|
38
|
+
persistGroups();
|
|
39
|
+
}
|
|
40
|
+
function untrackGroup(pid) {
|
|
41
|
+
if (!pid || !liveGroups.delete(pid)) return;
|
|
42
|
+
persistGroups();
|
|
43
|
+
}
|
|
44
|
+
function probe(command, args) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
execFile(command, args, { timeout: 1e4, env: { ...process.env, LC_ALL: "C" } }, (err, stdout) => {
|
|
47
|
+
resolve({ code: err ? typeof err.code === "number" ? err.code : 1 : 0, stdout: String(stdout) });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function exists(pid) {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function isOurGroup(g) {
|
|
60
|
+
if (!exists(-g.pid)) return false;
|
|
61
|
+
if (!exists(g.pid)) return true;
|
|
62
|
+
const lstart = (await probe("ps", ["-o", "lstart=", "-p", String(g.pid)])).stdout.trim();
|
|
63
|
+
const started = lstart ? Date.parse(lstart) : NaN;
|
|
64
|
+
return Number.isFinite(started) && Math.abs(started - g.startedAt) < 5e3;
|
|
65
|
+
}
|
|
66
|
+
async function reapOrphanGroups() {
|
|
67
|
+
if (process.platform === "win32") return 0;
|
|
68
|
+
let saved;
|
|
69
|
+
try {
|
|
70
|
+
saved = JSON.parse(readFileSync(GROUPS_FILE, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
let killed = 0;
|
|
75
|
+
const sameBoot = typeof saved?.boot === "number" && Math.abs(saved.boot - bootTime()) < 12e4;
|
|
76
|
+
if (sameBoot && Array.isArray(saved?.groups)) {
|
|
77
|
+
for (const g of saved.groups) {
|
|
78
|
+
if (!Number.isInteger(g?.pid) || g.pid <= 1 || g.pid === process.pid || liveGroups.has(g.pid)) continue;
|
|
79
|
+
if (isAgentContainerName(g.containerName)) await stopAgentContainer(g.containerName);
|
|
80
|
+
if (!await isOurGroup(g)) continue;
|
|
81
|
+
signalGroup(g.pid, "SIGKILL");
|
|
82
|
+
killed++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
persistGroups();
|
|
86
|
+
return killed;
|
|
87
|
+
}
|
|
88
|
+
function signalGroup(pid, signal) {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(-pid, signal);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function killGroup(pid, graceMs = 5e3) {
|
|
95
|
+
if (!pid) return;
|
|
96
|
+
signalGroup(pid, "SIGTERM");
|
|
97
|
+
if (dyingGroups.has(pid)) return;
|
|
98
|
+
const record = liveGroups.get(pid) ?? { pid, startedAt: Date.now() };
|
|
99
|
+
dyingGroups.set(pid, record);
|
|
100
|
+
if (record.containerName) {
|
|
101
|
+
void stopAgentContainer(record.containerName);
|
|
102
|
+
setTimeout(() => void stopAgentContainer(record.containerName), 300).unref();
|
|
103
|
+
}
|
|
104
|
+
persistGroups();
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
signalGroup(pid, "SIGKILL");
|
|
107
|
+
dyingGroups.delete(pid);
|
|
108
|
+
persistGroups();
|
|
109
|
+
}, graceMs).unref();
|
|
110
|
+
}
|
|
111
|
+
function runJsonlProcess(opts) {
|
|
112
|
+
return startJsonlProcess(opts).outcome;
|
|
113
|
+
}
|
|
114
|
+
function startJsonlProcess(opts) {
|
|
115
|
+
const piped = opts.input !== void 0 || !!opts.interactive;
|
|
116
|
+
const container = opts.agentContainer ? agentContainerLaunch(opts.command, opts.args, opts.cwd, opts.agentContainer.mode, opts.agentContainer.config, opts.agentContainer.readOnly) : void 0;
|
|
117
|
+
const child = spawn(container?.command ?? opts.command, container?.args ?? opts.args, {
|
|
118
|
+
cwd: opts.cwd,
|
|
119
|
+
env: opts.env ?? childEnv(),
|
|
120
|
+
detached: true,
|
|
121
|
+
stdio: [piped ? "pipe" : "ignore", "pipe", "pipe"]
|
|
122
|
+
});
|
|
123
|
+
trackGroup(child.pid, container?.name);
|
|
124
|
+
child.stdin?.on("error", () => {
|
|
125
|
+
});
|
|
126
|
+
if (opts.input !== void 0 && !opts.interactive) child.stdin?.end(opts.input);
|
|
127
|
+
let markResult = () => {
|
|
128
|
+
};
|
|
129
|
+
let pauseIdle = (_paused) => {
|
|
130
|
+
};
|
|
131
|
+
const outcome = new Promise((resolve) => {
|
|
132
|
+
let stderr = "";
|
|
133
|
+
let aborted = false;
|
|
134
|
+
let timedOut = false;
|
|
135
|
+
let stalled = false;
|
|
136
|
+
let resultSeen = false;
|
|
137
|
+
let killedAfterResult = false;
|
|
138
|
+
let settled = false;
|
|
139
|
+
const kill = () => killGroup(child.pid);
|
|
140
|
+
const onAbort = () => {
|
|
141
|
+
aborted = true;
|
|
142
|
+
kill();
|
|
143
|
+
};
|
|
144
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
if (opts.signal?.aborted) onAbort();
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
kill();
|
|
149
|
+
}, timerMs(validMs(opts.timeoutMs) ?? DEFAULT_TIMEOUT));
|
|
150
|
+
timer.unref();
|
|
151
|
+
const idleMs = validMs(opts.idleTimeoutMs);
|
|
152
|
+
let idle;
|
|
153
|
+
let idlePaused = false;
|
|
154
|
+
const alive = () => {
|
|
155
|
+
if (idleMs === void 0 || resultSeen || settled || idlePaused) return;
|
|
156
|
+
clearTimeout(idle);
|
|
157
|
+
idle = setTimeout(() => {
|
|
158
|
+
stalled = true;
|
|
159
|
+
kill();
|
|
160
|
+
}, timerMs(idleMs));
|
|
161
|
+
idle.unref();
|
|
162
|
+
};
|
|
163
|
+
pauseIdle = (paused) => {
|
|
164
|
+
idlePaused = paused;
|
|
165
|
+
clearTimeout(idle);
|
|
166
|
+
if (!paused) alive();
|
|
167
|
+
};
|
|
168
|
+
alive();
|
|
169
|
+
let grace;
|
|
170
|
+
markResult = () => {
|
|
171
|
+
if (resultSeen || settled) return;
|
|
172
|
+
resultSeen = true;
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
clearTimeout(idle);
|
|
175
|
+
grace = setTimeout(() => {
|
|
176
|
+
killedAfterResult = true;
|
|
177
|
+
killGroup(child.pid, 1e3);
|
|
178
|
+
child.stdout?.destroy();
|
|
179
|
+
child.stderr?.destroy();
|
|
180
|
+
finish(null);
|
|
181
|
+
}, timerMs(validMs(opts.resultGraceMs) ?? RESULT_GRACE_MS));
|
|
182
|
+
grace.unref();
|
|
183
|
+
};
|
|
184
|
+
const handleLine = (line) => {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
if (!trimmed) return;
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(trimmed);
|
|
190
|
+
} catch {
|
|
191
|
+
opts.onText?.(trimmed);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
opts.onJson(parsed);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
opts.onText?.(`[adapter error] ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const lines = lineSplitter(
|
|
201
|
+
MAX_LINE_BYTES,
|
|
202
|
+
handleLine,
|
|
203
|
+
(bytes) => opts.onText?.(`[code-flow skipped a ${Math.round(bytes / 1024 / 1024)} MB line of ${opts.command} output: lines over ${MAX_LINE_BYTES / 1024 / 1024} MB aren't read]`)
|
|
204
|
+
);
|
|
205
|
+
child.stdout.on("data", (chunk) => {
|
|
206
|
+
alive();
|
|
207
|
+
if (!settled) lines.push(chunk);
|
|
208
|
+
});
|
|
209
|
+
child.stdout.on("end", () => {
|
|
210
|
+
if (!settled) lines.end();
|
|
211
|
+
});
|
|
212
|
+
child.stderr.setEncoding("utf8");
|
|
213
|
+
child.stderr.on("data", (chunk) => {
|
|
214
|
+
alive();
|
|
215
|
+
stderr += chunk;
|
|
216
|
+
if (stderr.length > 64e3) stderr = stderr.slice(-64e3);
|
|
217
|
+
});
|
|
218
|
+
const finish = (exitCode) => {
|
|
219
|
+
if (settled) return;
|
|
220
|
+
lines.end();
|
|
221
|
+
settled = true;
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
clearTimeout(idle);
|
|
224
|
+
clearTimeout(grace);
|
|
225
|
+
untrackGroup(child.pid);
|
|
226
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
227
|
+
const outcome2 = { exitCode, stderr, aborted, timedOut, stalled, idleMs: stalled ? idleMs : void 0, resultSeen, killedAfterResult };
|
|
228
|
+
if (container) {
|
|
229
|
+
void (async () => {
|
|
230
|
+
await stopAgentContainer(container.name);
|
|
231
|
+
if (aborted || timedOut || stalled || exitCode !== 0) {
|
|
232
|
+
await new Promise((done) => setTimeout(done, 300));
|
|
233
|
+
await stopAgentContainer(container.name);
|
|
234
|
+
}
|
|
235
|
+
resolve(outcome2);
|
|
236
|
+
})();
|
|
237
|
+
} else resolve(outcome2);
|
|
238
|
+
};
|
|
239
|
+
child.on("error", (err) => {
|
|
240
|
+
stderr += `
|
|
241
|
+
${err.message}`;
|
|
242
|
+
finish(null);
|
|
243
|
+
});
|
|
244
|
+
child.on("exit", (code) => {
|
|
245
|
+
killGroup(child.pid, 2e3);
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
child.stdout?.destroy();
|
|
248
|
+
child.stderr?.destroy();
|
|
249
|
+
finish(code);
|
|
250
|
+
}, DRAIN_MS).unref();
|
|
251
|
+
});
|
|
252
|
+
child.on("close", (code) => finish(code));
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
send: (message) => {
|
|
256
|
+
const stdin = child.stdin;
|
|
257
|
+
if (!stdin || stdin.destroyed || stdin.writableEnded) return false;
|
|
258
|
+
stdin.write(`${JSON.stringify(message)}
|
|
259
|
+
`);
|
|
260
|
+
return true;
|
|
261
|
+
},
|
|
262
|
+
endInput: () => child.stdin?.end(),
|
|
263
|
+
kill: () => killGroup(child.pid),
|
|
264
|
+
resultSeen: () => markResult(),
|
|
265
|
+
pauseIdle: (paused) => pauseIdle(paused),
|
|
266
|
+
outcome
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function validMs(ms) {
|
|
270
|
+
return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : void 0;
|
|
271
|
+
}
|
|
272
|
+
function lineSplitter(maxBytes, onLine, onDropped) {
|
|
273
|
+
let parts = [];
|
|
274
|
+
let size = 0;
|
|
275
|
+
let dropped = 0;
|
|
276
|
+
let ended = false;
|
|
277
|
+
const emit = () => {
|
|
278
|
+
if (dropped) onDropped(dropped);
|
|
279
|
+
else if (size) onLine(Buffer.concat(parts, size).toString("utf8"));
|
|
280
|
+
parts = [];
|
|
281
|
+
size = 0;
|
|
282
|
+
dropped = 0;
|
|
283
|
+
};
|
|
284
|
+
return {
|
|
285
|
+
push(chunk) {
|
|
286
|
+
if (ended) return;
|
|
287
|
+
let start = 0;
|
|
288
|
+
for (; ; ) {
|
|
289
|
+
const nl = chunk.indexOf(10, start);
|
|
290
|
+
const piece = chunk.subarray(start, nl === -1 ? chunk.length : nl);
|
|
291
|
+
if (dropped) dropped += piece.length;
|
|
292
|
+
else if (size + piece.length > maxBytes) {
|
|
293
|
+
dropped = size + piece.length;
|
|
294
|
+
parts = [];
|
|
295
|
+
size = 0;
|
|
296
|
+
} else if (piece.length) {
|
|
297
|
+
parts.push(piece);
|
|
298
|
+
size += piece.length;
|
|
299
|
+
}
|
|
300
|
+
if (nl === -1) return;
|
|
301
|
+
emit();
|
|
302
|
+
start = nl + 1;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
/** The stream ended: a last line without a newline still counts. */
|
|
306
|
+
end() {
|
|
307
|
+
if (ended) return;
|
|
308
|
+
ended = true;
|
|
309
|
+
emit();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function captureOutput(command, args, timeoutMs = 3e4) {
|
|
314
|
+
if (args.includes("--help")) return captureHelp(command, args, timeoutMs);
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
execFile(command, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, env: childEnv() }, (err, stdout, stderr) => {
|
|
317
|
+
if (err) reject(new Error(`${command} ${args.join(" ")} failed: ${stderr.toString().trim() || err.message}`));
|
|
318
|
+
else resolve(stdout.toString());
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function captureHelp(command, args, timeoutMs) {
|
|
323
|
+
const fs = await import("node:fs/promises");
|
|
324
|
+
const dir = await fs.mkdtemp(join((await import("node:os")).tmpdir(), "codeflow-cli-help-"));
|
|
325
|
+
const path = join(dir, "help.txt");
|
|
326
|
+
const file = await fs.open(path, "wx", 384);
|
|
327
|
+
try {
|
|
328
|
+
await new Promise((resolve, reject) => {
|
|
329
|
+
const child = spawn(command, args, { env: childEnv(), stdio: ["ignore", file.fd, file.fd], timeout: timeoutMs, killSignal: "SIGKILL" });
|
|
330
|
+
child.on("error", reject);
|
|
331
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${command} help failed: process stopped (${code})`)));
|
|
332
|
+
});
|
|
333
|
+
if ((await file.stat()).size > 2e6) throw new Error("CLI help exceeds limit");
|
|
334
|
+
return await fs.readFile(path, "utf8");
|
|
335
|
+
} finally {
|
|
336
|
+
await file.close();
|
|
337
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function commandExists(binary) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
const child = spawn("which", [binary], { stdio: "ignore", env: childEnv() });
|
|
343
|
+
child.on("error", () => resolve(false));
|
|
344
|
+
child.on("close", (code) => resolve(code === 0));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function describeFailure(name, outcome) {
|
|
348
|
+
if (outcome.aborted) return `${name} was cancelled`;
|
|
349
|
+
if (outcome.timedOut) return `${name} timed out`;
|
|
350
|
+
if (outcome.stalled) return `${name} printed nothing for ${duration(outcome.idleMs ?? 0)} and was stopped (stalled)`;
|
|
351
|
+
const tail = stderrTail(outcome);
|
|
352
|
+
return `${name} exited with code ${outcome.exitCode}${tail ? `: ${tail}` : ""}`;
|
|
353
|
+
}
|
|
354
|
+
function processErrorKind(outcome) {
|
|
355
|
+
if (outcome.aborted) return "cancelled";
|
|
356
|
+
if (outcome.timedOut) return "timeout";
|
|
357
|
+
if (outcome.stalled) return "stalled";
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
function stderrTail(outcome, lines = 5) {
|
|
361
|
+
return outcome.stderr.trim().split("\n").slice(-lines).join("\n");
|
|
362
|
+
}
|
|
363
|
+
function unknownEvents(cli) {
|
|
364
|
+
const counts = /* @__PURE__ */ new Map();
|
|
365
|
+
let total = 0;
|
|
366
|
+
return {
|
|
367
|
+
add(type) {
|
|
368
|
+
const name = typeof type === "string" && type.trim() ? type.trim().slice(0, 60) : "(no type)";
|
|
369
|
+
total++;
|
|
370
|
+
if (counts.has(name) || counts.size < 20) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
371
|
+
},
|
|
372
|
+
get count() {
|
|
373
|
+
return total;
|
|
374
|
+
},
|
|
375
|
+
/** The status line for the turn (once: later calls return undefined). */
|
|
376
|
+
report() {
|
|
377
|
+
if (!total) return void 0;
|
|
378
|
+
const names = [...counts.entries()].map(([name, n]) => n > 1 ? `${name} (${n})` : name).join(", ");
|
|
379
|
+
const line = `${cli} emitted ${total} event${total === 1 ? "" : "s"} code-flow doesn't know: ${names}. If this turn's output looks incomplete, the CLI's format may have changed: update code-flow.`;
|
|
380
|
+
total = 0;
|
|
381
|
+
counts.clear();
|
|
382
|
+
return line;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function duration(ms) {
|
|
387
|
+
return ms >= 6e4 ? `${Math.round(ms / 6e4)} min` : `${Math.max(1, Math.round(ms / 1e3))} s`;
|
|
388
|
+
}
|
|
389
|
+
function truncate(text, max = 4e3) {
|
|
390
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
391
|
+
\u2026 [${text.length - max} more chars]` : text;
|
|
392
|
+
}
|
|
393
|
+
function stringify(value) {
|
|
394
|
+
if (typeof value === "string") return value;
|
|
395
|
+
try {
|
|
396
|
+
return JSON.stringify(value);
|
|
397
|
+
} catch {
|
|
398
|
+
return String(value);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export {
|
|
402
|
+
MAX_LINE_BYTES,
|
|
403
|
+
captureOutput,
|
|
404
|
+
commandExists,
|
|
405
|
+
describeFailure,
|
|
406
|
+
killGroup,
|
|
407
|
+
lineSplitter,
|
|
408
|
+
processErrorKind,
|
|
409
|
+
reapOrphanGroups,
|
|
410
|
+
runJsonlProcess,
|
|
411
|
+
startJsonlProcess,
|
|
412
|
+
stderrTail,
|
|
413
|
+
stringify,
|
|
414
|
+
trackGroup,
|
|
415
|
+
truncate,
|
|
416
|
+
unknownEvents,
|
|
417
|
+
untrackGroup
|
|
418
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { TEAM_TOOLS } from "../team-tools.js";
|
|
6
|
+
import { captureOutput } from "./process.js";
|
|
7
|
+
import { agyIsolationProblem, AGY_FILE_ONLY_LIMITATION } from "./agy-isolation.js";
|
|
8
|
+
const CACHE_MS = 10 * 6e4;
|
|
9
|
+
const UNKNOWN_CACHE_MS = 2 * 6e4;
|
|
10
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11
|
+
function adapterTeamCapabilities(provider) {
|
|
12
|
+
const key = `${provider}:${process.env.PATH}:${process.env.FAKE_UNKNOWN_FLAG ?? ""}:${process.env.FAKE_IGNORE_DELEGATION_FLAGS ?? ""}`;
|
|
13
|
+
const saved = cache.get(key);
|
|
14
|
+
if (saved && Date.now() - saved.at < saved.ttl) return saved.value;
|
|
15
|
+
const entry = { at: Date.now(), ttl: CACHE_MS, value: probe(provider) };
|
|
16
|
+
cache.set(key, entry);
|
|
17
|
+
void entry.value.then((capabilities) => {
|
|
18
|
+
if (capabilities.scopedTools === "unknown") entry.ttl = UNKNOWN_CACHE_MS;
|
|
19
|
+
}, () => cache.delete(key));
|
|
20
|
+
return entry.value;
|
|
21
|
+
}
|
|
22
|
+
async function probe(provider) {
|
|
23
|
+
if (provider.startsWith("acp:")) return { provider, structuredOutput: "unsupported", sessionResume: "unknown", liveInput: "unsupported", scopedTools: "unsupported", sandbox: "unsupported", nestedAgentControl: "unknown", nestedUsage: "unknown", evidence: ["Shared ACP v1 adapter; session loading is negotiated per connection. JSON outcomes use prompt instructions and engine validation. Local Full access only; no client filesystem, terminal or scoped team-tool capabilities."] };
|
|
24
|
+
if (provider.startsWith("plugin:")) return { provider, structuredOutput: "unknown", sessionResume: "unknown", liveInput: "unsupported", scopedTools: "unsupported", sandbox: "unknown", nestedAgentControl: "unknown", nestedUsage: "unknown", evidence: ["Owner-configured provider plugin. Built-in provider protocol fixtures and sandbox probes do not establish compatibility for this extension."] };
|
|
25
|
+
const [versionResult, helpResult] = await Promise.allSettled([
|
|
26
|
+
captureOutput(provider, ["--version"], 5e3),
|
|
27
|
+
captureOutput(provider, ["--help"], 5e3)
|
|
28
|
+
]);
|
|
29
|
+
const version = versionResult.status === "fulfilled" ? versionResult.value.trim().slice(0, 150) : void 0;
|
|
30
|
+
const help = helpResult.status === "fulfilled" ? helpResult.value : "";
|
|
31
|
+
const flags = (...values) => values.every((value) => help.includes(value));
|
|
32
|
+
const supported = (present, observed = !!help) => present ? "tested" : observed ? "unsupported" : "unknown";
|
|
33
|
+
let structuredOutput = supported(flags("--json-schema"));
|
|
34
|
+
let sessionResume = supported(flags(provider === "agy" ? "--conversation" : "--resume"));
|
|
35
|
+
let nestedAgentControl = provider === "claude" ? supported(flags("--disallowedTools")) : "unknown";
|
|
36
|
+
let scopedTools = "unknown";
|
|
37
|
+
let isolatedAgy = false;
|
|
38
|
+
const evidence = ["Adapter protocol and permissions are covered by fake-CLI regression tests; model behavior is evaluated separately."];
|
|
39
|
+
if (provider === "claude") {
|
|
40
|
+
scopedTools = flags("--mcp-config", "--strict-mcp-config", "--allowedTools", "--disallowedTools") ? "tested" : help ? "unsupported" : "unknown";
|
|
41
|
+
evidence.push("Per-turn stdio MCP config and explicit tool allowlist; built-in Agent, Task and team creation are denied for Auto turns.");
|
|
42
|
+
} else if (provider === "codex") {
|
|
43
|
+
const [execHelp, features] = await Promise.allSettled([captureOutput("codex", ["exec", "--help"], 5e3), captureOutput("codex", ["features", "list", "-c", "features.multi_agent=false", "-c", "features.multi_agent_v2.enabled=false"], 5e3)]);
|
|
44
|
+
const execFlags = execHelp.status === "fulfilled" ? execHelp.value : "";
|
|
45
|
+
structuredOutput = supported(execFlags.includes("--output-schema"), !!execFlags);
|
|
46
|
+
sessionResume = supported(/\bresume\b/.test(execFlags), !!execFlags);
|
|
47
|
+
nestedAgentControl = features.status === "fulfilled" && ["multi_agent", "multi_agent_v2"].every((key) => new RegExp(`^${key}\\s+\\S+\\s+false\\s*$`, "m").test(features.value)) ? "tested" : "unknown";
|
|
48
|
+
try {
|
|
49
|
+
const found = JSON.parse(await captureOutput("codex", ["mcp", "get", "codeflow_capability_probe", "--json", "-c", 'mcp_servers.codeflow_capability_probe={command="node",args=[],enabled_tools=["read_inbox"],tools={read_inbox={approval_mode="approve"}},startup_timeout_sec=5,tool_timeout_sec=10}'], 5e3));
|
|
50
|
+
scopedTools = found.transport?.type === "stdio" && found.enabled_tools?.includes("read_inbox") ? "tested" : "unknown";
|
|
51
|
+
} catch {
|
|
52
|
+
scopedTools = "unknown";
|
|
53
|
+
}
|
|
54
|
+
evidence.push("Per-turn mcp_servers override is checked with codex mcp get; app-server and exec share the same config. Managed turns also set agents.enabled=false, disable hooks and both multi-agent features. Capability probes advertise support; live execution is separate evidence.");
|
|
55
|
+
} else {
|
|
56
|
+
scopedTools = help && !help.includes("--mcp-config") ? "unsupported" : "unknown";
|
|
57
|
+
isolatedAgy = !await agyIsolationProblem();
|
|
58
|
+
sessionResume = "unsupported";
|
|
59
|
+
nestedAgentControl = isolatedAgy ? "tested" : "unknown";
|
|
60
|
+
evidence.push(isolatedAgy ? AGY_FILE_ONLY_LIMITATION + " macos-file-only-v1 / CLI 1.4.4 has bounded real-provider and kernel fixture evidence; current login entitlement is checked only by execution." : "No verified isolated Antigravity runtime is available. A sandbox help flag cannot establish inherited permissions. Legacy execution remains separate.");
|
|
61
|
+
}
|
|
62
|
+
evidence.push("Provider token events report parent-turn totals; a separate nested-agent token breakdown is not available.");
|
|
63
|
+
return {
|
|
64
|
+
provider,
|
|
65
|
+
version,
|
|
66
|
+
structuredOutput,
|
|
67
|
+
sessionResume,
|
|
68
|
+
liveInput: provider === "codex" ? supported(/\bapp-server\b/.test(help)) : "unsupported",
|
|
69
|
+
scopedTools,
|
|
70
|
+
sandbox: provider === "claude" ? "unsupported" : provider === "agy" ? isolatedAgy ? "tested" : "unknown" : help ? "tested" : "unknown",
|
|
71
|
+
nestedAgentControl,
|
|
72
|
+
nestedUsage: "unknown",
|
|
73
|
+
evidence
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const TEAM_MCP_NAME = "codeflow_team";
|
|
77
|
+
const TEAM_TOOL_ALLOWLIST = TEAM_TOOLS.map((tool) => `mcp__${TEAM_MCP_NAME}__${tool.name}`);
|
|
78
|
+
const TEAM_NESTED_DENYLIST = ["Agent", "Task", "TeamCreate", "TeamDelete", "SendMessage", "Workflow", "ListAgents", "RemoteTrigger", "CronCreate", "CronDelete", "CronList"];
|
|
79
|
+
async function prepareTeamTools(connection) {
|
|
80
|
+
const dir = await mkdtemp(join(tmpdir(), "codeflow-member-tools-"));
|
|
81
|
+
const credentialPath = join(dir, "connection.json");
|
|
82
|
+
const configPath = join(dir, "mcp.json");
|
|
83
|
+
const clientPath = fileURLToPath(new URL("../team-tool-client.mjs", import.meta.url));
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(credentialPath, JSON.stringify(connection), { mode: 384 });
|
|
86
|
+
const args = [clientPath, "--connection", credentialPath];
|
|
87
|
+
await writeFile(configPath, JSON.stringify({ mcpServers: { [TEAM_MCP_NAME]: { type: "stdio", command: process.execPath, args } } }), { mode: 384 });
|
|
88
|
+
const tools = TEAM_TOOLS.map((tool) => `${tool.name}={approval_mode="approve"}`).join(",");
|
|
89
|
+
const config = `mcp_servers.${TEAM_MCP_NAME}={command=${JSON.stringify(process.execPath)},args=${JSON.stringify(args)},enabled_tools=${JSON.stringify(TEAM_TOOLS.map((tool) => tool.name))},tools={${tools}},startup_timeout_sec=5,tool_timeout_sec=10}`;
|
|
90
|
+
return { claudeConfig: configPath, codexConfig: ["-c", config], codexThreadConfig: { [`mcp_servers.${TEAM_MCP_NAME}`]: { command: process.execPath, args, enabled_tools: TEAM_TOOLS.map((tool) => tool.name), tools: Object.fromEntries(TEAM_TOOLS.map((tool) => [tool.name, { approval_mode: "approve" }])), startup_timeout_sec: 5, tool_timeout_sec: 10 } }, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
await rm(dir, { recursive: true, force: true });
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
TEAM_MCP_NAME,
|
|
98
|
+
TEAM_NESTED_DENYLIST,
|
|
99
|
+
TEAM_TOOL_ALLOWLIST,
|
|
100
|
+
adapterTeamCapabilities,
|
|
101
|
+
prepareTeamTools
|
|
102
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
3
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { childEnv } from "./env.js";
|
|
6
|
+
const filteredContainers = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function filteredTaskContainer(raw, id, readOnly = []) {
|
|
8
|
+
if (raw.authVolume || !/^sha256:[a-f0-9]{64}$/.test(raw.image)) throw new Error("Filtered containers require an immutable image and no authentication volume");
|
|
9
|
+
if (!/^filtered-[a-f0-9]{24}$/.test(id) || readOnly.length > 500 || readOnly.some((path) => !path || path.startsWith("/") || /[\\\x00-\x1f]/.test(path) || path.split("/").some((part) => !part || part === "." || part === ".." || part === ".git"))) throw new Error("Invalid filtered container receipt");
|
|
10
|
+
const config = validateAgentContainer(raw);
|
|
11
|
+
filteredContainers.set(config, { id, readOnly: [...readOnly] });
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
function validateAgentContainer(raw) {
|
|
15
|
+
const value = raw;
|
|
16
|
+
if (!value || typeof value !== "object" || typeof value.image !== "string" || !/^[a-zA-Z0-9][\w./:@-]{0,199}$/.test(value.image)) throw new Error("Agent isolation needs a valid prebuilt Docker image");
|
|
17
|
+
if (value.authVolume !== void 0 && (typeof value.authVolume !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value.authVolume))) throw new Error("Agent authentication must use an explicitly named Docker volume, never a host path");
|
|
18
|
+
if (value.network !== void 0 && typeof value.network !== "boolean") throw new Error("Agent container network must be enabled or disabled explicitly");
|
|
19
|
+
if (value.memoryMb !== void 0 && (!Number.isInteger(value.memoryMb) || value.memoryMb < 128 || value.memoryMb > 65536)) throw new Error("Agent container memory must be 128\u201365536 MB");
|
|
20
|
+
if (value.cpus !== void 0 && (!Number.isFinite(value.cpus) || value.cpus < 0.1 || value.cpus > 64)) throw new Error("Agent container CPUs must be 0.1\u201364");
|
|
21
|
+
return {
|
|
22
|
+
image: value.image,
|
|
23
|
+
...value.authVolume ? { authVolume: value.authVolume } : {},
|
|
24
|
+
...value.network === void 0 ? {} : { network: value.network },
|
|
25
|
+
...value.memoryMb === void 0 ? {} : { memoryMb: value.memoryMb },
|
|
26
|
+
...value.cpus === void 0 ? {} : { cpus: value.cpus }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function agentContainerLaunch(command, args, cwd, mode, raw, extraReadOnly = []) {
|
|
30
|
+
const filtered = filteredContainers.get(raw);
|
|
31
|
+
const container = validateAgentContainer(raw);
|
|
32
|
+
if (!["claude", "codex", "agy"].includes(command)) throw new Error("Only supported official agent CLIs can run in an agent container");
|
|
33
|
+
const root = realpathSync(cwd);
|
|
34
|
+
if (!lstatSync(root).isDirectory()) throw new Error("Agent workspace must be a directory");
|
|
35
|
+
const mounts = /* @__PURE__ */ new Map([[root, mode === "read"]]);
|
|
36
|
+
for (const path of readOnlyGitMetadata(root)) mounts.set(path, true);
|
|
37
|
+
for (const path of extraReadOnly) mounts.set(realpathSync(path), true);
|
|
38
|
+
for (const relative of filtered?.readOnly ?? []) {
|
|
39
|
+
const target = realpathSync(join(root, relative));
|
|
40
|
+
if (!target.startsWith(`${root}/`) || !lstatSync(target).isFile()) throw new Error("Filtered read-only mounts must be regular files inside the export");
|
|
41
|
+
mounts.set(target, true);
|
|
42
|
+
}
|
|
43
|
+
for (const path of mounts.keys()) if (!isAbsolute(path) || /[,\n\r\0]/.test(path)) throw new Error("Docker isolation cannot mount paths containing commas or line breaks");
|
|
44
|
+
const name = `codeflow-agent-${randomUUID().replace(/-/g, "")}`;
|
|
45
|
+
const user = typeof process.getuid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : ["--user", "65532:65532"];
|
|
46
|
+
return {
|
|
47
|
+
command: "docker",
|
|
48
|
+
name,
|
|
49
|
+
args: [
|
|
50
|
+
"run",
|
|
51
|
+
"--rm",
|
|
52
|
+
"--init",
|
|
53
|
+
"--interactive",
|
|
54
|
+
"--pull=never",
|
|
55
|
+
"--name",
|
|
56
|
+
name,
|
|
57
|
+
"--label",
|
|
58
|
+
"codeflow.managed-agent=true",
|
|
59
|
+
"--cap-drop=ALL",
|
|
60
|
+
"--security-opt=no-new-privileges",
|
|
61
|
+
...filtered ? ["--label", `codeflow.filtered-task=${filtered.id}`] : [],
|
|
62
|
+
...filtered ? ["--read-only", "--tmpfs", `/home/codeflow:rw,nosuid,nodev,size=134217728,uid=${typeof process.getuid === "function" ? process.getuid() : 65532},gid=${typeof process.getgid === "function" ? process.getgid() : 65532},mode=0700`] : [],
|
|
63
|
+
"--pids-limit",
|
|
64
|
+
"256",
|
|
65
|
+
"--memory",
|
|
66
|
+
`${container.memoryMb ?? 4096}m`,
|
|
67
|
+
"--cpus",
|
|
68
|
+
String(container.cpus ?? 2),
|
|
69
|
+
...user,
|
|
70
|
+
"--workdir",
|
|
71
|
+
root,
|
|
72
|
+
...[...mounts].flatMap(([path, readOnly]) => ["--mount", `type=bind,source=${path},target=${path}${readOnly ? ",readonly" : ""}`]),
|
|
73
|
+
...container.authVolume ? ["--mount", `type=volume,source=${container.authVolume},target=/home/codeflow`] : [],
|
|
74
|
+
"--tmpfs",
|
|
75
|
+
"/tmp:rw,nosuid,nodev,size=268435456",
|
|
76
|
+
"--env",
|
|
77
|
+
"HOME=/home/codeflow",
|
|
78
|
+
"--env",
|
|
79
|
+
"CODEX_HOME=/home/codeflow/.codex",
|
|
80
|
+
"--env",
|
|
81
|
+
"GIT_TERMINAL_PROMPT=0",
|
|
82
|
+
"--env",
|
|
83
|
+
"GIT_OPTIONAL_LOCKS=0",
|
|
84
|
+
"--env",
|
|
85
|
+
"NO_COLOR=1",
|
|
86
|
+
"--env",
|
|
87
|
+
"GIT_CONFIG_GLOBAL=/dev/null",
|
|
88
|
+
"--env",
|
|
89
|
+
"GIT_CONFIG_SYSTEM=/dev/null",
|
|
90
|
+
"--env",
|
|
91
|
+
"GIT_CONFIG_COUNT=1",
|
|
92
|
+
"--env",
|
|
93
|
+
"GIT_CONFIG_KEY_0=core.excludesFile",
|
|
94
|
+
"--env",
|
|
95
|
+
"GIT_CONFIG_VALUE_0=/dev/null",
|
|
96
|
+
...container.network === false ? ["--network", "none"] : [],
|
|
97
|
+
"--entrypoint",
|
|
98
|
+
command,
|
|
99
|
+
container.image,
|
|
100
|
+
...args
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function readOnlyGitMetadata(root) {
|
|
105
|
+
const marker = join(root, ".git");
|
|
106
|
+
let info;
|
|
107
|
+
try {
|
|
108
|
+
info = lstatSync(marker);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code === "ENOENT") return [];
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
if (info.isSymbolicLink()) throw new Error("Agent isolation refuses a symlinked .git marker");
|
|
114
|
+
if (info.isDirectory()) return [marker];
|
|
115
|
+
if (!info.isFile()) throw new Error("Agent workspace has an invalid .git marker");
|
|
116
|
+
const gitdir = /^gitdir: (.+)\r?\n?$/.exec(readFileSync(marker, "utf8"));
|
|
117
|
+
if (!gitdir) throw new Error("Agent workspace has an invalid worktree .git marker");
|
|
118
|
+
const admin = realpathSync(resolve(root, gitdir[1]));
|
|
119
|
+
const common = realpathSync(resolve(admin, readFileSync(join(admin, "commondir"), "utf8").trim()));
|
|
120
|
+
const backlink = realpathSync(resolve(admin, readFileSync(join(admin, "gitdir"), "utf8").trim()));
|
|
121
|
+
if (backlink !== realpathSync(marker) || dirname(admin) !== join(common, "worktrees") || !lstatSync(join(common, "objects")).isDirectory() || !lstatSync(join(common, "HEAD")).isFile()) throw new Error("Agent worktree metadata does not belong to this workspace");
|
|
122
|
+
return [marker, common];
|
|
123
|
+
}
|
|
124
|
+
const isAgentContainerName = (name) => typeof name === "string" && /^codeflow-agent-[a-f0-9]{32}$/.test(name);
|
|
125
|
+
function stopAgentContainer(name) {
|
|
126
|
+
if (!isAgentContainerName(name)) return Promise.resolve();
|
|
127
|
+
return new Promise((resolve2) => {
|
|
128
|
+
execFile("docker", ["rm", "--force", name], { env: childEnv(), timeout: 5e3 }, () => resolve2());
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function stopAgentContainerSync(name) {
|
|
132
|
+
if (isAgentContainerName(name)) spawnSync("docker", ["rm", "--force", name], { env: childEnv(), timeout: 1500, stdio: "ignore" });
|
|
133
|
+
}
|
|
134
|
+
function agentContainerCapabilities(container) {
|
|
135
|
+
const configured = validateAgentContainer(container);
|
|
136
|
+
return {
|
|
137
|
+
runtime: "docker",
|
|
138
|
+
image: configured.image,
|
|
139
|
+
network: configured.network !== false,
|
|
140
|
+
credentialSource: configured.authVolume ? "named-volume" : "image",
|
|
141
|
+
liveTeamTools: false,
|
|
142
|
+
nativeCodexApprovals: true,
|
|
143
|
+
hostHomeMounted: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
agentContainerCapabilities,
|
|
148
|
+
agentContainerLaunch,
|
|
149
|
+
filteredTaskContainer,
|
|
150
|
+
isAgentContainerName,
|
|
151
|
+
readOnlyGitMetadata,
|
|
152
|
+
stopAgentContainer,
|
|
153
|
+
stopAgentContainerSync,
|
|
154
|
+
validateAgentContainer
|
|
155
|
+
};
|