@spencerboucher/switchyard 0.6.0 → 0.7.0
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/README.md +6 -6
- package/dist/bridge/switchyard-terminal.js +62 -1
- package/dist/switchyard.mjs +869 -643
- package/package.json +1 -1
- package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
- package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
- package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
- package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
- package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BDt1bxwz.js} +2 -2
- package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BDt1bxwz.js.map} +1 -1
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
- package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
- package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
- package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
- package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
- package/web-dist/assets/attention-Bya8XHKI.js +2 -0
- package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
- package/web-dist/assets/automations-DiXKRcnE.js +2 -0
- package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
- package/web-dist/assets/board-BrLaJwLJ.js +10 -0
- package/web-dist/assets/board-BrLaJwLJ.js.map +1 -0
- package/web-dist/assets/c-Cz4nF6n_.js +1 -0
- package/web-dist/assets/c-SSSlmE39.js +2 -0
- package/web-dist/assets/c-SSSlmE39.js.map +1 -0
- package/web-dist/assets/code-DKLO8bzz.js +11 -0
- package/web-dist/assets/code-DKLO8bzz.js.map +1 -0
- package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
- package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
- package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
- package/web-dist/assets/css-BGIxQoYs.js +2 -0
- package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
- package/web-dist/assets/css-DgaLhXQH.js +1 -0
- package/web-dist/assets/describe-DnvH6_fl.js +5 -0
- package/web-dist/assets/describe-DnvH6_fl.js.map +1 -0
- package/web-dist/assets/diff-woXpYk--.js +2 -0
- package/web-dist/assets/diff-woXpYk--.js.map +1 -0
- package/web-dist/assets/{dist-Bgkh2hc5.js → dist-BE_V9dwJ.js} +2 -2
- package/web-dist/assets/dist-BE_V9dwJ.js.map +1 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
- package/web-dist/assets/enroll-DUDpbJoB.js +2 -0
- package/web-dist/assets/enroll-DUDpbJoB.js.map +1 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
- package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
- package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
- package/web-dist/assets/go-rLFTqkRN.js +2 -0
- package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
- package/web-dist/assets/html-BMn1ogTm.js +2 -0
- package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
- package/web-dist/assets/html-BWDvXlHg.js +1 -0
- package/web-dist/assets/index-DvRcMGEe.css +2 -0
- package/web-dist/assets/index-vIzH1m8u.js +11 -0
- package/web-dist/assets/index-vIzH1m8u.js.map +1 -0
- package/web-dist/assets/java-BtIQNnN0.js +2 -0
- package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
- package/web-dist/assets/java-_ATy9zCa.js +1 -0
- package/web-dist/assets/javascript-BzbQS41l.js +2 -0
- package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
- package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
- package/web-dist/assets/json-qhed-kSA.js +2 -0
- package/web-dist/assets/json-qhed-kSA.js.map +1 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
- package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
- package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
- package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
- package/web-dist/assets/logic-xb-QF00E.js +2 -0
- package/web-dist/assets/logic-xb-QF00E.js.map +1 -0
- package/web-dist/assets/lua-DjuecEsk.js +2 -0
- package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
- package/web-dist/assets/lua-fPRBWD-V.js +1 -0
- package/web-dist/assets/{more-CRGJsz6E.js → more-BP5x45cs.js} +2 -2
- package/web-dist/assets/{more-CRGJsz6E.js.map → more-BP5x45cs.js.map} +1 -1
- package/web-dist/assets/now--XjJC37b.js +2 -0
- package/web-dist/assets/now--XjJC37b.js.map +1 -0
- package/web-dist/assets/python-gzcpVVnB.js +2 -0
- package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
- package/web-dist/assets/rust-DpiIf5JO.js +2 -0
- package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
- package/web-dist/assets/scss-8bKmll0B.js +2 -0
- package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
- package/web-dist/assets/send-BKjswHxB.js +2 -0
- package/web-dist/assets/send-BKjswHxB.js.map +1 -0
- package/web-dist/assets/settings-D8T1esWq.js +7 -0
- package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
- package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
- package/web-dist/assets/shared-CZ7fcu_B.js.map +1 -0
- package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
- package/web-dist/assets/sql-CNu3Or2W.js +2 -0
- package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
- package/web-dist/assets/sql-CfM2IJm3.js +1 -0
- package/web-dist/assets/states--geW2Ntl.js +2 -0
- package/web-dist/assets/states--geW2Ntl.js.map +1 -0
- package/web-dist/assets/swift-CyEgAFGc.js +2 -0
- package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
- package/web-dist/assets/terminal-login-BJxV78au.js +2 -0
- package/web-dist/assets/terminal-login-BJxV78au.js.map +1 -0
- package/web-dist/assets/toml-CcmNWLt0.js +2 -0
- package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
- package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
- package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
- package/web-dist/assets/tsx-CGosfAUg.js +1 -0
- package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
- package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
- package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
- package/web-dist/assets/useNow-jxdoOC_1.js +2 -0
- package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-jxdoOC_1.js.map} +1 -1
- package/web-dist/assets/xml-DuKfbPI6.js +1 -0
- package/web-dist/assets/xml-WY0zc4PG.js +2 -0
- package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
- package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
- package/web-dist/assets/yaml-DijMHRDA.js +2 -0
- package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
- package/web-dist/index.html +4 -3
- package/web-dist/sw.js +1 -1
- package/web-dist/sw.js.map +1 -1
- package/web-dist/assets/AutomationView-DgZn3ltn.js +0 -2
- package/web-dist/assets/AutomationView-DgZn3ltn.js.map +0 -1
- package/web-dist/assets/InteractionCard-cvPWKSu1.js +0 -4
- package/web-dist/assets/InteractionCard-cvPWKSu1.js.map +0 -1
- package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
- package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
- package/web-dist/assets/Overlay-oYnnwvFM.js +0 -2
- package/web-dist/assets/Overlay-oYnnwvFM.js.map +0 -1
- package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
- package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
- package/web-dist/assets/attention-Bxg9W15n.js +0 -2
- package/web-dist/assets/attention-Bxg9W15n.js.map +0 -1
- package/web-dist/assets/automations-DdB97dnL.js +0 -2
- package/web-dist/assets/automations-DdB97dnL.js.map +0 -1
- package/web-dist/assets/board-s4LuDPcg.js +0 -10
- package/web-dist/assets/board-s4LuDPcg.js.map +0 -1
- package/web-dist/assets/code-B0KXn7lD.js +0 -8
- package/web-dist/assets/code-B0KXn7lD.js.map +0 -1
- package/web-dist/assets/describe-C_3FJuSs.js +0 -5
- package/web-dist/assets/describe-C_3FJuSs.js.map +0 -1
- package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
- package/web-dist/assets/enroll-DijmoolY.js +0 -2
- package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
- package/web-dist/assets/index-C5n2QB-Y.css +0 -2
- package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
- package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
- package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
- package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
- package/web-dist/assets/now-D6lDofYy.js +0 -2
- package/web-dist/assets/now-D6lDofYy.js.map +0 -1
- package/web-dist/assets/settings-Csal1F-W.js +0 -8
- package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
- package/web-dist/assets/states-DrTQuYXw.js +0 -2
- package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
- package/web-dist/assets/useNow-Bc_NlDY3.js +0 -2
package/README.md
CHANGED
|
@@ -111,15 +111,15 @@ npm install -g @spencerboucher/switchyard
|
|
|
111
111
|
switchyard up
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
`up` runs the coordinator and a host in one process at `http://localhost:7400`,
|
|
114
|
+
`up` runs the coordinator and a host in one process at `http://localhost:7400`, prints and opens a single-use sign-in link in your browser on first start. Add a project in Settings › Projects (a path to a git repository), then start a session or create a task.
|
|
115
115
|
|
|
116
116
|
**Run it as a server** with the container image (it runs only the coordinator):
|
|
117
117
|
|
|
118
118
|
```sh
|
|
119
119
|
docker run -d --name switchyard -p 127.0.0.1:7400:7400 -v switchyard-data:/data \
|
|
120
120
|
-e SWITCHYARD_PUBLIC_URL=https://switchyard.example.com \
|
|
121
|
-
ghcr.io/justmytwospence/switchyard:0.
|
|
122
|
-
|
|
121
|
+
ghcr.io/justmytwospence/switchyard:0.7.0
|
|
122
|
+
# Single sign-on is required off localhost: see docs/auth.md
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
Put HTTPS in front of it (a reverse proxy, or a VPN or tailnet; the installed phone app, notifications and passkeys need HTTPS). Then, on each machine that should run agents, create a host token in Settings › Hosts › Add host and run:
|
|
@@ -136,8 +136,8 @@ Settings › Hosts › Add host shows these three commands with your token fille
|
|
|
136
136
|
|
|
137
137
|
## Security model
|
|
138
138
|
|
|
139
|
-
- **A server is owned before it serves anything.**
|
|
140
|
-
- **Only you add devices.** A
|
|
139
|
+
- **A server is owned before it serves anything.** Off localhost, the owner is declared in an OpenID Connect configuration (required). There is never an "unclaimed" web page.
|
|
140
|
+
- **Only you add devices.** A browser gets in by single sign-on that matches the configured owner or by a passkey; a terminal is approved from a browser that is already signed in. Each device has its own credential that you can revoke; signing out of an identity provider never breaks an installed app.
|
|
141
141
|
- **Hosts dial out** with their own revocable credentials; the coordinator holds no provider credentials and needs no access to your home directory or Docker socket.
|
|
142
142
|
- **Agents run with the host user's privileges.** Running the coordinator in a container does not sandbox the agents; run hosts as a user whose access you are comfortable giving an agent.
|
|
143
143
|
|
|
@@ -155,7 +155,7 @@ Switchyard is young and moving quickly. It is tested against Pi `>=1.4.0 <1.6.0`
|
|
|
155
155
|
| Guide | Covers |
|
|
156
156
|
|---|---|
|
|
157
157
|
| [Deployment](docs/deployment.md) | Single machine, server, reverse proxies, VPNs, hosts, environment reference, privilege model |
|
|
158
|
-
| [Sign-in and devices](docs/auth.md) | The owner model,
|
|
158
|
+
| [Sign-in and devices](docs/auth.md) | The owner model, single sign-on, passkeys, terminal approval, single sign-on, host enrollment, revocation |
|
|
159
159
|
| [CLI](docs/cli.md) | Every `switchyard` command and credentials |
|
|
160
160
|
| [Terminal sessions](docs/terminal-sessions.md) | Pi's own TUI for every session, from any terminal, and the herdr integration |
|
|
161
161
|
| [Operations](docs/operations.md) | Backup, restore, migrations, retention, logs, health, diagnostics |
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* Dialogs: every extension's `ctx.ui` is the same object, so replacing `select`, `confirm`,
|
|
12
12
|
* `input` and `editor` on it routes all dialogs through the race below. When that object cannot be
|
|
13
13
|
* patched, `remoteDialogs` is false and the host tells remote clients to answer in the terminal.
|
|
14
|
+
*
|
|
15
|
+
* Other extensions that draw a custom terminal dialog can also ask the same question as a
|
|
16
|
+
* web-only dialog through `switchyard.remote-ui.v1` on `pi.events`; the first answer wins.
|
|
14
17
|
*/
|
|
15
18
|
import { randomUUID } from "node:crypto";
|
|
16
19
|
import { createConnection } from "node:net";
|
|
17
20
|
import { modelInfo } from "./pi-model.js";
|
|
18
21
|
const PATCHED = Symbol.for("switchyard.terminal");
|
|
19
22
|
const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
|
|
23
|
+
const REMOTE_UI_CHANNEL = "switchyard.remote-ui.v1";
|
|
20
24
|
function messageText(message) {
|
|
21
25
|
const content = message?.content;
|
|
22
26
|
if (typeof content === "string")
|
|
@@ -82,6 +86,7 @@ export default function switchyardTerminal(pi) {
|
|
|
82
86
|
const early = [];
|
|
83
87
|
const queue = { steering: [], followUp: [] };
|
|
84
88
|
let socket = null;
|
|
89
|
+
let remoteBacked = 0;
|
|
85
90
|
const send = (obj) => {
|
|
86
91
|
try {
|
|
87
92
|
if (!socket || socket.destroyed)
|
|
@@ -219,6 +224,44 @@ export default function switchyardTerminal(pi) {
|
|
|
219
224
|
});
|
|
220
225
|
});
|
|
221
226
|
};
|
|
227
|
+
/** A dialog shown only to remote clients (the caller shows its own in the terminal). */
|
|
228
|
+
const remoteOnly = (method, title, fields, signal) => {
|
|
229
|
+
const dflt = method === "confirm" ? false : undefined;
|
|
230
|
+
if (signal?.aborted)
|
|
231
|
+
return Promise.resolve(dflt);
|
|
232
|
+
const id = randomUUID();
|
|
233
|
+
const request = { type: "extension_ui_request", id, method, title, ...fields };
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
let done = false;
|
|
236
|
+
const onAbort = () => {
|
|
237
|
+
send({
|
|
238
|
+
type: "extension_ui_closed",
|
|
239
|
+
id,
|
|
240
|
+
by: signal?.reason === "pi" ? "pi" : "terminal",
|
|
241
|
+
});
|
|
242
|
+
finish(dflt);
|
|
243
|
+
};
|
|
244
|
+
const finish = (value) => {
|
|
245
|
+
if (done)
|
|
246
|
+
return;
|
|
247
|
+
done = true;
|
|
248
|
+
remoteBacked--;
|
|
249
|
+
pending.delete(id);
|
|
250
|
+
signal?.removeEventListener("abort", onAbort);
|
|
251
|
+
resolve(value);
|
|
252
|
+
};
|
|
253
|
+
remoteBacked++;
|
|
254
|
+
pending.set(id, { request, onRemote: (body) => finish(parseBody(method, body)) });
|
|
255
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
256
|
+
send(request);
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
const remoteUi = {
|
|
260
|
+
select: (title, options, opts) => remoteOnly("select", title, { options }, opts?.signal),
|
|
261
|
+
confirm: (title, message, opts) => remoteOnly("confirm", title, { message }, opts?.signal),
|
|
262
|
+
input: (title, placeholder, opts) => remoteOnly("input", title, placeholder !== undefined ? { placeholder } : {}, opts?.signal),
|
|
263
|
+
editor: (title, prefill, opts) => remoteOnly("editor", title, prefill !== undefined ? { prefill } : {}, opts?.signal),
|
|
264
|
+
};
|
|
222
265
|
const fire = (method, fields) => send({ type: "extension_ui_request", id: randomUUID(), method, ...fields });
|
|
223
266
|
const patchUi = (c) => {
|
|
224
267
|
try {
|
|
@@ -515,6 +558,18 @@ export default function switchyardTerminal(pi) {
|
|
|
515
558
|
catch {
|
|
516
559
|
socket = null;
|
|
517
560
|
}
|
|
561
|
+
try {
|
|
562
|
+
pi.events?.on?.(REMOTE_UI_CHANNEL, (data) => {
|
|
563
|
+
const r = data;
|
|
564
|
+
if (r?.version !== 1 || r.ui || !socket)
|
|
565
|
+
return;
|
|
566
|
+
r.ui = remoteUi;
|
|
567
|
+
r.provider = "switchyard";
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// optional service
|
|
572
|
+
}
|
|
518
573
|
// ---- events ----
|
|
519
574
|
const on = (name, fn) => {
|
|
520
575
|
try {
|
|
@@ -596,7 +651,13 @@ export default function switchyardTerminal(pi) {
|
|
|
596
651
|
on("model_select", () => sendModelState());
|
|
597
652
|
on("thinking_level_select", () => sendModelState());
|
|
598
653
|
on("session_before_compact", (event) => send({ type: "compaction_start", reason: event?.reason ?? "manual" }));
|
|
599
|
-
on("ui_prompt_start", (event) => send({
|
|
654
|
+
on("ui_prompt_start", (event) => send({
|
|
655
|
+
type: "ui_prompt",
|
|
656
|
+
event: event.type,
|
|
657
|
+
kind: event.kind,
|
|
658
|
+
title: event.title ?? null,
|
|
659
|
+
remoteBacked: event.kind === "custom" && remoteBacked > 0,
|
|
660
|
+
}));
|
|
600
661
|
on("ui_prompt_end", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
|
|
601
662
|
on("input", (event) => {
|
|
602
663
|
if (event?.source !== "interactive" || !event.streamingBehavior)
|