@axonity-ai/mcp 0.6.0 → 0.7.0
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/README.md +176 -14
- package/dist/client.d.ts +15 -2
- package/dist/client.js +8 -1
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +88 -0
- package/dist/contract.js +227 -0
- package/dist/contract.js.map +1 -0
- package/dist/errors.d.ts +15 -3
- package/dist/errors.js +27 -3
- package/dist/errors.js.map +1 -1
- package/dist/generated/listFilters.d.ts +19 -0
- package/dist/generated/listFilters.js +78 -0
- package/dist/generated/listFilters.js.map +1 -0
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/authoringSpec.d.ts +76 -0
- package/dist/tools/authoringSpec.js +169 -0
- package/dist/tools/authoringSpec.js.map +1 -0
- package/dist/tools/company.d.ts +5 -2
- package/dist/tools/company.js +56 -2
- package/dist/tools/company.js.map +1 -1
- package/dist/tools/conventions.d.ts +16 -0
- package/dist/tools/conventions.js +191 -19
- package/dist/tools/conventions.js.map +1 -1
- package/dist/tools/credentials.d.ts +20 -0
- package/dist/tools/credentials.js +41 -0
- package/dist/tools/credentials.js.map +1 -1
- package/dist/tools/extras.d.ts +17 -0
- package/dist/tools/extras.js +139 -9
- package/dist/tools/extras.js.map +1 -1
- package/dist/tools/register.d.ts +27 -0
- package/dist/tools/register.js +19 -1
- package/dist/tools/register.js.map +1 -1
- package/dist/tools/runs.d.ts +11 -0
- package/dist/tools/runs.js +169 -26
- package/dist/tools/runs.js.map +1 -1
- package/dist/tools/secrets.d.ts +33 -0
- package/dist/tools/secrets.js +69 -0
- package/dist/tools/secrets.js.map +1 -0
- package/dist/tools/subworkflows.d.ts +20 -0
- package/dist/tools/subworkflows.js +42 -0
- package/dist/tools/subworkflows.js.map +1 -0
- package/dist/tools/validation.js +74 -4
- package/dist/tools/validation.js.map +1 -1
- package/dist/tools/versions.d.ts +9 -0
- package/dist/tools/versions.js +36 -0
- package/dist/tools/versions.js.map +1 -1
- package/dist/tools/workflowMutations.js +44 -12
- package/dist/tools/workflowMutations.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -40,10 +40,16 @@ tenant + scope on every call, so the connector is not a trust boundary.
|
|
|
40
40
|
|
|
41
41
|
## Tools
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
220+ tools total. `axonity_conventions` (read this first) covers the authoring
|
|
44
44
|
rules — drafts vs live, optimistic locking, per-entity fields, delete/restore,
|
|
45
45
|
and how to tell a retryable error from one that will never succeed.
|
|
46
46
|
|
|
47
|
+
What the connector does **not** state is as deliberate as what it does: the
|
|
48
|
+
mutation commands, the step types, the trigger types, the schedule-rule shapes
|
|
49
|
+
and the three value vocabularies are all read live from
|
|
50
|
+
`get_workflow_authoring_spec`, because every one of those lists drifted while it
|
|
51
|
+
was kept here. A conformance test asserts their absence.
|
|
52
|
+
|
|
47
53
|
### The generic entity family
|
|
48
54
|
|
|
49
55
|
Ten entities — **workflow, agent, tool, skill, policy, reference_doc, persona,
|
|
@@ -65,12 +71,45 @@ entity gets every verb (see the per-entity notes below for the exceptions):
|
|
|
65
71
|
Exceptions: `persona` has no `create_persona` (create only via
|
|
66
72
|
`create_agent_persona`).
|
|
67
73
|
|
|
74
|
+
Some list routes narrow in the query, which is where narrowing belongs — the
|
|
75
|
+
backend applies it before the rows come back:
|
|
76
|
+
`list_workflows({ stageId?, capabilityId? })`,
|
|
77
|
+
`list_agents({ includeSystem? })`,
|
|
78
|
+
`list_policies({ scope?, ownerId? })`,
|
|
79
|
+
`list_reference_docs({ scope?, ownerId? })`,
|
|
80
|
+
`list_prompt_snippets({ deleted? })`.
|
|
81
|
+
|
|
82
|
+
That table is **generated** from the pinned schema's own query parameters
|
|
83
|
+
(`npm run generate:filters` → `src/generated/listFilters.ts`), not hand-listed.
|
|
84
|
+
A filter the backend adds is exposed as soon as the snapshot is refreshed, and
|
|
85
|
+
a test fails if the two have parted. The argument names are camelCase; the wire
|
|
86
|
+
keeps whatever spelling each route declares, which is not consistent between
|
|
87
|
+
them and is not something a caller should have to know.
|
|
88
|
+
|
|
68
89
|
Plus:
|
|
90
|
+
- `get_workflow_authoring_spec` — everything **this deploy** can be built from,
|
|
91
|
+
read live from the server (`GET /workflows/operations`): the mutation
|
|
92
|
+
`operations`, the `triggerTypes`, the `stepTypes` (including the ones you may
|
|
93
|
+
not author, each with the reason and what to write instead), the
|
|
94
|
+
`scheduleRuleKinds` (each with an example the backend round-trips through its
|
|
95
|
+
own parser as it serves it), and **three vocabularies for a value's type that
|
|
96
|
+
are not interchangeable** — `parameterTypes` (a trigger parameter's or
|
|
97
|
+
workflow constant's `type`), `outputKinds` (a step output's or input's
|
|
98
|
+
`kind` — narrower, *different key*) and `schemaFieldKinds` (inside a field's
|
|
99
|
+
`schema`, which wins where present). Getting that last group wrong is silent:
|
|
100
|
+
a `kind` written on a trigger parameter is not a 422, it is a key nothing
|
|
101
|
+
reads, so the value falls back to text. Every list is generated from the
|
|
102
|
+
registry that *enforces* it, so the connector states none of them and a new
|
|
103
|
+
value is discoverable without a release here. `operations` is an index by
|
|
104
|
+
default; pass `types` for a command's live payload schema. `rulesVersion` is
|
|
105
|
+
a content hash over all seven — same hash, nothing to re-fetch.
|
|
69
106
|
- `apply_workflow_mutations` for structural workflow edits (add steps, connect
|
|
70
107
|
edges) via mutation commands, sequenced and version-threaded for you.
|
|
71
108
|
- `replace_workflow_document` for one-shot full-document replacement in a single
|
|
72
109
|
atomic PUT.
|
|
73
|
-
- `read_workflow_trigger_parameters` —
|
|
110
|
+
- `read_workflow_trigger_parameters` — how to start the workflow:
|
|
111
|
+
`{ triggers, constants }`, every start with its own parameters and a `pinned`
|
|
112
|
+
flag marking the values the author owns.
|
|
74
113
|
- `bulk_delete_workflows` — soft-delete several at once (each with its own
|
|
75
114
|
`expectedVersion`).
|
|
76
115
|
|
|
@@ -87,7 +126,9 @@ For the ten versioned entities (including `flow`):
|
|
|
87
126
|
| `list_deleted_<entity>_versions` | Restore candidates for the row above. |
|
|
88
127
|
| `restore_deleted_<entity>_version` | Undo the delete above. No version check. |
|
|
89
128
|
| `read_<entity>_published` | The live snapshot, as opposed to the draft. |
|
|
90
|
-
| `
|
|
129
|
+
| `create_<entity>_major_version` | Cut a new **named** major version — "Save As" on the current draft. |
|
|
130
|
+
| `ensure_<entity>_major_version` | Make sure a working draft major version exists. Idempotent. |
|
|
131
|
+
| `name_<entity>_major_version` | Rename an existing major version (label a release). |
|
|
91
132
|
|
|
92
133
|
`{version}` (an int) and `{versionId}` (a UUID) are two different identifiers
|
|
93
134
|
across these routes — the tool parameter names say which.
|
|
@@ -107,7 +148,16 @@ across these routes — the tool parameter names say which.
|
|
|
107
148
|
`attach_reference_to_agent`, and a `detach_*_from_*` for each. Detaching
|
|
108
149
|
removes the link only — the skill or policy itself is untouched. Read the
|
|
109
150
|
links back with `list_agent_skills`, `list_agent_policies`,
|
|
110
|
-
`list_agent_reference_docs`.
|
|
151
|
+
`list_agent_reference_docs` and `list_workflow_skills`. The three agent
|
|
152
|
+
read-backs take an optional `workflowId` for the **composed runtime view** —
|
|
153
|
+
what the agent's prompt actually assembles inside that workflow, with a
|
|
154
|
+
`linkSource` per row saying why each item is there.
|
|
155
|
+
- **What uses this?**: `list_workflows_using({ entityKind, entityId })` names
|
|
156
|
+
the workflows that reference a tool, agent, flow, output schema or workflow,
|
|
157
|
+
**and the steps they reference it in**, with `draft`/`published` per hit.
|
|
158
|
+
`list_dependent_agents({ entityKind, entityId })` is the same question for a
|
|
159
|
+
skill, policy or reference doc. Ask before editing anything shared — the
|
|
160
|
+
alternative is validating every workflow in the tenant.
|
|
111
161
|
- **Prompt elements (placement)**: a `prompt_snippet` is a library item; it only
|
|
112
162
|
takes effect once placed into a flow step's prompt stack.
|
|
113
163
|
`read_workflow_prompt_stacks` / `read_flow_prompt_stacks` resolve a
|
|
@@ -119,13 +169,33 @@ across these routes — the tool parameter names say which.
|
|
|
119
169
|
- **Company** (the tenant's single company document — a singleton, no id):
|
|
120
170
|
`read_company`, `update_company` (whole-document save with `expectedVersion`),
|
|
121
171
|
`list_company_versions`, `read_company_version`, `restore_company_version`,
|
|
122
|
-
`name_company_major_version`, `
|
|
172
|
+
`name_company_major_version`, `create_company_major_version`,
|
|
173
|
+
`ensure_company_major_version`, `read_company_published`,
|
|
174
|
+
`apply_company_mutation` (one validated, version-safe command — preferred
|
|
175
|
+
over the whole-document `update_company`, the same way
|
|
176
|
+
`apply_workflow_mutations` is preferred for a workflow), and
|
|
123
177
|
`request_publish_company` (takes no id — the
|
|
124
178
|
server resolves your tenant's one company; direct company publish is closed to
|
|
125
179
|
service tokens).
|
|
180
|
+
- **Subworkflows**: `list_callable_workflows` — which workflows a `subprocess`
|
|
181
|
+
step may call, each with its `parameters` and `outcomes`, and a
|
|
182
|
+
`blockedReason` for the ones that cannot (never published, or no
|
|
183
|
+
`subprocess-invocation` trigger). Authoring both halves is ordinary
|
|
184
|
+
`apply_workflow_mutations` work — a callable workflow's signature goes into
|
|
185
|
+
the `add_trigger` call itself. `validate_workflow` **does** check a subprocess
|
|
186
|
+
target now (missing, self-call, deleted, unpublished, not callable), but
|
|
187
|
+
`list_callable_workflows` is still what you run first: it is how you pick a
|
|
188
|
+
target and read the interface you are binding to.
|
|
189
|
+
- **Secrets** (read-only): `list_secrets`, `read_secret` — the catalogue a
|
|
190
|
+
connector's `authConfig.secretId` points at. Values are never returned by any
|
|
191
|
+
Axonity route; `valueKeys` says which keys a human has filled in, so you can
|
|
192
|
+
tell an unfinished secret from a finished one before wiring to it. Creating or
|
|
193
|
+
changing a secret is a human act in Axonity (#39).
|
|
126
194
|
- **Catalog & cloning**: `list_system_tools` (read-only catalog — enabling one
|
|
127
195
|
for an agent is `update_agent` with the id added to `systemToolIds`),
|
|
128
|
-
`clone_flow`, `clone_prompt_snippet
|
|
196
|
+
`clone_flow`, `clone_prompt_snippet`, `list_tool_packages` (the import
|
|
197
|
+
allowlist `validate_tool_code` judges against), `list_templates` /
|
|
198
|
+
`read_template`.
|
|
129
199
|
- `list_deleted_prompt_snippets` calls `/api/v1/prompt-snippets/deleted`; the
|
|
130
200
|
backend returns it as `{ items: [... ] }`, and the tool forwards that response
|
|
131
201
|
unchanged.
|
|
@@ -157,14 +227,41 @@ workflow and really executes), `cancel_run`, `delete_run`, `list_runs`,
|
|
|
157
227
|
`bulk_delete_runs`. There is no findings endpoint — evaluation means reading a
|
|
158
228
|
run's validator verdicts and its trace.
|
|
159
229
|
|
|
160
|
-
|
|
230
|
+
**Which start, and what it wants.** `read_workflow_trigger_parameters` answers
|
|
231
|
+
`{ triggers, constants }` — every way the workflow can be started, each with its
|
|
232
|
+
own parameters. Pass the one you mean to `start_workflow_run` as `triggerId`;
|
|
233
|
+
omitting it fires the first, which on a workflow with a button *and* a schedule
|
|
234
|
+
is an arbitrary choice. A parameter marked `pinned` is one the **author** owns:
|
|
235
|
+
it is overwritten on every run, so a caller must not send it.
|
|
236
|
+
|
|
237
|
+
**`read_run` omits the workflow snapshot by default.** It is immutable, it is
|
|
238
|
+
never the answer to a question about the run, and it measured 81% of one real
|
|
239
|
+
response — an oversized response turns a call that succeeded into an error.
|
|
240
|
+
Pass `includeSnapshot: true` when you actually want to see what executed.
|
|
241
|
+
|
|
242
|
+
**A run can park rather than finish.** `read_run_waiting_on` says what it is
|
|
243
|
+
waiting for; `answer_run_question` answers an `ask_user` step and
|
|
244
|
+
`send_run_message` sends a turn to a conversation run. Both record the input as
|
|
245
|
+
a person's, so use them on runs you started. Deciding a **plan approval** and
|
|
246
|
+
restarting a stuck run are deliberately absent — the first is the human review
|
|
247
|
+
the step exists to get, the second is `require_admin` and a service token is
|
|
248
|
+
always `role="member"`. Both are recorded in `test/exclusions.test.ts`.
|
|
249
|
+
|
|
250
|
+
**Inside a launch**: `read_run_items_summary` is the roll-up ("4,415 processed ·
|
|
251
|
+
12 failed"), `list_run_items({ outcome })` the paged rows — filter in the query,
|
|
252
|
+
because the failures are scattered and sifting page one finds none of them.
|
|
253
|
+
`list_run_tasks` and `read_run_for_each_progress` cover the children a run set
|
|
254
|
+
in motion.
|
|
255
|
+
|
|
256
|
+
`list_workflow_runs({ workflowId, status?, archivedOnly?, limit?, cursor? })` returns one
|
|
161
257
|
**page** — `{ items, nextCursor, pageSize, hasMore }`, 20 by default and 200 at
|
|
162
258
|
most — so follow `nextCursor` while `hasMore` is true rather than treating the
|
|
163
259
|
first page as the answer. It also lists **launches**, not runs: the per-item runs
|
|
164
260
|
a FOR EACH creates stay inside their launch, so a launch over 4,415 people is one
|
|
165
|
-
entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is
|
|
166
|
-
|
|
167
|
-
`
|
|
261
|
+
entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is now
|
|
262
|
+
paged the same way — `{ items, nextCursor, pageSize, hasMore }`, walked with
|
|
263
|
+
`cursor`. It stopped answering with a bare array when the backend converted the
|
|
264
|
+
route; the `offset` it used to take is no longer read.
|
|
168
265
|
|
|
169
266
|
### Approvals
|
|
170
267
|
|
|
@@ -173,6 +270,23 @@ entry carrying `forEachProgress`. `list_runs` is the tenant-wide list and is
|
|
|
173
270
|
`request_publish_*` was approved or rejected. Approving and rejecting are
|
|
174
271
|
human-only actions in Axonity.
|
|
175
272
|
|
|
273
|
+
`request_publish_release({ workflowId, changeSummary? })` proposes a **release**:
|
|
274
|
+
a workflow *and everything its run needs* — the agents it runs, their tools and
|
|
275
|
+
personas, the flows it pins, the memory scoped to those agents — as ONE approval.
|
|
276
|
+
Prefer it over a request per entity. Taking a tenant live entity-by-entity means
|
|
277
|
+
a hundred-odd approvals, none of which means anything on its own, and a human
|
|
278
|
+
asked that many times is not reviewing. `list_publish_releases` and
|
|
279
|
+
`get_publish_release` read one back — the release's members, and its readiness
|
|
280
|
+
recomputed as of now.
|
|
281
|
+
|
|
282
|
+
Unlike `request_publish_bulk`, a release is **all-or-nothing and in dependency
|
|
283
|
+
order**: approving it publishes every member or none, so a workflow can never go
|
|
284
|
+
live calling a tool that did not. The response carries the bundle's verdict —
|
|
285
|
+
`ready`, `changedCount` of `totalCount` (unchanged members are already live and
|
|
286
|
+
ride along), `members` with why each is there, and `blockers` that name the
|
|
287
|
+
member in the way. Requesting is ours; deciding stays human, like everywhere
|
|
288
|
+
else here.
|
|
289
|
+
|
|
176
290
|
## What this is for — and what it is not
|
|
177
291
|
|
|
178
292
|
**Authoring.** An agent composing an entity from intent: drafting a workflow,
|
|
@@ -202,15 +316,22 @@ These are enforced by the backend, not merely by convention:
|
|
|
202
316
|
- **Secrets never pass through the agent.** A connector's `authConfig` accepts
|
|
203
317
|
placeholders only; a write carrying something that looks like a real
|
|
204
318
|
credential is rejected before it leaves the connector. Tenant secrets
|
|
205
|
-
(`/api/v1/secrets`)
|
|
206
|
-
|
|
319
|
+
(`/api/v1/secrets`) are **readable and unwritable**: `list_secrets` /
|
|
320
|
+
`read_secret` give the catalogue and `valueKeys` (which keys are filled, never
|
|
321
|
+
their values) so an agent can point `authConfig.secretId` at the right entry,
|
|
322
|
+
and no tool can create, change or delete one — the backend refuses a service
|
|
323
|
+
token there too. A secret's `metadata` is stored unencrypted and readable
|
|
324
|
+
tenant-wide (axonity-flow#908), so credential-shaped entries in it are
|
|
325
|
+
withheld on read and listed under `metadataRedacted`.
|
|
207
326
|
- **Errors carry a machine-readable `code`, not just prose.** A 409 can mean a
|
|
208
327
|
stale write (retry) or a live reference conflict (don't — see
|
|
209
328
|
`axonity_conventions`); the connector tells them apart by `code`, never by
|
|
210
329
|
matching the message text.
|
|
211
330
|
- **No tool crosses the authority boundary.** A test drives the whole registered
|
|
212
|
-
surface and fails the build if any tool targets a publish / approve /
|
|
213
|
-
service-token / deploy route (`test/exclusions.test.ts`).
|
|
331
|
+
surface and fails the build if any tool targets a publish / approve /
|
|
332
|
+
secret-write / service-token / deploy route (`test/exclusions.test.ts`). The
|
|
333
|
+
rules are method-aware: `GET /api/v1/secrets` is allowed, every write verb on
|
|
334
|
+
it is not.
|
|
214
335
|
- **Guidance can't silently drift from the backend.** The field/enum facts the
|
|
215
336
|
connector states are pinned to a vendored snapshot of the backend OpenAPI
|
|
216
337
|
schema; `test/conformance.test.ts` fails if a route or documented enum
|
|
@@ -236,6 +357,18 @@ Publishing runs from a maintainer's machine, not from CI. npm restricts tokens
|
|
|
236
357
|
that bypass 2FA for direct publishing, so a stored `NPM_TOKEN` cannot ship this
|
|
237
358
|
package; `npm login` is the supported path.
|
|
238
359
|
|
|
360
|
+
**Check the contract first.** Shipping with a stale snapshot is how the
|
|
361
|
+
connector fell nine operations behind the backend without any test noticing:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npm run check:contract # expects ../axonity-flow; override with AXONITY_FLOW_PATH
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
It dumps the schema from a local `axonity-flow` checkout and diffs it against
|
|
368
|
+
`test/fixtures/openapi.snapshot.json`, naming every operation that moved. It
|
|
369
|
+
runs here rather than in the release workflow because the publish itself runs
|
|
370
|
+
here — a gate in CI cannot stop a local `npm publish`.
|
|
371
|
+
|
|
239
372
|
Run `npm login` in a real terminal — it prints a URL and waits for you to finish
|
|
240
373
|
in the browser, so it needs a session that stays attached (an editor's 2-minute
|
|
241
374
|
command timeout will kill it mid-flow).
|
|
@@ -257,3 +390,32 @@ was cut from a state CI cannot install.
|
|
|
257
390
|
|
|
258
391
|
Keep npm 11 locally: Node 20 bundles npm 10, whose resolver writes an
|
|
259
392
|
incompatible lockfile tree. CI pins npm 11 for the same reason.
|
|
393
|
+
|
|
394
|
+
## Staying level with the backend
|
|
395
|
+
|
|
396
|
+
The snapshot every drift guard reads is only as fresh as the last time someone
|
|
397
|
+
regenerated it — and once it wasn't: it sat nine operations behind the backend
|
|
398
|
+
while the whole suite stayed green, because a snapshot that has not seen a route
|
|
399
|
+
cannot report it missing.
|
|
400
|
+
|
|
401
|
+
**Before a release, check it** (see §Releasing): `npm run check:contract` dumps
|
|
402
|
+
the schema from a local `axonity-flow` checkout and names every operation that
|
|
403
|
+
moved. It compares the contract surface — query parameters, request body,
|
|
404
|
+
response shape — and not descriptions, so backend docstring churn does not cry
|
|
405
|
+
wolf. This is the gate that counts, because the publish runs here too.
|
|
406
|
+
|
|
407
|
+
**At startup, the connector asks the deploy what it mounts.** It calls
|
|
408
|
+
`GET /api/v1/contract` once before accepting its first tool call and, if this
|
|
409
|
+
backend lacks a route this build needs, says which on stderr — instead of
|
|
410
|
+
failing on the twentieth call, mid-task. It is a diagnostic and never a
|
|
411
|
+
dependency: a backend too old to serve `/contract`, an unreachable one, or a
|
|
412
|
+
slow one all degrade to the previous behaviour and the connector starts
|
|
413
|
+
normally. The result is cached on the `contractHash` the route returns, so an
|
|
414
|
+
unchanged deploy costs one request.
|
|
415
|
+
|
|
416
|
+
**Not here: a scheduled job.** Watching for drift on a timer belongs in
|
|
417
|
+
`axonity-flow`, not in this repository. The schema lives there, its CI already
|
|
418
|
+
has the backend's dependencies installed, and it can read this repository's
|
|
419
|
+
pinned snapshot over plain HTTPS because this repository is public — where the
|
|
420
|
+
reverse needs a credential for a private repo, with an approval policy and an
|
|
421
|
+
expiry behind it. Tracked in axonity-mcp#48.
|
package/dist/client.d.ts
CHANGED
|
@@ -8,8 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { AxonityConfig } from "./config.js";
|
|
10
10
|
export type Json = Record<string, unknown>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/** One scalar a query parameter may carry. */
|
|
12
|
+
type QueryScalar = string | number | boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Query-string values a route may take. `undefined` entries are omitted.
|
|
15
|
+
*
|
|
16
|
+
* An ARRAY is serialised the way FastAPI reads a `list[…]` query parameter:
|
|
17
|
+
* the key REPEATED, once per element (`?status=pending&status=running`). Not
|
|
18
|
+
* every list-shaped filter on the backend is spelled that way — `GET /runs`
|
|
19
|
+
* takes its statuses as one comma-separated string while
|
|
20
|
+
* `GET /workflows/{id}/runs` repeats the key — so the caller picks the shape
|
|
21
|
+
* its route actually declares. An empty array contributes nothing, which is
|
|
22
|
+
* what "no filter" means.
|
|
23
|
+
*/
|
|
24
|
+
export type Query = Record<string, QueryScalar | QueryScalar[] | undefined>;
|
|
13
25
|
export declare class AxonityClient {
|
|
14
26
|
private readonly config;
|
|
15
27
|
constructor(config: AxonityConfig);
|
|
@@ -31,3 +43,4 @@ export declare class AxonityClient {
|
|
|
31
43
|
*/
|
|
32
44
|
del<T = unknown>(path: string, query?: Query): Promise<T>;
|
|
33
45
|
}
|
|
46
|
+
export {};
|
package/dist/client.js
CHANGED
|
@@ -18,8 +18,15 @@ export class AxonityClient {
|
|
|
18
18
|
return base;
|
|
19
19
|
const params = new URLSearchParams();
|
|
20
20
|
for (const [key, value] of Object.entries(query)) {
|
|
21
|
-
if (value
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const item of value)
|
|
25
|
+
params.append(key, String(item));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
22
28
|
params.set(key, String(value));
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
const qs = params.toString();
|
|
25
32
|
return qs ? `${base}?${qs}` : base;
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoB5E,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE9C,GAAG,CAAC,IAAY,EAAE,KAAa;QACrC,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;QAChF,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS;YAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,MAAM,IAAI,IAAI,KAAK;oBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7D,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CACX,MAAc,EACd,IAAY,EACZ,IAAc,EACd,KAAa;QAEb,IAAI,IAAc,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE;gBACxC,MAAM;gBACN,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE;oBAC5C,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACtE;gBACD,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;aAC5D,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,6EAA6E;YAC7E,MAAM,IAAI,eAAe,CACvB,sCAAsC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI;gBAC1D,yCAAyC,EAC3C,CAAC,EACD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAEjD,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,oEAAoE;YACpE,0EAA0E;YAC1E,2EAA2E;YAC3E,mEAAmE;YACnE,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC3E,IAAI,IAAI;gBAAE,MAAM,IAAI,CAAC;YAErB,yEAAyE;YACzE,iEAAiE;YACjE,iEAAiE;YACjE,MAAM,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,MAAW,CAAC;IACrB,CAAC;IAED,GAAG,CAAc,IAAY,EAAE,KAAa;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,CAAc,IAAY,EAAE,IAAc;QAC5C,OAAO,IAAI,CAAC,OAAO,CAAI,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,GAAG,CAAc,IAAY,EAAE,IAAc;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAc,IAAY,EAAE,IAAc;QAC7C,OAAO,IAAI,CAAC,OAAO,CAAI,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAc,IAAY,EAAE,KAAa;QAC1C,OAAO,IAAI,CAAC,OAAO,CAAI,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the deploy what it can do, once, at startup (axonity-mcp#48 M2).
|
|
3
|
+
*
|
|
4
|
+
* `src/errors.ts` already turns a 404/405 into a "backend version skew" message
|
|
5
|
+
* — but only AFTER the call has failed, which means a user is mid-task on the
|
|
6
|
+
* one call that mattered. And it decides which 404s count from `ROUTES_ADDED_IN`,
|
|
7
|
+
* a list this repository keeps by hand about routes the server owns. That is the
|
|
8
|
+
* shape of #32 and #44, and it has the same failure mode: a route added here
|
|
9
|
+
* without an entry there reports as "no such id" forever.
|
|
10
|
+
*
|
|
11
|
+
* `GET /api/v1/contract` (axonity-flow#806) is the proactive replacement. It
|
|
12
|
+
* returns every mounted route, derived from the running app so it cannot drift,
|
|
13
|
+
* plus a hash to cache on. It is authenticated but TENANT-AGNOSTIC — a
|
|
14
|
+
* read-only service token is enough, so this check never needs write scope.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS DOES NOT DO: fail. A connector that refuses to start because it
|
|
17
|
+
* could not reach a diagnostic endpoint is worse than the drift it reports —
|
|
18
|
+
* every path here degrades to today's behaviour, including a backend too old to
|
|
19
|
+
* serve `/contract` at all. Everything it says goes to STDERR, because stdout is
|
|
20
|
+
* the MCP channel and a stray byte there breaks the protocol.
|
|
21
|
+
*/
|
|
22
|
+
import type { AxonityClient } from "./client.js";
|
|
23
|
+
/** `GET /api/v1/contract`. `routes` are "METHOD /path/{template}" strings. */
|
|
24
|
+
export interface ContractResponse {
|
|
25
|
+
buildVersion?: unknown;
|
|
26
|
+
environment?: unknown;
|
|
27
|
+
routes?: unknown;
|
|
28
|
+
contractHash?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/** The minimal `.tool()` surface the registrar needs — same shape as index.ts. */
|
|
31
|
+
export interface ProbeServer {
|
|
32
|
+
tool: (name: string, description: string, schema: Record<string, unknown>, handler: (args: never) => Promise<unknown>) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One filled-in value per argument name any tool takes, so a blind replay
|
|
36
|
+
* reaches as much of the route surface as possible.
|
|
37
|
+
*
|
|
38
|
+
* This is how the connector knows which routes it needs WITHOUT keeping a list
|
|
39
|
+
* of them: it runs its own tools against a recording client and watches. A
|
|
40
|
+
* hand-written route list would be the very thing #48 M2 exists to delete.
|
|
41
|
+
*
|
|
42
|
+
* Shared with `conformance.test.ts` and `exclusions.test.ts` deliberately. They
|
|
43
|
+
* each carried their own copy, the copies had to be edited in lockstep, and a
|
|
44
|
+
* probe fixture that has drifted silently narrows what the sweep reaches.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PROBE_ARGS: Record<string, unknown>;
|
|
47
|
+
/** A route a tool actually called, as observed by the recording client. */
|
|
48
|
+
export interface CalledRoute {
|
|
49
|
+
method: string;
|
|
50
|
+
path: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every route this build's tools call, found by replaying them.
|
|
54
|
+
*
|
|
55
|
+
* Nothing leaves the process: the client is a recorder. A handler that rejects
|
|
56
|
+
* the probe arguments is simply skipped — it contributes no route, which makes
|
|
57
|
+
* this a LOWER bound on what the connector needs and therefore safe: the check
|
|
58
|
+
* can miss a route, but it cannot invent one.
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectCalledRoutes(register: (server: ProbeServer, client: AxonityClient) => void): Promise<CalledRoute[]>;
|
|
61
|
+
/** Does a concrete path match a "METHOD /a/{b}/c" contract entry? */
|
|
62
|
+
export declare function matchesRoute(call: CalledRoute, entry: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Routes this build needs that the deploy does not mount, as "METHOD /path".
|
|
65
|
+
*
|
|
66
|
+
* Deduplicated by template rather than by concrete path: `read_workflow` and
|
|
67
|
+
* `update_workflow` both hit `/workflows/{id}`, and reporting one absence twice
|
|
68
|
+
* makes a short list look like an emergency.
|
|
69
|
+
*/
|
|
70
|
+
export declare function missingFromContract(calls: CalledRoute[], routes: string[]): string[];
|
|
71
|
+
export interface ContractCheckOptions {
|
|
72
|
+
/** Base URL, used to key the cache so two tenants do not share a verdict. */
|
|
73
|
+
apiUrl: string;
|
|
74
|
+
/** Where to write the report. Defaults to stderr — stdout is the MCP channel. */
|
|
75
|
+
log?: (message: string) => void;
|
|
76
|
+
/** How long to wait before giving up on the check. Defaults to 5s. */
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
/** Skip the cache. Used by tests, and by a caller that wants a fresh verdict. */
|
|
79
|
+
ignoreCache?: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Ask the deploy what it mounts, and say — once, at startup — whether this
|
|
83
|
+
* build needs anything it does not have.
|
|
84
|
+
*
|
|
85
|
+
* Returns what it concluded, so a caller (and a test) can tell "checked, all
|
|
86
|
+
* present" from "could not check". Never throws.
|
|
87
|
+
*/
|
|
88
|
+
export declare function reportContractSkew(client: AxonityClient, register: (server: ProbeServer, client: AxonityClient) => void, options: ContractCheckOptions): Promise<"ok" | "missing" | "unavailable" | "cached" | "timeout">;
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the deploy what it can do, once, at startup (axonity-mcp#48 M2).
|
|
3
|
+
*
|
|
4
|
+
* `src/errors.ts` already turns a 404/405 into a "backend version skew" message
|
|
5
|
+
* — but only AFTER the call has failed, which means a user is mid-task on the
|
|
6
|
+
* one call that mattered. And it decides which 404s count from `ROUTES_ADDED_IN`,
|
|
7
|
+
* a list this repository keeps by hand about routes the server owns. That is the
|
|
8
|
+
* shape of #32 and #44, and it has the same failure mode: a route added here
|
|
9
|
+
* without an entry there reports as "no such id" forever.
|
|
10
|
+
*
|
|
11
|
+
* `GET /api/v1/contract` (axonity-flow#806) is the proactive replacement. It
|
|
12
|
+
* returns every mounted route, derived from the running app so it cannot drift,
|
|
13
|
+
* plus a hash to cache on. It is authenticated but TENANT-AGNOSTIC — a
|
|
14
|
+
* read-only service token is enough, so this check never needs write scope.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS DOES NOT DO: fail. A connector that refuses to start because it
|
|
17
|
+
* could not reach a diagnostic endpoint is worse than the drift it reports —
|
|
18
|
+
* every path here degrades to today's behaviour, including a backend too old to
|
|
19
|
+
* serve `/contract` at all. Everything it says goes to STDERR, because stdout is
|
|
20
|
+
* the MCP channel and a stray byte there breaks the protocol.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { AxonityApiError, ROUTES_ADDED_IN } from "./errors.js";
|
|
26
|
+
/**
|
|
27
|
+
* One filled-in value per argument name any tool takes, so a blind replay
|
|
28
|
+
* reaches as much of the route surface as possible.
|
|
29
|
+
*
|
|
30
|
+
* This is how the connector knows which routes it needs WITHOUT keeping a list
|
|
31
|
+
* of them: it runs its own tools against a recording client and watches. A
|
|
32
|
+
* hand-written route list would be the very thing #48 M2 exists to delete.
|
|
33
|
+
*
|
|
34
|
+
* Shared with `conformance.test.ts` and `exclusions.test.ts` deliberately. They
|
|
35
|
+
* each carried their own copy, the copies had to be edited in lockstep, and a
|
|
36
|
+
* probe fixture that has drifted silently narrows what the sweep reaches.
|
|
37
|
+
*/
|
|
38
|
+
export const PROBE_ARGS = {
|
|
39
|
+
id: "x", workflowId: "x", agentId: "x", toolId: "x", runId: "x", approvalId: "x",
|
|
40
|
+
flowId: "x", snippetId: "x", flowStepId: "x", linkId: "x", webhookId: "x",
|
|
41
|
+
scheduleId: "x", triggerId: "x", secretId: "x", versionId: "x", version: 1,
|
|
42
|
+
majorVersion: 1, expectedVersion: 1, displayOrder: 0, name: "x",
|
|
43
|
+
cronExpr: "0 0 * * *", conditionText: "x", repeatIntervalMinutes: 5,
|
|
44
|
+
target: "system", confirm: true, document: {}, fields: {},
|
|
45
|
+
mutations: [{ type: "add_step", payload: {} }], snippetIds: ["a"], runIds: ["a"],
|
|
46
|
+
workflows: [{ id: "x", expectedVersion: 1 }],
|
|
47
|
+
functions: [{ name: "f", code: "def f(): pass" }], code: "x",
|
|
48
|
+
requests: [{ entityType: "tool", entityId: "x" }],
|
|
49
|
+
entityKind: "skill", entityId: "x", batchId: "x", stepId: "x", answer: "x",
|
|
50
|
+
message: "x", templateId: "x", releaseId: "x", payload: {},
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Every route this build's tools call, found by replaying them.
|
|
54
|
+
*
|
|
55
|
+
* Nothing leaves the process: the client is a recorder. A handler that rejects
|
|
56
|
+
* the probe arguments is simply skipped — it contributes no route, which makes
|
|
57
|
+
* this a LOWER bound on what the connector needs and therefore safe: the check
|
|
58
|
+
* can miss a route, but it cannot invent one.
|
|
59
|
+
*/
|
|
60
|
+
export async function collectCalledRoutes(register) {
|
|
61
|
+
const calls = [];
|
|
62
|
+
const record = (method) => async (path) => {
|
|
63
|
+
calls.push({ method, path });
|
|
64
|
+
return {};
|
|
65
|
+
};
|
|
66
|
+
const recorder = {
|
|
67
|
+
get: record("GET"),
|
|
68
|
+
post: record("POST"),
|
|
69
|
+
put: record("PUT"),
|
|
70
|
+
patch: record("PATCH"),
|
|
71
|
+
del: record("DELETE"),
|
|
72
|
+
};
|
|
73
|
+
const handlers = [];
|
|
74
|
+
register({ tool: (_n, _d, _s, handler) => handlers.push(handler) }, recorder);
|
|
75
|
+
for (const handler of handlers) {
|
|
76
|
+
try {
|
|
77
|
+
await handler(PROBE_ARGS);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* Arg-shape mismatch is fine — only routes that fired are collected. */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return calls;
|
|
84
|
+
}
|
|
85
|
+
/** Split a path into segments, dropping any query string. */
|
|
86
|
+
function segments(path) {
|
|
87
|
+
return path.split("?")[0].replace(/^\/+|\/+$/g, "").split("/");
|
|
88
|
+
}
|
|
89
|
+
/** Does a concrete path match a "METHOD /a/{b}/c" contract entry? */
|
|
90
|
+
export function matchesRoute(call, entry) {
|
|
91
|
+
const space = entry.indexOf(" ");
|
|
92
|
+
if (space < 0)
|
|
93
|
+
return false;
|
|
94
|
+
if (entry.slice(0, space).toUpperCase() !== call.method.toUpperCase())
|
|
95
|
+
return false;
|
|
96
|
+
const template = segments(entry.slice(space + 1));
|
|
97
|
+
const actual = segments(call.path);
|
|
98
|
+
return (template.length === actual.length &&
|
|
99
|
+
template.every((part, i) => (part.startsWith("{") && part.endsWith("}")) || part === actual[i]));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Routes this build needs that the deploy does not mount, as "METHOD /path".
|
|
103
|
+
*
|
|
104
|
+
* Deduplicated by template rather than by concrete path: `read_workflow` and
|
|
105
|
+
* `update_workflow` both hit `/workflows/{id}`, and reporting one absence twice
|
|
106
|
+
* makes a short list look like an emergency.
|
|
107
|
+
*/
|
|
108
|
+
export function missingFromContract(calls, routes) {
|
|
109
|
+
const missing = new Set();
|
|
110
|
+
for (const call of calls) {
|
|
111
|
+
if (routes.some((entry) => matchesRoute(call, entry)))
|
|
112
|
+
continue;
|
|
113
|
+
missing.add(`${call.method} ${call.path}`);
|
|
114
|
+
}
|
|
115
|
+
return [...missing].sort();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What added a route, when this repository happens to know.
|
|
119
|
+
*
|
|
120
|
+
* `ROUTES_ADDED_IN` stops being a source of truth here and becomes ANNOTATION:
|
|
121
|
+
* the contract check has already established that a route is missing, and this
|
|
122
|
+
* only enriches the sentence. An entry that is absent costs a clause, not a
|
|
123
|
+
* wrong answer — which is the whole difference from using it to DECIDE whether
|
|
124
|
+
* a 404 counts.
|
|
125
|
+
*/
|
|
126
|
+
function attribute(route) {
|
|
127
|
+
const path = route.slice(route.indexOf(" ") + 1);
|
|
128
|
+
const known = ROUTES_ADDED_IN.find((entry) => entry.pattern.test(path));
|
|
129
|
+
return known ? `${route} — added by ${known.addedBy}` : route;
|
|
130
|
+
}
|
|
131
|
+
/** Where the "already checked this contract" marker lives, per API URL. */
|
|
132
|
+
function cachePath(apiUrl) {
|
|
133
|
+
// Not a security boundary and not precious: a lost cache costs one replay.
|
|
134
|
+
const key = Buffer.from(apiUrl).toString("base64url").slice(0, 40);
|
|
135
|
+
return join(tmpdir(), `axonity-mcp-contract-${key}.json`);
|
|
136
|
+
}
|
|
137
|
+
function readCachedHash(apiUrl) {
|
|
138
|
+
try {
|
|
139
|
+
const raw = JSON.parse(readFileSync(cachePath(apiUrl), "utf8"));
|
|
140
|
+
return typeof raw.contractHash === "string" ? raw.contractHash : undefined;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeCachedHash(apiUrl, contractHash) {
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(cachePath(apiUrl), JSON.stringify({ contractHash }));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* A cache that cannot be written is a slower startup, not a failure. */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Resolve `promise`, or `undefined` if it takes longer than `ms`. */
|
|
155
|
+
async function withTimeout(promise, ms) {
|
|
156
|
+
let timer;
|
|
157
|
+
try {
|
|
158
|
+
return await Promise.race([
|
|
159
|
+
promise,
|
|
160
|
+
new Promise((resolve) => {
|
|
161
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
162
|
+
// Do not hold the process open for a check that is already too late.
|
|
163
|
+
timer.unref?.();
|
|
164
|
+
}),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (timer)
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Ask the deploy what it mounts, and say — once, at startup — whether this
|
|
174
|
+
* build needs anything it does not have.
|
|
175
|
+
*
|
|
176
|
+
* Returns what it concluded, so a caller (and a test) can tell "checked, all
|
|
177
|
+
* present" from "could not check". Never throws.
|
|
178
|
+
*/
|
|
179
|
+
export async function reportContractSkew(client, register, options) {
|
|
180
|
+
const log = options.log ?? ((message) => console.error(message));
|
|
181
|
+
let contract;
|
|
182
|
+
try {
|
|
183
|
+
contract = await withTimeout(client.get("/api/v1/contract"), options.timeoutMs ?? 5_000);
|
|
184
|
+
}
|
|
185
|
+
catch (cause) {
|
|
186
|
+
// A backend too old to serve /contract must degrade to today's behaviour,
|
|
187
|
+
// not announce a problem it cannot substantiate. 404/405 is exactly that
|
|
188
|
+
// backend; anything else (401, network) will surface on the first real call
|
|
189
|
+
// with a better message than this check could write.
|
|
190
|
+
const status = cause instanceof AxonityApiError ? cause.status : 0;
|
|
191
|
+
if (status !== 404 && status !== 405 && status !== 0) {
|
|
192
|
+
log(`Axonity: could not read the backend contract (${status}). ` +
|
|
193
|
+
"Continuing — this is a diagnostic, not a dependency.");
|
|
194
|
+
}
|
|
195
|
+
return "unavailable";
|
|
196
|
+
}
|
|
197
|
+
if (contract === undefined)
|
|
198
|
+
return "timeout";
|
|
199
|
+
const routes = Array.isArray(contract.routes)
|
|
200
|
+
? contract.routes.filter((entry) => typeof entry === "string")
|
|
201
|
+
: [];
|
|
202
|
+
if (routes.length === 0)
|
|
203
|
+
return "unavailable";
|
|
204
|
+
const hash = typeof contract.contractHash === "string" ? contract.contractHash : "";
|
|
205
|
+
if (!options.ignoreCache && hash && readCachedHash(options.apiUrl) === hash) {
|
|
206
|
+
return "cached";
|
|
207
|
+
}
|
|
208
|
+
const missing = missingFromContract(await collectCalledRoutes(register), routes);
|
|
209
|
+
if (missing.length === 0) {
|
|
210
|
+
// Only a CLEAN result is cached. Caching a problem would report it once and
|
|
211
|
+
// then go quiet, which is worse than never having checked.
|
|
212
|
+
if (hash)
|
|
213
|
+
writeCachedHash(options.apiUrl, hash);
|
|
214
|
+
return "ok";
|
|
215
|
+
}
|
|
216
|
+
const build = typeof contract.buildVersion === "string" ? contract.buildVersion : "unknown";
|
|
217
|
+
const environment = typeof contract.environment === "string" ? contract.environment : "unknown";
|
|
218
|
+
log(`Axonity: this backend does not have ${missing.length} route(s) this ` +
|
|
219
|
+
`connector needs.\n` +
|
|
220
|
+
` Backend: ${build} (${environment}) at ${options.apiUrl}\n` +
|
|
221
|
+
missing.map((route) => ` - ${attribute(route)}`).join("\n") +
|
|
222
|
+
`\nThe backend is OLDER than this connector. Tools that use those routes ` +
|
|
223
|
+
`will fail; everything else works. Tell your human which Axonity ` +
|
|
224
|
+
`environment this token points at, or pin an older connector.`);
|
|
225
|
+
return "missing";
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=contract.js.map
|