Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/plans/production-readiness-and-selling-2026-09-11.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Selling before 6 is true sells a liability. Everything below is ordered by that.
| C1 The funnel is not measured weekly | 49 signups / 30 d known only because someone ran a script today | A scheduled weekly report: signup → typed → got a reply → connected a channel → day-2 return → day-7 active, with the numbers in a pod, not a chat log. The script in this plan's appendix is the seed | GTM + Sharpen |
| C2 Hosted default caps at 1 agent | `DEFAULT_AGENTS_PER_USER = 1` | Decide the free/Pro split of hosted agents (D2) and make the cap the pricing table, not a beta constant | **Sam** |
| C3 Channel connect in < 2 minutes | Telegram proven; Slack/Discord install UX untested by a stranger; 6 connected channels total | One recorded stranger run per channel (Telegram, Slack, Discord) from the Connectors page, timed; fixes filed from the recording. **C3 exit record (stranger run 2026-09-12, throwaway account, commonly.me):** timings as they stand — Telegram connect → enable code 2m04s wall clock (~10 s net); code → first relay not reached (the live build did not name the bot; no Telegram account on the run); Slack not runnable (not enabled on commonly.me); Discord not runnable (not on the list). Eight stumbles, all on main: TASK-021 bot handle unset in the build #1699 `50dea44d`; TASK-022 signup did not sign the user in #1701 `d7814143`; TASK-023 code half, one definition of Slack enabled #1700 `c99261c2`; TASK-025 pod picker label #1702 `5456486f`; TASK-026 enable-code copy and expiry #1704 `089f52ec`; TASK-027 Telegram card copy #1705 `3d2bc281`; TASK-028 zero-pod state #1706 `3e44ad97`; C3-9 connector row crush #1703 `25c45c26`. Two rows stay with Sam: TASK-023's credential half (the Slack OAuth app and its SM values) and TASK-024 (Discord on the list). None of it is live until the next Deploy Dev; the Telegram enable step is re-run after it, and the Slack and Discord timings wait on Sam's two rows | Connectors + UX Lead gates |
| C4 The Tools page and approval card (pieces 3–4) | pieces 3–4 on main (#1667/#1670/#1669); the GitHub tool is `not_configured` on commonly.me | An admin installs the GitHub App once; a stranger grants a tool to a room and approves an action — one recorded stranger run from the Tools page, timed from opening Tools to the approved action landing; fixes filed from the recording. **C4 exit record (stranger run 2026-09-16, throwaway account, commonly.me on `00e7ebc9`): NOT REACHED.** Tools opened 04:55:23Z, blocked 04:55:33Z (~10 s): the catalogue returns `available: false`, `unavailableReason: not_configured`, `connections: []` — the chart maps no `GITHUB_APP_ID` / `GITHUB_APP_PRIVATE_KEY` (verified unset in the running pod), and a `github-app` Integration row is admin-only, so no fresh account can get past the not-yet row. Three rows filed: TASK-033 GitHub App credentials, **Sam** (a company GitHub App, its App ID and private key into Secret Manager, then the chart mapping — Kai's PR, `optional: true`, HELD like #1685 until the values exist — then deploy, then verify the env in the running pod); TASK-034 accepted as a product fact, not a stumble: a member cannot install the App themselves until piece 6 (§10), which stays unscheduled — hence the exit criterion above; TASK-035 the not-enabled tool row is inert (no aside, no button, "ask your operator" has no affordance) while the Discord · WhatsApp channel row gets an Ask link — same Ask affordance, Kai builds, UX Lead gate before merge. The timed run re-runs once TASK-033 is live | Connectors + UX Lead gates |
| C4 The Tools page and approval card (pieces 3–4) | pieces 3–4 on main (#1667/#1670/#1669); the GitHub tool is `not_configured` on commonly.me | An admin installs the GitHub App once, grants the tool to a room and approves each write; a stranger in that room mentions the seat and reads the trail — one recorded stranger run from the Tools page, timed from opening Tools to the approved action landing; fixes filed from the recording. (#1714 wrote *a stranger grants … and approves*; the run showed the stranger can do neither — the mint takes `grantedBy` from the Connection's owner and 403s anyone else (§9 of the room-grants plan), and approval carries the granter's authority. The stranger's part is the room, the mention and the trail.) **C4 exit record (stranger run 2026-09-18, throwaway account, commonly.me, seat on cli 0.1.45 with #1743 applied by hand): REACHED.** Tools opened as the stranger 11:32:00Z (T0, GitHub *not granted*); admin minted the grant 11:32:51Z (list/create/close issues, write-with-confirm, 7 d); stranger reload 11:33Z shows the granted row and an empty trail (T1). First approved write landed 12:10:11Z — **38m11s from Tools open**, of which ~28 min were the seat, not the page: the seat's first answer (11:34Z) came from the host's `gh` under operator credentials and left no trail row (TASK-051), then the server-declared confined environment crashed every spawn (#1743) until 12:00:04Z. Measured after confinement: list 12:03:58Z → ok 12:04:39Z, no park; create 12:07:02Z → parked 12:07:44Z → approved 12:10:10Z → issue #1745 12:10:11Z, authored by the App; close 12:11:39Z → parked 12:12:20Z → approved 12:16:28Z → closed 12:16:29Z. Reads did not park, both writes did, each executed write carries its approval id; trail read as the stranger 12:20Z: five rows, 0 refused. Stumbles filed from the recording, all `68854#c4-N`: TASK-039 grant projection into the seat's environment (#1721), TASK-040 admin App step has no Tools affordance (#1722), TASK-048 self-serve install ships no MCP entry (#1741), TASK-049 daemon-spawned seats miss the pi key, TASK-050 audience label renders people as *an agent*, TASK-051 host-credential bypass, TASK-052 self-serve environment declares no sandbox, TASK-053 the plan-doc rule (this record), TASK-054 pi has no HTTP MCP transport (#1740), TASK-055 **P0**: the agent-config PATCH gated on membership only, so a member could rewrite a seat's environment and the owner's daemon ran it (#1746, merged 48a5ea92; deploy requested), TASK-056 the approval card renders as an empty bubble to a non-approver, TASK-057 the resolve 400 does not name its enum, TASK-058 the trail keeps counting resolved approvals as awaiting. The 2026-09-16 attempt (`00e7ebc9`) was NOT REACHED at the `not_configured` row: TASK-033 App credentials (Sam), TASK-034 members cannot install the App until piece 6, TASK-035 the inert not-enabled row (#1713). Evidence: `docs/design/evidence/c4-run-2026-09-18-*.png`, eight stranger-view captures at 1200 — T0 Tools with GitHub not granted; T1 and T2 the granted row at 1m and 33m (the trail sat below the fold in both, so the empty → one-call progression is carried by the ledger reads in the pod, Vera 69552 and 69566, whose row timestamps start at 12:04:39Z, not by those two captures); T2 the seat's list reply; T3 the create card and its approved state; T4 the close card and the trail scrolled into view, re-shot ~12:35Z (the row reads 31m), five rows, 0 refused, still counting the two resolved approvals as awaiting (TASK-058). | Connectors + UX Lead gates |
| C5 Empty states and the second visit | scripted starter workspace shipped 07-03; unmeasured since | C1's day-2 number decides whether this is a gap | GTM |

### D. Billing and packaging
Expand Down
9 changes: 6 additions & 3 deletions docs/plans/tools-catalogue-room-grants.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Sam's review note on order is taken: **one first-party tool works end to end in
| 0 | ADR-001 amendment — `McpServer`, manifest rules, `RoomGrant` | Wren, PR #1658 | open; Vera gates §3 | the vocabulary every row below uses |
| 1 | The grant record — model, mint, attenuate, revoke-with-cascade | Kai | 2–3 d | a grant can be narrowed and killed, and the server is the one deciding |
| 2 | The broker — a Commonly-hosted MCP server per grant, the trail row, and the first-party GitHub tool Installable | Kai | 4–6 d | an agent calls a real tool through a grant it never holds, and the room can read who did what |
| 2b | The confirmation floor — the broker parks every irreversible tool whatever the grant's tier, and every write under `write-with-confirm` | Kai, PR #1664 | hours | the granter cannot switch off the human yes for an action that cannot be unmade (Sam 67512) |
| 2b | The confirmation floor — the broker parks every irreversible tool whatever the grant's tier, and every write under `write-with-confirm` | Kai, PR #1664 | hours | the granter cannot switch off the human yes for an action that cannot be unmade (Sam 67512); C4 added the seat half of the floor — a public seat runs confined by default, §4 |
| 3 | Approval for parked calls — the existing `propose-action` consent path (`ApprovalAction`) as the pending-call envelope | Kai | 1–2 d | a person with authority consents to the exact call before the broker executes it, once; nothing new is invented for it |
| 4 | The page — the Tools list with per-member rows first, then the grant aside and the trail; option A | Kai builds, UX Lead gates | 3–4 d | the page draws what the server enforces at the time it draws it, and only that |
| 5 | The manifest parser — `.claude-plugin` / `.cursor-plugin` → Installable, with the validation in ADR-001 §1–§2 | Kai | 2–3 d | a third-party server becomes a catalogue row |
Expand Down Expand Up @@ -68,7 +68,7 @@ The shape is ADR-001 §3 as amended, and this section names only what the builde
**Decision: the broker is an MCP server the backend hosts, one URL per grant, authenticated by the agent's own runtime token.** Not a separate service, not a sidecar, not a token the agent carries. Reasons, in order of weight:

1. **Attribution is the auth, not a field.** The agent connects with `${COMMONLY_AGENT_TOKEN}`; the broker resolves the agent from it, checks the agent is in the grant's effective audience, and writes the trail row with that identity. There is no "grant token" the agent could leak or an injected agent could forge, because there is no second credential.
2. **It is already the shape ADR-008 declares.** An `McpServer` component projects to one `environment.mcp[]` entry: `{ name, transport: 'http', url: '${COMMONLY_API_URL}/api/mcp/grants/<grantId>', headers: { Authorization: 'Bearer ${COMMONLY_AGENT_TOKEN}' } }`. Both placeholders are the two that `MCP_PLACEHOLDERS` in `backend/routes/agentBinding.ts` already lets through (#1598), so no daemon or adapter change is needed for an agent to reach it — the daemon writes the same `mcp-config.json` it writes today.
2. **It is already the shape ADR-008 declares.** An `McpServer` component projects to one `environment.mcp[]` entry: `{ name, transport: 'http', url: '${COMMONLY_API_URL}/api/mcp/grants/<grantId>', headers: { Authorization: 'Bearer ${COMMONLY_AGENT_TOKEN}' } }`. Both placeholders are the two that `MCP_PLACEHOLDERS` in `backend/routes/agentBinding.ts` already lets through (#1598). The claim that followed here — *no daemon or adapter change is needed* — did not survive C4 (2026-09-18). The broker entry reaches a seat only through the daemon's work list: #1721 projects each active grant into the audience seats' `environment.mcp` at `GET /api/agent-binding/assigned`, which `cli/src/lib/daemon-supervisor.js` reads — so a seat that `commonly agent run` starts from a token file under its own launchd plist never asks for it and stays hand-configured (TASK-039); the claude adapter needed #1743 before a public-sandboxed seat could spawn with an http-only server beside the stdio one; the pi adapter has no HTTP MCP transport at all (#1740, TASK-054); codex seats cannot consume a URL transport yet (#1721). A projection is a declaration; the spawn argv is the load.
3. **The ledger is Postgres, the broker is stateless, so it can sit on the spot pool** (ADR-015). The one long-lived thing — the GitHub App token refresh — already lives in `githubAppService`.

**What it does per call**, in order: authenticate the agent → load the grant → refuse if revoked, expired, or the agent is outside `audience ∩ pod.members` → refuse if the tool is not in `tools` → refuse if the tool's required `writeMode` exceeds the grant's → if the tool is irreversible, or the grant is `write-with-confirm` and the tool is a write, park it (the piece-2b predicate; piece 3 turns the park into an approval) without consuming budget → reserve one budget slot on every grant in the lineage, root first, in one transaction, rolling back if any is exhausted (a child draws down its parent and stays under its own cap; Vera 67467, Wren 67471) → execute with the server-held credential → write the trail row → return the result. Every refusal also writes a trail row with `outcome: 'refused'` and the reason code, because a room reading the trail should see what agents *tried*.
Expand All @@ -92,6 +92,8 @@ ToolCall {

Args are digested and not stored, deliberately: a Gmail search string or an issue body is the content the granter did not consent to have copied into Commonly's own store, and the page's trail needs *who, what tool, when, what happened* — not the payload. Recording payloads is the same follow-on the hook lane already carries separately (+2–3 d there), and it is not folded in here either.

**A grant binds a seat, not an agent — and only a seat with no other way to reach the provider.** Everything above authorises one path: the seat's runtime token, the broker URL in its environment, the server-held credential. None of it constrains the seat's *other* paths, and C4 measured the consequence (2026-09-18): the first answer c4-smoke gave to *list the open issues* came from the host's `gh` CLI under the operator's credentials, and the trail stayed at 0 calls — correctly, because the trail can only show calls that went through the grant (TASK-051). The trail is evidence about the grant, never about the seat. So the confirmation floor (piece 2b) has a seat half as well as a broker half: a seat whose room holds people the operator does not control runs confined — no shell, no web tools, no reads outside its workspace, MCP limited to the declared servers — and that sandbox is the default the daemon derives for such a seat, not an option an environment may leave unset (TASK-052: a mode-less `trust: 'public'` derives the platform mode, Seatbelt workspace on darwin and bwrap on Linux, and throws rather than fall through to the unconfined spawn — Vera 69548, Wren 69553; an operator-authored local environment keeps its own choice). Two more holes the run found sit on the same boundary and close the same way: a declared environment is executed by the owner's daemon, so who may write it is an authority question and not a membership one (#1746 — the installation's installer or an instance admin, 403 `installer_only`), and the daemon refuses a declared server that would run a foreign command or ship the seat token off-instance even when the server is wrong (#1744). The seat half's tests live with the adapters and the daemon guard, not with the broker.

**Named tests:**

1. `writes one trail row per call with the agent from the token` — a request body carrying a different `agentUserId` is ignored and the token's identity is written.
Expand Down Expand Up @@ -178,7 +180,8 @@ The contract is ADR-001 §1–§2 as amended and Kai's 67390/67395/67399; nothin
| "what asks a person first" | `ApprovalAction` with two action types, neither for a tool; `DecisionRequest` is advisory by contract | `actionType: 'tool_call'` on `ApprovalAction` (piece 3) |
| a member's own Gmail or GitHub as the credential | `Integration` rows for Telegram/Slack/Discord; `githubAppService` for the App; `connectorSecrets` holds Slack's bot token by reference | the App is the slice's Connection; per-person OAuth is piece 6 (§10) |
| a third-party tool row | no parser | piece 5 |
| the agent reaching the tool at all | ADR-008 `mcp[]` + the `MCP_PLACEHOLDERS` guard | exists; the broker URL is a placeholder-only entry, no adapter change |
| the agent reaching the tool at all | ADR-008 `mcp[]` + the `MCP_PLACEHOLDERS` guard | the broker URL is a placeholder-only entry; reaching a seat took #1721 (projection), #1743 (claude Seatbelt + http server) and still excludes pi (#1740) and codex — §4 |
| the agent reaching the provider by no path *but* the grant | nothing — a spawn is as confined as its environment says, and a self-serve environment says nothing (TASK-052) | public sandbox derived by default (TASK-052), installer-or-admin PATCH (#1746), daemon declared-server guard (#1744) — §4 |

## 9. Open questions, and who rules

Expand Down
Loading