agent-dag 3.8.0 → 3.8.2

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.
@@ -40,7 +40,7 @@
40
40
  document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
41
41
  })();
42
42
  </script>
43
- <script type="module" crossorigin src="/assets/index-Tzpp82OH.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-D4ZAGkpT.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-ByAgTqB8.css">
45
45
  </head>
46
46
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Run it with npx ccdeck.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,6 +31,18 @@
31
31
  "about a defect in the type, and the suite refuses both it and no space at",
32
32
  "all."
33
33
  ],
34
+ "3.8.2": [
35
+ {
36
+ "title": "\u2328\ufe0f A closed-behind-your-back dialog no longer kills every keyboard shortcut",
37
+ "body": "Open the context ring on a session's card and leave it open. The session finishes, and a couple of minutes later the canvas evicts it \u2014 or you press Clear. The dialog vanished, and from that moment every shortcut in the deck stopped working: Space, C, R, F, L, H, U, A, J, K, T, M, and the trash button too. Only reloading the page brought them back.\n\nThe deck was still counting the dialog as open, because it remembered which session you had opened it for rather than whether anything was on screen. It now closes the dialog when its session leaves the board, which is what should have happened in the first place. The end-of-session recap had the same problem and is fixed with it."
38
+ }
39
+ ],
40
+ "3.8.1": [
41
+ {
42
+ "title": "\ud83d\udd12 Security: update if the deck runs on a Mac",
43
+ "body": "A local process able to reach the deck's port \u2014 no token, no browser, no tab \u2014 could get a string of its choosing compiled as AppleScript when the deck raised a desktop notification with no page open. On macOS that is code running as you.\n\nThe strings were already passed as arguments rather than pasted into the script, which is normally the whole defence. It is not enough for `osascript`: its option parser reads any argument starting with a dash as an option, so a name beginning with `-e` was taken as more script instead of as text. Every place the deck hands `osascript` or `notify-send` something from outside now carries an explicit end-of-options marker, so nothing from outside can be read as an instruction again.\n\nNothing to change on your side beyond updating. If you would rather keep the deck off the desktop entirely, the Notifications switch in the sound menu does that, and `AGENTS_DECK_NO_NOTIFY=1` does it at launch \u2014 but the fix is the thing to have."
44
+ }
45
+ ],
34
46
  "3.8.0": [
35
47
  {
36
48
  "title": "\ud83d\udd15 Notifications now have an off switch",
@@ -102,10 +102,26 @@ export async function notify(title, body, platform = process.platform, deps = {}
102
102
  // shell tool this descends from built its notification by interpolation and
103
103
  // that is the one place it had left the pattern it had banned everywhere
104
104
  // else.
105
+ //
106
+ // AND `--` BEFORE THE OPERANDS, WHICH IS THE HALF THAT WAS MISSING. Passing
107
+ // a string as argv is not the same as it being treated as data: osascript's
108
+ // option parser reads any LEADING-DASH operand as an option, so a title
109
+ // beginning with `-e` was taken as a second script chunk and COMPILED. The
110
+ // note above asserted the argv form made an attacker-chosen host name safe;
111
+ // it did not.
112
+ //
113
+ // The reach was the whole of it. `title` is `basename(cwd) — ccdeck` from
114
+ // block-notify.mjs, `cwd` arrives in the body of `POST /api/event`, and
115
+ // that route is in OPEN_MUTATIONS — no token, no browser identity, which is
116
+ // exactly the sandboxed-subprocess-with-loopback-egress case the gate
117
+ // comments name. Verified on Darwin 25.5 with no payload at all: without
118
+ // `--`, a leading `-e` operand answers "The run handler is specified more
119
+ // than once", which is the compiler reporting on the attacker's string;
120
+ // with `--`, the same string arrives as `item 1 of argv`.
105
121
  const r = await exec("osascript", [
106
122
  "-e",
107
123
  'on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run',
108
- title, body,
124
+ "--", title, body,
109
125
  ]).catch(() => null);
110
126
  return r?.ok === true;
111
127
  }
@@ -158,7 +174,14 @@ export async function notify(title, body, platform = process.platform, deps = {}
158
174
  ], { env: { ...process.env, CCDECK_TOAST_TITLE: title, CCDECK_TOAST_BODY: body } }).catch(() => null);
159
175
  return r?.ok === true;
160
176
  }
161
- const r = await exec("notify-send", [title, body]).catch(() => null);
177
+ // `--` for the same reason as the darwin branch above, and it is worth having
178
+ // even though the consequence here is smaller: notify-send parses a leading
179
+ // dash as an option too, so an attacker-chosen title could suppress or
180
+ // misdirect the notification. There is no script compiler behind it, so this
181
+ // is a broken notification rather than an execution — fixed anyway, because
182
+ // the rule is "never let a user string be read as an option" and a rule with
183
+ // an exception is a rule somebody will apply to the wrong call next time.
184
+ const r = await exec("notify-send", ["--", title, body]).catch(() => null);
162
185
  return r?.ok === true;
163
186
  }
164
187
 
@@ -172,7 +195,10 @@ export async function closeTab(browserKey, url, platform = process.platform, dep
172
195
  // and says so in its own contract, so `osascript -` would read an empty
173
196
  // script and report success having done nothing. The URL still travels as
174
197
  // argv, which is the part that matters.
175
- const r = await exec("osascript", ["-e", closeTabScript(app), url]).catch(() => null);
198
+ // `--` here too. The url comes from a real http(s) history row so it cannot
199
+ // begin with a dash today, but that is a property of the caller rather than
200
+ // of this line, and the next caller will not know it.
201
+ const r = await exec("osascript", ["-e", closeTabScript(app), "--", url]).catch(() => null);
176
202
  if (!r?.ok) return { ok: false, reason: "script_failed" };
177
203
  const said = String(r.stdout ?? "").trim();
178
204
  return { ok: said === "closed", reason: said || "unknown" };
@@ -185,7 +211,10 @@ export async function quitBrowser(browserKey, platform = process.platform, deps
185
211
  const exec = deps.run ?? run;
186
212
  if (platform === "darwin") {
187
213
  const r = await exec("osascript", [
188
- "-e", 'on run argv\ntell application (item 1 of argv) to quit\nend run', app,
214
+ // `--`, as everywhere else that hands osascript an operand. `app` comes
215
+ // from a fixed table so it is safe by construction; the separator costs
216
+ // nothing and means no reader has to go and check that.
217
+ "-e", 'on run argv\ntell application (item 1 of argv) to quit\nend run', "--", app,
189
218
  ]).catch(() => null);
190
219
  return { ok: r?.ok === true, reason: r?.ok ? "quit" : "script_failed" };
191
220
  }