@axonity-ai/mcp 0.1.0 → 0.3.2
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 +172 -24
- package/dist/client.d.ts +11 -2
- package/dist/client.js +28 -10
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +53 -10
- package/dist/errors.js +163 -19
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.js +135 -4
- package/dist/index.js.map +1 -1
- package/dist/tools/company.d.ts +16 -0
- package/dist/tools/company.js +81 -0
- package/dist/tools/company.js.map +1 -0
- package/dist/tools/conventions.d.ts +10 -4
- package/dist/tools/conventions.js +389 -23
- package/dist/tools/conventions.js.map +1 -1
- package/dist/tools/credentials.d.ts +34 -0
- package/dist/tools/credentials.js +106 -0
- package/dist/tools/credentials.js.map +1 -0
- package/dist/tools/extras.d.ts +14 -5
- package/dist/tools/extras.js +85 -40
- package/dist/tools/extras.js.map +1 -1
- package/dist/tools/promptPlacement.d.ts +15 -0
- package/dist/tools/promptPlacement.js +99 -0
- package/dist/tools/promptPlacement.js.map +1 -0
- package/dist/tools/register.d.ts +67 -10
- package/dist/tools/register.js +77 -22
- package/dist/tools/register.js.map +1 -1
- package/dist/tools/runs.d.ts +14 -0
- package/dist/tools/runs.js +103 -0
- package/dist/tools/runs.js.map +1 -0
- package/dist/tools/triggers.d.ts +13 -0
- package/dist/tools/triggers.js +128 -0
- package/dist/tools/triggers.js.map +1 -0
- package/dist/tools/validation.d.ts +13 -0
- package/dist/tools/validation.js +116 -0
- package/dist/tools/validation.js.map +1 -0
- package/dist/tools/versions.d.ts +37 -0
- package/dist/tools/versions.js +104 -0
- package/dist/tools/versions.js.map +1 -0
- package/dist/tools/workflowMutations.d.ts +22 -0
- package/dist/tools/workflowMutations.js +118 -23
- package/dist/tools/workflowMutations.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# @axonity-ai/mcp — Axonity Flow MCP connector
|
|
2
2
|
|
|
3
3
|
A local [Model Context Protocol](https://modelcontextprotocol.io) server that lets
|
|
4
|
-
an external agent (e.g. **Claude Code** on your laptop) read, draft, and
|
|
5
|
-
**workflows, agents, tools, skills, policies
|
|
6
|
-
|
|
4
|
+
an external agent (e.g. **Claude Code** on your laptop) read, draft, update, and
|
|
5
|
+
recoverably delete **workflows, agents, tools, skills, policies, reference docs,
|
|
6
|
+
personas, output schemas, prompt snippets and flows** in your Axonity tenant —
|
|
7
|
+
the same verbs the internal Builder team has, minus direct publish.
|
|
7
8
|
|
|
8
9
|
It runs on your machine and talks to Axonity **only over the public REST API**,
|
|
9
10
|
authenticated with a per-tenant **service token**. The backend re-enforces
|
|
@@ -13,7 +14,11 @@ tenant + scope on every call, so the connector is not a trust boundary.
|
|
|
13
14
|
|
|
14
15
|
1. **Mint a service token** in Axonity → **Settings → API tokens**. Copy it once
|
|
15
16
|
(it starts with `axs_`); you won't see it again. Use a **read-only** token if
|
|
16
|
-
you only want the agent to read
|
|
17
|
+
you only want the agent to read — it is genuinely enforced, any write from it
|
|
18
|
+
is refused with a 403. Every token expires (you choose 7, 15, 30, 60 or 90
|
|
19
|
+
days at mint time; there is no "never"), and there is no way for the agent to
|
|
20
|
+
check remaining lifetime in advance — an expired token fails exactly like a
|
|
21
|
+
revoked one, so mint a fresh one when that happens.
|
|
17
22
|
2. **Add the connector to Claude Code:**
|
|
18
23
|
|
|
19
24
|
```bash
|
|
@@ -26,43 +31,186 @@ tenant + scope on every call, so the connector is not a trust boundary.
|
|
|
26
31
|
`AXONITY_API_URL` is optional (defaults to the Axonity SaaS URL); set it if you
|
|
27
32
|
self-host.
|
|
28
33
|
|
|
34
|
+
Do **not** pass extra CLI arguments to `axonity-mcp`; startup only reads
|
|
35
|
+
environment variables. If arguments are supplied, the process exits with a
|
|
36
|
+
clear usage-style error.
|
|
37
|
+
|
|
29
38
|
3. Ask Claude Code things like *"list my Axonity workflows"*, *"create a workflow
|
|
30
39
|
called Onboarding"*, or *"add a step to workflow X"*.
|
|
31
40
|
|
|
32
41
|
## Tools
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
200+ tools total. `axonity_conventions` (read this first) covers the authoring
|
|
44
|
+
rules — drafts vs live, optimistic locking, per-entity fields, delete/restore,
|
|
45
|
+
and how to tell a retryable error from one that will never succeed.
|
|
46
|
+
|
|
47
|
+
### The generic entity family
|
|
48
|
+
|
|
49
|
+
Ten entities — **workflow, agent, tool, skill, policy, reference_doc, persona,
|
|
50
|
+
output_schema, prompt_snippet, flow** — share one shape, though not every
|
|
51
|
+
entity gets every verb (see the per-entity notes below for the exceptions):
|
|
39
52
|
|
|
40
53
|
| Tool (per `<entity>`) | What it does |
|
|
41
54
|
|------|--------------|
|
|
42
|
-
| `list_<
|
|
55
|
+
| `list_<plural>` | List the tenant's entities. |
|
|
43
56
|
| `read_<entity>` | Read one by id (incl. its version — read before you update). |
|
|
44
57
|
| `create_<entity>` | Create a new **draft**. |
|
|
45
|
-
| `update_<entity>` | Update a draft (
|
|
58
|
+
| `update_<entity>` | Update a draft (`expectedVersion` in the body; 409 on a stale write). |
|
|
59
|
+
| `delete_<entity>` | Soft-delete. **Recoverable** — see `restore_<entity>`. |
|
|
60
|
+
| `restore_<entity>` | Undo a delete. No version check. |
|
|
61
|
+
| `list_deleted_<plural>` | Restore candidates. |
|
|
62
|
+
| `discard_<entity>_draft` | Reset the draft to the last published state. |
|
|
46
63
|
| `request_publish_<entity>` | Ask for a draft to be published — creates a pending approval; never publishes. |
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
Exceptions: `persona` has no `create_persona` (create only via
|
|
66
|
+
`create_agent_persona`).
|
|
67
|
+
|
|
68
|
+
Plus:
|
|
69
|
+
- `apply_workflow_mutations` for structural workflow edits (add steps, connect
|
|
70
|
+
edges) via mutation commands, sequenced and version-threaded for you.
|
|
71
|
+
- `replace_workflow_document` for one-shot full-document replacement in a single
|
|
72
|
+
atomic PUT.
|
|
73
|
+
- `read_workflow_trigger_parameters` — what input a workflow's triggers expect.
|
|
74
|
+
- `bulk_delete_workflows` — soft-delete several at once (each with its own
|
|
75
|
+
`expectedVersion`).
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
### Version history, rollback, and version-level delete
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
For the ten versioned entities (including `flow`):
|
|
80
|
+
|
|
81
|
+
| Tool | What it does |
|
|
82
|
+
|------|--------------|
|
|
83
|
+
| `list_<entity>_versions` | List version history (checkpoints + named majors). |
|
|
84
|
+
| `read_<entity>_version` | Read one, by **integer checkpoint number**. |
|
|
85
|
+
| `restore_<entity>_version` | Roll the draft back to an old version (`expectedVersion` in body). |
|
|
86
|
+
| `delete_<entity>_version` | Remove one history entry. Draft and published version are protected. |
|
|
87
|
+
| `list_deleted_<entity>_versions` | Restore candidates for the row above. |
|
|
88
|
+
| `restore_deleted_<entity>_version` | Undo the delete above. No version check. |
|
|
89
|
+
| `read_<entity>_published` | The live snapshot, as opposed to the draft. |
|
|
90
|
+
| `name_<entity>_major_version` | Give a major version a name (label a release). |
|
|
91
|
+
|
|
92
|
+
`{version}` (an int) and `{versionId}` (a UUID) are two different identifiers
|
|
93
|
+
across these routes — the tool parameter names say which.
|
|
94
|
+
|
|
95
|
+
### Also
|
|
96
|
+
|
|
97
|
+
- **Personas**: `read_agent_persona`, `create_agent_persona` — agent-scoped,
|
|
98
|
+
since a persona can only be created through its agent. Everything else about
|
|
99
|
+
a persona (list, read, update, delete/restore, versions) is the generic
|
|
100
|
+
entity family above.
|
|
55
101
|
- **Connectors** (a tool of type `connector`): `create_connector`,
|
|
56
102
|
`update_connector` — `authConfig` must be placeholders only; a human fills real
|
|
57
|
-
secrets in Axonity.
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
secrets in Axonity. (`create_tool`/`update_tool` carry the same guard, so a
|
|
104
|
+
connector authored either way is covered.)
|
|
105
|
+
- **Attach / detach memory**: `attach_skill_to_agent`,
|
|
106
|
+
`attach_skill_to_workflow`, `attach_policy_to_agent`,
|
|
107
|
+
`attach_reference_to_agent`, and a `detach_*_from_*` for each. Detaching
|
|
108
|
+
removes the link only — the skill or policy itself is untouched. Read the
|
|
109
|
+
links back with `list_agent_skills`, `list_agent_policies`,
|
|
110
|
+
`list_agent_reference_docs`.
|
|
111
|
+
- **Prompt elements (placement)**: a `prompt_snippet` is a library item; it only
|
|
112
|
+
takes effect once placed into a flow step's prompt stack.
|
|
113
|
+
`read_workflow_prompt_stacks` / `read_flow_prompt_stacks` resolve a
|
|
114
|
+
workflow/flow to its steps and each step's `system`/`user` stacks (this is how
|
|
115
|
+
you find the `flowStepId`s). Then `attach_prompt_snippet_to_flow_step`
|
|
116
|
+
(`target` = `system`|`user`, with an order), `update_flow_step_prompt`,
|
|
117
|
+
`reorder_flow_step_prompts`, `detach_prompt_snippet_from_flow_step`, and
|
|
118
|
+
`list_flow_step_prompts` / `list_wildcard_prompts`.
|
|
119
|
+
- **Company** (the tenant's single company document — a singleton, no id):
|
|
120
|
+
`read_company`, `update_company` (whole-document save with `expectedVersion`),
|
|
121
|
+
`list_company_versions`, `read_company_version`, `restore_company_version`,
|
|
122
|
+
`name_company_major_version`, `read_company_published`, and
|
|
123
|
+
`request_publish_company` (takes no id — the
|
|
124
|
+
server resolves your tenant's one company; direct company publish is closed to
|
|
125
|
+
service tokens).
|
|
126
|
+
- **Catalog & cloning**: `list_system_tools` (read-only catalog — enabling one
|
|
127
|
+
for an agent is `update_agent` with the id added to `systemToolIds`),
|
|
128
|
+
`clone_flow`, `clone_prompt_snippet`.
|
|
129
|
+
- `list_deleted_prompt_snippets` calls `/api/v1/prompt-snippets/deleted`; the
|
|
130
|
+
backend returns it as `{ items: [... ] }`, and the tool forwards that response
|
|
131
|
+
unchanged.
|
|
132
|
+
|
|
133
|
+
### Validate and run before you publish
|
|
60
134
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
135
|
+
| Tool | What it does |
|
|
136
|
+
|------|--------------|
|
|
137
|
+
| `validate_workflow` | Structural + schema check of a workflow document. Stateless and read-only-token safe; does not verify referenced agents/tools exist. |
|
|
138
|
+
| `analyze_workflow_reachable_outputs` | What a given step can read from upstream — bind inputs to real fields instead of guessing. Stateless and read-only-token safe. |
|
|
139
|
+
| `validate_tool_code` | Syntax and banned-pattern check for Python tool code. Stateless and read-only-token safe. |
|
|
140
|
+
| `format_tool_code` | Format tool code with Black. Stateless and read-only-token safe. |
|
|
141
|
+
| `execute_tool` | Actually RUN tool code (not just validate it) and see the real output. |
|
|
142
|
+
| `execute_stored_connector` | Test-run an already-saved connector. The backend decrypts its real secret server-side — the agent supplies only input parameters and never sees the secret. |
|
|
143
|
+
|
|
144
|
+
### Triggers — what makes a workflow run
|
|
145
|
+
|
|
146
|
+
`list_/create_/delete_` for **webhook triggers** (plus
|
|
147
|
+
`rotate_webhook_trigger`), **cron schedules**, and **conditional triggers**
|
|
148
|
+
(plus `update_conditional_trigger`). Trigger deletes are **hard** deletes with
|
|
149
|
+
no restore, and a webhook token is shown **once** at create or rotate.
|
|
150
|
+
|
|
151
|
+
### Runs — evaluating what you built
|
|
152
|
+
|
|
153
|
+
`start_workflow_run` (test a workflow you built — it runs the **published**
|
|
154
|
+
workflow and really executes), `cancel_run`, `delete_run`, `list_runs`,
|
|
155
|
+
`list_workflow_runs`, `read_run`, `read_run_trace`, `read_run_cost`,
|
|
156
|
+
`read_runs_summary`, `archive_run` / `unarchive_run`, `bulk_archive_runs` /
|
|
157
|
+
`bulk_delete_runs`. There is no findings endpoint — evaluation means reading a
|
|
158
|
+
run's validator verdicts and its trace.
|
|
159
|
+
|
|
160
|
+
### Approvals
|
|
161
|
+
|
|
162
|
+
`list_publish_approvals({ status?, limit?, offset? })` and
|
|
163
|
+
`get_publish_approval({ approvalId })` — how you find out whether a
|
|
164
|
+
`request_publish_*` was approved or rejected. Approving and rejecting are
|
|
165
|
+
human-only actions in Axonity.
|
|
166
|
+
|
|
167
|
+
## What this is for — and what it is not
|
|
168
|
+
|
|
169
|
+
**Authoring.** An agent composing an entity from intent: drafting a workflow,
|
|
170
|
+
writing a tool, wiring memory onto an agent, and checking its own work. That is
|
|
171
|
+
what these tools are built for.
|
|
172
|
+
|
|
173
|
+
**Not bulk migration.** Do not use the connector to move many entities verbatim
|
|
174
|
+
from one place to another. Axonity's config export/import moves bytes with no
|
|
175
|
+
model in the path and fails closed on secrets; content routed through an agent
|
|
176
|
+
can be subtly altered in transit, which is precisely the risk a fidelity
|
|
177
|
+
migration cannot take.
|
|
178
|
+
|
|
179
|
+
## Guardrails
|
|
180
|
+
|
|
181
|
+
These are enforced by the backend, not merely by convention:
|
|
182
|
+
|
|
183
|
+
- **The connector never publishes.** `request_publish_*` creates a **pending
|
|
184
|
+
approval**; a human approves it in Axonity, and only then does the draft go
|
|
185
|
+
live. A direct publish from a service token is refused with a 403, so there is
|
|
186
|
+
no tool for it and no way around it.
|
|
187
|
+
- **A read-only token is genuinely read-only for mutations.** Any write from a token
|
|
188
|
+
without the `write` scope is refused with a 403.
|
|
189
|
+
- **The four stateless analysis tools are the exception:** `validate_workflow`,
|
|
190
|
+
`analyze_workflow_reachable_outputs`, `validate_tool_code`, and `format_tool_code`
|
|
191
|
+
can be called by read-only and write tokens because they never mutate state.
|
|
192
|
+
- **The token is tenant-bound.** An agent cannot reach another tenant.
|
|
193
|
+
- **Secrets never pass through the agent.** A connector's `authConfig` accepts
|
|
194
|
+
placeholders only; a write carrying something that looks like a real
|
|
195
|
+
credential is rejected before it leaves the connector. Tenant secrets
|
|
196
|
+
(`/api/v1/secrets`) aren't wrapped at all — the backend refuses any service
|
|
197
|
+
token there outright.
|
|
198
|
+
- **Errors carry a machine-readable `code`, not just prose.** A 409 can mean a
|
|
199
|
+
stale write (retry) or a live reference conflict (don't — see
|
|
200
|
+
`axonity_conventions`); the connector tells them apart by `code`, never by
|
|
201
|
+
matching the message text.
|
|
202
|
+
- **No tool crosses the authority boundary.** A test drives the whole registered
|
|
203
|
+
surface and fails the build if any tool targets a publish / approve / secret /
|
|
204
|
+
service-token / deploy route (`test/exclusions.test.ts`).
|
|
205
|
+
- **Guidance can't silently drift from the backend.** The field/enum facts the
|
|
206
|
+
connector states are pinned to a vendored snapshot of the backend OpenAPI
|
|
207
|
+
schema; `test/conformance.test.ts` fails if a route or documented enum
|
|
208
|
+
diverges (see `test/fixtures/README.md`).
|
|
209
|
+
|
|
210
|
+
**One known exception, not enforced:** a framework-provided `flow` is meant to
|
|
211
|
+
be read-only to a tenant, but the backend does not actually block
|
|
212
|
+
`update_flow`/`delete_flow` against one. Prefer `clone_flow` over editing a
|
|
213
|
+
framework flow in place.
|
|
66
214
|
|
|
67
215
|
## Development
|
|
68
216
|
|
package/dist/client.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { AxonityConfig } from "./config.js";
|
|
10
10
|
export type Json = Record<string, unknown>;
|
|
11
|
+
/** Query-string values a route may take. `undefined` entries are omitted. */
|
|
12
|
+
export type Query = Record<string, string | number | boolean | undefined>;
|
|
11
13
|
export declare class AxonityClient {
|
|
12
14
|
private readonly config;
|
|
13
15
|
constructor(config: AxonityConfig);
|
|
@@ -16,9 +18,16 @@ export declare class AxonityClient {
|
|
|
16
18
|
* Issue a request and return the parsed JSON body. Non-2xx responses are
|
|
17
19
|
* thrown as typed errors; a 204/empty body resolves to `undefined`.
|
|
18
20
|
*/
|
|
19
|
-
request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
|
20
|
-
get<T = unknown>(path: string): Promise<T>;
|
|
21
|
+
request<T = unknown>(method: string, path: string, body?: unknown, query?: Query): Promise<T>;
|
|
22
|
+
get<T = unknown>(path: string, query?: Query): Promise<T>;
|
|
21
23
|
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
22
24
|
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
23
25
|
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
26
|
+
/**
|
|
27
|
+
* DELETE. The optimistic lock travels as a QUERY parameter on these routes,
|
|
28
|
+
* not in the body — and its spelling differs by entity family
|
|
29
|
+
* (`expectedVersion` vs `expected_version`), so the caller passes it in.
|
|
30
|
+
* A 204 with no body resolves to `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
del<T = unknown>(path: string, query?: Query): Promise<T>;
|
|
24
33
|
}
|
package/dist/client.js
CHANGED
|
@@ -12,17 +12,26 @@ export class AxonityClient {
|
|
|
12
12
|
constructor(config) {
|
|
13
13
|
this.config = config;
|
|
14
14
|
}
|
|
15
|
-
url(path) {
|
|
16
|
-
|
|
15
|
+
url(path, query) {
|
|
16
|
+
const base = `${this.config.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17
|
+
if (!query)
|
|
18
|
+
return base;
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
for (const [key, value] of Object.entries(query)) {
|
|
21
|
+
if (value !== undefined)
|
|
22
|
+
params.set(key, String(value));
|
|
23
|
+
}
|
|
24
|
+
const qs = params.toString();
|
|
25
|
+
return qs ? `${base}?${qs}` : base;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Issue a request and return the parsed JSON body. Non-2xx responses are
|
|
20
29
|
* thrown as typed errors; a 204/empty body resolves to `undefined`.
|
|
21
30
|
*/
|
|
22
|
-
async request(method, path, body) {
|
|
31
|
+
async request(method, path, body, query) {
|
|
23
32
|
let resp;
|
|
24
33
|
try {
|
|
25
|
-
resp = await fetch(this.url(path), {
|
|
34
|
+
resp = await fetch(this.url(path, query), {
|
|
26
35
|
method,
|
|
27
36
|
headers: {
|
|
28
37
|
Authorization: `Bearer ${this.config.token}`,
|
|
@@ -39,15 +48,15 @@ export class AxonityClient {
|
|
|
39
48
|
const text = await resp.text();
|
|
40
49
|
const parsed = text ? safeJson(text) : undefined;
|
|
41
50
|
if (!resp.ok) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw errorForStatus(resp.status,
|
|
51
|
+
// Pass the FULL body — errorForStatus reads `code`/`retryable`/`message`
|
|
52
|
+
// off the structured error envelope when present, and falls back
|
|
53
|
+
// gracefully for a bare `detail` string/array from older routes.
|
|
54
|
+
throw errorForStatus(resp.status, parsed);
|
|
46
55
|
}
|
|
47
56
|
return parsed;
|
|
48
57
|
}
|
|
49
|
-
get(path) {
|
|
50
|
-
return this.request("GET", path);
|
|
58
|
+
get(path, query) {
|
|
59
|
+
return this.request("GET", path, undefined, query);
|
|
51
60
|
}
|
|
52
61
|
post(path, body) {
|
|
53
62
|
return this.request("POST", path, body);
|
|
@@ -58,6 +67,15 @@ export class AxonityClient {
|
|
|
58
67
|
patch(path, body) {
|
|
59
68
|
return this.request("PATCH", path, body);
|
|
60
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* DELETE. The optimistic lock travels as a QUERY parameter on these routes,
|
|
72
|
+
* not in the body — and its spelling differs by entity family
|
|
73
|
+
* (`expectedVersion` vs `expected_version`), so the caller passes it in.
|
|
74
|
+
* A 204 with no body resolves to `undefined`.
|
|
75
|
+
*/
|
|
76
|
+
del(path, query) {
|
|
77
|
+
return this.request("DELETE", path, undefined, query);
|
|
78
|
+
}
|
|
61
79
|
}
|
|
62
80
|
function safeJson(text) {
|
|
63
81
|
try {
|
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,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -5,31 +5,74 @@
|
|
|
5
5
|
* clean, actionable message to the agent — never a raw stack trace or an
|
|
6
6
|
* opaque status code.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Mirrors the backend's `ErrorCode` enum (`src/error_codes.py`) — the stable,
|
|
10
|
+
* machine-readable vocabulary every structured error response carries as
|
|
11
|
+
* `code`. An external agent has no access to the backend repository, so this
|
|
12
|
+
* is the one field worth branching on; `message` is human-readable prose that
|
|
13
|
+
* can be reworded without notice.
|
|
14
|
+
*/
|
|
15
|
+
export type ErrorCode = "internal_error" | "bad_request" | "unauthorized" | "forbidden" | "not_found" | "gone" | "payload_too_large" | "service_unavailable" | "upstream_error" | "validation_error" | "config_secret_leak" | "config_import_error" | "conflict" | "version_conflict" | "reference_conflict" | "idempotency_conflict" | "already_terminal" | "concurrency_error" | "admin_session_required" | "rate_limited" | "trigger_concurrency_exceeded" | "config_deploy_disabled" | "git_provider_error";
|
|
16
|
+
/** Codes where re-issuing the SAME request can succeed later. Mirrors `RETRYABLE_CODES`. */
|
|
17
|
+
export declare const RETRYABLE_CODES: ReadonlySet<ErrorCode>;
|
|
18
|
+
/**
|
|
19
|
+
* Render the server's `detail` as a compact, readable line.
|
|
20
|
+
*
|
|
21
|
+
* FastAPI returns a bare string for a raised error and a list of
|
|
22
|
+
* `{loc, msg, type}` objects for a 422 schema rejection. The list form is what
|
|
23
|
+
* names the offending field, so it is flattened to `body.fieldName: message`
|
|
24
|
+
* rather than dumped as JSON — the agent has to act on it, not read it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function describeDetail(detail: unknown): string | undefined;
|
|
8
27
|
export declare class AxonityApiError extends Error {
|
|
9
28
|
/** HTTP status that produced this error. */
|
|
10
29
|
readonly status: number;
|
|
11
|
-
/**
|
|
30
|
+
/** The server's response body, when present (may be the structured envelope). */
|
|
12
31
|
readonly detail?: unknown | undefined;
|
|
32
|
+
/** The envelope's `code`, when the server sent one — branch on this, not on `message`. */
|
|
33
|
+
readonly code?: string | undefined;
|
|
34
|
+
/** Whether re-issuing the same request could succeed. `undefined` = unknown. */
|
|
35
|
+
readonly retryable?: boolean | undefined;
|
|
13
36
|
constructor(message: string,
|
|
14
37
|
/** HTTP status that produced this error. */
|
|
15
38
|
status: number,
|
|
16
|
-
/**
|
|
17
|
-
detail?: unknown | undefined
|
|
39
|
+
/** The server's response body, when present (may be the structured envelope). */
|
|
40
|
+
detail?: unknown | undefined,
|
|
41
|
+
/** The envelope's `code`, when the server sent one — branch on this, not on `message`. */
|
|
42
|
+
code?: string | undefined,
|
|
43
|
+
/** Whether re-issuing the same request could succeed. `undefined` = unknown. */
|
|
44
|
+
retryable?: boolean | undefined);
|
|
18
45
|
}
|
|
19
46
|
/** 409 — the entity was modified since the agent read it. Re-read and retry. */
|
|
20
47
|
export declare class VersionConflictError extends AxonityApiError {
|
|
21
|
-
constructor(
|
|
48
|
+
constructor(body?: unknown);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 409 — the entity is referenced by something published or live. Both this and
|
|
52
|
+
* `VersionConflictError` are HTTP 409; the `code` is what tells them apart, and
|
|
53
|
+
* it matters: retrying THIS one can never succeed on its own.
|
|
54
|
+
*/
|
|
55
|
+
export declare class ReferenceConflictError extends AxonityApiError {
|
|
56
|
+
constructor(body?: unknown);
|
|
57
|
+
}
|
|
58
|
+
/** 404 — no such entity in this tenant (or it was deleted). */
|
|
59
|
+
export declare class NotFoundError extends AxonityApiError {
|
|
60
|
+
constructor(body?: unknown);
|
|
22
61
|
}
|
|
23
|
-
/** 401 — the service token is missing, unknown, or revoked. */
|
|
62
|
+
/** 401 — the service token is missing, unknown, expired, or revoked. */
|
|
24
63
|
export declare class AuthError extends AxonityApiError {
|
|
25
|
-
constructor(
|
|
64
|
+
constructor(body?: unknown);
|
|
26
65
|
}
|
|
27
66
|
/** 403 — the token lacks the scope for this action (e.g. read-only on a write). */
|
|
28
67
|
export declare class ForbiddenError extends AxonityApiError {
|
|
29
|
-
constructor(
|
|
68
|
+
constructor(body?: unknown);
|
|
30
69
|
}
|
|
31
70
|
/**
|
|
32
|
-
* Map an HTTP response to the right error type. `
|
|
33
|
-
*
|
|
71
|
+
* Map an HTTP response to the right error type. `body` is the parsed response
|
|
72
|
+
* body — ideally the structured `{error, code, retryable, message, detail?}`
|
|
73
|
+
* envelope, but may be a bare FastAPI `detail` string/array for routes that
|
|
74
|
+
* predate it. When a recognised `code` is present it takes priority over the
|
|
75
|
+
* HTTP status, because the status alone cannot distinguish e.g. a retryable
|
|
76
|
+
* version conflict from a non-retryable reference conflict — both are 409.
|
|
34
77
|
*/
|
|
35
|
-
export declare function errorForStatus(status: number,
|
|
78
|
+
export declare function errorForStatus(status: number, body?: unknown): AxonityApiError;
|
package/dist/errors.js
CHANGED
|
@@ -5,58 +5,202 @@
|
|
|
5
5
|
* clean, actionable message to the agent — never a raw stack trace or an
|
|
6
6
|
* opaque status code.
|
|
7
7
|
*/
|
|
8
|
+
/** Codes where re-issuing the SAME request can succeed later. Mirrors `RETRYABLE_CODES`. */
|
|
9
|
+
export const RETRYABLE_CODES = new Set([
|
|
10
|
+
"version_conflict",
|
|
11
|
+
"rate_limited",
|
|
12
|
+
"trigger_concurrency_exceeded",
|
|
13
|
+
]);
|
|
14
|
+
/** Recognise the structured `{error, code, retryable, message, detail?}` shape. */
|
|
15
|
+
function asEnvelope(body) {
|
|
16
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
17
|
+
return undefined;
|
|
18
|
+
const b = body;
|
|
19
|
+
return {
|
|
20
|
+
message: typeof b.message === "string" ? b.message : undefined,
|
|
21
|
+
code: typeof b.code === "string" ? b.code : undefined,
|
|
22
|
+
retryable: typeof b.retryable === "boolean" ? b.retryable : undefined,
|
|
23
|
+
detail: "detail" in b ? b.detail : undefined,
|
|
24
|
+
affectedEntities: "affectedEntities" in b ? b.affectedEntities : undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render the server's `detail` as a compact, readable line.
|
|
29
|
+
*
|
|
30
|
+
* FastAPI returns a bare string for a raised error and a list of
|
|
31
|
+
* `{loc, msg, type}` objects for a 422 schema rejection. The list form is what
|
|
32
|
+
* names the offending field, so it is flattened to `body.fieldName: message`
|
|
33
|
+
* rather than dumped as JSON — the agent has to act on it, not read it.
|
|
34
|
+
*/
|
|
35
|
+
export function describeDetail(detail) {
|
|
36
|
+
if (detail === undefined || detail === null)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof detail === "string")
|
|
39
|
+
return detail.trim() || undefined;
|
|
40
|
+
if (Array.isArray(detail)) {
|
|
41
|
+
const lines = detail
|
|
42
|
+
.map((item) => {
|
|
43
|
+
if (typeof item === "string")
|
|
44
|
+
return item;
|
|
45
|
+
if (typeof item !== "object" || item === null)
|
|
46
|
+
return String(item);
|
|
47
|
+
const { loc, msg, message } = item;
|
|
48
|
+
const where = Array.isArray(loc) ? loc.join(".") : undefined;
|
|
49
|
+
const what = typeof msg === "string" ? msg : typeof message === "string" ? message : undefined;
|
|
50
|
+
if (where && what)
|
|
51
|
+
return `${where}: ${what}`;
|
|
52
|
+
return what ?? where ?? JSON.stringify(item);
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
return lines.length ? lines.join("; ") : undefined;
|
|
56
|
+
}
|
|
57
|
+
if (typeof detail === "object") {
|
|
58
|
+
const { detail: nested, message } = detail;
|
|
59
|
+
if (typeof nested === "string")
|
|
60
|
+
return nested;
|
|
61
|
+
if (typeof message === "string")
|
|
62
|
+
return message;
|
|
63
|
+
return JSON.stringify(detail);
|
|
64
|
+
}
|
|
65
|
+
return String(detail);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Render a response body for a human, whether it's the new structured
|
|
69
|
+
* envelope, a bare FastAPI `detail` array/string, or something else entirely.
|
|
70
|
+
* Falls back to `describeDetail` when the body doesn't look like the envelope,
|
|
71
|
+
* so older/simpler error bodies still render exactly as before.
|
|
72
|
+
*/
|
|
73
|
+
function describeBody(body) {
|
|
74
|
+
const env = asEnvelope(body);
|
|
75
|
+
if (!env)
|
|
76
|
+
return describeDetail(body);
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (env.message)
|
|
79
|
+
parts.push(env.message);
|
|
80
|
+
const detailText = describeDetail(env.detail);
|
|
81
|
+
if (detailText && detailText !== env.message)
|
|
82
|
+
parts.push(detailText);
|
|
83
|
+
if (Array.isArray(env.affectedEntities) && env.affectedEntities.length > 0) {
|
|
84
|
+
parts.push(`Affects: ${env.affectedEntities.map(String).join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
return parts.length ? parts.join(" ") : describeDetail(body);
|
|
87
|
+
}
|
|
88
|
+
/** Append the server's rendered body to a base message, when there is one. */
|
|
89
|
+
function withDetail(base, body) {
|
|
90
|
+
const described = describeBody(body);
|
|
91
|
+
return described ? `${base} ${described}` : base;
|
|
92
|
+
}
|
|
8
93
|
export class AxonityApiError extends Error {
|
|
9
94
|
status;
|
|
10
95
|
detail;
|
|
96
|
+
code;
|
|
97
|
+
retryable;
|
|
11
98
|
constructor(message,
|
|
12
99
|
/** HTTP status that produced this error. */
|
|
13
100
|
status,
|
|
14
|
-
/**
|
|
15
|
-
detail
|
|
101
|
+
/** The server's response body, when present (may be the structured envelope). */
|
|
102
|
+
detail,
|
|
103
|
+
/** The envelope's `code`, when the server sent one — branch on this, not on `message`. */
|
|
104
|
+
code,
|
|
105
|
+
/** Whether re-issuing the same request could succeed. `undefined` = unknown. */
|
|
106
|
+
retryable) {
|
|
16
107
|
super(message);
|
|
17
108
|
this.status = status;
|
|
18
109
|
this.detail = detail;
|
|
110
|
+
this.code = code;
|
|
111
|
+
this.retryable = retryable;
|
|
19
112
|
this.name = "AxonityApiError";
|
|
20
113
|
}
|
|
21
114
|
}
|
|
22
115
|
/** 409 — the entity was modified since the agent read it. Re-read and retry. */
|
|
23
116
|
export class VersionConflictError extends AxonityApiError {
|
|
24
|
-
constructor(
|
|
25
|
-
|
|
26
|
-
|
|
117
|
+
constructor(body) {
|
|
118
|
+
const env = asEnvelope(body);
|
|
119
|
+
super(withDetail("Version conflict: the entity changed since you read it. Read it again to " +
|
|
120
|
+
"get the current version, then re-apply your change.", body), 409, body, env?.code ?? "version_conflict", env?.retryable ?? true);
|
|
27
121
|
this.name = "VersionConflictError";
|
|
28
122
|
}
|
|
29
123
|
}
|
|
30
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* 409 — the entity is referenced by something published or live. Both this and
|
|
126
|
+
* `VersionConflictError` are HTTP 409; the `code` is what tells them apart, and
|
|
127
|
+
* it matters: retrying THIS one can never succeed on its own.
|
|
128
|
+
*/
|
|
129
|
+
export class ReferenceConflictError extends AxonityApiError {
|
|
130
|
+
constructor(body) {
|
|
131
|
+
const env = asEnvelope(body);
|
|
132
|
+
super(withDetail("Reference conflict: this entity is referenced by something published or " +
|
|
133
|
+
"live. This is NOT a stale-read problem — re-reading and retrying will " +
|
|
134
|
+
"never succeed. Detach the reference, or unpublish the referencing " +
|
|
135
|
+
"entity, then try again.", body), 409, body, env?.code ?? "reference_conflict", env?.retryable ?? false);
|
|
136
|
+
this.name = "ReferenceConflictError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** 404 — no such entity in this tenant (or it was deleted). */
|
|
140
|
+
export class NotFoundError extends AxonityApiError {
|
|
141
|
+
constructor(body) {
|
|
142
|
+
const env = asEnvelope(body);
|
|
143
|
+
super(withDetail("Not found (404): no such entity in this tenant. Check the id, or list " +
|
|
144
|
+
"the collection to find it. A deleted entity also reads as not found — " +
|
|
145
|
+
"list_deleted_* first if you suspect that.", body), 404, body, env?.code ?? "not_found", env?.retryable ?? false);
|
|
146
|
+
this.name = "NotFoundError";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** 401 — the service token is missing, unknown, expired, or revoked. */
|
|
31
150
|
export class AuthError extends AxonityApiError {
|
|
32
|
-
constructor(
|
|
33
|
-
|
|
34
|
-
|
|
151
|
+
constructor(body) {
|
|
152
|
+
const env = asEnvelope(body);
|
|
153
|
+
super(withDetail("Authentication failed (401): the service token is missing, invalid, expired, " +
|
|
154
|
+
"or revoked. The backend deliberately returns the same message for all four " +
|
|
155
|
+
"causes, so do not guess which one it is and do not retry — tell your human " +
|
|
156
|
+
"the token is dead and a new one must be minted in Axonity → Settings → API tokens.", body), 401, body, env?.code ?? "unauthorized", env?.retryable ?? false);
|
|
35
157
|
this.name = "AuthError";
|
|
36
158
|
}
|
|
37
159
|
}
|
|
38
160
|
/** 403 — the token lacks the scope for this action (e.g. read-only on a write). */
|
|
39
161
|
export class ForbiddenError extends AxonityApiError {
|
|
40
|
-
constructor(
|
|
41
|
-
|
|
42
|
-
|
|
162
|
+
constructor(body) {
|
|
163
|
+
const env = asEnvelope(body);
|
|
164
|
+
super(withDetail("Forbidden (403): this service token is not allowed to perform that action. " +
|
|
165
|
+
"A read-only token cannot create, update, delete, or request-publish, and no " +
|
|
166
|
+
"token may publish directly — use request_publish_* and let a human approve.", body), 403, body, env?.code ?? "forbidden", env?.retryable ?? false);
|
|
43
167
|
this.name = "ForbiddenError";
|
|
44
168
|
}
|
|
45
169
|
}
|
|
46
170
|
/**
|
|
47
|
-
* Map an HTTP response to the right error type. `
|
|
48
|
-
*
|
|
171
|
+
* Map an HTTP response to the right error type. `body` is the parsed response
|
|
172
|
+
* body — ideally the structured `{error, code, retryable, message, detail?}`
|
|
173
|
+
* envelope, but may be a bare FastAPI `detail` string/array for routes that
|
|
174
|
+
* predate it. When a recognised `code` is present it takes priority over the
|
|
175
|
+
* HTTP status, because the status alone cannot distinguish e.g. a retryable
|
|
176
|
+
* version conflict from a non-retryable reference conflict — both are 409.
|
|
49
177
|
*/
|
|
50
|
-
export function errorForStatus(status,
|
|
178
|
+
export function errorForStatus(status, body) {
|
|
179
|
+
const env = asEnvelope(body);
|
|
180
|
+
switch (env?.code) {
|
|
181
|
+
case "version_conflict":
|
|
182
|
+
return new VersionConflictError(body);
|
|
183
|
+
case "reference_conflict":
|
|
184
|
+
return new ReferenceConflictError(body);
|
|
185
|
+
case "not_found":
|
|
186
|
+
return new NotFoundError(body);
|
|
187
|
+
case "unauthorized":
|
|
188
|
+
return new AuthError(body);
|
|
189
|
+
case "forbidden":
|
|
190
|
+
case "admin_session_required":
|
|
191
|
+
return new ForbiddenError(body);
|
|
192
|
+
}
|
|
51
193
|
switch (status) {
|
|
52
194
|
case 401:
|
|
53
|
-
return new AuthError(
|
|
195
|
+
return new AuthError(body);
|
|
54
196
|
case 403:
|
|
55
|
-
return new ForbiddenError(
|
|
197
|
+
return new ForbiddenError(body);
|
|
198
|
+
case 404:
|
|
199
|
+
return new NotFoundError(body);
|
|
56
200
|
case 409:
|
|
57
|
-
return new VersionConflictError(
|
|
201
|
+
return new VersionConflictError(body);
|
|
58
202
|
default:
|
|
59
|
-
return new AxonityApiError(`Axonity API request failed (${status}).`, status,
|
|
203
|
+
return new AxonityApiError(withDetail(`Axonity API request failed (${status}).`, body), status, body, env?.code, env?.retryable);
|
|
60
204
|
}
|
|
61
205
|
}
|
|
62
206
|
//# sourceMappingURL=errors.js.map
|