feat(console): Create from template view (CHOO-2656) - #405
Conversation
4382925 to
a6994cc
Compare
|
All contributors have signed the CLA. ✅ |
56fba8c to
ce83b33
Compare
e7f8895 to
2dba04f
Compare
|
recheck |
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
810313b to
fbc2d4c
Compare
|
recheck |
Three-step flow: paste YAML or pick a file → fill a form generated from the template's params block → POST to /rooms/from-yaml and navigate to the new room. Paramless templates skip straight to creation. Agent-name params (named exactly "agent" or ending in "_agent") render as a combobox over the live agent list with an inline warning when the name doesn't match. Required fields block submission with inline errors before any server call. Server 400s are mapped back to the offending field. Registered as "Templates" in the workspace sidebar nav. One appended entry each in view-ids.ts, view-registry.ts, and workspace-nav.tsx.
The package is "type": "module" so __dirname is undefined at runtime, crashing electron-vite dev on launch. import.meta.dirname is the ESM equivalent and works in both dev and packaged builds.
Two-column layout: form on left, "What this creates" summary panel on right showing a live preview of the room name (interpolated from inputs), which agents will be added, and template source info. Field labels now use humanized param names with description as helper text underneath. Agent-name fields show an inline "exists ✓" or "not found" badge inside the input. Fields with unchanged defaults show "(default kept)". Buttons are stacked full-width: "Back to template" + "Create room". Step indicator "Step N of 2" in the header. Parser now also extracts the agents list from the template's room block for the summary panel preview.
Make the YAML paste area taller (min-h-64) and vertically resizable so large templates are comfortable to work with. Use describeFailure's detail field directly for server errors instead of the HTTP-prefixed message, so the user sees "Unknown agents: foo" rather than "Could not create the room from this template. (HTTP 400: ...)".
…rors Pull the server's detail field from the RpcError before describeFailure wraps it with an HTTP prefix, so the user sees "Unknown agents: foo" rather than "HTTP 400: Unknown agents: foo".
Server side: new GET /gateway/rooms/template-schema endpoint returning the JSON Schema for a valid room template document, generated from Pydantic's TemplateDocument model (RoomSpec + ParamSpec). Console side: fetches the schema when the Templates view loads and validates the parsed YAML against it (via ajv) at the step 1→2 transition. Schema violations surface as parse errors before the form, so the user learns their template is invalid before filling anything out. Graceful degradation: if the server doesn't support the endpoint (404), validation is skipped. ajv added as an explicit dependency (was already present as a transitive dep).
31dd4fe to
b6fb543
Compare
When the schema endpoint returns 404 (server lacks params support), the parser now rejects unknown top-level keys like params: at step 1 with a clear message instead of letting the user fill a form that will fail at create time. Adds drag-and-drop support to the source step textarea — drop a YAML file onto it and it reads the same way the file picker does.
When the schema endpoint is unavailable, skip validation and let the server decide on create. The previous fallback rejected params: on any server without the schema endpoint, which blocked servers that support params but predate this PR's schema endpoint.
The room view reads from a MobX store that caches the rooms list. Navigating immediately after creation showed "still loading" because the store had no entry for the new room. Call refreshSidebarRoomState(true) before navigating — same pattern as CreateRoomModal.
The server reports users/refs/docs that couldn't be added in failedAttachments, but the Console discarded them. Now shows a warning toast naming each failed item so the user knows what didn't land — e.g. "abel.dantas (no account with this name is known on the bridge)".
| function parseYaml(yamlText: string): Record<string, unknown> { | ||
| let doc: Record<string, unknown>; | ||
| try { | ||
| const parsed = load(yamlText); |
There was a problem hiding this comment.
Risk: Affected versions of js-yaml are vulnerable to Inefficient Algorithmic Complexity / Uncontrolled Resource Consumption. js-yaml is vulnerable to CPU exhaustion when parsing untrusted YAML: the maxTotalMergeKeys budget is only charged for keys that a merge (<<) source actually contributes, so empty mappings cost nothing against the limit. A small document that merges a long sequence of empty mappings repeatedly forces O(N*K) work while the counter stays flat, stalling the process. Merge keys are part of the default schema for every load entrypoint (load, loadAll, and the 3.x safeLoad/safeLoadAll), and lowering maxTotalMergeKeys does not mitigate it.
Fix: Upgrade this library to at least version 4.3.2 at switch/console/pnpm-lock.yaml:3988.
Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-69712, GHSA-2883-xcg3-v3hh, CVE-2026-84375
🍰 Fixed in commit 258ee83 🍰
There was a problem hiding this comment.
Fixed in 258ee83: the pnpm override floor moves to 4.3.2 (the fixed release), so the transitive 4.3.1 is gone from the lockfile. The app's direct js-yaml is 5.x, outside the affected range.
The existing override mapped <4.3.0 to >=4.3.0, which resolved to 4.3.1 — still vulnerable to merge-key CPU exhaustion. Floor is now 4.3.2, the fixed release. Transitive only; the app's direct js-yaml (5.x) is not in the affected range.
|
Verified E2E on a local build: pasted a params template, form renders with live interpolation, room created and bridged to Slack, agent responsive in the room. |
… (CHOO-2719) Add sender_kind to MessagePayload (event stream) and read_context timeline entries so agents can see whether a message is from the platform. Set to "platform" when the message content carries PLATFORM_MARKER, None otherwise. This completes the end-to-end coherence requirement: storage (content marker), event stream (sender_kind field), and read_context (sender_kind in entries).
Route platform-marked messages through the admin_message rendering path so they appear on Slack/Mattermost/etc as the Switch app identity, not as a raw "admin" sender. Uses the same rendering as admin system messages but without suppressing agent addressing.
Room templates can now send a message after provisioning via a new
kickoff field: message, targets (default: all agents), and sender
("platform" default, or "creator" for explicit impersonation).
The default path sends as the platform through send_platform_message,
subject to normal addressing policy checks. Creator impersonation
is opt-in and visible via the on_behalf_of metadata.
… (CHOO-2719) The existing test used a SimpleNamespace without a type field, which broke when resolve_sender started checking client.type == "admin". Updated the mock to include type="bridge" (non-admin) and added a new test verifying admin clients resolve to platform principal.
on_behalf_of was passing the raw user_id (a UUID), making creator impersonation invisible to humans and agents reading the marker. Now resolves to User.name via UserStore, falling back to the UUID only if the user row is missing.
The paste step now uses Monaco Editor with YAML syntax highlighting, line numbers, code folding, and auto-expanding height (grows with content, min 256px, max 600px). Drag-and-drop still works on the surrounding div. Monaco is already a dependency (@monaco-editor/react + monaco-editor). The gateway has a similar YAML editor (CodeMirror-based) — this uses the same concept but with the Console's existing Monaco integration. NOT tested: visual rendering (no display on this VM). Typecheck, lint, format all green.
…2656) A template param could only be a string, number, boolean or enum, so a param meant to name an agent was a free-text string and the Console guessed from the param's name whether to offer an agent picker. Nothing checked the value until provisioning failed part-way through. A param can now be typed `agent`, `bridge`, `room` or `user`. The value is still the entity's name, but the server checks it exists before creating anything (RoomYamlService.check_entity_params, called from the from-yaml route between parse and provision), failing with a message that names the param so the form can point at the field. Users resolve on the bridge the room will land on, through the same lookup room creation uses (RoomService.resolve_bridge_users). parse_template returns the declared params and their resolved values alongside the spec; parse keeps its old shape for existing callers. Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
… components The new-room dialog, the add-agents-to-room modal and the add-agent-to- rooms modal each carried their own copy of the same search box, chosen tile and bridge tile. The template wizard needs the same controls, so this moves them to renderer/lib/components (PickerCombobox, ChosenTile and the agent, room and bridge flavours on top of it) and points the three modals at them. No visual change to the modals. bridgeUnusableReason takes a needsChannelCreation flag because a surface that only names a bridge does not care whether it can create channels, only whether it is running. Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
… wizard (CHOO-2656) The wizard offered an agent picker only when a param was named `agent` or ended in `_agent`, and that picker was a text box with a datalist. Bridges and rooms had no picker at all, and the template's fixed members were a read-only list you could only shrink. The parser now carries the param's declared type through instead of guessing from its name. Entity-typed params render the same controls the new-room dialog uses: a search box that turns into a tile for agents, rooms and users, and the messaging-app tile grid for bridges. Users come from the server's known users, a live directory search, and the typed name as a last resort, since the server looks unseen names up on create. The fixed agents and users lists become the same tile grid with a search box to add more, not only remove. A bridge param that has been set drives the summary panel, so the creator identity and "channel on X" line follow the pick rather than going blank as they did for an interpolated bridge. Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
…m sender kind meets the template wizard Both kickoff paths are kept for now, the top-level kickoff posted through the creator's puppet and the room-level kickoff posted as the platform; the follow-up collapses them onto the platform sender. Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
…OO-2719) The kickoff a template posts had two senders on this branch: the creator's puppet, which impersonates a person, and 437's platform sender, which agents deny by default so every template was dead until each agent's policy was edited. Neither is what we want. Now there is one path. The platform posts the kickoff through the admin client, and the platform marker names the creator. When an agent receives it, the resolver judges the message as that person: an open policy admits them, an owner rule admits them when they own the agent, a users list admits them through a claimed account (allows_on_behalf_of). The platform's own messages, with nobody behind them, keep 437's opt-in. The authority is per event and nothing is written to any policy. Only the admin client's marker is read, so a copy of it on a human's or agent's event borrows nothing. The room sees who it came from: bridges render it as the Switch app prefixed "On behalf of <name>", and the agent payload and read_context carry sender_kind and on_behalf_of. Also fixed from 437's review: the open-policy fast path in permitted() let a bare platform message through; the admin client returning None on a failed send was reported as success; and the kickoff fired before the agents had joined, which drops the event, so provisioning now waits for the sender and the template's agents and names any that were late. Gone: post_as_user and the Switch-originated relay marker in bridge_core, post_kickoff in room_service, the room-level kickoff mapping with its targets and sender fields. The top-level kickoff string stays.
…he gateway (CHOO-2719) Both addressing policy editors rebuilt each rule from the four dimensions they know, so saving any policy dropped the platform flag 437 added, and the gateway one dropped owner and owner_agents too. Each now carries the fields through and offers a "Switch itself" checkbox for the platform's own messages. The agent runtime's MessagePayload mirror gains sender_kind and on_behalf_of.
…2656) The summary said the kickoff was posted as you, and the identity warning said it could not be posted without a linked account. Neither is true now that the platform posts it: the copy names Switch as the sender and the warning narrows to the channel invite, which still needs the account.
… import them The wizard imported a function from the main-process parser, and the renderer's Vite build refuses that at load time, so the Templates view failed to open on the dev build. The types and the entity-type check now live under shared/, and both the parser and the wizard read them there. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
Two sessions on one machine each drove "the" dev Console today and kept killing each other's, because the dev build has one userData dir, one renderer port and one debug port. Two dev-only env overrides fix that: SWITCH_CONSOLE_USER_DATA_DIR picks the userData dir and SWITCH_CONSOLE_RENDERER_PORT the Vite port; the debug port was already a flag. A packaged app ignores both. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…ount on (CHOO-2656)
A template with {$creator} in users: on a bridged room used to fall back
to the gateway account name when the creator had no linked identity
there. That name is not a platform handle, so the invite failed quietly
and the creator got a private channel they could not enter. The create
is now refused with a message naming the app to link.
The kickoff also names the creator the way the bridge knows them: the
platform marker carries the linked handle, the bridge renders "On behalf
of @handle", and an agent answers that person rather than "@admin", the
admin client that carried the message.
Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
… hand-offs in one click (CHOO-2656)
Two things the first real run showed. The wizard let you create a room
that invites you as {$creator} while the server had no account of yours
on the bridge, with only an amber note; now Create is disabled and the
note carries a Link-your-account button that opens the existing claim
flow and unlocks the form. And a coder/reviewer template bounced its
hand-off, because agents created from the Console answer only their
owner: the wizard now checks the picked agents' policies against each
other and offers "Let them hear each other", which adds the owner's
agents (or the named agent) to the blocked ones' rules.
Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…CHOO-2656) The kickoff took half a screen at the top of every channel: the attribution line, the mentions, and the per-agent instructions, followed by each agent's session notice and plan at the same level. Now the channel gets one line, "Template kickoff on behalf of @handle", and the kickoff text goes as a reply in that message's thread. The agents are addressed by the threaded message, so their notices, plans and hand-offs land in the thread too. The bridge no longer prefixes a threaded platform message, nor one whose body already names the person, so the headline is not attributed twice. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…its thread (CHOO-2656) The threaded kickoff pulled the whole coder/reviewer conversation into its thread, since an agent answers where the message that addressed it lives. The kickoff body now carries a reply_in_channel flag in the platform marker; the agent client reads a flagged message as top-level, so plans, hand-offs and session notices land in the channel and the headline keeps exactly one reply, the kickoff text. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
Flagging the kickoff body moved the agents' notices to the channel but not their work: an agent that starts a session on the notification catches up through read_context, and there the body still hung under the headline, so the plan and everything after it followed it into the thread. The history now reads a reply_in_channel platform message as its own root. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…e why A comment that cites a ticket or narrates the run that prompted it goes stale in weeks. The comments added on this branch now say what a reader needs a year from now and nothing else: why the platform sender exists, why a kickoff needs the creator on the bridge, why owner-only agents cannot hear each other. Em dashes and arrows are gone from prose, test names and user-facing strings alike, per the house style. Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…s for (CHOO-2665) The room-template wizard that landed with #405 picks agents from what the server has, so a slot naming an agent that does not exist no longer happens and the "+ Create" offer on it has nothing to attach to; the code behind it goes. The identity warning in the template dialog now says what the link is for on this server: the template puts you in the room as {$creator}, which is filled only from a claimed identity. Claude-Session: https://claude.ai/code/session_01GpmTz1MFhce8ekJoPZgqBn
Folds the core half of work/group-templates (#407) onto the template format as it stands after #405, so one YAML file can declare a room group, several rooms under it, and directed links between them. Before: a template made exactly one room. #407 added the group shape but sat on a base from before parse_template, builtins and kickoff landed, and could no longer merge. Now: parse_template tells the shapes apart by their top-level key. A `room:` document works as before, kickoff at the top level. A `group:` + `rooms:` (+ `links:`) document gives a GroupSpec; each room carries its own `kickoff:`, and a top-level one is refused with a message saying where it goes. Params and server builtins interpolate over the whole document, mapping keys included, so an alias map written `"{bot}": helper` resolves. Rooms accept `aliases:` and export them back. provision_group creates the group row, each room in order (filed under the group, its kickoff posted as the creator), then the links; a room that fails is reported in `errors` and the rest still go ahead. POST /rooms/from-yaml returns whichever result matches the document, entity params are checked across every room, and /rooms/template-schema describes both shapes. Agents get the same through a `create_room_from_yaml` operation, acting as their owner. The 45 Console files and two migrations in #407's diff were drift from an older base and are already on main; none of them are touched here. Claude-Session: https://claude.ai/code/session_01GpmTz1MFhce8ekJoPZgqBn
…s for (CHOO-2665) The room-template wizard that landed with #405 picks agents from what the server has, so a slot naming an agent that does not exist no longer happens and the "+ Create" offer on it has nothing to attach to; the code behind it goes. The identity warning in the template dialog now says what the link is for on this server: the template puts you in the room as {$creator}, which is filled only from a claimed identity.
Folds the core half of work/group-templates (#407) onto the template format as it stands after #405, so one YAML file can declare a room group, several rooms under it, and directed links between them. Before: a template made exactly one room. #407 added the group shape but sat on a base from before parse_template, builtins and kickoff landed, and could no longer merge. Now: parse_template tells the shapes apart by their top-level key. A `room:` document works as before, kickoff at the top level. A `group:` + `rooms:` (+ `links:`) document gives a GroupSpec; each room carries its own `kickoff:`, and a top-level one is refused with a message saying where it goes. Params and server builtins interpolate over the whole document, mapping keys included, so an alias map written `"{bot}": helper` resolves. Rooms accept `aliases:` and export them back. provision_group creates the group row, each room in order (filed under the group, its kickoff posted as the creator), then the links; a room that fails is reported in `errors` and the rest still go ahead. POST /rooms/from-yaml returns whichever result matches the document, entity params are checked across every room, and /rooms/template-schema describes both shapes. Agents get the same through a `create_room_from_yaml` operation, acting as their owner. The 45 Console files and two migrations in #407's diff were drift from an older base and are already on main; none of them are touched here.







Brief
CHOO-2656 — Console: Create from template. Part of the template import epic: today importing a template means a curl command against an endpoint. This builds the human path in the Console so a non-technical person can turn a shared template file into a working room.
Summary
.yaml/.ymlfile