Swades-Agent / Dockerfile
Phase-Technologies's picture
Update Dockerfile
435da9a verified
Raw
History Blame Contribute Delete
24.7 kB
FROM node:22-slim
# 1. Install system utilities required for git operations
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
# 2. Configure directory structure with correct permission scopes for Hugging Face (User 1000)
WORKDIR /appw
RUN mkdir -p /appw/server/workspaces /appw/server/public && chown -R 1000:1000 /appw
# Switch to the non-root node user
USER 1000
# 3. Clone repository to a temporary directory, merge files into the active workdir, and run npm install
RUN git clone https://github.com/Electroiscoding/Swades-Agent.git /tmp/swades \
&& cp -r /tmp/swades/. /appw/ \
&& rm -rf /tmp/swades \
&& npm install
# 4. Write the Backend Broker
RUN cat << 'EOF' > server/index.js
const express = require("express");
const { spawn, execSync } = require("child_process");
const http = require("http");
const WebSocket = require("ws");
const path = require("path");
const fs = require("fs");
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const PORT = process.env.PORT || 7860;
const WORKSPACE_BASE = path.join(__dirname, "workspaces");
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || "";
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || "";
const activeWebsocketPool = new Map();
if (!fs.existsSync(WORKSPACE_BASE)) {
fs.mkdirSync(WORKSPACE_BASE, { recursive: true });
}
app.use(express.static(path.join(__dirname, "public")));
app.get("/login/github", (req, res) => {
const { socketId } = req.query;
if (!socketId) return res.send("Active channel window connection identifier tracking key missing.");
res.redirect(`https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=repo&state=${socketId}`);
});
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
if (!code || !state) return res.send("Authorization metadata parameters corrupted.");
try {
const response = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, client_secret: GITHUB_CLIENT_SECRET, code })
});
const data = await response.json();
const token = data.access_token;
const userRes = await fetch("https://api.github.com/user", {
headers: { "Authorization": `token ${token}`, "User-Agent": "Swades-Orchestrator" }
});
const userData = await userRes.json();
const reposRes = await fetch("https://api.github.com/user/repos?per_page=100&sort=updated", {
headers: { "Authorization": `token ${token}`, "User-Agent": "Swades-Orchestrator" }
});
const reposData = await reposRes.json();
const formattedRepos = Array.isArray(reposData) ? reposData.map(r => ({ name: r.full_name, url: r.clone_url })) : [];
const clientSocket = activeWebsocketPool.get(state);
if (clientSocket && clientSocket.readyState === WebSocket.OPEN) {
clientSocket.send(JSON.stringify({
type: "AUTH_COMPLETE",
token: token,
username: userData.login,
avatar: userData.avatar_url,
repos: formattedRepos
}));
}
res.send(`
<script>
window.close();
</script>
Identity authenticated. Syncing dashboard...
`);
} catch (err) {
res.status(500).send("Handshake Sync breakdown: " + err.message);
}
});
wss.on("connection", (ws) => {
let boundSocketId = null;
let agentProcess = null;
ws.on("message", async (message) => {
try {
const payload = JSON.parse(message);
if (payload.type === "register_channel") {
boundSocketId = payload.socketId;
activeWebsocketPool.set(boundSocketId, ws);
return;
}
if (payload.type === "hydrate_repos") {
try {
const reposRes = await fetch("https://api.github.com/user/repos?per_page=100&sort=updated", {
headers: { "Authorization": `token ${payload.token}`, "User-Agent": "Swades-Orchestrator" }
});
const reposData = await reposRes.json();
const formattedRepos = Array.isArray(reposData) ? reposData.map(r => ({ name: r.full_name, url: r.clone_url })) : [];
ws.send(JSON.stringify({
type: "REPO_HYDRATION_COMPLETE",
repos: formattedRepos
}));
} catch (e) {
ws.send(JSON.stringify({ type: "system", data: "⚠️ Session validation expired. Please re-authenticate." }));
}
return;
}
if (payload.type === "run") {
if (agentProcess) {
return ws.send(JSON.stringify({ type: "system", data: "[Busy] Thread execution lock active." }));
}
const { task, autonomous, userApiKey, userModel, repoUrl, githubToken } = payload;
if (!userApiKey || !repoUrl || !githubToken) {
return ws.send(JSON.stringify({ type: "system", data: "❌ Setup Error: Verification attributes empty." }));
}
const sessionID = "session_" + Date.now();
const userWorkspace = path.join(WORKSPACE_BASE, sessionID);
ws.send(JSON.stringify({ type: "system", data: "⏳ Provisioning clean workspace sandbox container nodes..." }));
try {
const cleanRepo = repoUrl.trim().replace(/^https?:\/\//, "");
const authenticatedRepoUrl = `https://x-access-token:${githubToken}@${cleanRepo}`;
execSync(`git clone ${authenticatedRepoUrl} ${userWorkspace}`, { stdio: "ignore" });
execSync(`git remote set-url origin ${authenticatedRepoUrl}`, { cwd: userWorkspace });
ws.send(JSON.stringify({ type: "system", data: "🧠 Activating Swades context engine layer..." }));
const args = [path.join(__dirname, "../src/index.js"), task];
if (autonomous) args.push("--autonomous");
agentProcess = spawn("node", args, {
cwd: path.join(__dirname, ".."),
env: {
...process.env,
API_KEY: userApiKey,
BASE_URL: "https://openrouter.ai/api/v1",
MODEL: userModel || "openrouter/free",
WORKDIR: userWorkspace
}
});
agentProcess.stdout.on("data", (data) => {
ws.send(JSON.stringify({ type: "output", data: data.toString() }));
});
agentProcess.stderr.on("data", (data) => {
ws.send(JSON.stringify({ type: "output", data: data.toString() }));
});
agentProcess.on("close", (code) => {
agentProcess = null;
ws.send(JSON.stringify({ type: "system", data: "🏁 Task sequence terminated. Evaluating modification tree..." }));
try {
const statusCheck = execSync(`git status --porcelain`, { cwd: userWorkspace }).toString().trim();
if (!statusCheck) {
ws.send(JSON.stringify({ type: "system", data: "ℹ No changes detected. Tree matches origin exactly. Skipping branch creation." }));
return;
}
const branchName = `swades-patch-${Math.floor(1000 + Math.random() * 9000)}`;
execSync(`git checkout -b ${branchName}`, { cwd: userWorkspace, stdio: "ignore" });
execSync(`git config --local user.email "agent@swades.ai" && git config --local user.name "Swades Agent Hub"`, { cwd: userWorkspace });
execSync(`git add . && git commit -m "feat: automated engineering updates via Swades ReAct core loop"`, { cwd: userWorkspace });
execSync(`git push origin ${branchName}`, { cwd: userWorkspace });
const cleanRepoWebPath = repoUrl.replace(/\.git$/, "").replace(/\/$/, "");
const prUrl = `${cleanRepoWebPath}/tree/${branchName}`;
ws.send(JSON.stringify({
type: "success_link",
data: `\x1b[32m✔ Engineering adjustments published upstream!\x1b[0m`,
link: prUrl
}));
} catch (gitErr) {
ws.send(JSON.stringify({ type: "system", data: `❌ Workspace synchronization failed: ${gitErr.message}` }));
}
});
} catch (setupErr) {
ws.send(JSON.stringify({ type: "system", data: `❌ Workspace Initialization Failure: ${setupErr.message}` }));
}
}
if (payload.type === "input" && agentProcess) {
agentProcess.stdin.write(payload.data);
}
} catch (e) {
ws.send(JSON.stringify({ type: "system", data: e.message }));
}
});
ws.on("close", () => {
if (agentProcess) agentProcess.kill();
if (boundSocketId) activeWebsocketPool.delete(boundSocketId);
});
});
server.listen(PORT, "0.0.0.0", () => console.log("System Online"));
EOF
# 5. Write the Premium Silicon Valley Chat Interface Layout with Infinite Session Persistence
RUN cat << 'EOF' > server/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swades Core Workspace</title>
<style>
:root { --bg: #000000; --panel: #0a0a0a; --border: #1c1c1c; --text: #f5f5f5; --muted: #737373; --user-msg: #171717; --accent: #ffffff; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 0; display: flex; flex-direction: column; height: 100vh; -webkit-font-smoothing: antialiased; }
.config-bar { background: var(--panel); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; gap: 20px; }
.inputs-wrapper { display: flex; gap: 12px; flex: 1; align-items: center; }
input { background: #000000; border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; color: var(--text); font-family: "SF Mono", Menlo, monospace; font-size: 13px; box-sizing: border-box; transition: border-color 0.15s ease; }
input:focus { border-color: var(--muted); outline: none; }
.repo-select-container { position: relative; width: 320px; display: none; }
.dropdown-menu { position: absolute; top: calc(100% + 6px); left: 0; width: 100%; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; max-height: 240px; overflow-y: auto; z-index: 100; display: none; box-shadow: 0 10px 30px rgba(0,0,0,0.7); }
.dropdown-item { padding: 10px 14px; font-family: "SF Mono", monospace; font-size: 13px; color: #d4d4d4; cursor: pointer; border-bottom: 1px solid #141414; }
.dropdown-item:hover { background: #141414; color: #fff; }
.user-profile-identity { display: none; align-items: center; gap: 14px; }
.user-profile-identity img { width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); }
.user-profile-identity .meta { display: flex; flex-direction: column; gap: 2px; }
.user-profile-identity .name { font-size: 13px; font-weight: 500; font-family: "SF Mono", monospace; color: var(--text); }
.user-profile-identity .logout-trigger { font-size: 11px; color: var(--muted); cursor: pointer; text-decoration: underline; background: transparent; border: none; padding: 0; text-align: left; }
.user-profile-identity .logout-trigger:hover { color: #ef4444; }
.oauth-btn { background: #24292e; color: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 10px 18px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background 0.15s; height: 38px; box-sizing: border-box; }
.oauth-btn:hover { background: #2f363d; }
.chat-container { flex: 1; overflow-y: auto; padding: 40px 24px; display: flex; flex-direction: column; gap: 24px; max-width: 840px; width: 100%; margin: 0 auto; box-sizing: border-box; }
.message { display: flex; flex-direction: column; gap: 6px; max-width: 100%; }
.message .sender { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.message .bubble { padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border); font-size: 14px; line-height: 1.6; white-space: pre-wrap; position: relative; }
.message.user { align-self: flex-end; max-width: 85%; }
.message.user .bubble { background: var(--user-msg); border-color: transparent; }
.message.agent .bubble { background: var(--panel); font-family: "SF Mono", Menlo, monospace; font-size: 13px; color: #d4d4d4; border-color: var(--border); }
.message.system-status .bubble { background: transparent; border-style: dashed; color: var(--muted); font-size: 13px; }
.message.success-status .bubble { border-color: #262626; background: #0a0a0a; color: #e5e5e5; display: flex; flex-direction: column; gap: 14px; }
.action-btn-link { display: inline-flex; align-items: center; justify-content: center; background: var(--accent); color: var(--bg); font-weight: 500; text-decoration: none; padding: 10px 16px; border-radius: 6px; font-size: 13px; font-family: -apple-system, sans-serif; transition: opacity 0.15s; width: fit-content; }
.action-btn-link:hover { opacity: 0.9; }
.footer-input-area { border-top: 1px solid var(--border); background: var(--bg); padding: 24px; }
.input-box-wrapper { max-width: 840px; margin: 0 auto; display: flex; gap: 12px; align-items: center; background: var(--panel); border: 1px solid var(--border); padding: 8px 8px 8px 16px; border-radius: 24px; }
.input-box-wrapper input { flex: 1; border: none; background: transparent; padding: 8px 0; color: #fff; font-size: 14px; width: 100%; }
.input-box-wrapper input:focus { outline: none; }
.controls { display: flex; align-items: center; gap: 16px; }
button.send { padding: 10px 20px; background: var(--accent); color: var(--bg); border: none; border-radius: 18px; cursor: pointer; font-weight: 500; font-size: 13px; }
.toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); cursor: pointer; user-select: none; }
.toggle input { width: auto; margin: 0; accent-color: var(--text); }
</style>
</head>
<body>
<div class="config-bar">
<div class="inputs-wrapper">
<input type="password" id="key" style="width: 240px;" placeholder="OpenRouter API Key">
<input type="text" id="model" style="width: 160px;" placeholder="Model" value="openrouter/free">
<div class="repo-select-container" id="repoContainer">
<input type="text" id="repoSearch" placeholder="Search your repositories..." autocomplete="off">
<div class="dropdown-menu" id="repoDropdown"></div>
</div>
</div>
<button class="oauth-btn" id="githubAuthBtn">
<svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
<span>Connect GitHub</span>
</button>
<div class="user-profile-identity" id="userProfile">
<img id="userAvatar" src="" alt="Avatar">
<div class="meta">
<span id="usernameDisplay" class="name"></span>
<button id="logoutBtn" class="logout-trigger">Log Out</button>
</div>
</div>
</div>
<div class="chat-container" id="chatContainer">
<div class="message system-status">
<div class="sender">System</div>
<div class="bubble" id="statusMessage">● Swades session channel online. Checking authentication state layers...</div>
</div>
</div>
<div class="footer-input-area">
<div class="input-box-wrapper">
<input type="text" id="taskPrompt" placeholder="Instruct the agent workflow scope...">
<div class="controls">
<label class="toggle"><input type="checkbox" id="autoMode" checked> director_autonomy</label>
<button class="send" id="sendBtn">Send</button>
</div>
</div>
</div>
<script>
const localSocketChannelId = "ch_" + Math.random().toString(36).substring(2, 15);
let activeToken = "";
let selectedRepoUrl = "";
let repositoryCacheList = [];
const chatContainer = document.getElementById("chatContainer");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(`${protocol}//${window.location.host}`);
let currentAgentBubble = null;
const authBtn = document.getElementById("githubAuthBtn");
const userProfile = document.getElementById("userProfile");
const repoContainer = document.getElementById("repoContainer");
const repoSearch = document.getElementById("repoSearch");
const repoDropdown = document.getElementById("repoDropdown");
socket.onopen = () => {
socket.send(JSON.stringify({ type: "register_channel", socketId: localSocketChannelId }));
const cachedToken = localStorage.getItem("swades_persistent_token");
const cachedUser = localStorage.getItem("swades_persistent_user");
const cachedAvatar = localStorage.getItem("swades_persistent_avatar");
if (cachedToken && cachedUser && cachedAvatar) {
activeToken = cachedToken;
authBtn.style.display = "none";
userProfile.style.display = "flex";
document.getElementById("userAvatar").src = cachedAvatar;
document.getElementById("usernameDisplay").textContent = cachedUser;
repoContainer.style.display = "block";
document.getElementById("statusMessage").textContent = "● Session restored from browser layer. Fetching repository list updates...";
socket.send(JSON.stringify({ type: "hydrate_repos", token: cachedToken }));
} else {
document.getElementById("statusMessage").textContent = "● No persistent profile token found. Please tap 'Connect GitHub' to initialize.";
}
};
authBtn.onclick = () => {
const w = 600, h = 650;
const left = (screen.width/2)-(w/2), top = (screen.height/2)-(h/2);
window.open(`/login/github?socketId=${localSocketChannelId}`, "GitHub Authentication", `width=${w},height=${h},top=${top},left=${left}`);
};
document.getElementById("logoutBtn").onclick = () => {
localStorage.clear();
window.location.reload();
};
socket.onmessage = (event) => {
const packet = JSON.parse(event.data);
if (packet.type === "AUTH_COMPLETE") {
activeToken = packet.token;
repositoryCacheList = packet.repos;
localStorage.setItem("swades_persistent_token", packet.token);
localStorage.setItem("swades_persistent_user", packet.username);
localStorage.setItem("swades_persistent_avatar", packet.avatar);
authBtn.style.display = "none";
userProfile.style.display = "flex";
document.getElementById("userAvatar").src = packet.avatar;
document.getElementById("usernameDisplay").textContent = packet.username;
repoContainer.style.display = "block";
populateDropdown(repositoryCacheList);
appendMessage("System", `✔ Identity recognized: ${packet.username}. Mounted ${repositoryCacheList.length} repositories into selection arrays.`, "system-status");
return;
}
if (packet.type === "REPO_HYDRATION_COMPLETE") {
repositoryCacheList = packet.repos;
populateDropdown(repositoryCacheList);
appendMessage("System", `✔ Repositories successfully synchronized. ready for target instructions.`, "system-status");
return;
}
if (packet.type === "system") {
appendMessage("System", packet.data, "system-status");
currentAgentBubble = null;
}
else if (packet.type === "success_link") {
const msgDiv = document.createElement("div");
msgDiv.className = "message success-status";
const senderDiv = document.createElement("div");
senderDiv.className = "sender"; senderDiv.textContent = "System";
const bubbleDiv = document.createElement("div");
bubbleDiv.className = "bubble";
bubbleDiv.innerHTML = `<span>${packet.data}</span><a class="action-btn-link" href="${packet.link}" target="_blank">View Patch Branch →</a>`;
msgDiv.appendChild(senderDiv);
msgDiv.appendChild(bubbleDiv);
chatContainer.appendChild(msgDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
currentAgentBubble = null;
}
else if (packet.type === "output") {
const cleanChunk = packet.data.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
if (cleanChunk.trim() === "") return;
if (!currentAgentBubble) {
currentAgentBubble = appendMessage("Swades Engine", cleanChunk, "agent");
} else {
currentAgentBubble.textContent += cleanChunk;
}
chatContainer.scrollTop = chatContainer.scrollHeight;
}
};
repoSearch.addEventListener("input", (e) => {
const criteria = e.target.value.toLowerCase();
const filtered = repositoryCacheList.filter(r => r.name.toLowerCase().includes(criteria));
populateDropdown(filtered);
repoDropdown.style.display = "block";
});
repoSearch.addEventListener("focus", () => {
if(repositoryCacheList.length > 0) repoDropdown.style.display = "block";
});
document.addEventListener("click", (e) => {
if (!repoContainer.contains(e.target)) repoDropdown.style.display = "none";
});
function populateDropdown(items) {
repoDropdown.innerHTML = "";
if(items.length === 0) {
const fallback = document.createElement("div");
fallback.className = "dropdown-item";
fallback.textContent = "No repositories detected.";
repoDropdown.appendChild(fallback);
return;
}
items.forEach(repo => {
const dItem = document.createElement("div");
dItem.className = "dropdown-item";
dItem.textContent = repo.name;
dItem.onclick = () => {
repoSearch.value = repo.name;
selectedRepoUrl = repo.url;
repoDropdown.style.display = "none";
};
repoDropdown.appendChild(dItem);
});
}
function appendMessage(sender, text, typeClass) {
const msgDiv = document.createElement("div");
msgDiv.className = `message ${typeClass}`;
const senderDiv = document.createElement("div");
senderDiv.className = "sender"; senderDiv.textContent = sender;
const bubbleDiv = document.createElement("div");
bubbleDiv.className = "bubble"; bubbleDiv.textContent = text;
msgDiv.appendChild(senderDiv); msgDiv.appendChild(bubbleDiv);
chatContainer.appendChild(msgDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
return bubbleDiv;
}
document.getElementById("sendBtn").onclick = () => {
const promptInput = document.getElementById("taskPrompt");
const taskText = promptInput.value.trim();
const k = document.getElementById("key").value.trim();
if (!taskText) return;
if (!activeToken) return alert("Verification Failed: Connect GitHub profile scopes first.");
if (!selectedRepoUrl) return alert("Friction Alert: Selection required. Please select a repository from the list.");
if (!k) return alert("Configuration values missing: OpenRouter Key populated context field required.");
appendMessage("User Request", taskText, "user");
promptInput.value = "";
currentAgentBubble = null;
socket.send(JSON.stringify({
type: "run",
task: taskText,
autonomous: document.getElementById("autoMode").checked,
userApiKey: k,
userModel: document.getElementById("model").value.trim(),
repoUrl: selectedRepoUrl,
githubToken: activeToken
}));
};
document.getElementById("taskPrompt").addEventListener("keydown", (e) => {
if (e.key === "Enter") document.getElementById("sendBtn").click();
});
</script>
</body>
</html>
EOF
# 6. Install clean web interfaces
RUN npm install express ws --prefix server
ENV PORT=7860
EXPOSE 7860
# 7. Start application server instance
CMD ["node", "server/index.js"]