@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.
- package/LICENSE +201 -0
- package/README.md +242 -0
- package/firedrill/agent.target.json +17 -0
- package/firedrill/baseline.scenario.json +5 -0
- package/firedrill/bounds.scenario.json +23 -0
- package/firedrill/conformance.suite.json +23 -0
- package/firedrill/large-channels.scenario.json +4251 -0
- package/firedrill/many-workspaces.scenario.json +1623 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/tools/unified/behavior.mjs +69 -0
- package/firedrill/tools/unified/lib/assoc.mjs +61 -0
- package/firedrill/tools/unified/lib/enums.mjs +14 -0
- package/firedrill/tools/unified/lib/errors.mjs +50 -0
- package/firedrill/tools/unified/lib/events.mjs +27 -0
- package/firedrill/tools/unified/lib/identity.mjs +59 -0
- package/firedrill/tools/unified/lib/query.mjs +154 -0
- package/firedrill/tools/unified/lib/shapes.mjs +176 -0
- package/firedrill/tools/unified/lib/store.mjs +75 -0
- package/firedrill/tools/unified/lib/time.mjs +59 -0
- package/firedrill/tools/unified/lib/util.mjs +92 -0
- package/firedrill/tools/unified/lib/validate.mjs +114 -0
- package/firedrill/tools/unified/lib/wire.mjs +120 -0
- package/firedrill/tools/unified/ops/channels.mjs +45 -0
- package/firedrill/tools/unified/ops/connections.mjs +151 -0
- package/firedrill/tools/unified/ops/crm.mjs +126 -0
- package/firedrill/tools/unified/ops/deals.mjs +123 -0
- package/firedrill/tools/unified/ops/messages.mjs +133 -0
- package/firedrill/tools/unified/ops/pipelines.mjs +20 -0
- package/firedrill/tools/unified/ops/resource.mjs +114 -0
- package/firedrill/tools/unified/unified.tool.json +14068 -0
- package/firedrill/unified-bounds.drill.json +148 -0
- package/firedrill/unified-connections-flow.drill.json +226 -0
- package/firedrill/unified-crm-flow.drill.json +576 -0
- package/firedrill/unified-denied.drill.json +83 -0
- package/firedrill/unified-error-coverage.drill.json +528 -0
- package/firedrill/unified-fresh-actor.drill.json +83 -0
- package/firedrill/unified-invalid-workspace.drill.json +933 -0
- package/firedrill/unified-large-channels.drill.json +68 -0
- package/firedrill/unified-many-workspaces.drill.json +943 -0
- package/firedrill/unified-mcp-core-aliases.drill.json +113 -0
- package/firedrill/unified-messaging-flow.drill.json +326 -0
- package/firedrill/unified-rate-limited.drill.json +933 -0
- package/firedrill/unified-scoped-token.drill.json +113 -0
- package/firedrill/unified-size-bounds.drill.json +263 -0
- package/firedrill/unified-write-committed-lost.drill.json +107 -0
- package/firedrill/unified-write-unavailable.drill.json +528 -0
- package/firedrill/world.json +2958 -0
- package/firedrill/write-committed-lost.scenario.json +11 -0
- package/firedrill/write-unavailable.scenario.json +11 -0
- package/firedrill.json +5 -0
- package/package.json +52 -0
- package/starter.json +2446 -0
- package/test/conformance.mjs +39 -0
- package/test/flows/connections.mjs +87 -0
- package/test/flows/coverage.mjs +64 -0
- package/test/flows/crm.mjs +124 -0
- package/test/flows/faults.mjs +82 -0
- package/test/flows/identity.mjs +84 -0
- package/test/flows/messaging.mjs +82 -0
- package/test/flows/size.mjs +71 -0
- 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,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
|
+
}
|