-
Notifications
You must be signed in to change notification settings - Fork 1
Add container-native multiagent control server #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| .git | ||
| .multiagent | ||
| .worktrees | ||
| InternalServices | ||
| benchmarks | ||
| evaluation | ||
| prod-mcp | ||
| patch.txt | ||
| **/node_modules | ||
| **/target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| name: container | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | ||
| - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 | ||
| - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5 | ||
| id: metadata | ||
| with: | ||
| images: ghcr.io/${{ github.repository_owner }}/multiagent | ||
| tags: | | ||
| type=raw,value=main,enable={{is_default_branch}} | ||
| type=sha,prefix=sha- | ||
| - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 | ||
| with: | ||
| context: . | ||
| push: true | ||
| tags: ${{ steps.metadata.outputs.tags }} | ||
| labels: ${{ steps.metadata.outputs.labels }} | ||
| platforms: linux/amd64 | ||
| provenance: mode=max | ||
| sbom: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| FROM rust:1-bookworm AS multiagent-builder | ||
|
|
||
| WORKDIR /src | ||
| COPY . . | ||
| RUN cargo build --release --locked | ||
|
|
||
| FROM node:22-bookworm-slim | ||
|
|
||
| ARG CODEX_VERSION=0.145.0 | ||
| ARG CLAUDE_CODE_VERSION=2.1.239 | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends awscli ca-certificates git python3 tmux \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && npm install --global "@openai/codex@${CODEX_VERSION}" "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" | ||
|
|
||
| WORKDIR /opt/multiagent | ||
| COPY control-server/package*.json control-server/ | ||
| RUN cd control-server && npm ci --omit=dev | ||
| COPY . . | ||
| COPY --from=multiagent-builder /src/target/release/multiagent /opt/multiagent/bin/multiagent | ||
| RUN chmod +x launch.sh bin/*.sh bin/*.mjs \ | ||
| && useradd --create-home --home-dir /var/lib/multiagent --uid 10001 multiagent \ | ||
| && mkdir -p /var/lib/multiagent/state /var/lib/multiagent/repositories \ | ||
| && chown -R multiagent:multiagent /var/lib/multiagent | ||
|
|
||
| USER 10001:10001 | ||
| ENV HOME=/var/lib/multiagent/home \ | ||
| CODEX_HOME=/var/lib/multiagent/codex \ | ||
| CLAUDE_CONFIG_DIR=/var/lib/multiagent/claude \ | ||
| MULTIAGENT_LAUNCHER_ROOT=/opt/multiagent \ | ||
| MULTIAGENT_STATE_DIR=/var/lib/multiagent/state \ | ||
| MULTIAGENT_REPOSITORY_ROOT=/var/lib/multiagent/repositories \ | ||
| PORT=8080 | ||
| EXPOSE 8080 | ||
| ENTRYPOINT ["/opt/multiagent/bin/container-entrypoint.sh"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| export HOME="${HOME:-/var/lib/multiagent/home}" | ||
| export CODEX_HOME="${CODEX_HOME:-/var/lib/multiagent/codex}" | ||
| export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-/var/lib/multiagent/claude}" | ||
| export MULTIAGENT_STATE_DIR="${MULTIAGENT_STATE_DIR:-/var/lib/multiagent/state}" | ||
| export MULTIAGENT_REPOSITORY_ROOT="${MULTIAGENT_REPOSITORY_ROOT:-/var/lib/multiagent/repositories}" | ||
| mkdir -p "$HOME" "$CODEX_HOME" "$CLAUDE_CONFIG_DIR" "$MULTIAGENT_STATE_DIR" "$MULTIAGENT_REPOSITORY_ROOT" | ||
| if [[ -n "${MULTIAGENT_STATE_S3_URI:-}" && ! -f "$MULTIAGENT_STATE_DIR/control-server/sessions.json" ]]; then | ||
| aws s3 sync "$MULTIAGENT_STATE_S3_URI" "$MULTIAGENT_STATE_DIR" --only-show-errors || true | ||
| fi | ||
| exec node /opt/multiagent/control-server/src/server.mjs | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #!/usr/bin/env node | ||
| import crypto from "node:crypto"; | ||
| import readline from "node:readline"; | ||
|
|
||
| const username = process.argv[2]; | ||
| if (!username || !/^[a-zA-Z0-9._-]{1,64}$/.test(username)) { | ||
| console.error("usage: bin/hash-password.mjs USERNAME"); | ||
| process.exit(2); | ||
| } | ||
| const terminal = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true }); | ||
| terminal.question("Password: ", (password) => { | ||
| terminal.close(); | ||
| if (password.length < 12) { console.error("password must be at least 12 characters"); process.exit(2); } | ||
| const salt = crypto.randomBytes(16); | ||
| const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }); | ||
| console.log(JSON.stringify({ username, passwordHash: `scrypt$16384$8$1$${salt.toString("base64url")}$${hash.toString("base64url")}` })); | ||
| }); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "multiagent-control-server", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "start": "node src/server.mjs" | ||
| }, | ||
| "dependencies": { | ||
| "ws": "8.21.3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| const $ = (selector) => document.querySelector(selector); | ||
| let active = null; | ||
| let socket = null; | ||
| let sessions = []; | ||
|
|
||
| async function api(url, options = {}) { | ||
| const response = await fetch(url, { headers: { "content-type": "application/json", ...(options.headers || {}) }, ...options }); | ||
| const body = await response.json().catch(() => ({})); | ||
| if (!response.ok) throw new Error(body.error || `Request failed (${response.status})`); | ||
| return body; | ||
| } | ||
|
|
||
| function showLogin() { $("#login").hidden = false; $("#console").hidden = true; } | ||
| function showConsole(username) { $("#username").textContent = username; $("#login").hidden = true; $("#console").hidden = false; } | ||
|
|
||
| async function refreshSessions() { | ||
| sessions = (await api("/api/sessions")).sessions; | ||
| const target = $("#sessions"); | ||
| target.replaceChildren(...sessions.map((session) => { | ||
| const button = document.createElement("button"); | ||
| button.className = `session${active === session.id ? " active" : ""}`; | ||
| button.innerHTML = `<strong>${session.id}</strong><small>${session.live ? "live" : session.status} · ${session.repository}</small>`; | ||
| button.onclick = () => selectSession(session.id); | ||
| return button; | ||
| })); | ||
| if (active && !sessions.some((session) => session.id === active)) selectSession(null); | ||
| } | ||
|
|
||
| function selectSession(id) { | ||
| if (socket) socket.close(); | ||
| active = id; | ||
| const session = sessions.find((candidate) => candidate.id === id); | ||
| $("#active-name").textContent = session?.id || "Select a session"; | ||
| $("#active-repo").textContent = session?.repository || ""; | ||
| $("#live-dot").classList.toggle("live", Boolean(session?.live)); | ||
| const running = Boolean(session?.live); | ||
| $("#resume").disabled = !session || running; | ||
| $("#restart").disabled = !running; | ||
| $("#checkpoint").disabled = !running; | ||
| $("#pause").disabled = !running; | ||
| $("#complete").disabled = !running; | ||
| $("#archive").disabled = !session || running || session.status === "archived"; | ||
| $("#message").disabled = !running; | ||
| $("#send").disabled = !running; | ||
| $("#report").textContent = ""; | ||
| if (!session) { $("#terminal").textContent = "No orchestrator selected."; return refreshSessions(); } | ||
| api(`/api/sessions/${id}/report`).then(({ report, transcript }) => { | ||
| $("#report").textContent = report || (transcript ? `Trace references\n${transcript.traceReferences.join("\n")}` : ""); | ||
| }).catch(() => {}); | ||
| $("#terminal").textContent = "Connecting to orchestrator…"; | ||
| const protocol = location.protocol === "https:" ? "wss" : "ws"; | ||
| socket = new WebSocket(`${protocol}://${location.host}/api/sessions/${id}/terminal`); | ||
| socket.onmessage = ({ data }) => { | ||
| const message = JSON.parse(data); | ||
| if (message.type === "output") { | ||
| const terminal = $("#terminal"); | ||
| const atBottom = terminal.scrollHeight - terminal.scrollTop - terminal.clientHeight < 80; | ||
| terminal.textContent = message.output; | ||
| $("#live-dot").classList.toggle("live", message.live); | ||
| if (atBottom) terminal.scrollTop = terminal.scrollHeight; | ||
| } | ||
| if (message.type === "error") $("#terminal").textContent += `\n[control error] ${message.error}`; | ||
| }; | ||
| socket.onclose = () => $("#live-dot").classList.remove("live"); | ||
| refreshSessions(); | ||
| } | ||
|
|
||
| $("#login-form").onsubmit = async (event) => { | ||
| event.preventDefault(); | ||
| const data = Object.fromEntries(new FormData(event.currentTarget)); | ||
| try { const me = await api("/api/login", { method: "POST", body: JSON.stringify(data) }); showConsole(me.username); await refreshSessions(); } | ||
| catch (error) { $("#login-error").textContent = error.message; } | ||
| }; | ||
| $("#logout").onclick = async () => { await api("/api/logout", { method: "POST" }); if (socket) socket.close(); showLogin(); }; | ||
| $("#message-form").onsubmit = (event) => { | ||
| event.preventDefault(); | ||
| const text = $("#message").value; | ||
| if (!text.trim() || socket?.readyState !== WebSocket.OPEN) return; | ||
| socket.send(JSON.stringify({ type: "input", text })); | ||
| $("#message").value = ""; | ||
| }; | ||
| for (const action of ["restart", "resume", "checkpoint", "pause", "complete", "archive"]) $("#" + action).onclick = async () => { | ||
| if (!active) return; | ||
| await api(`/api/sessions/${active}/${action}`, { method: "POST" }); | ||
| await refreshSessions(); | ||
| if (action === "restart" || action === "resume") selectSession(active); | ||
| }; | ||
| $("#new-session").onclick = async () => { | ||
| const repositories = (await api("/api/repositories")).repositories; | ||
| const select = $("#create-form select"); | ||
| select.replaceChildren(...repositories.map((name) => new Option(name, name))); | ||
| $("#create-dialog").showModal(); | ||
| }; | ||
| $("#cancel-create").onclick = () => $("#create-dialog").close(); | ||
| $("#create-form").onsubmit = async (event) => { | ||
| event.preventDefault(); | ||
| try { | ||
| const body = Object.fromEntries(new FormData(event.currentTarget)); | ||
| await api("/api/sessions", { method: "POST", body: JSON.stringify(body) }); | ||
| $("#create-dialog").close(); | ||
| await refreshSessions(); | ||
| selectSession(body.id); | ||
| } catch (error) { $("#create-error").textContent = error.message; } | ||
| }; | ||
|
|
||
| try { const me = await api("/api/me"); showConsole(me.username); await refreshSessions(); } catch { showLogin(); } | ||
| setInterval(() => $("#console").hidden || refreshSessions().catch(() => {}), 5000); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>Multiagent Control</title> | ||
| <link rel="stylesheet" href="/styles.css"> | ||
| </head> | ||
| <body> | ||
| <div class="grain"></div> | ||
| <section id="login" class="login-shell"> | ||
| <form id="login-form" class="login-card"> | ||
| <p class="eyebrow">Movement Infrastructure</p> | ||
| <h1>Operator<br>Console</h1> | ||
| <label>Username<input name="username" autocomplete="username" required></label> | ||
| <label>Password<input name="password" type="password" autocomplete="current-password" required></label> | ||
| <button type="submit">Enter control room</button> | ||
| <p id="login-error" class="error"></p> | ||
| </form> | ||
| </section> | ||
|
|
||
| <main id="console" hidden> | ||
| <header> | ||
| <div><p class="eyebrow">Multiagent</p><h1>Task Control</h1></div> | ||
| <div class="operator"><span id="username"></span><button id="logout" class="quiet">Log out</button></div> | ||
| </header> | ||
| <div class="layout"> | ||
| <aside> | ||
| <div class="aside-head"><h2>Tasks</h2><button id="new-session" title="New task">+</button></div> | ||
| <div id="sessions"></div> | ||
| </aside> | ||
| <section class="terminal-panel"> | ||
| <div class="terminal-head"> | ||
| <div><span id="live-dot" class="dot"></span><strong id="active-name">Select a session</strong><small id="active-repo"></small></div> | ||
| <div class="actions"><button id="resume" disabled>Resume</button><button id="restart" disabled>Restart</button><button id="checkpoint" disabled>Checkpoint</button><button id="pause" disabled>Pause</button><button id="complete" disabled>Complete</button><button id="archive" disabled>Archive</button></div> | ||
| </div> | ||
| <pre id="report"></pre> | ||
| <pre id="terminal">No orchestrator selected.</pre> | ||
| <form id="message-form"> | ||
| <textarea id="message" rows="3" placeholder="Send an instruction to the orchestrator" disabled></textarea> | ||
| <button id="send" type="submit" disabled>Send message</button> | ||
| </form> | ||
| </section> | ||
| </div> | ||
| </main> | ||
|
|
||
| <dialog id="create-dialog"> | ||
| <form id="create-form"> | ||
| <p class="eyebrow">Bootstrap</p><h2>New task</h2> | ||
| <label>Task ID<input name="id" pattern="[a-z0-9][a-z0-9-]{0,62}" required></label> | ||
| <label>Repository<select name="repository" required></select></label> | ||
| <div class="dialog-actions"><button type="button" id="cancel-create" class="quiet">Cancel</button><button type="submit">Start orchestrator</button></div> | ||
| <p id="create-error" class="error"></p> | ||
| </form> | ||
| </dialog> | ||
| <script type="module" src="/app.js"></script> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we need this ? I think the deployment side will start the multiagent process and then server start in parallel as a socket to connect to the tmux