@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.
Files changed (163) hide show
  1. package/README.md +6 -6
  2. package/dist/bridge/switchyard-terminal.js +62 -1
  3. package/dist/switchyard.mjs +869 -643
  4. package/package.json +1 -1
  5. package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
  6. package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
  7. package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
  8. package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
  9. package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
  10. package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
  11. package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BDt1bxwz.js} +2 -2
  12. package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BDt1bxwz.js.map} +1 -1
  13. package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
  14. package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
  15. package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
  16. package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
  17. package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
  18. package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
  19. package/web-dist/assets/attention-Bya8XHKI.js +2 -0
  20. package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
  21. package/web-dist/assets/automations-DiXKRcnE.js +2 -0
  22. package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
  23. package/web-dist/assets/board-BrLaJwLJ.js +10 -0
  24. package/web-dist/assets/board-BrLaJwLJ.js.map +1 -0
  25. package/web-dist/assets/c-Cz4nF6n_.js +1 -0
  26. package/web-dist/assets/c-SSSlmE39.js +2 -0
  27. package/web-dist/assets/c-SSSlmE39.js.map +1 -0
  28. package/web-dist/assets/code-DKLO8bzz.js +11 -0
  29. package/web-dist/assets/code-DKLO8bzz.js.map +1 -0
  30. package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
  31. package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
  32. package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
  33. package/web-dist/assets/css-BGIxQoYs.js +2 -0
  34. package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
  35. package/web-dist/assets/css-DgaLhXQH.js +1 -0
  36. package/web-dist/assets/describe-DnvH6_fl.js +5 -0
  37. package/web-dist/assets/describe-DnvH6_fl.js.map +1 -0
  38. package/web-dist/assets/diff-woXpYk--.js +2 -0
  39. package/web-dist/assets/diff-woXpYk--.js.map +1 -0
  40. package/web-dist/assets/{dist-Bgkh2hc5.js → dist-BE_V9dwJ.js} +2 -2
  41. package/web-dist/assets/dist-BE_V9dwJ.js.map +1 -0
  42. package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
  43. package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
  44. package/web-dist/assets/enroll-DUDpbJoB.js +2 -0
  45. package/web-dist/assets/enroll-DUDpbJoB.js.map +1 -0
  46. package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
  47. package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
  48. package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
  49. package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
  50. package/web-dist/assets/go-rLFTqkRN.js +2 -0
  51. package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
  52. package/web-dist/assets/html-BMn1ogTm.js +2 -0
  53. package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
  54. package/web-dist/assets/html-BWDvXlHg.js +1 -0
  55. package/web-dist/assets/index-DvRcMGEe.css +2 -0
  56. package/web-dist/assets/index-vIzH1m8u.js +11 -0
  57. package/web-dist/assets/index-vIzH1m8u.js.map +1 -0
  58. package/web-dist/assets/java-BtIQNnN0.js +2 -0
  59. package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
  60. package/web-dist/assets/java-_ATy9zCa.js +1 -0
  61. package/web-dist/assets/javascript-BzbQS41l.js +2 -0
  62. package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
  63. package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
  64. package/web-dist/assets/json-qhed-kSA.js +2 -0
  65. package/web-dist/assets/json-qhed-kSA.js.map +1 -0
  66. package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
  67. package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
  68. package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
  69. package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
  70. package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
  71. package/web-dist/assets/logic-xb-QF00E.js +2 -0
  72. package/web-dist/assets/logic-xb-QF00E.js.map +1 -0
  73. package/web-dist/assets/lua-DjuecEsk.js +2 -0
  74. package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
  75. package/web-dist/assets/lua-fPRBWD-V.js +1 -0
  76. package/web-dist/assets/{more-CRGJsz6E.js → more-BP5x45cs.js} +2 -2
  77. package/web-dist/assets/{more-CRGJsz6E.js.map → more-BP5x45cs.js.map} +1 -1
  78. package/web-dist/assets/now--XjJC37b.js +2 -0
  79. package/web-dist/assets/now--XjJC37b.js.map +1 -0
  80. package/web-dist/assets/python-gzcpVVnB.js +2 -0
  81. package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
  82. package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
  83. package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
  84. package/web-dist/assets/rust-DpiIf5JO.js +2 -0
  85. package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
  86. package/web-dist/assets/scss-8bKmll0B.js +2 -0
  87. package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
  88. package/web-dist/assets/send-BKjswHxB.js +2 -0
  89. package/web-dist/assets/send-BKjswHxB.js.map +1 -0
  90. package/web-dist/assets/settings-D8T1esWq.js +7 -0
  91. package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
  92. package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
  93. package/web-dist/assets/shared-CZ7fcu_B.js.map +1 -0
  94. package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
  95. package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
  96. package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
  97. package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
  98. package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
  99. package/web-dist/assets/sql-CNu3Or2W.js +2 -0
  100. package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
  101. package/web-dist/assets/sql-CfM2IJm3.js +1 -0
  102. package/web-dist/assets/states--geW2Ntl.js +2 -0
  103. package/web-dist/assets/states--geW2Ntl.js.map +1 -0
  104. package/web-dist/assets/swift-CyEgAFGc.js +2 -0
  105. package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
  106. package/web-dist/assets/terminal-login-BJxV78au.js +2 -0
  107. package/web-dist/assets/terminal-login-BJxV78au.js.map +1 -0
  108. package/web-dist/assets/toml-CcmNWLt0.js +2 -0
  109. package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
  110. package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
  111. package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
  112. package/web-dist/assets/tsx-CGosfAUg.js +1 -0
  113. package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
  114. package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
  115. package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
  116. package/web-dist/assets/useNow-jxdoOC_1.js +2 -0
  117. package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-jxdoOC_1.js.map} +1 -1
  118. package/web-dist/assets/xml-DuKfbPI6.js +1 -0
  119. package/web-dist/assets/xml-WY0zc4PG.js +2 -0
  120. package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
  121. package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
  122. package/web-dist/assets/yaml-DijMHRDA.js +2 -0
  123. package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
  124. package/web-dist/index.html +4 -3
  125. package/web-dist/sw.js +1 -1
  126. package/web-dist/sw.js.map +1 -1
  127. package/web-dist/assets/AutomationView-DgZn3ltn.js +0 -2
  128. package/web-dist/assets/AutomationView-DgZn3ltn.js.map +0 -1
  129. package/web-dist/assets/InteractionCard-cvPWKSu1.js +0 -4
  130. package/web-dist/assets/InteractionCard-cvPWKSu1.js.map +0 -1
  131. package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
  132. package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
  133. package/web-dist/assets/Overlay-oYnnwvFM.js +0 -2
  134. package/web-dist/assets/Overlay-oYnnwvFM.js.map +0 -1
  135. package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
  136. package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
  137. package/web-dist/assets/attention-Bxg9W15n.js +0 -2
  138. package/web-dist/assets/attention-Bxg9W15n.js.map +0 -1
  139. package/web-dist/assets/automations-DdB97dnL.js +0 -2
  140. package/web-dist/assets/automations-DdB97dnL.js.map +0 -1
  141. package/web-dist/assets/board-s4LuDPcg.js +0 -10
  142. package/web-dist/assets/board-s4LuDPcg.js.map +0 -1
  143. package/web-dist/assets/code-B0KXn7lD.js +0 -8
  144. package/web-dist/assets/code-B0KXn7lD.js.map +0 -1
  145. package/web-dist/assets/describe-C_3FJuSs.js +0 -5
  146. package/web-dist/assets/describe-C_3FJuSs.js.map +0 -1
  147. package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
  148. package/web-dist/assets/enroll-DijmoolY.js +0 -2
  149. package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
  150. package/web-dist/assets/index-C5n2QB-Y.css +0 -2
  151. package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
  152. package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
  153. package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
  154. package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
  155. package/web-dist/assets/now-D6lDofYy.js +0 -2
  156. package/web-dist/assets/now-D6lDofYy.js.map +0 -1
  157. package/web-dist/assets/settings-Csal1F-W.js +0 -8
  158. package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
  159. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
  160. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
  161. package/web-dist/assets/states-DrTQuYXw.js +0 -2
  162. package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
  163. 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`, sets the server up on first start and opens a single-use sign-in link in your browser. Add a project in Settings › Projects (a path to a git repository), then start a session or create a task.
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.6.0
122
- docker exec switchyard switchyard init # prints a single-use sign-in link for your first device
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.** Ownership comes from `switchyard init` on the server, or from an owner declared in an OpenID Connect configuration. There is never an "unclaimed" web page.
140
- - **Only you add devices.** A new device gets in by a pairing link or QR code made from a device you already use, by a passkey, or by single sign-on that matches the configured owner. Each device has its own credential that you can revoke; signing out of an identity provider never breaks an installed app.
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, pairing, passkeys, single sign-on, host enrollment, revocation |
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({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
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)