@firedrill-tools/unified 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 (61) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +242 -0
  3. package/firedrill/agent.target.json +17 -0
  4. package/firedrill/baseline.scenario.json +5 -0
  5. package/firedrill/bounds.scenario.json +23 -0
  6. package/firedrill/conformance.suite.json +23 -0
  7. package/firedrill/large-channels.scenario.json +4251 -0
  8. package/firedrill/many-workspaces.scenario.json +1623 -0
  9. package/firedrill/rate-limited.scenario.json +11 -0
  10. package/firedrill/tools/unified/behavior.mjs +69 -0
  11. package/firedrill/tools/unified/lib/assoc.mjs +61 -0
  12. package/firedrill/tools/unified/lib/enums.mjs +14 -0
  13. package/firedrill/tools/unified/lib/errors.mjs +50 -0
  14. package/firedrill/tools/unified/lib/events.mjs +27 -0
  15. package/firedrill/tools/unified/lib/identity.mjs +59 -0
  16. package/firedrill/tools/unified/lib/query.mjs +154 -0
  17. package/firedrill/tools/unified/lib/shapes.mjs +176 -0
  18. package/firedrill/tools/unified/lib/store.mjs +75 -0
  19. package/firedrill/tools/unified/lib/time.mjs +59 -0
  20. package/firedrill/tools/unified/lib/util.mjs +92 -0
  21. package/firedrill/tools/unified/lib/validate.mjs +114 -0
  22. package/firedrill/tools/unified/lib/wire.mjs +120 -0
  23. package/firedrill/tools/unified/ops/channels.mjs +45 -0
  24. package/firedrill/tools/unified/ops/connections.mjs +151 -0
  25. package/firedrill/tools/unified/ops/crm.mjs +126 -0
  26. package/firedrill/tools/unified/ops/deals.mjs +123 -0
  27. package/firedrill/tools/unified/ops/messages.mjs +133 -0
  28. package/firedrill/tools/unified/ops/pipelines.mjs +20 -0
  29. package/firedrill/tools/unified/ops/resource.mjs +114 -0
  30. package/firedrill/tools/unified/unified.tool.json +14068 -0
  31. package/firedrill/unified-bounds.drill.json +148 -0
  32. package/firedrill/unified-connections-flow.drill.json +226 -0
  33. package/firedrill/unified-crm-flow.drill.json +576 -0
  34. package/firedrill/unified-denied.drill.json +83 -0
  35. package/firedrill/unified-error-coverage.drill.json +528 -0
  36. package/firedrill/unified-fresh-actor.drill.json +83 -0
  37. package/firedrill/unified-invalid-workspace.drill.json +933 -0
  38. package/firedrill/unified-large-channels.drill.json +68 -0
  39. package/firedrill/unified-many-workspaces.drill.json +943 -0
  40. package/firedrill/unified-mcp-core-aliases.drill.json +113 -0
  41. package/firedrill/unified-messaging-flow.drill.json +326 -0
  42. package/firedrill/unified-rate-limited.drill.json +933 -0
  43. package/firedrill/unified-scoped-token.drill.json +113 -0
  44. package/firedrill/unified-size-bounds.drill.json +263 -0
  45. package/firedrill/unified-write-committed-lost.drill.json +107 -0
  46. package/firedrill/unified-write-unavailable.drill.json +528 -0
  47. package/firedrill/world.json +2958 -0
  48. package/firedrill/write-committed-lost.scenario.json +11 -0
  49. package/firedrill/write-unavailable.scenario.json +11 -0
  50. package/firedrill.json +5 -0
  51. package/package.json +52 -0
  52. package/starter.json +2446 -0
  53. package/test/conformance.mjs +39 -0
  54. package/test/flows/connections.mjs +87 -0
  55. package/test/flows/coverage.mjs +64 -0
  56. package/test/flows/crm.mjs +124 -0
  57. package/test/flows/faults.mjs +82 -0
  58. package/test/flows/identity.mjs +84 -0
  59. package/test/flows/messaging.mjs +82 -0
  60. package/test/flows/size.mjs +71 -0
  61. package/test/lib.mjs +133 -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,242 @@
