Repository navigation
feat(PG-1023): an Inbox card shows a Keychain value on the owner's click - #1063
Conversation
A reveal card names one of Playground's own Keychain items by its full service and why; its Show draws the value for 30 seconds and its Copy puts it on the clipboard, each fetched by POST /api/v1/inbox/:id/reveal in that one response and stored nowhere. A secret ask's keychain sink takes any playground- service too. Both secret routes refuse the agent sign-in, and reveal refuses a closed or archived card.
|
Shots from a fixture stack at 912a686, taken in headless Chrome at a 1280x900 viewport. The machine and every value shown are made up. There is no before shot, since canary has no such card: canary's own
|
ReviewBot · round 1: 4/5 at 03660f1 · 0 blocking, 4 should-fix, 2 nitsRound 1 at 03660f1 (canary 03e3db6 merged in, clean; the PR's own commits 912a686 and 97b32a3). Read against issue #1023's build, limits and Done when, the plan the Planner and the builder approved, and the Planner's three points. Spec (the right thing)
Standards (built well)
Checks at 03660f1, read right before this post: Patch (wording only)Covers the two Standards should-fixes and the first nit. Checked in the clone: diff --git a/api/hono/src/lib/dashboard/inbox.ts b/api/hono/src/lib/dashboard/inbox.ts
index d77759b3..9e9d911e 100644
--- a/api/hono/src/lib/dashboard/inbox.ts
+++ b/api/hono/src/lib/dashboard/inbox.ts
@@ -308,7 +308,7 @@ export type KeychainRead = (item: string) => Promise<string | null>
const OPTIONS_MESSAGE = `An ask offers 1 to ${MAX_OPTIONS} options, each with a label: one for a yes or no, two or more for lettered choices`
-// What Approve does, one shape per kind of approval the owner keeps (playbook's ask rule): an extension accept, an apps change or a grant's System Settings pane, which the dashboard carries out itself, a Slack message, a ship of canary, a new Linear ticket, a delete, a schema, a budget, or a plain ask, whose words say what its poster does on Approve, for a decision no other shape fits (a production port, a work request from outside the chain, or a step only he can take, such as plugging in the Mac, where Approve means done and Deny means he can't). An ask labels its options in ELI5 / ASD-STE100, one line each: one option and deny for a yes or no, or 2 to 5 for lettered choices, A to E, then None of these (choicesOf); the card shows its text as the quoted block above them. A secret asks for a value instead of a yes, which goes straight to its sink and is kept nowhere.
+// What Approve does, one shape per kind of approval the owner keeps (playbook's ask rule): an extension accept, an apps change or a grant's System Settings pane, which the dashboard carries out itself, a Slack message, a ship of canary, a new Linear ticket, a delete, a schema, a budget, or a plain ask, whose words say what its poster does on Approve, for a decision no other shape fits (a production port, a work request from outside the chain, or a step only he can take, such as plugging in the Mac, where Approve means done and Deny means he can't). An ask labels its options in ELI5 / ASD-STE100, one line each: one option and deny for a yes or no, or 2 to 5 for lettered choices, A to E, then None of these (choicesOf); the card shows its text as the quoted block above them. A secret asks for a value instead of a yes, which goes straight to its sink and is kept nowhere. A reveal shows the owner a Keychain item's value on the owner's click instead, kept nowhere either.
export const previewSchema = z.discriminatedUnion("type", [
z.strictObject({
approve: oneLine(LIMITS.line).optional(),
diff --git a/extensions/core/rules/ask.md b/extensions/core/rules/ask.md
index c6e06e5f..40d26806 100644
--- a/extensions/core/rules/ask.md
+++ b/extensions/core/rules/ask.md
@@ -6,7 +6,7 @@ Decide every two-way door yourself and record it; only a one-way door goes to th
The owner reads the Inbox, not the chat, so a question asked in chat goes unanswered, and a session waiting on one holds up the factory.
A two-way door is a choice that can be undone (a design, an order, scope inside an approved ask, upkeep of our own tools with a backup first, a recovery a machine can finish): take the recommendation, record it in an ADR when its reason should outlast the work, else on the issue, and tell the owner only when it changes what the owner sees or does.
A one-way door is a send that commits the owner to something new, an approval in the owner's name, a ship outside the standing yes, a new ticket, an irreversible delete or breaking change, a breaking schema or production data change, or a budget, each one decision whose preview is exactly what Approve does.
-A step only the owner's hands can take (a login, a plug, a key) goes as a plain ask whose text is that step, and a secret value the owner holds goes to a Vercel project or a Keychain item `KEYCHAIN_ITEMS` allows through a `secret` ask, whose card hands it straight to its sink, or anywhere else where the item says (1Password or a file), so it never reaches chat or any stored row.
+A step only the owner's hands can take (a login, a plug, a key) goes as a plain ask whose text is that step, and a secret value the owner holds goes to a Vercel project or a Keychain item `keychainSpec` allows (Slack's, Yard's or another `playground-` item) through a `secret` ask, whose card hands it straight to its sink, or anywhere else where the item says (1Password or a file), so it never reaches chat or any stored row.
A message to a developer about their work posts directly, while a message to anyone else, and a post in a public channel other than the review channel, comes to the Inbox as a draft first; the org's own skill names who is who.
An ask to quit, restart, close or stop something posts only once nothing depends on it, its `checked` line saying what the four checks found, since a stop made on a session's word can take the Dashboard down with it.
One decision is one item and one thread is one card: a sub-choice goes in the main item's preview, progress edits the item in place, and a later outcome arrives as an update.
diff --git a/web/next/src/components/dashboard/inbox.tsx b/web/next/src/components/dashboard/inbox.tsx
index ca4268ff..36e91ffd 100644
--- a/web/next/src/components/dashboard/inbox.tsx
+++ b/web/next/src/components/dashboard/inbox.tsx
@@ -488,7 +488,6 @@ function Shown({ preview }: { preview: Pick<InboxUpdate, "attachments" | "text">
)
}
-// Exactly what Approve does, the card's description: our own words around the session's, a Slack message's plain text that hides nothing and an ask's rendered as its chat would show them.
// How long a shown value stays on screen before it hides itself.
export const REVEAL_SHOWN_MS = 30_000
@@ -546,6 +545,7 @@ function RevealField({ id }: { id: string }) {
)
}
+// Exactly what Approve does, the card's description: our own words around the session's, a Slack message's plain text that hides nothing and an ask's rendered as its chat would show them.
function Preview({ item }: { item: Extract<InboxItem, { kind: "decision" }> }) {
const preview = item.preview
switch (preview.type) {ThreadsNone open; this round opens one per finding with a line in the diff. Checked
Not verified
ReviewBot · round 1. Push back on any finding you think is wrong. |
…ts Reply stays in its box A reply or an answer's note on a reveal card that holds the card's value is refused before anything is sent, a reveal card's Edit is refused, and its Reply keeps to its own box rather than the chat's composer. ask.md names keychainSpec, and a test shows the request log holds no reveal's value.
ReviewBot · round 2: 3/5 at 1af2e05 · 1 blocking, 0 should-fix, 2 nitsRound 2 at 1af2e05: the PR's own 7fc4369, a merge of canary c1ca5a4 (32e767f, one conflict, the imports in Spec (the right thing)
Standards (built well)
Checks at 1af2e05, read right before this post: GitGuardian green, ThreadsClosed 2 at 7fc4369 (the moved Checked
Not verified
ReviewBot · round 2. Push back on any finding you think is wrong. |
# Conflicts: # web/next/src/components/dashboard/inbox.tsx
Show and Copy fetch the value through POST /reveal outside settle(), which the hosted build stands down, and its relay carries no write, so the hosted Inbox draws neither. read-only.test pins the gate and asks.md's hosted rule names the two buttons. REVEAL_SHOWN_MS is no longer exported, since only its own file reads it.
1af2e05 to
9362085
Compare
ReviewBot · round 3: 5/5 at 9362085 · 0 blocking, 0 should-fix, 3 nitsRound 3 at 9362085, force-pushed from 1af2e05: the PR's own 7fc4369, a plain merge of canary fe8cb9b (c5bfeee, whose remerge diff drops the import conflict's markers and nothing else), then the PR's own 9362085 with the three hosted edits. Spec (the right thing)
Standards (built well)
The fresh full read (both axes, from the merge base fe8cb9b over the whole diff) found nothing blocking or should-fix; its nits are listed above for the worker to take or leave. check-types: PR CI green at 9362085 on canary fe8cb9b ThreadsClosed 3 at 9362085: round 2's merge edits (now commit 9362085 of the PR's own), round 1's Checked
Not verified
ReviewBot · round 3. Push back on any finding you think is wrong. |
# Conflicts: # tests/api/hono/src/lib/relay/protocol.test.ts # tests/web/next/src/components/dashboard/read-only.test.ts
RELAY_WRITES, the writes a paired key signs, matches neither the secret ask's Submit nor a reveal card's Show by path, and names no secret or reveal route, beside the reads' check.
…buntu-26.04 (#1076) ## TL;DR - 🧪 The run-slot lock tests stop failing now and then on GitHub's Linux runner. One such failure held up a ship. - 🔍 The cause was the runner's own `mkdir` command, which can tell two programs that each made the same folder. The tests now use the runner's other `mkdir`, which tells only one, as the Mac's does. ## Summary ```diff tests/scripts/ops/run-slot-fake.ts, machine() slowChild("python3", bin) + if (GNU_MKDIR) // Bun.which("gnumkdir"): ubuntu-26.04 only + bin/mkdir = exec gnumkdir "$@" ``` - GitHub's `ubuntu-latest` is ubuntu-26.04, whose `/usr/bin/mkdir` is uutils coreutils 0.10.0 (`rust-coreutils` 0.10.0-1ubuntu2~26.04.1). Its `create_dir` checks `path.exists()`, then maps a failed `mkdir(2)` to success with `Err(_) if path.is_dir() => Ok(())`, even without `-p`. So when two processes race, both exit 0. - run-slot.sh takes every lock with a plain `mkdir`: the gauges refresh lock, the run slots and the model slots. On that runner, two looks can each hold the gauges lock and both run `top`: "eight looks with no reading yet run top once" got 6. - The image keeps GNU's mkdir as `gnumkdir`, and each fake machine's bin now runs that one wherever it exists. On the Mac and on GNU images nothing changes. - `.agents/testing.md`, Traps: one new trap, the racing `mkdir` and how a test avoids it. **Decisions:** - **The fix is in the test, not in run-slot.sh.** run-slot.sh runs for real only on the Mac, whose BSD `mkdir` is atomic: 60 rounds of the eight-look case there each ran one `top`. Only CI runs the script on Linux. - **The dead-lock takeover case stays out of this PR.** It is a separate race, not yet reproduced alone (Evidence, #959's row), now #1077, so this PR keeps to the two "run top once" cases #956 names. - **The reported lead is refuted.** The lead was that a reading stamped with the look's start time goes stale after a refresh slower than 10 s. The red run took 1.6 s, and six one-second tops in 1.6 s means they ran side by side. A stale stamp cannot do that, while two lock holders can. - **Nothing is posted upstream.** uutils fixed this in 0.11.0 (uutils/coreutils#13970, uutils/coreutils#13986), and Ubuntu's request to bring that fix to 26.04 (LP #2171333) already has a patch waiting for a sponsor. A comment would add no new facts. Once 26.04 takes that patch, the shim is harmless and can go. ## Evidence - **Cause, in an `ubuntu:26.04` container:** 8 racing `mkdir L`, 200 rounds each: | mkdir | rounds with more than one winner | | --- | --- | | `/usr/bin/mkdir` (uutils 0.10.0) | 33 of 200, and 38 of 200 on a second run | | `gnumkdir` (GNU 9.7) | 0 of 200 | - **No repro without uutils:** the eight-look case ran exactly one `top` in 60 of 60 rounds on the Mac (BSD mkdir) and 80 of 80 on `oven/bun:debian` (GNU mkdir, bash 5.2). - **Before / after in `ubuntu:26.04`** (`--cpus 2`, Linux bun 1.4.4): | run | canary's fake | this head's fake | | --- | --- | --- | | probe: 60 rounds of 8 looks with no reading, rounds that ran `top` more than once | 10 of 60 (2 or 3 tops) | 0 of 60 | | `run-slot-gauges-lock.test.ts`, 20 runs, the two "run top once" cases | failed in 6 of 20: the stale-reading case (line 46) in runs 1, 14 and 15, the no-reading case (line 52) in runs 5, 7 and 13 | failed in 0 of 20 | - **At this head on the Mac:** 20 runs in a row of `bun run test:file tests/scripts/ops/run-slot-gauges-lock.test.ts`: 20 of 20 passed, each 10 pass, 0 fail. `bun run check-types:tests`: no errors. `bun run test:skills`: all 13 passed. - **Sightings on CI:** - canary at d216f23, Suite (8): "eight looks with no reading yet run top once", expected 1, received 6, in 1631 ms. - #1063 at 1af2e05, Suite (7) (job 127580442051), ubuntu-26.04: "eight looks at a stale reading run top once", expected 1, received 2, in 1526 ms. The same case fails 3 of 20 on canary's fake above and 0 of 20 at this head. - #959 at 3006e0a, attempt 1, Suite (5) (job 127180040487): line 124 of the same file, "eight looks at a dead lock together each read the stale or the new reading", expected true, received false, in 1627 ms. **Not this race, and not fixed here.** That case still failed 1 of 20 runs at this head in `ubuntu:26.04` (run 12, line 125), with GNU's `mkdir` in the fake. A probe of the case alone found 0 bad rounds in 60 with GNU's `mkdir` and 0 in 60 with all 104 `gnu*` tools on PATH, so no tool swap is shown to fix it. It is #1077. - **doc-sync:** the change touches the test fake alone. `git grep -n -iE "mkdir"` over `.agents`, `scripts/README.md`, `scripts/ops/README.md`, `scripts/hooks/README.md`, `docs` and the core skills finds no line that calls `mkdir` atomic. The new trap in `.agents/testing.md` is the one place that says it is not on ubuntu-26.04. ## Merge Danger **Door:** two-way A revert brings back the uutils `mkdir` in the fake, and the flake with it. **Blast Radius:** small Tests only: every `run-slot*.test.ts` machine on a runner that has `gnumkdir`. run-slot.sh and every other script are unchanged. ## Links - Closes #956 - uutils/coreutils#13970 (the report) and uutils/coreutils#13986 (the fix, in 0.11.0) - #1077: the dead-lock takeover race, left out of this PR - Ubuntu: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2171333 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ReviewBot · round 4: 5/5 at 1406197 · 0 blocking, 0 should-fix, 4 nitsRound 4 at 1406197: a plain merge of canary 752a41d (dd48185, #1044 in; two conflicts, both test files, each kept both sides, and Spec (the right thing)
Standards (built well)
The fresh full read (both axes, from the merge base 752a41d over the whole diff) found nothing blocking or should-fix. Two Spec-axis notes were checked and stand as they are: the owner-rung refusal is held router-wide by check-types: PR CI green at 1406197 on canary 25501e1 ThreadsNone open. Checked
Not verified
ReviewBot · round 4. Push back on any finding you think is wrong. |
|
Merged by bun run pr:merge at 1406197.
|
…d battery.ts (#1127) ## TL;DR - The battery check now shows ask's own refusal when an Inbox post fails, not bun's "script exited" line. A `hold=` on a roster row that is not parked now stops with a reason, where before it was dropped without a word. - Six smaller review notes from merged PRs land with them: a relay comment, a reveal-card guard limit, one shared test type, two plainer lines in the stack rules, and a test comment that says what its row guards. ## Summary The review nits left on #1063, #1004, #1079, #1093, #1120 and `battery.ts`, as #1102 lists them: ```text packages/message/src/index.ts:39 the stay-on-the-Mac list names the reveal card api/hono/src/lib/dashboard/inbox.ts holdsRevealed matches a value of 8+ characters (REVEAL_MATCH_MIN) tests/.../ops-tick/fake-run.ts exports Answers; three tests use it .agents/dashboard/stack.md:29-30 "the CA stays when the trash refuses its leaves"; all four shareCa() false answers packages/scripts/src/ops-awake/battery.ts bun run --silent ... ask scripts/roster.ts parkHold hold=<ref> on a row left not parked: refused, naming status=parked tests/packages/settings/src/items.test.ts the other-item rev row's comment says what it guards ``` ```diff parkHold(entry, pairs, moved) if entry ends not parked + if the call names a non-empty hold + refuse: "... so add status=parked to the same call" drop its hold ``` ### Decisions - **REVEAL_MATCH_MIN = 8 narrows what the reveal guard matches.** A `playground-` item may hold a value of 1 character (`ANY_VALUE` in `keychain-items.ts`), and a substring match of a short value would refuse most comments on its card. So a value under 8 characters is no match, and a comment holding it goes. A short value can then reach its poster in a comment: the guard is a best-effort catch of a pasted Copy, and its card still shows the value only on the owner's click. - A call that leaves the park and names a hold (`status=working hold=#5`) is refused too, where #1093 dropped the hold. Its test changed with it. ## Evidence - **Before**, measured: canary's `battery.ts`, `roster.ts` and `inbox.ts` swapped in at d014737, `bun run test:file` on their three test files: 244 pass, 3 fail, exactly the new rows: - `ask runs under bun run --silent, so a refusal reads as ask's own line` (the argv held no `--silent`) - `a hold on a row the call leaves not parked is refused naming status=parked, and stores nothing` (exit 0) - `a reveal card's value under 8 characters refuses no comment` (the reply was refused); at 668a65c the row is `a reveal card's value refuses a comment from 8 characters on: 7 is no match` **After**, measured at d014737, through run-slot: - `bun run test:file` on battery, roster, inbox, protocol and the three ops-tick files: 355 pass, 0 fail - `bun run check-types` and `bun run check-types:tests`: both exit 0 - `bun run test:skills`, since `scripts/roster.ts` is a script: exit 0, every suite passed - **The minimum is pinned at 8**, measured at 900cc5c (round 1's should-fix): `bun run test:file tests/api/hono/src/lib/dashboard/inbox.test.ts` is 183 pass, 0 fail at the head; with `REVEAL_MATCH_MIN = 7` and with `= 9` swapped in, each is 182 pass, 1 fail, the minimum's row alone (a 7-character value `about t` goes, an 8-character `about th` is refused, on the comment `about the tab`). - Docs, each read for its new rule with a writing-for-agents pass: - `.agents/dashboard/asks.md:355` and `web/next/content/docs/inbox.mdx:122`: the 8 characters, checked against `REVEAL_MATCH_MIN` in `api/hono/src/lib/dashboard/inbox.ts` - `extensions/core/skills/playbook/ROSTER.md:46-47`: the refusal, checked against `parkHold` in `scripts/roster.ts` and its test row - `.agents/dashboard/stack.md:29-30`: checked against `shareCa()`'s four `false` answers in `api/hono/src/lib/dev/proxy.ts` - The changed-term grep for the old hold drop (`git grep "drops its .hold."` over the docs) finds ROSTER.md:46 alone, now naming the refusal; `REVEAL_MATCH_MIN` is named in inbox.ts and asks.md alone. ## Merge Danger **Door:** two-way Each change is small and reverts on its own. **Blast Radius:** small `roster.ts set` refuses one more call shape, so a lead's script that sent `hold=` to a working row now exits 2 with the fix named. A reveal card's comment holding a value under 8 characters now goes. ## Links - Closes #1102 - Review nits from #1063, #1004, #1079 and #1093 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… Your message (#1140) ## TL;DR - ✉️ In your Inbox, a Slack draft's words now sit under **Your message**, not "Your reply". Reply means one thing: a comment back to the session that posted the card. - 🗂️ The In progress, Later, Archive and Done rows now act by where a card sits, not by the row's words. Renaming a row changes only what it shows: Archive's ✓ still dismisses each card, and Done's ✓ still marks what is unread as read. ## Summary ```diff Slack draft card (web/next/src/components/dashboard/inbox.tsx) - So far … / Your reply / <the words to send> + So far … / Your message / <the words to send> the shelves under the list (inbox.tsx, Inbox) - [label, icon, cards, place]; key, value, open shelves and a link's shelf by label - label === "Archive" -> Dismiss all; label === "Done" -> Mark all read, unread, muted - label !== "Later" && label !== "In progress" -> show the ✓ + [place, icon, cards]; key, value, open shelves and a link's shelf by place + shelfRow(place, cards) web/next/src/lib/inbox.ts + archived -> { bulk: { each: true, ids: every card } } + done -> { bulk: { each: false, ids: the unread }, unread, muted at 0 unread } + progress, snoozed -> { bulk: null } + SHELF_LABELS[place] -> the row's words, and the ✓'s name: Dismiss all in <words>, Mark all in <words> as read ``` Two-way decisions taken here: - The draft label reads **Your message**, matching the box's own name (`Message to <to>`); "To send" was the other choice. - The Preview row keeps its `"Preview"` key: it is not a shelf and has no place. ## Evidence - **Tests**, red first: with `web/next/src/lib/inbox.ts` and `inbox.tsx` back at canary, both test files fail at import (`Export named 'SHELF_LABELS' not found`), 0 pass, 2 fail, since canary has no place-keyed shelf to rename. At e0db978, 367 pass, then the packages build, web and repo `check-types` and `check-types:tests`; at the head d84fdce, every web test that reads `inbox.tsx` or `lib/inbox` (15 files) gives 571 pass, 0 fail, then the packages build and web `check-types` (repo-wide checks left to CI during the ship's load hold). ```text shelfRow archived: every card, each in turn; done: the unread, muted at 0; progress, snoozed: no ✓; an empty shelf: no ✓ Inbox a whole Inbox rendered with fake cards: Archive's header names Dismiss all in Archive, Done's Mark all in Done as read with 1 unread, In progress and Later no ✓; Done with nothing unread reads muted, its ✓ gone rename every row renamed (Bench, Tally, Forge, Drift): the same Inbox, word for word once the words are put back draft So far, then Your message, then the box; never Your reply ``` ib-6's and #1048's tests (`reply`, `drafts`, `outbox`, `verify reply-focus`) pass in the same run. - **After the canary merge:** 8c32aeb takes canary to 6b24f2b with no conflict and a tree equal to `git merge-tree`'s; none of its 6 commits touches the Inbox, its tests or its docs. - **Round 1 (3/5):** `read-only.test.ts` still pinned the old shelf guard (`shelf.length > 0 && !config.hosted`), which CI caught; it now pins `{sweep && !config.hosted && (`, and every web test that reads `inbox.tsx` or `lib/inbox` runs in this PR's harness (15 files). The behaviour rows now pin each shelf's count badge (Done's counting its read history) and Done's renamed ✓ name (`Mark all in Tally as read`), which two mutations had left green; `shelfRow`'s one click is named `sweep` in the map, since `bulk` is the Inbox's own write helper and a destructured name keeps TypeScript's null narrowing inside `onDismiss`. - **Before:** on a fixture stack at canary 5b69245, 1280x800, a fake Slack draft card reads its words under `Your reply` ([shots](#1140 (comment))). **After:** the same card at e0db978 reads `Your message`. Driven in the browser: Archive's header reads `Dismiss all in Archive` and its row opens by place; Dismiss all moved the card to Done, muted at 0 unread with its ✓ gone. `verify screenshots /dashboard` at 1280x800, 1782x972 and 390x844, light and dark, in the same comment. - **Docs and skills**, each through `bun run precommit:light` and a writing-for-agents read: `.agents/dashboard/asks.md` (Your message; a shelf's place decides its row, its words from `SHELF_LABELS`), `skills/zerostarter/design/SKILL.md` (the same, and the draft label), `web/next/content/docs/inbox.mdx` (Your message), `packages/scripts/src/verify/dashboard-map.json`. doc-sync: no other doc names the draft label or how a shelf row decides. - **Folded in** from an earlier saved patch for #985's wording: a secret ask's Reply refuses words equal to *what the field holds*, so a value still in the field never reaches a chat through Reply (asks.md, inbox.mdx, dashboard-map.json), since `commentRefused` compares only with the field. - **Changed-term greps** (`git grep -l -F`, tests aside): `Your reply`: 0. `label === "` / `label !== "`: no hit in the Inbox; the rest are other components' own labels. `value={label}`: 0. `Dismiss all in Archive`, `Mark all in Done as read`: asks.md and the design skill, each still the row's words. `the value itself`, `equal to the field`: no hit left about the secret ask. ## Merge Danger **Door:** two-way Browser only: no API, payload or stored value changes. A revert brings back the old label and the label-keyed rows. **Blast Radius:** Inbox Every shelf's row in the Inbox: what each one's ✓, count and muting do now comes from its place. No open state is kept across page loads, so the new keys carry nothing over. ## Links - Closes #1074 - Builds on #978 (Later, Archive, Reply), #1006 and #1063; folds in a saved patch for #985's secret-ask wording 🤖 Generated with [Claude Code](https://claude.com/claude-code)



TL;DR
playground-glm, and says why you need it. Its Show draws the key for 30 seconds, and its Copy puts it on your clipboard without drawing it.Summary
Departures from the issue and the plan:
shown, but the Done when asks for no record, and a read route that writes nothing is simpler. The builder accepted this one.FIXTURE_REVEAL), not a fake Keychain kept in the fixture's scratch folder. The builder accepted this one too.entries,errorandworkersframes (DashboardFrameinapi/hono/src/lib/dashboard/hub.ts) and an open chat'sactivity, none of them an Inbox row, andrevealSecrettakes nodeliverand writes no row, so the Inbox's own reads see nothing new. The response log has a test (tests/api/hono/src/lib/base-app.test.ts).Round 1 added a guard: a reply or an answer's note on a reveal card that holds its value, such as a Copy pasted into the card's own comment box, is refused before anything is sent (
holdsRevealed). A reveal card's Reply stays in that box rather than the chat's composer, and its Edit is refused.After canary's read-only hosted build (c1ca5a4), a reveal card on the hosted build draws no Show or Copy, since its relay carries no POST: the card names the item and its why, and the read-only line stands for the buttons. That hiding is the second layer: the hosted API mounts no
/revealor/secretroute at all, sincehosted.tsmounts sign-in, health and Yard alone, whichtests/api/hono/src/hosted.test.tspins by listing every mounted path.Evidence
inboxInputSchema): a reveal card readsrefused, and a secret ask whose keychain sink namesplayground-glmreadsrefused.After (this head): both read
accepted.The fixture stack's shots are in this PR's first comment. They show the card masked, then showing the fixture's made-up value after Show, and a secret ask saving to
playground-glm.made-up-value-from-the-fixtureand turned to Hide. 30 seconds later the field read masked again.Copied, and the field stayed masked.200,cache-control: no-store. On the secret ask's card it answered400 Only a reveal card shows a value.AGENT_SIGNIN_ENABLED=true, curl): the agent readGET /api/v1/inboxwith200, so it passes the owner rung. It then got403 A secret moves on the owner's own click alonefrom bothPOST .../revealandPOST .../secret.tests/api/hono/src/routers/inbox.test.ts> "Submit and Show each carry refuseAgent": red on canary's router (12 pass, 1 fail), green at this head (13 pass).tests/api/hono/src/lib/dashboard/inbox.test.ts, the Postgres-backed store with Docker reachable: 173 pass, 0 fail, 0 skipped.playground-service alone.playground-glmafter refusinga"b.tests/api/hono/src/lib/dashboard/secret-sink.test.ts> a fixture stack's reveal runs nosecurityand shows the made-up value.tests/api/hono/src/lib/keychain.test.ts:playground-service and refusesgithub.com,Chrome Safe Storage,toStringand others.tests/api/hono/src/lib/relay/protocol.test.ts: the relay carries neither secret route by any method, and neitherRELAY_GETSnor the paired key's signedRELAY_WRITESnames either one. A reveal card is not inRELAY_ANSWER_KINDS, so Yard cannot answer one either.tests/api/hono/src/lib/dashboard/inbox.test.ts, round 1:That is the card's value: it stays out of a comment, sends nothing and leaves no table holding it, while other words go.tests/api/hono/src/lib/base-app.test.ts: the request log every build mounts writesPOST /api/v1/inbox/i1/revealand none of the response's value.tests/web/next/src/components/dashboard/read-only.test.tspins the reveal's!config.hostedgate beside the secret field's.hosted.test.ts, and the relay'smac.test.tsandrelay-write.test.ts), at the merge of canary 752a41d.tests/scripts/ops/run-slot-gauges-lock.test.ts(Expected: 1, Received: 2), the known flake run-slot gauges lock test flakes: eight looks with no reading run top 6 times #956, which this PR does not touch, andAudit, Lint, and Buildfailed on that suite alone. The head after it reruns CI..agents/dashboard/asks.md:/reveal;reveal;keychainSpec;ask newexample;.agents/dashboard/writes.md: Show and Copy are confirmations, and refuseAgent is on both secret writes.extensions/core/skills/to-inbox/SKILL.mdanddocs/playground/to-inbox.md: a reveal card's row.web/next/content/docs/inbox.mdx: Give a secret namesplayground-items and the Show a secret card.packages/scripts/src/verify/dashboard-map.json:POST /api/v1/inbox/:id/reveal.extensions/core/rules/ask.md: the secret line now nameskeychainSpec(round 1).grant.{0,20}schema.{0,20}secretand others): asks.md:296's link-free list alone, which now namesreveal.keychain,KEYCHAIN_ITEMSandsecret askover the docs: the hits in yard.md, the Slack extension and setup.md name the slack and yard items, which keep their names and checks.Merge Danger
Door: two-way
The change is a new card type, a new read route and a wider keychain allowlist, with no schema change. A revert removes the card and the route. A card posted before the revert stays in the Inbox, and its preview no longer parses.
Blast Radius: Inbox
Any session can now post a card that shows a
playground-Keychain item, such as the Claude login or GLM keys. The value still moves only on the owner's click on this Mac, the card names the item in full, and a closed card reads nothing. A value shown in the dashboard's page sits in that browser's memory for up to 30 seconds, so a session driving that same browser could read it. Playground's sessions drive their own headless browsers, never the owner's.Links
🤖 Generated with Claude Code