@firedrill-tools/slack 0.1.1

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 (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +369 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/baseline.scenario.json +5 -0
  5. package/firedrill/bounds.scenario.json +112 -0
  6. package/firedrill/conformance.suite.json +21 -0
  7. package/firedrill/history-unavailable.scenario.json +11 -0
  8. package/firedrill/open-long-handles.scenario.json +263 -0
  9. package/firedrill/open-member-bound.scenario.json +24 -0
  10. package/firedrill/post-rate-limited.scenario.json +11 -0
  11. package/firedrill/response-budget.scenario.json +24 -0
  12. package/firedrill/slack-bounds.drill.json +428 -0
  13. package/firedrill/slack-denied.drill.json +74 -0
  14. package/firedrill/slack-fresh-install.drill.json +132 -0
  15. package/firedrill/slack-history-unavailable.drill.json +83 -0
  16. package/firedrill/slack-invalid-auth.drill.json +408 -0
  17. package/firedrill/slack-mcp-aliases.drill.json +219 -0
  18. package/firedrill/slack-open-long-handles.drill.json +82 -0
  19. package/firedrill/slack-open-member-bound.drill.json +135 -0
  20. package/firedrill/slack-post-rate-limited.drill.json +96 -0
  21. package/firedrill/slack-response-budget.drill.json +118 -0
  22. package/firedrill/slack-thread-many-repliers.drill.json +100 -0
  23. package/firedrill/slack-ts-sequence-full.drill.json +165 -0
  24. package/firedrill/slack-visibility.drill.json +110 -0
  25. package/firedrill/slack-web-api-flow.drill.json +896 -0
  26. package/firedrill/thread-many-repliers.scenario.json +79 -0
  27. package/firedrill/tools/slack/app/assets/ATTRIBUTION.md +40 -0
  28. package/firedrill/tools/slack/app/assets/fonts/OFL.txt +93 -0
  29. package/firedrill/tools/slack/app/assets/fonts/lato-latin-400.woff2 +0 -0
  30. package/firedrill/tools/slack/app/assets/fonts/lato-latin-700.woff2 +0 -0
  31. package/firedrill/tools/slack/app/assets/fonts/lato-latin-900.woff2 +0 -0
  32. package/firedrill/tools/slack/app/assets/slack-wordmark.svg +1 -0
  33. package/firedrill/tools/slack/app/assets/slack.svg +1 -0
  34. package/firedrill/tools/slack/app/site/app.js +2202 -0
  35. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-400.woff2 +0 -0
  36. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-700.woff2 +0 -0
  37. package/firedrill/tools/slack/app/site/assets/fonts/lato-latin-900.woff2 +0 -0
  38. package/firedrill/tools/slack/app/site/assets/slack-wordmark.svg +1 -0
  39. package/firedrill/tools/slack/app/site/assets/slack.svg +1 -0
  40. package/firedrill/tools/slack/app/site/chrome.css +73 -0
  41. package/firedrill/tools/slack/app/site/chrome.js +96 -0
  42. package/firedrill/tools/slack/app/site/icons.js +96 -0
  43. package/firedrill/tools/slack/app/site/index.html +253 -0
  44. package/firedrill/tools/slack/app/site/styles.css +2719 -0
  45. package/firedrill/tools/slack/app/site/ui.js +491 -0
  46. package/firedrill/tools/slack/behavior.mjs +1171 -0
  47. package/firedrill/tools/slack/lib/access.mjs +138 -0
  48. package/firedrill/tools/slack/lib/budget.mjs +67 -0
  49. package/firedrill/tools/slack/lib/ids.mjs +146 -0
  50. package/firedrill/tools/slack/lib/search.mjs +172 -0
  51. package/firedrill/tools/slack/lib/wire.mjs +144 -0
  52. package/firedrill/tools/slack/slack.tool.json +6114 -0
  53. package/firedrill/ts-sequence-full.scenario.json +38 -0
  54. package/firedrill/world.json +2008 -0
  55. package/firedrill.json +5 -0
  56. package/package.json +62 -0
  57. package/starter.json +1562 -0
  58. package/test/conformance.mjs +1166 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Reload Tech Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,369 @@
1
+ # @firedrill-tools/slack
2
+
3
+ A synthetic **Slack workspace** for [Firedrill](https://firedrill.run) drills. It simulates one workspace behind a
4
+ bounded subset of the **Slack Web API** (`/api/<method>`, the same paths, argument names, form encoding and response
5
+ bodies the official SDKs use) and exposes the eight tool names of the archived reference **Slack MCP server** as aliases.
6
+ It ships a **browser app that recreates the Slack desktop client** (see Browser app) over the same operations. Everything
7
+ lives in the Firedrill world: posting a message stores a row that other members of the same synthetic workspace can read.
8
+ No Slack service is ever contacted and no real workspace can be reached.
9
+
10
+ Tool id `slack` · package version `0.1.1` · engine `>=0.1.0 <0.2.0` · Apache-2.0.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ firedrill tool add @firedrill-tools/slack --install
16
+ firedrill serve
17
+ ```
18
+
19
+ `tool add --install` on a fresh project creates the `local-dev` actor with every operation granted and copies the 87
20
+ starter rows (one workspace). In an existing project it changes nothing silently: add the exact grants you want to the
21
+ actor that represents your agent and, optionally, the attribute that selects which workspace member it acts as:
22
+
23
+ ```json
24
+ {
25
+ "id": "agent",
26
+ "attributes": { "userId": "U01DANA0001", "teamId": "T01EXAMPLE0" },
27
+ "grants": [
28
+ { "packageId": "slack", "operationId": "auth.test" },
29
+ { "packageId": "slack", "operationId": "conversations.list" },
30
+ { "packageId": "slack", "operationId": "conversations.history" },
31
+ { "packageId": "slack", "operationId": "chat.post-message" }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ ### Identities
37
+
38
+ The actor attribute `userId` selects the member the actor acts as and must be the id of a `users` row; an actor whose
39
+ `userId` matches no member fails every operation with `INVALID_AUTH` (HTTP 401, Slack `invalid_auth`), which is what a
40
+ token that resolves to no workspace member does. **Without the attribute the actor acts as the workspace's first active
41
+ human member in `users` row-id order** (deactivated and bot users are skipped; Dana Reyes, `U01DANA0001`, in the starter
42
+ data), so the grant-only `local-dev` actor that `tool add --install` creates is a member from its first call and needs
43
+ nothing added by hand; only a workspace without any active human member answers `invalid_auth` for such an actor. Set
44
+ `userId` explicitly to act as someone else. `teamId` is optional and must equal the workspace id when present. The 25
45
+ operation ids that can be granted are listed in the table below.
46
+
47
+ ## Starting data
48
+
49
+ `starter.json` (and the author world in `firedrill/world.json`) describes the fictional workspace **Example Team**
50
+ (`T01EXAMPLE0`, `https://example-team.slack.com/`) at virtual time 2026-09-14T09:00Z:
51
+
52
+ - **6 members**: Dana Reyes (`U01DANA0001`, admin/owner), Sam Okafor, Priya Natarajan (contractor, member of `#general`
53
+ and `#random` only), Lee Marsh (deactivated), Ops Bot (`is_bot`, `B01OPSBOT01`) and Mira Chen (Singapore time zone,
54
+ status `:palm_tree:`). Addresses use `example.test`.
55
+ - **10 conversations**: `#general` (cannot be archived, one pinned welcome message), `#engineering` (15 messages: a
56
+ four-reply thread with one `thread_broadcast`, an edited message, `+1`/`eyes` reactions, a bot message with Block Kit
57
+ `blocks`; exactly three pages with `limit=5`), `#incidents` (private, a two-reply alert thread, one pinned runbook),
58
+ `#random`, `#launch-q3` (archived), `#design` (public, Dana is not a member), `#leadership` (private, invisible to Dana),
59
+ a Dana ↔ Sam IM, a Dana/Sam/Mira group DM and the empty `#onboarding`.
60
+ - **38 messages**, **30 memberships**, **2 pins**; the workspace `id_sequence` starts at 50, so generated ids
61
+ (`C000000001F`, `D000000001G`, …) never collide with authored rows. `message_sequence` is the six-digit `ts` fraction
62
+ and restarts in every virtual second (`message_second` records which second it belongs to; authored data may omit it),
63
+ so the first generated `ts` is `1789376400.000001`. An allocation skips any fraction an authored row already occupies.
64
+
65
+ Replace it with your own scenarios whenever you like — nothing in the behavior depends on these rows.
66
+
67
+ ## Operations
68
+
69
+ Canonical MCP names are `slack.<operation>`; the alias column lists the reference Slack MCP server tool name exposed in
70
+ addition. Read methods exist as `GET` (query string) **and** `POST`, write methods as `POST`. A `POST` body is
71
+ `application/x-www-form-urlencoded` (what `@slack/web-api` and `slack_sdk` send; also assumed when no content type is
72
+ given) **or** an `application/json` object, as the Web API accepts with a bearer token: `blocks` may then be a real array
73
+ and booleans real booleans.
74
+
75
+ | operation | MCP alias | HTTP route | notes |
76
+ |---|---|---|---|
77
+ | `auth.test` | — | `GET`/`POST /api/auth.test` | `url`, `team`, `user`, `team_id`, `user_id`, `bot_id` for bot users |
78
+ | `users.list` | `slack_get_users` | `GET`/`POST /api/users.list` | `limit` (≤ 1000), `cursor`; deactivated and bot users included |
79
+ | `users.info` | — | `GET`/`POST /api/users.info` | `user` |
80
+ | `users.profile.get` | `slack_get_user_profile` | `GET`/`POST /api/users.profile.get` | `user` optional (defaults to the acting member) |
81
+ | `conversations.list` | `slack_list_channels` | `GET`/`POST /api/conversations.list` | `types` (public_channel, private_channel, mpim, im), `exclude_archived`, `limit`, `cursor` |
82
+ | `conversations.info` | — | `GET`/`POST /api/conversations.info` | `include_num_members`; `is_member` always computed |
83
+ | `conversations.members` | — | `GET`/`POST /api/conversations.members` | `limit`, `cursor` |
84
+ | `conversations.create` | — | `POST /api/conversations.create` | `name` (leading `#` stripped, lower-cased), `is_private`; emits `channel.created` |
85
+ | `conversations.join` | — | `POST /api/conversations.join` | public channels only; repeat join returns `warning: already_in_channel` |
86
+ | `conversations.invite` | — | `POST /api/conversations.invite` | `users` comma list; all-or-nothing; one `channel_join` message per invitee |
87
+ | `conversations.open` | — | `POST /api/conversations.open` | `users` (1 → IM, 2–8 → group DM) or `channel`; `return_im` |
88
+ | `conversations.archive` | — | `POST /api/conversations.archive` | not `#general`, not IMs; no unarchive |
89
+ | `conversations.set-topic` | — | `POST /api/conversations.setTopic` | `topic` ≤ 250 characters |
90
+ | `conversations.history` | `slack_get_channel_history` | `GET`/`POST /api/conversations.history` | newest first, `limit` (≤ 999), `cursor`, `oldest`/`latest`/`inclusive`, `has_more`, `pin_count` |
91
+ | `conversations.replies` | `slack_get_thread_replies` | `GET`/`POST /api/conversations.replies` | parent first then replies oldest first; `ts`; `limit`, `cursor`, `oldest`/`latest` |
92
+ | `chat.post-message` | `slack_post_message` | `POST /api/chat.postMessage` | `text` and/or `blocks` (JSON string in a form body, real array in a JSON body), `thread_ts`, `reply_broadcast`; emits `message.posted` |
93
+ | `chat.reply` | `slack_reply_to_thread` | — (use `chat.postMessage` with `thread_ts`) | the reference server's threaded-reply contract |
94
+ | `chat.update` | — | `POST /api/chat.update` | own messages only; sets `edited` |
95
+ | `chat.delete` | — | `POST /api/chat.delete` | own messages, or anyone's for `is_admin`; a parent with replies becomes a `tombstone`; emits `message.deleted` |
96
+ | `reactions.add` | `slack_add_reaction` | `POST /api/reactions.add` | `name` with or without colons; 50 distinct reactions per message; emits `reaction.added` |
97
+ | `reactions.remove` | — | `POST /api/reactions.remove` | |
98
+ | `pins.add` / `pins.remove` | — | `POST /api/pins.add`, `…/pins.remove` | keeps `pinned_to` on the message in sync; `pins.add` refuses the 101st pin of a conversation with `too_many_rows` (Slack's limit is 100 pins) |
99
+ | `pins.list` | — | `GET`/`POST /api/pins.list` | newest pin first, with `permalink` and `pinned_to` |
100
+ | `search.messages` | — | `GET`/`POST /api/search.messages` | `query`, `count` (≤ 100), `page`, `sort` score/timestamp, `sort_dir` |
101
+
102
+ The MCP aliases accept the argument names of the reference server (`channel_id`, `user_id`, `thread_ts`, `timestamp`,
103
+ `reaction`, `limit`, `cursor`); the canonical operations accept both those spellings and the Web API ones (`channel`,
104
+ `user`, `ts`, `name`) and fail with `INVALID_ARGUMENTS` when both are given. Every success body carries `"ok": true` and is
105
+ returned verbatim over HTTP; MCP structured content is the same object.
106
+
107
+ ### Access model (bot-token shaped)
108
+
109
+ - Public channels are visible to every member (`list`, `info`, `members`); private channels, IMs and group DMs only to
110
+ their members — for anyone else they are `channel_not_found`.
111
+ - Reading history/replies/pins and every write requires membership (`not_in_channel`). `conversations.join` is the way
112
+ into a public channel; it is `method_not_supported_for_channel_type` for private channels, IMs and group DMs.
113
+ - Archived conversations stay readable and refuse every write (`is_archived`).
114
+ - `chat.update` needs authorship (`cant_update_message`); `chat.delete` needs authorship or `is_admin`
115
+ (`cant_delete_message`). Deactivated members cannot be invited or messaged (`user_not_found`).
116
+ - Framework grants authorize calling an operation; a grant-less actor gets `denied` (HTTP 403 with a Slack-shaped
117
+ `missing_scope` body naming the operation).
118
+
119
+ ### Errors and status codes
120
+
121
+ Error bodies are Slack-shaped: `{ "ok": false, "error": "<snake_case>", "response_metadata": { "messages": ["…"] } }`
122
+ (the `messages` array only when the handler adds detail, e.g. `invalid_arguments`). **The HTTP status is 4xx/5xx**, not
123
+ Slack's usual 200 — see [Protocol compatibility](#protocol-compatibility). Mapping: 400 `invalid_arguments`,
124
+ `invalid_cursor`, `invalid_name`, `no_text`, `no_query`, `is_archived`, `already_archived`,
125
+ `method_not_supported_for_channel_type`, `cant_invite_self`, `too_many_reactions`, `invalid_blocks_format` (a `blocks`
126
+ form field that is not JSON), `invalid_blocks` (JSON that is not an array of at most 50 typed block objects, nests deeper
127
+ than 32 levels or encodes to more than 100 KB), `response_too_large` (this package's own error: one message, match or pin
128
+ set alone exceeds the response byte budget — see Limitations), `too_many_rows` (this package's own
129
+ error, not a Slack one: a bounded read found more rows than the workspace's bound — including the lookup a reply to a thread
130
+ with a full `reply_users` list makes — or a conversation is at its pin bound —
131
+ see Limitations) · 401 `invalid_auth` · 403 `not_in_channel`, `cant_update_message`, `cant_delete_message` · 404
132
+ `channel_not_found`, `user_not_found`, `message_not_found`, `thread_not_found`, `no_reaction`, `not_pinned` · 409
133
+ `name_taken`, `already_in_channel`, `already_reacted`, `already_pinned` · 429 `ratelimited` (+ `Retry-After: 30`) · 503
134
+ `service_unavailable`. Framework outcomes are rendered Slack-shaped too: schema violations → 400 `invalid_arguments`,
135
+ missing grant → 403 `missing_scope`, unknown method → 404 `framework.HTTP_ROUTE_NOT_FOUND` (the framework's own body),
136
+ `GET` on a write route → 405. A `POST` body that cannot be mapped to a call at all — JSON that does not parse
137
+ (Slack: `invalid_json`), a JSON value that is not an object (`json_not_object`), or a content type other than form or
138
+ JSON — answers the framework's 400 `framework.HTTP_REQUEST_MAPPING_FAILED` body, whose text names the Slack error; the
139
+ Tool cannot declare those as Slack-shaped errors because the request never reaches an operation.
140
+
141
+ ### Search query subset
142
+
143
+ Whitespace-separated terms (case-insensitive substring of `text`, all must match), `"quoted phrases"`, `-negation`, and
144
+ the modifiers `in:#name` / `in:name` / `in:<#C…>` / `in:@handle` (IM), `from:@handle` / `from:<@U…>` / `from:me`,
145
+ `is:thread`, `has:pin`, `has:reaction`, `has::emoji:`, `before:` / `after:` / `on:` (`YYYY-MM-DD`, UTC) and
146
+ `during:YYYY-MM`. Only conversations the acting member belongs to are searched; `tombstone` and `channel_join` messages are
147
+ skipped. Score = 10 per matching term, +5 for a whole-word match, +1 for a top-level message; ties break by `ts`
148
+ descending. Any other modifier (`to:`, `has:link`, `has:file`, `is:saved`, `with:`, `during:` with a day, …) fails with
149
+ 400 `invalid_arguments` — real Slack treats unknown modifiers as text. A `query` holding U+FFFD (the framework decodes
150
+ malformed percent-encoding such as `%E0%A4%A` in query strings and form bodies to U+FFFD; a correctly encoded
151
+ `%EF%BF%BD` is rejected the same way) fails with 400 `invalid_arguments` instead of an empty result; `%ZZ` stays literal
152
+ text, and non-ASCII searches (`café`, `漢字`, emoji) work. `conversations.list` `types` is an enumerated list, so a
153
+ mangled value is already `invalid_arguments`; `users.list` has no free-text filter. Pagination is page-based (`count`/`page`); a page
154
+ past the end returns an empty page.
155
+
156
+ ## Protocol compatibility
157
+
158
+ - **HTTP**: paths, methods, argument names, form encoding, success bodies, cursors (`response_metadata.next_cursor`,
159
+ `""` on the last page) and error strings follow the public Slack Web API documentation for the subset above.
160
+ **Not verified against `@slack/web-api` or `slack_sdk`**; the manifest's `compatibility` list is empty. The
161
+ `slack-web-api` connection recipe maps `SLACK_API_URL` / `SLACK_BOT_TOKEN` to the world's HTTP URL and token for a
162
+ test process (`@slack/web-api`: `slackApiUrl = SLACK_API_URL + "/api/"`; `slack_sdk`: `base_url`).
163
+ - **MCP**: the eight tool names and argument names follow the README of the archived reference Slack MCP server
164
+ (`modelcontextprotocol/servers-archived`, `src/slack`) as read on 2026-09-14; outputs are the Slack bodies it returned
165
+ verbatim. **Not verified against that server or against Slack's hosted MCP server**; the conformance suite drives the
166
+ aliases with raw JSON-RPC over Streamable HTTP.
167
+
168
+ ### Error envelope: HTTP status versus Slack's `ok: false`
169
+
170
+ Real Slack answers **HTTP 200** with `{"ok":false,"error":"…"}` for every platform error except rate limits (HTTP 429)
171
+ and outages (5xx). This package cannot reproduce that. The framework's manifest contract for HTTP routes
172
+ (`@firedrill-run/contracts`, `HttpRouteContractSchema`: `response.errors[].status` is `z.number().int().min(400).max(599)`)
173
+ requires every declared error of a route to map to a status between 400 and 599, and the manifest validator refuses a
174
+ route whose error map does not cover the operation's declared errors — so a 200 cannot be declared for an error and the
175
+ framework, which owns status codes, sends the mapped 4xx. What this package does instead: the **body** is Slack's exactly
176
+ (`{"ok":false,"error":"channel_not_found"}`, `response_metadata.messages` where Slack adds detail, `Retry-After` on 429)
177
+ while the status is the 4xx listed under "Errors and status codes". Effect on the official SDKs, read from their published
178
+ source and **not exercised here** (the `compatibility` list stays empty):
179
+
180
+ - **`@slack/web-api` (Node)**: `WebClient` treats a non-200 response that is not a 429 as an HTTP-level failure and rejects
181
+ with an error whose `code` is `slack_webapi_http_error` (`ErrorCode.HTTPError`, carrying `statusCode`, `statusMessage`,
182
+ `headers` and the parsed `body`), not the `slack_webapi_platform_error` (`ErrorCode.PlatformError`) whose `data.error`
183
+ holds the Slack error string. Code that branches on `error.code === ErrorCode.PlatformError` or reads `error.data.error`
184
+ therefore does not see `is_archived`, `channel_not_found` and friends; read `error.body.error` instead, and expect the
185
+ client's retry policy to treat such responses as it treats any HTTP error before rejecting. 429 (`ratelimited` +
186
+ `Retry-After`) and 503 (`service_unavailable`) behave as with Slack itself.
187
+ - **`slack_sdk` (Python)**: `WebClient` raises `SlackApiError` both for a non-200 status and for `ok: false`, so the
188
+ exception type is the same as with Slack and `e.response["error"]` still holds the Slack error string; only
189
+ `e.response.status_code` differs (4xx instead of 200).
190
+ - Agents and hand-written clients that branch on `body.ok`/`body.error` keep working; agents that treat any non-200 as a
191
+ transport failure do not — that is the deviation to plan for.
192
+
193
+ ## Browser app
194
+
195
+ `firedrill serve` prints an app link (also **Tools → Open app** in the inspector). The app is a faithful recreation of the
196
+ Slack desktop web client in its default *Aubergine* theme, built so that a daily Slack user recognises every screen: the
197
+ 44 px top bar with back/forward/history controls, the centred search field ("Search Example Team", `Ctrl/⌘ K`) and help;
198
+ the 70 px workspace switcher rail (workspace tile, Home / DMs / Activity / More, the round **+** and your avatar with a
199
+ presence dot); the 260 px sidebar with the workspace name menu, filter and compose buttons, collapsible **Channels**
200
+ (`#` / lock glyphs, bold for conversations with activity since you last opened them, "Add channels" → create or browse)
201
+ and **Direct messages** (avatars with presence, group DMs with a member-count tile, "New message"); the white
202
+ conversation pane with the `# channel ▾` header (opens channel details), topic, member pill with stacked avatars,
203
+ **Messages / Pins** tabs, day-divider pills ("Today", "Yesterday", weekdays, "Thu, Aug 20th"), 36 px rounded-square letter
204
+ avatars, bold sender names with an **APP** badge for bots, hover times on consecutive messages, "(edited)" markers,
205
+ "replied to a thread:" preambles for broadcast replies, grey system lines for joins, italic tombstones, reaction pills
206
+ (your own in blue), "N replies · Last reply …" thread footers with reply avatars, the hover toolbar (✅ 👀 🙌 quick
207
+ reactions, add reaction, reply in thread, more → pin/unpin, copy link, edit, delete) and Slack's channel intro card;
208
+ the composer with the formatting toolbar (bold, italic, strike, link, lists, quote, code, code block as mrkdwn
209
+ markers), `+`, formatting toggle, emoji picker, `@` mention menu and the green send button (Enter sends, Shift+Enter
210
+ newline, `↑` edits your last message, Esc cancels); the 400 px **Thread** panel with the "N replies" divider and a
211
+ reply composer with "Also send to #channel"; the right-hand **Profile** panel (large avatar, title, status, local time,
212
+ Message button, contact information); the channel **details modal** (About with editable topic, Members with search and
213
+ "Add people", Settings with "Archive channel"); **Create a channel** (name with `#` prefix and 80-character counter,
214
+ Public/Private), **Edit topic**, **Add people** / **New message** pickers with chips, blue-highlight context menus and
215
+ Slack-red confirmations for delete and archive; the **Search** results view (query heading, Messages tab with count,
216
+ In / From / Pinned filter chips, Most relevant / Most recent sort, highlighted terms, `1–20 of N` pager, click to jump
217
+ and flash the message); **Browse channels** (All channels, search, member counts, Join / Joined ✓, Archived badge);
218
+ a **People** directory; and the "About this workspace" panel showing the official Slack mark and wordmark. Routes
219
+ mirror the client's conversation ids (`#C…`, `#C…/thread/<ts>`, `#search/<query>`, `#browse`, `#people`, `#pins`).
220
+ Below 768 px the rail hides behind a hamburger, the sidebar becomes a drawer and the thread/profile panel a full-width
221
+ sheet, so nothing scrolls horizontally at 375 px.
222
+
223
+ Every control calls the Tool's own operations through `/_firedrill/client.js`: history, replies, members and users
224
+ paginate with the Web API cursors ("Load older messages"), search sends the typed query verbatim (an unsupported
225
+ modifier surfaces the operation's `invalid_arguments` inline), every mutation carries a fresh idempotency key that is
226
+ reused only when the same uncertain action is retried, the page polls `getContext().revision` every 2 s and refreshes
227
+ the sidebar, the open conversation, thread or search without discarding composer text, and loading skeletons, empty
228
+ states (channel intro, "No pinned items yet", "No results"), errors (503 fault card with Retry, 429 fault note under the
229
+ composer that keeps your text), join bars for public channels you are not in, archived banners, `invalid_auth` and
230
+ permission-denied (`missing_scope`) states are rendered. All record text goes through `textContent`; mrkdwn markers,
231
+ `<@U…>` mentions, `<#C…>` channel links, `<url|label>` links and `:shortcodes:` are rendered from text nodes and styled
232
+ spans only. Presence is not modelled by the backend, so other members show the hollow "away" dot and only you show
233
+ active; "unread" bold is a per-tab convenience (activity since you last opened the conversation in this tab), not a
234
+ server-side `last_read`. A UI write is visible over HTTP/MCP and vice versa because both hit the same operations; reset
235
+ restores both.
236
+
237
+ The app is served from `firedrill/tools/slack/app/site/` (`ui.root`), which bundles Lato 400/700/900 (SIL OFL 1.1) and
238
+ the official Slack mark and wordmark; `firedrill/tools/slack/app/assets/ATTRIBUTION.md` records every asset's source
239
+ URL, hash and licence (the framework serves only html/css/js/json/image/font files, so the Markdown record and the
240
+ licence text sit beside, not inside, the served root). Screenshots live in the author repository (`specs/slack/*.png`),
241
+ not in the package.
242
+
243
+ ## Faults and events
244
+
245
+ | id | kind | effect |
246
+ |---|---|---|
247
+ | `post-rate-limited` | fault on `chat.post-message`, `chat.reply`, `chat.update` | 429 `ratelimited`, `Retry-After: 30`; nothing is stored, no event |
248
+ | `history-unavailable` | fault on `conversations.history`, `search.messages` | 503 `service_unavailable`; replies, info, pins keep working |
249
+ | `message.posted` | event | `{ channel, ts, user, thread_ts?, subtype?, text }` from `chat.post-message` / `chat.reply` (not for `channel_join` system messages) |
250
+ | `message.deleted` | event | `{ channel, ts, user, tombstoned }` |
251
+ | `reaction.added` | event | `{ channel, ts, user, reaction }` |
252
+ | `channel.created` | event | `{ channel, name, creator, is_private }` (from `conversations.create`, not from `conversations.open`) |
253
+
254
+ Activate a fault in a scenario with `"faults": [{ "packageId": "slack", "faultId": "post-rate-limited" }]`.
255
+
256
+ ## Conformance
257
+
258
+ `firedrill tool test slack` runs fourteen drills twice (Web API flow over GET and POST routes with form and JSON bodies, MCP aliases, member
259
+ visibility, denied actor, `invalid_auth` actor, both faults, a fresh-install actor without identity attributes, the
260
+ workspace bounds — scenario `bounds` lowers every bound of the `workspace` row below the authored rows — the member
261
+ bound of `conversations.open` — scenario `open-member-bound` sets `limits.members` to 2 — and group DMs of members with
262
+ 21-character handles — scenario `open-long-handles` — a thread that already has 50 distinct repliers — scenario
263
+ `thread-many-repliers` — and a 4,000-byte response budget — scenario `response-budget` sets `limits.response_bytes`, and
264
+ the drill pages users, conversations, history, replies and search with multi-byte text, checks every page stays within
265
+ the budget and returns every item exactly once, then gets `response_too_large` from an oversized message and pin set —
266
+ and a full `ts` second — scenario `ts-sequence-full` seeds `message_sequence 999997` in the frozen virtual second with an
267
+ authored message at `.999998`; the drill posts the last `.999999` and then gets `too_many_rows` from posting, replying,
268
+ joining and inviting with nothing written)
269
+ from `firedrill/`
270
+ with the Node-only target
271
+ `test/conformance.mjs`, checks that every operation, declared error, event and fault was observed, and compares the two
272
+ passes. Author-written checks are not certification of Slack fidelity.
273
+
274
+ ## Limitations
275
+
276
+ - A bounded subset: 25 operations, 24 Slack methods, 35 HTTP routes. Every other method (`conversations.leave/rename/
277
+ setPurpose/kick/unarchive/mark/close`, `chat.postEphemeral/scheduleMessage/getPermalink/meMessage`, `reactions.get/
278
+ list`, `files.*`, `bookmarks.*`, `canvases.*`, `usergroups.*`, `users.lookupByEmail/setPresence/getPresence/profile.set`,
279
+ `team.info`, `emoji.list`, `views.*`, `apps.*`, `oauth.*`, `admin.*`, `search.all/files`, `rtm.*`) returns the framework
280
+ 404; a `GET` on a declared write path returns 405. Events API, Socket Mode, interactivity payloads, Block Kit
281
+ rendering, Slack Connect and Enterprise Grid are not emulated.
282
+ - **Errors use HTTP 4xx/5xx status codes** with Slack-shaped `{ ok: false, error }` bodies. Real Slack answers HTTP 200 for
283
+ `ok: false` (except 429 and 5xx); the framework's route contract only allows 400–599 for declared errors, so this cannot
284
+ be reproduced — see "Error envelope" under Protocol compatibility for the exact constraint and what each official SDK
285
+ does with it.
286
+ - `POST` bodies are form-encoded or JSON objects only; `multipart/form-data` (Slack accepts it) is refused with the
287
+ framework 400 described above. The legacy `token` form/query parameter is not accepted; the bearer token is the
288
+ isolated world token. OAuth scopes, `xoxb`/`xoxp` token types, app installation and per-method `missing_scope` are not
289
+ emulated — a grant-less actor gets `missing_scope` for everything.
290
+ - Not verified against `@slack/web-api`, `slack_sdk`, Slack's hosted MCP server or the archived reference MCP server.
291
+ - Access is bot-token shaped: membership is required to read history/replies/pins and to write; user-token nuances
292
+ (reading public channels without joining, admin visibility of private channels, `account_inactive` for deactivated
293
+ actors) are not modelled. A thread's `reply_users` lists at most its first 50 distinct repliers; replies from
294
+ further participants are still posted and counted in `reply_count` and `reply_users_count` (which counts every distinct
295
+ replier), they are just not added to the list.
296
+ - `ts` values are unique and ordered but the fractional part is a per-second sequence, not sub-second wall time; all
297
+ times are virtual. One virtual second holds at most **999,999** message timestamps; when the clock does not advance,
298
+ the next `chat.postMessage`, `chat.reply`, `conversations.join` or `conversations.invite` fails with 400
299
+ `too_many_rows` ("message ts limit reached …") and writes nothing. `edited.ts` is the virtual second with a `.000000`
300
+ fraction.
301
+ - Search is substring-based over the documented modifier subset; unsupported modifiers fail with 400 instead of being
302
+ treated as text; no ranking parity, stemming, `to:`, file/link filters, `search.all`, or cursor pagination.
303
+ - Messages store `text` and opaque `blocks`; no mrkdwn parsing, link unfurling, mention resolution on the server
304
+ (`<@U…>` is stored verbatim), attachments, files, edit history, message metadata or permalink resolution (permalinks
305
+ are string literals built from the workspace row and resolve nowhere).
306
+ - Tombstoning, `channel_join` system messages and reply counters follow Slack loosely; there are no `channel_leave`,
307
+ `channel_topic` or `channel_archive` system messages, no unread/`last_read` tracking, no typing or presence.
308
+ - **Row bounds, never silent truncation.** The workspace serves at most 1,000 users, 1,000 conversations, 1,000 members
309
+ per conversation, 10,000 messages per conversation and 100 pins per conversation (Slack's own pin limit); a world may
310
+ set its own with `limits: { users, conversations, members, messages, pins }` on the `workspace` row (each 1–10,000).
311
+ Every operation that has to read a whole set — `users.list`, `conversations.list`, `conversations.info` with
312
+ `include_num_members`, `conversations.members`, `conversations.create`, `conversations.join`, `conversations.invite`,
313
+ `conversations.open`, `conversations.set-topic` on an IM or group DM, `conversations.history`, `conversations.replies`,
314
+ `chat.delete` of a reply (it recounts the thread), a threaded `chat.postMessage` to a thread whose `reply_users` list is
315
+ already full (it looks for an earlier reply by the caller, before the new `ts` is allocated), `pins.add`, `pins.list` and `search.messages` — reads one row past
316
+ the bound and fails with 400 `too_many_rows` ("messages in C01GENERAL1 exceed the supported bound of N rows") when the
317
+ authored data exceeds it, instead of returning the first N rows; `pins.add` also refuses to exceed the pin bound.
318
+ `conversations.open` refuses to create an IM or group DM whose member count (the requested users plus the caller)
319
+ exceeds the member bound, before any id is allocated and whether or not `return_im` is set: 400 `too_many_rows`
320
+ ("members of the requested conversation (3) exceed the supported bound of 2 rows (workspace.limits.members)"). A
321
+ refused call stores nothing and consumes no id, and no error names a conversation id the call would have created;
322
+ `conversations.create` and `conversations.open` build the returned channel from the rows they just wrote instead of
323
+ rescanning them. Reads
324
+ and writes by id (`auth.test`, `users.info`, `users.profile.get`, `conversations.info` without a member count,
325
+ `chat.postMessage` (other than the full-thread case above), `chat.update`, `reactions.*`, `pins.remove`, `conversations.archive`) never scan and keep working.
326
+ `too_many_rows` is this package's error string, not Slack's.
327
+ - **Response byte budget, never an oversized body.** Every list and read sizes its page by the UTF-8 bytes of the encoded
328
+ JSON body (computed from code points, so CJK and emoji text count 3–4 bytes) as well as by count: 900 KB by default,
329
+ lower when the `workspace` row sets `limits.response_bytes` (1,024–900,000). `users.list`, `conversations.list`,
330
+ `conversations.members`, `conversations.history` and `conversations.replies` stop filling a page before the budget and
331
+ return `has_more` / `response_metadata.next_cursor`, so a page can hold fewer items than `limit` and the cursor returns
332
+ every item exactly once. `search.messages` keeps page arithmetic uniform: `per_page` / `paging.count` is the requested
333
+ `count`, lowered only as far as needed for every page of the result set to fit, so `page` N+1 starts where page N ended.
334
+ `pins.list` has no pagination in the Web API, so a pin set whose body exceeds the budget answers 400
335
+ `response_too_large`; a single message or match that alone exceeds the budget does the same in history, replies and
336
+ search (users, conversations and member ids with `too_many_rows`; unreachable within the stored schema bounds). With
337
+ the default budget a single message exceeds it only in extreme cases (`text` is at most 40,000 characters and
338
+ `blocks` at most 100 KB, so it takes reaction lists naming thousands of users).
339
+ - **Group DM names stay within 80 characters.** A group DM is named `mpdm-<handle>--<handle>-1` from its members' sorted
340
+ handles, as in Slack. Handles may be 21 characters and a group DM may have nine members, so that form can exceed the
341
+ 80-character conversation name; such names keep the first 69 characters of the plain form (trailing `-`/`.` removed)
342
+ and end with `-<8 hex digits>-1`, an FNV-1a digest of the full handle list
343
+ (`mpdm-dana.reyes--longhandle1xxxxxxxxxx--longhandle2xxxxxxxxxx--longha-98de6b61-1`). Different member sets keep
344
+ distinct names; Slack's own naming of very large group DMs is not reproduced.
345
+ - The browser app covers the 25 operations only: no huddles, canvases, files, bookmarks, "Later", saved items,
346
+ notifications/Activity feed, presence, typing indicators, server-side unread counts, message forwarding, custom
347
+ emoji, Block Kit rendering (blocks are stored opaque and the message `text` is shown), channel rename/leave/unarchive
348
+ or purpose editing (the backend has no such operations).
349
+ - **Client chrome that is rendered but not simulated.** These controls sit where Slack puts them, with Slack-style glyphs,
350
+ hover states and tooltips, and open a short "Not simulated by this Tool" panel instead of acting: the rail's **Later**
351
+ tab; sidebar **Threads**, **Huddles**, **Drafts & sent** and **Add apps**; the channel **Files** tab and **+** (add tab);
352
+ the **huddle** button and its caret; the message toolbar's **Forward** and **Save for later**; the composer's **video
353
+ clip**, **audio clip** and **shortcuts** (slash) buttons and the send button's **schedule** caret, in both the channel
354
+ composer and the thread reply composer (which carries the same formatting toolbar, +, format toggle, emoji and mention
355
+ controls as the channel composer); and the rail **Activity** tab, which has no activity or notification feed.
356
+ **Directories** opens a People / Channels menu (over `users.list` and `conversations.list`). The **+** attach menu's
357
+ "Attach files" row is visibly disabled because `files.*` is not part of this Tool.
358
+ Nothing in these panels calls an operation or invents workspace data.
359
+ - All data is fictional; the package contains no real workspace, member, token or copied content and must never be
360
+ pointed at a production Slack workspace.
361
+
362
+ ## Trademarks
363
+
364
+ Slack and the Slack logo are trademarks of Slack Technologies, LLC, a Salesforce company. They are used here only to
365
+ identify the service this package simulates inside a test environment; this package is an independent Firedrill Tool and
366
+ is not affiliated with, sponsored by or endorsed by Slack or Salesforce. The logo files under
367
+ `firedrill/tools/slack/app/` are unmodified official assets whose sources are listed in
368
+ `firedrill/tools/slack/app/assets/ATTRIBUTION.md`; the bundled Lato font is licensed under the SIL Open Font License 1.1
369
+ (`firedrill/tools/slack/app/assets/fonts/OFL.txt`).
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "target": {
4
+ "id": "conformance-agent",
5
+ "kind": "command",
6
+ "bindings": [
7
+ "http",
8
+ "mcp"
9
+ ],
10
+ "executable": "node",
11
+ "arguments": [
12
+ "test/conformance.mjs"
13
+ ],
14
+ "workingDirectory": ".",
15
+ "timeoutMs": 60000
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "baseline",
4
+ "title": "Baseline: the fictional Example Team workspace exactly as authored in world.json (no faults)"
5
+ }