@saws/open-design-service 2.0.0-beta.17

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.
@@ -0,0 +1,25 @@
1
+ ARG OPEN_DESIGN_IMAGE=ghcr.io/nexu-io/od:0.21.0
2
+ FROM ${OPEN_DESIGN_IMAGE}
3
+
4
+ ARG CODEX_VERSION=0.151.0
5
+ ARG CLAUDE_CODE_VERSION=2.1.252
6
+
7
+ USER root
8
+
9
+ RUN apk add --no-cache bash git libgcc libstdc++ ripgrep && \
10
+ apk add --no-cache --virtual .gateway-build-deps python3 make g++ && \
11
+ npm install --global --omit=dev --allow-scripts=@anthropic-ai/claude-code \
12
+ "@openai/codex@${CODEX_VERSION}" \
13
+ "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" && \
14
+ mkdir -p /opt/saws-open-design-gateway /agent-home /workspace && \
15
+ chown -R open-design:open-design /agent-home /workspace
16
+
17
+ COPY --chown=open-design:open-design gateway/package.json /opt/saws-open-design-gateway/package.json
18
+ RUN cd /opt/saws-open-design-gateway && \
19
+ npm install --omit=dev && \
20
+ apk del .gateway-build-deps && \
21
+ npm cache clean --force
22
+
23
+ COPY --chown=open-design:open-design gateway /opt/saws-open-design-gateway
24
+
25
+ USER open-design
@@ -0,0 +1,188 @@
1
+ const ui = {
2
+ notice: document.querySelector("#notice"),
3
+ passwordForm: document.querySelector("#password-form"),
4
+ password: document.querySelector("#password"),
5
+ agents: document.querySelector("#agents"),
6
+ requirement: document.querySelector("#requirement"),
7
+ codexStatus: document.querySelector("#codex-status"),
8
+ claudeStatus: document.querySelector("#claude-status"),
9
+ codexConnect: document.querySelector("#codex-connect"),
10
+ claudeConnect: document.querySelector("#claude-connect"),
11
+ codexDevice: document.querySelector("#codex-device"),
12
+ codexUrl: document.querySelector("#codex-url"),
13
+ codexCode: document.querySelector("#codex-code"),
14
+ terminalWrap: document.querySelector("#terminal-wrap"),
15
+ terminalClose: document.querySelector("#terminal-close"),
16
+ };
17
+
18
+ let csrf;
19
+ let codexPoll;
20
+ let terminalSocket;
21
+
22
+ async function request(path, options = {}) {
23
+ const response = await fetch(path, {
24
+ ...options,
25
+ headers: {
26
+ "content-type": "application/json",
27
+ ...(options.method && options.method !== "GET" ? { "x-csrf-token": csrf } : undefined),
28
+ ...options.headers,
29
+ },
30
+ });
31
+ const body = await response.json().catch(() => ({}));
32
+ if (!response.ok) throw new Error(body.error || `Request failed (${response.status})`);
33
+ return body;
34
+ }
35
+
36
+ function showNotice(message, error = false) {
37
+ ui.notice.hidden = !message;
38
+ ui.notice.textContent = message || "";
39
+ ui.notice.classList.toggle("error", error);
40
+ }
41
+
42
+ function setAgentStatus(element, connected) {
43
+ element.textContent = connected ? "Connected" : "Not connected";
44
+ element.classList.toggle("ok", connected);
45
+ }
46
+
47
+ async function refresh() {
48
+ const status = await request("/__saws/status");
49
+ if (!status.session) {
50
+ ui.passwordForm.hidden = false;
51
+ ui.agents.hidden = true;
52
+ return;
53
+ }
54
+ ui.passwordForm.hidden = true;
55
+ ui.agents.hidden = false;
56
+ setAgentStatus(ui.codexStatus, status.codex);
57
+ setAgentStatus(ui.claudeStatus, status.claude);
58
+ ui.requirement.textContent = `Access requires ${status.requirement === "any" ? "Codex or Claude Code" : status.requirement === "all" ? "both Codex and Claude Code" : status.requirement}.`;
59
+ if (status.satisfied) location.replace("/");
60
+ }
61
+
62
+ ui.passwordForm.addEventListener("submit", async (event) => {
63
+ event.preventDefault();
64
+ showNotice("");
65
+ try {
66
+ await request("/__saws/session", {
67
+ method: "POST",
68
+ body: JSON.stringify({ password: ui.password.value }),
69
+ });
70
+ ui.password.value = "";
71
+ await refresh();
72
+ } catch (error) {
73
+ showNotice(error.message, true);
74
+ }
75
+ });
76
+
77
+ ui.codexConnect.addEventListener("click", async () => {
78
+ ui.codexConnect.disabled = true;
79
+ showNotice("Starting Codex device login…");
80
+ try {
81
+ await request("/__saws/login/codex", { method: "POST", body: "{}" });
82
+ clearInterval(codexPoll);
83
+ codexPoll = setInterval(pollCodex, 1500);
84
+ await pollCodex();
85
+ } catch (error) {
86
+ showNotice(error.message, true);
87
+ ui.codexConnect.disabled = false;
88
+ }
89
+ });
90
+
91
+ async function pollCodex() {
92
+ try {
93
+ const login = await request("/__saws/login/codex");
94
+ if (login.verificationUrl) {
95
+ ui.codexDevice.hidden = false;
96
+ ui.codexUrl.href = login.verificationUrl;
97
+ ui.codexCode.textContent = login.userCode;
98
+ showNotice("Waiting for Codex authorization in your browser.");
99
+ }
100
+ if (login.state === "complete") {
101
+ clearInterval(codexPoll);
102
+ ui.codexConnect.disabled = false;
103
+ showNotice("Codex connected.");
104
+ await refresh();
105
+ } else if (login.state === "error") {
106
+ clearInterval(codexPoll);
107
+ ui.codexConnect.disabled = false;
108
+ showNotice(login.error || "Codex login failed.", true);
109
+ }
110
+ } catch (error) {
111
+ clearInterval(codexPoll);
112
+ ui.codexConnect.disabled = false;
113
+ showNotice(error.message, true);
114
+ }
115
+ }
116
+
117
+ ui.claudeConnect.addEventListener("click", async () => {
118
+ ui.claudeConnect.disabled = true;
119
+ showNotice("");
120
+ try {
121
+ const login = await request("/__saws/login/claude", { method: "POST", body: "{}" });
122
+ openTerminal(login.id);
123
+ } catch (error) {
124
+ ui.claudeConnect.disabled = false;
125
+ showNotice(error.message, true);
126
+ }
127
+ });
128
+
129
+ function openTerminal(id) {
130
+ ui.terminalWrap.hidden = false;
131
+ const terminal = new Terminal({
132
+ cursorBlink: true,
133
+ convertEol: true,
134
+ theme: { background: "#000000" },
135
+ });
136
+ const fit = new FitAddon.FitAddon();
137
+ terminal.loadAddon(fit);
138
+ terminal.open(document.querySelector("#terminal"));
139
+ fit.fit();
140
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
141
+ terminalSocket = new WebSocket(
142
+ `${protocol}//${location.host}/__saws/terminal?id=${encodeURIComponent(id)}`,
143
+ );
144
+ terminal.onData(
145
+ (data) =>
146
+ terminalSocket?.readyState === WebSocket.OPEN &&
147
+ terminalSocket.send(JSON.stringify({ type: "input", data })),
148
+ );
149
+ terminal.onResize(
150
+ ({ cols, rows }) =>
151
+ terminalSocket?.readyState === WebSocket.OPEN &&
152
+ terminalSocket.send(JSON.stringify({ type: "resize", cols, rows })),
153
+ );
154
+ terminalSocket.addEventListener("open", () =>
155
+ terminalSocket.send(
156
+ JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows }),
157
+ ),
158
+ );
159
+ terminalSocket.addEventListener("message", async (event) => {
160
+ const message = JSON.parse(event.data);
161
+ if (message.type === "data") terminal.write(message.data);
162
+ if (message.type === "exit") {
163
+ terminal.write(`\r\n[authentication process exited]\r\n`);
164
+ ui.claudeConnect.disabled = false;
165
+ await refresh();
166
+ }
167
+ });
168
+ terminalSocket.addEventListener("close", () => {
169
+ ui.claudeConnect.disabled = false;
170
+ });
171
+ window.addEventListener("resize", () => fit.fit(), { passive: true });
172
+ }
173
+
174
+ ui.terminalClose.addEventListener("click", () => {
175
+ terminalSocket?.send(JSON.stringify({ type: "cancel" }));
176
+ terminalSocket?.close();
177
+ ui.terminalWrap.hidden = true;
178
+ ui.claudeConnect.disabled = false;
179
+ });
180
+
181
+ (async () => {
182
+ try {
183
+ csrf = (await request("/__saws/csrf")).token;
184
+ await refresh();
185
+ } catch (error) {
186
+ showNotice(error.message, true);
187
+ }
188
+ })();
@@ -0,0 +1,85 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <meta name="color-scheme" content="dark" />
7
+ <title>OpenDesign authentication</title>
8
+ <link rel="stylesheet" href="/__saws/assets/xterm.css" />
9
+ <link rel="stylesheet" href="/__saws/assets/style.css" />
10
+ </head>
11
+ <body>
12
+ <main class="shell">
13
+ <section class="card">
14
+ <p class="eyebrow">SAWS · OpenDesign</p>
15
+ <h1>Connect your design agents</h1>
16
+ <p class="lede">
17
+ This gateway protects OpenDesign and keeps your Codex and Claude Code sessions in
18
+ persistent container storage.
19
+ </p>
20
+ <div id="notice" class="notice" hidden></div>
21
+ <form id="password-form" hidden>
22
+ <label for="password">Application password</label>
23
+ <div class="row">
24
+ <input
25
+ id="password"
26
+ name="password"
27
+ type="password"
28
+ autocomplete="current-password"
29
+ required
30
+ />
31
+ <button type="submit">Continue</button>
32
+ </div>
33
+ </form>
34
+ <section id="agents" hidden>
35
+ <div class="requirement" id="requirement"></div>
36
+ <div class="agent-grid">
37
+ <article class="agent">
38
+ <div>
39
+ <h2>Codex</h2>
40
+ <span id="codex-status" class="pill">Checking</span>
41
+ </div>
42
+ <p>
43
+ Sign in with your ChatGPT subscription using OpenAI’s headless device-code flow.
44
+ </p>
45
+ <button id="codex-connect">Connect Codex</button>
46
+ <div id="codex-device" class="device" hidden>
47
+ <a id="codex-url" target="_blank" rel="noreferrer">Open verification page</a>
48
+ <code id="codex-code"></code>
49
+ <p>
50
+ Enter this one-time code after signing in. Device login must be enabled in your
51
+ ChatGPT security or workspace settings.
52
+ </p>
53
+ </div>
54
+ </article>
55
+ <article class="agent">
56
+ <div>
57
+ <h2>Claude Code</h2>
58
+ <span id="claude-status" class="pill">Checking</span>
59
+ </div>
60
+ <p>
61
+ Sign in with a Claude.ai Pro or Max subscription through the supported Claude Code
62
+ login command.
63
+ </p>
64
+ <button id="claude-connect">Connect Claude Code</button>
65
+ </article>
66
+ </div>
67
+ <div id="terminal-wrap" hidden>
68
+ <div class="terminal-head">
69
+ <span>Claude Code authentication</span
70
+ ><button id="terminal-close" class="quiet">Cancel</button>
71
+ </div>
72
+ <div id="terminal"></div>
73
+ <p class="hint">
74
+ This terminal is constrained to <code>claude auth login</code>; it is not a shell.
75
+ Follow the displayed URL and paste only when Claude Code asks.
76
+ </p>
77
+ </div>
78
+ </section>
79
+ </section>
80
+ </main>
81
+ <script src="/__saws/assets/xterm.js"></script>
82
+ <script src="/__saws/assets/addon-fit.js"></script>
83
+ <script src="/__saws/assets/app.js"></script>
84
+ </body>
85
+ </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "saws-open-design-gateway-runtime",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "@xterm/addon-fit": "0.11.0",
7
+ "@xterm/xterm": "6.0.0",
8
+ "node-pty": "1.1.0",
9
+ "ws": "8.21.3"
10
+ },
11
+ "allowScripts": {
12
+ "node-pty": true
13
+ }
14
+ }