1
+ # @firedrill-tools/unified
2
+
3
+ A synthetic **Unified.to workspace** for [Firedrill](https://firedrill.run): one fictional workspace holding a handful of
4
+ **synthetic connections** (each standing for an end-customer's authorised integration) and the unified **CRM** (contact,
5
+ company, deal, pipeline) and **Messaging** (channel, message) data models behind them, exposed through a bounded subset of
6
+ the Unified.to REST API (`/unified/connection`, `/crm/{connection_id}/…`, `/messaging/{connection_id}/…`) and through the
7
+ five Core-mode tool names of the Unified MCP server. Agents that talk to Unified.to with a REST client pointed at a custom
8
+ base URL, or over MCP, can be exercised against it without a Unified.to account, an API key or network access.
9
+
10
+ Everything is computed from the world's state: object ids come from a counter row, timestamps from Firedrill's virtual
11
+ clock, visibility from the calling actor's attributes. Nothing here contacts Unified.to or any underlying platform (HubSpot,
12
+ Slack, Gmail, …); a connection's `integration_type` is a label, and "sending" a message only stores a synthetic message row.
13
+
14
+ Package id `unified`, version `0.1.0`, engine `>=0.1.0 <0.2.0`, license Apache-2.0. **Backend only** — there is no browser
15
+ app; browse state through the REST routes, the MCP names or the Firedrill inspector.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ firedrill tool add @firedrill-tools/unified --install
21
+ firedrill serve --scenario baseline --no-open
22
+ ```
23
+
24
+ `firedrill serve` prints the HTTP and MCP endpoints and their tokens. A new world receives the starter data (61 rows) and
25
+ the grants below. Existing worlds do not change silently: add the grants and (optionally) the `starter.json` rows to your
26
+ own world or scenario.
27
+
28
+ ### Grants and identity
29
+
30
+ Every actor that should use the Tool needs the grants for the operations it may call (`packageId: "unified"`):
31
+
32
+ ```
33
+ connections.list connections.get connections.create connections.update connections.remove
34
+ contacts.list contacts.get contacts.create contacts.update contacts.remove
35
+ companies.list companies.get companies.create companies.update companies.remove
36
+ deals.list deals.get deals.create deals.update deals.remove
37
+ pipelines.list pipelines.get channels.list channels.get
38
+ messages.list messages.get messages.create messages.update messages.remove
39
+ ```
40
+
41
+ and these optional actor **attributes**, which model a Unified.to workspace API key:
42
+
43
+ | attribute | meaning |
44
+ |---|---|
45
+ | `workspaceId` (optional) | Selects the workspace: the `workspaces` row with that id. A claimed id with no row fails every operation with `UNAUTHORIZED` (HTTP 401, `Unauthorized`), like an invalid key. **Absent** (e.g. the actor `firedrill tool add` creates): the caller is the first `workspaces` row in row-id order — `68c8000000000000000000a1` "Brightline Sales Ops" in the starter data. This fallback scans at most 100 workspace rows; with more than 100 it fails `FAILED_PRECONDITION` instead of guessing. |
46
+ | `connectionIds` (optional, string array) | A connection-scoped key (the Unified MCP server's per-connection URL). Absent = every connection of the workspace. Present = `connections.list` returns only these, every other operation on a connection outside the list fails `NOT_FOUND` exactly as for a non-existent id, and `connections.create` fails `FORBIDDEN`. |
47
+
48
+ Framework grants decide whether an actor may call an operation at all (a missing grant is refused by the framework with
49
+ HTTP 403 before the Tool runs); the attributes decide what a call may see and change. The bearer token string itself is
50
+ never interpreted.
51
+
52
+ ### Connections
53
+
54
+ Two connection recipes are declared: `unified-rest` (`UNIFIED_BASE_URL` = the HTTP binding, replacing
55
+ `https://api.unified.to`; `UNIFIED_API_KEY` = its token, sent as `Authorization: Bearer …`) and `unified-mcp`
56
+ (`UNIFIED_MCP_URL` / `UNIFIED_MCP_TOKEN` for the MCP binding, Streamable HTTP with a Bearer token). The values are isolated
57
+ world tokens for test processes only.
58
+
59
+ ## Starting data
60
+
61
+ `starter.json` (and the conformance `firedrill/world.json`) contain 61 fictional rows at virtual time 2026-09-16T09:00:00Z
62
+ (`virtualTimeUs 1789549200000000`, copied into the world by `firedrill tool add`, so records created in a fresh install are
63
+ stamped after every seeded record). Workspace `68c8000000000000000000a1` **Brightline Sales Ops** (`north-america`) has
64
+ nine connections, each in a different state:
65
+
66
+ | connection id (suffix) | integration | categories | state |
67
+ |---|---|---|---|
68
+ | `…0201` crm-main | `hubspot` | crm | healthy; all six `crm_*` read/write permissions + `crm_pipeline_read`; `external_xref acct-brightline`; authorised user Maya Lindqvist (`u-maya`) |
69
+ | `…0202` crm-readonly | `pipedrive` | crm | only `crm_contact_read` (writes and other objects answer 403) |
70
+ | `…0203` chat | `slack` | messaging | all messaging permissions; posts as Ops Bot (`u-opsbot`) |
71
+ | `…0204` mail | `gmail` | messaging | channels `INBOX`, `SENT`, `DRAFT` resolve the `channel_id` aliases |
72
+ | `…0205` paused | `hubspot` | crm | `is_paused true` → 403 on every data call |
73
+ | `…0206` broken | `salesforce` | crm | `last_unhealthy_code "401"` → 401 "likely broken" |
74
+ | `…0207` hris-only | `bamboohr` | hris | 501 on every CRM and messaging route |
75
+ | `…0208` sandbox-crm | `hubspot` | crm | `environment Sandbox` (hidden from `GET /unified/connection` unless `env=Sandbox`; its one contact is reachable) |
76
+ | `…0209` sandbox-omni | `hubspot` | crm, messaging | `environment Sandbox`; every CRM and messaging permission, no data rows — every list answers `[]`, so the whole surface is reachable through one connection id |
77
+
78
+ On crm-main: 2 pipelines (Sales Pipeline with Qualification 10 % → Proposal 40 % → Negotiation 70 % → Closed Won / Closed
79
+ Lost; an inactive Renewals pipeline), 5 companies (Northwind Traders, Contoso Robotics, Fabrikam Studio, inactive Tailspin
80
+ Freight without contacts, 山田物産 with a CJK address), 8 contacts (one without e-mail, one with accents, one with a CJK
81
+ display name, one unassociated), 6 deals (one in the inactive pipeline, one closed won, one closed lost with `lost_reason`,
82
+ one with `currency null`). On chat: 4 channels (`#sales-emea` is a sub-channel of `#sales`, `#exec-private` is private) and
83
+ 16 messages (a thread with three replies, a mention, reactions, an attachment record, a 2,982-character message, an
84
+ unread CJK/emoji message, buttons). On mail: 3 alias channels and 4 e-mails. Seeded ids end in `…0201`–`…0814`; ids issued
85
+ at runtime start at `68c800000000000000001001` and count up in creation order.
86
+
87
+ ## Operations
88
+
89
+ | operation | MCP alias | HTTP route |
90
+ |---|---|---|
91
+ | `connections.list` | `list_unified_connections` | `GET /unified/connection` |
92
+ | `connections.get` | `get_unified_connection` | `GET /unified/connection/{id}` |
93
+ | `connections.create` | `create_unified_connection` | `POST /unified/connection` |
94
+ | `connections.update` | `update_unified_connection` | `PUT` and `PATCH /unified/connection/{id}` |
95
+ | `connections.remove` | `remove_unified_connection` | `DELETE /unified/connection/{id}` |
96
+ | `contacts.list` / `get` / `create` / `update` / `remove` | — | `GET`, `GET`, `POST`, `PUT`+`PATCH`, `DELETE` on `/crm/{connection_id}/contact[/{id}]` |
97
+ | `companies.*` | — | same pattern on `/crm/{connection_id}/company[/{id}]` |
98
+ | `deals.*` | — | same pattern on `/crm/{connection_id}/deal[/{id}]` |
99
+ | `pipelines.list` / `pipelines.get` | — | `GET /crm/{connection_id}/pipeline[/{id}]` |
100
+ | `channels.list` / `channels.get` | — | `GET /messaging/{connection_id}/channel[/{id}]` |
101
+ | `messages.list` / `get` / `create` / `update` / `remove` | — | `GET`, `GET`, `POST`, `PUT`+`PATCH`, `DELETE` on `/messaging/{connection_id}/message[/{id}]` |
102
+
103
+ 29 operations, 34 routes. Canonical names are also reachable as `POST /v1/operations/unified/<operation>` and as MCP tools
104
+ `unified.<operation>`; the canonical input and output shapes **are** the REST shapes (snake_case fields of the Unified.to
105
+ `Connection`, `CrmContact`, `CrmCompany`, `CrmDeal`, `CrmPipeline`, `MessagingChannel`, `MessagingMessage` schemas, with
106
+ `connection_id` and `id` folded into the argument object). Every route takes `Authorization: Bearer <token>`.
107
+
108
+ Behaviour highlights, all computed from state:
109
+
110
+ - **Lists** return a bare JSON array (no envelope, total or cursor): a page shorter than `limit` means there are no more
111
+ records, as Unified.to documents. `limit` 1–100 (default 100; larger values are capped to 100; `0`, negative or non-numeric
112
+ → 400), `offset` ≥ 0, `updated_gte` (ISO-8601 date or date-time; a value without a zone is UTC; impossible dates → 400),
113
+ `sort` ∈ `name|updated_at|created_at` (default creation order; ties by id), `order` ∈ `asc|desc`, `query` (case-folded
114
+ literal substring over name/e-mail fields — contacts: `name`, `first_name`, `last_name`, `emails[].email`; companies:
115
+ `name`, `emails`, `domains`, `websites`; deals and channels: `name`; messages: `subject`, `message`, author name/e-mail;
116
+ wildcards are literal characters; a U+FFFD from malformed percent-encoding → 400), `fields` (comma-separated or repeated;
117
+ `id` is always included; unknown names ignored; **`raw` is returned only when requested**, otherwise every other field).
118
+ Reference filters (`company_id`, `deal_id`, `contact_id`, `pipeline_id`, `parent_id`, `channel_id`) must be well-formed
119
+ 24-hex ids (400 otherwise); an unknown id matches nothing. `channel_id` also accepts `INBOX`, `SENT` and `DRAFT`, resolved
120
+ to the connection's channel carrying that alias (none → `[]`). Messages add `type=READ|UNREAD`, `start_gte`/`end_lt` on
121
+ `created_at`, `user_id` (author) and `user_mentioned_id`; channels add `type=PUBLIC|PRIVATE` and `parent_id`; connections
122
+ add `env` (default `Production`), `categories` (every listed category must be present) and `external_xref` (exact).
123
+ - **Writes** ignore `id`, `created_at`, `updated_at`, `connection_id` and `workspace_id` in bodies (read-only, as Unified.to
124
+ does) and refuse unknown fields with 400 naming the field (caller text clipped). `PUT` and `PATCH` are both **merge**
125
+ updates of the top-level fields present: `null` clears a nullable field, an array replaces the array. Every value is
126
+ checked against the state schema (string lengths, array sizes, enums, e-mail shape, 3-letter upper-case `currency`,
127
+ `probability` 0–100, `employees` ≥ 0) before anything is written; a failed or denied call writes nothing.
128
+ - **Contacts** need one of `name`, `first_name`, `last_name` or an e-mail; `name` derives from first/last name when absent.
129
+ **Companies** need `name`. Association arrays are kept **symmetric** within a connection: writing `company_ids` on a
130
+ contact adds or removes the contact in each company's `contact_ids` (likewise `deal_ids` ⇄ `contact_ids`/`company_ids`
131
+ on deals), only rows whose membership changes are rewritten, references to another connection or an unknown id → 400,
132
+ and removing an object drops its id from every associated row.
133
+ - **Deals**: `stages[0].id` must be a stage of `pipelines[0].id`; a stage alone implies its pipeline, a pipeline alone takes
134
+ its first active stage; names in the references are filled from the pipeline row. `probability` defaults to the stage's
135
+ `deal_probability` unless the body sets it; `closed_at` is stamped with virtual now when a deal enters a stage with
136
+ `is_closed` and cleared when it leaves one.
137
+ - **Messages** need one of `message`, `message_html`, `message_markdown` and either `channels[0].id` (an existing channel,
138
+ ≤ 8 channels) or `parent_id` (an existing message of the connection — the reply inherits its channels). `author_member`
139
+ in the body is ignored: the author is the connection's authorised identity (`auth.name`, `auth.emails[0]`,
140
+ `auth.user_id`). The parent's `has_children` is maintained on create and remove; replies keep their `parent_id` when the
141
+ parent is removed. `messages.update` accepts only `message`, `message_html`, `message_markdown`, `subject` and
142
+ `is_unread` (clearing every text field → 400).
143
+ - **Connections**: `connections.create` yields an already-authorised connection (`integration_name` = title-cased
144
+ `integration_type`, `is_paused false`, `last_healthy_at` = now); `categories` must list ≥ 1 of the Unified.to categories,
145
+ every permission's prefix (`crm_` → `crm`, `messaging_` → `messaging`) must be one of them, `auth` holds only `name`,
146
+ `emails` and `user_id` (never tokens), and `token`/unknown fields are refused. `integration_type` and `categories` cannot be
147
+ changed afterwards. `connections.remove` deletes every contact, company, deal, pipeline, channel and message stored under
148
+ the connection (one `object.deleted` per data row) — or refuses as a whole when a namespace exceeds the scan bound.
149
+ - **Per-connection access rules**, in order: workspace (401 `Unauthorized`), connection resolution (404 `Connection not
150
+ found`, also for a malformed id → 400 `Invalid connection_id`), health (`last_unhealthy_code` set → 401 `The connection is
151
+ likely broken and requires recreation`; `is_paused` → 403 `Connection is paused; monthly plan limit exceeded`), category
152
+ (`crm`/`messaging` not in `categories` → 501 `The requested functionality is not supported by this integration`),
153
+ permission (`crm_contact_read`, `crm_contact_write`, …, `messaging_message_write` missing → 403 `The connection lacks the
154
+ required permissions or scopes: <permission>`), then validation (400) and existence (404 `Contact not found`, …).
155
+ - **Bounds that fail loudly instead of truncating**: each connection may hold at most `workspaces.limits.max_rows_per_namespace`
156
+ rows per namespace (10,000 in the starter data, never more; a scenario can lower it); a scan or write past the bound
157
+ fails `FAILED_PRECONDITION` (HTTP 500, `State exceeds the supported bound of N rows for <namespace>`). A list page whose
158
+ UTF-8 encoding would exceed 900,000 bytes fails `PAYLOAD_TOO_LARGE` (HTTP 413, `Response exceeds 1 MB; lower limit or
159
+ restrict fields`) — offset paging cannot express a shorter page. `raw` objects are copied key by key (≤ 8 levels, ≤ 64
160
+ keys per level, ≤ 16 KB; `constructor`/`prototype` keys refused). JSON bodies nested deeper than 512 levels, a query
161
+ parameter repeated more than 16 times, and non-JSON bodies on JSON routes are refused with 400 before validation.
162
+
163
+ Errors use the envelope `{ "statusCode": <status>, "message": "<text>", "error": "<reason phrase>" }` — Unified.to publishes
164
+ its status-code meanings but no error body schema, so this envelope is the Tool's documented choice; branch on the HTTP
165
+ status as you must against the real API. Statuses: 400 `BAD_REQUEST`, 401 `UNAUTHORIZED`, 403 `FORBIDDEN`, 404 `NOT_FOUND`,
166
+ 413 `PAYLOAD_TOO_LARGE`, 429 `RATE_LIMITED` (+ `Retry-After: 1`), 500 `INTERNAL_ERROR` / `FAILED_PRECONDITION`, 501
167
+ `NOT_IMPLEMENTED`. Framework outcomes are rendered in the same envelope: schema-invalid arguments 400, an actor without the
168
+ grant 403 `Forbidden`, an unknown route 404 (the framework's own body). Successful `DELETE`s answer 200 `{}`.
169
+
170
+ ## Events and faults
171
+
172
+ Events mirror the Unified.to webhook vocabulary (`created | updated | deleted`, object types `crm_contact`, `crm_company`,
173
+ `crm_deal`, `messaging_message`): `object.created` and `object.updated` (`changed_fields: ["*"]`) on every create — Unified.to
174
+ documents that "updated" fires for new records too — `object.updated` with the changed top-level field names on an
175
+ update that changed something (association maintenance on other rows emits one per rewritten row), `object.deleted` on
176
+ remove and per cascaded row of `connections.remove`. Pipelines and channels never emit (they are read-only here).
177
+
178
+ Faults (select them in a scenario with `faults: [{ packageId: "unified", faultId }]`):
179
+
180
+ | fault | timing | effect |
181
+ |---|---|---|
182
+ | `rate-limited` | before, every operation | `429` `Too many requests to Unified.to` with `Retry-After: 1`; nothing runs. |
183
+ | `write-unavailable` | before, the 15 write operations | `500 Internal Server Error`; nothing is written, reads keep working. |
184
+ | `write-committed-lost` | after commit, `messages.create` | The message **is** stored, the parent's `has_children` updated and both events emitted, yet the caller sees 500. Unified.to has no idempotency key on `POST`, so a blind retry posts a duplicate — list the channel and check before re-sending. |
185
+
186
+ ## Conformance
187
+
188
+ `firedrill tool test unified` runs sixteen drills twice over the HTTP and MCP bindings (a scripted target,
189
+ `test/conformance.mjs` + `test/flows/*.mjs`, Node built-ins only) across five actors (`admin`, `fresh` without attributes,
190
+ `scoped`, `ghost` with an unknown workspace, `auditor` without grants) and seven scenarios (`baseline`, one per fault,
191
+ `bounds` with the scan bound lowered to 5, `many-workspaces` with 102 workspaces, `large-channels` with two 350-member
192
+ channels): the CRM flow (every route, filter, projection, association and stage rule, every connection state), the
193
+ messaging flow, the connections flow, the MCP aliases, scoped and unknown identities, a framework-denied actor, a fresh
194
+ actor, an error-coverage sweep (404/501/403/400 on every data operation), size bounds (schema limits, prototype keys,
195
+ 5,000-character keys, byte-bounded pages over 16 KB `raw` payloads, 20,000-character texts and CJK messages, long and
196
+ repeated query parameters) and one drill per fault and bound. Together they observe every operation, every declared error
197
+ of every operation, all three events and all three faults, and the repeat pass proves determinism.
198
+
199
+ ## Protocol compatibility
200
+
201
+ The HTTP surface follows the Unified.to OpenAPI document (`api.unified.to/openapi.json`, version 1.0, fetched 2026-09-16)
202
+ field by field for the routes above — paths, methods, query parameters, snake_case bodies, bare-array pages, 200 on
203
+ `DELETE` — so the official `unified-typescript-sdk` / `unified-python-sdk` with their server URL pointed at the HTTP binding
204
+ are the intended clients. **No official client has been run against this Tool**: compatibility is by reference, not
205
+ verified, and the manifest `compatibility` array is empty. The five MCP aliases reproduce the Core-mode connection tool names
206
+ documented for the Unified MCP server and take the REST field names; they were not verified against the live server.
207
+ Unified.to's REST API accepts a bare token in `Authorization`; this Tool requires the `Bearer` scheme.
208
+
209
+ ## Limitations
210
+
211
+ - A bounded synthetic subset of one aggregator workspace: connections, CRM contact/company/deal/pipeline, messaging
212
+ channel/message. Absent (404): every other category (HRIS, ATS, accounting, ticketing, storage, …), CRM `lead`, `event`,
213
+ `picklist`/`taxonomy`, pipeline and channel create/update/remove, messaging `event`, `/passthrough`, webhooks,
214
+ integrations, API-call logs, issues, `mcp_url`, the OAuth authorisation flow, `X-Mock`, `env=Sandbox` mock data
215
+ (Sandbox is only a connection attribute), non-JSON `Accept` formats, EU/AU hosts, `hide_sensitive`, `include_external_tools`.
216
+ - Connections are created **already authorised** with the display identity given in `auth`; the real endpoint is one
217
+ step of an OAuth flow. `auth` never carries tokens or secrets, and identity comes from actor attributes, not from the
218
+ bearer token string.
219
+ - Every connection behaves identically according to its `categories` and `permissions`; integration-specific field
220
+ support, per-platform rate limits and platform quirks are not modelled. Pipelines and channels are read-only reference
221
+ data (consumers seed them); a pipeline holds at most 20 stages with 128-character names, so `pipelines.list` can never
222
+ exceed the page budget and does not declare `PAYLOAD_TOO_LARGE`.
223
+ - The error envelope, `Retry-After: 1`, 403 for a paused connection, 413 for an oversized page and 500 for the scan bound
224
+ are this Tool's choices (Unified.to documents none of them). `limit` above 100 is capped silently (Unified.to's documented
225
+ ceiling). `PUT` and `PATCH` both merge; a full-replace `PUT` is not distinguished.
226
+ - `raw` is stored and returned verbatim when requested via `fields`; the `raw` and `expand` query parameters are accepted
227
+ and ignored. `query` is a plain case-folded substring match; `sort=name` on messages sorts by `subject`.
228
+ - Association symmetry is enforced within one connection only; cross-connection references are rejected with 400.
229
+ - Events are Firedrill events, not webhook deliveries (no `sig256`, `nonce`, delivery types or `hook_url`).
230
+ - Malformed percent-encoding in a **path** segment is answered by the framework with 404 before the Tool runs; unknown
231
+ routes answer the framework's 404 body, not the envelope above.
232
+
233
+ ## Trademarks
234
+
235
+ Unified.to and the integration names used as labels (HubSpot, Pipedrive, Slack, Gmail, Salesforce, BambooHR, Zoho, Teams)
236
+ belong to their owners and appear only to identify the simulated service and integration types in a test environment.
237
+ This package is independently maintained and implies no affiliation with or endorsement by any of them.
238
+
239
+ ## Safety
240
+
241
+ Tools and conformance targets are trusted local executable code, not a sandbox. Review before running. Keep credentials and
242
+ generated worlds/reports out of the package and repository; the package file list excludes `.firedrill/`.
@@ -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": 120000
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "baseline",
4
+ "title": "Brightline Sales Ops workspace as seeded: 8 connections, CRM and messaging data, no faults"
5
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "bounds",
4
+ "title": "The workspace scan bound is lowered to 5 rows per namespace per connection, so crm-main contacts and chat messages exceed it",
5
+ "state": [
6
+ {
7
+ "action": "upsert",
8
+ "packageId": "unified",
9
+ "namespace": "workspaces",
10
+ "rowId": "68c8000000000000000000a1",
11
+ "value": {
12
+ "id": "68c8000000000000000000a1",
13
+ "name": "Brightline Sales Ops",
14
+ "region": "north-america",
15
+ "limits": {
16
+ "max_rows_per_namespace": 5
17
+ },
18
+ "created_at": "2026-06-01T09:00:00.000Z",
19
+ "updated_at": "2026-06-01T09:00:00.000Z"
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "conformance",
4
+ "title": "Unified.to Tool conformance: every operation, declared error, event and fault",
5
+ "drills": [
6
+ "unified-crm-flow",
7
+ "unified-messaging-flow",
8
+ "unified-connections-flow",
9
+ "unified-mcp-core-aliases",
10
+ "unified-scoped-token",
11
+ "unified-invalid-workspace",
12
+ "unified-denied",
13
+ "unified-fresh-actor",
14
+ "unified-error-coverage",
15
+ "unified-rate-limited",
16
+ "unified-write-unavailable",
17
+ "unified-write-committed-lost",
18
+ "unified-bounds",
19
+ "unified-many-workspaces",
20
+ "unified-large-channels",
21
+ "unified-size-bounds"
22
+ ]
23
+ }