@anchrd/intel-api 0.25.0 → 0.26.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/dist/bundle/bundle.js +61 -9
- package/dist/flows/flows.js +40 -18
- package/dist/http/http.js +24 -4
- package/dist/intel/intel.js +2 -2
- package/dist/mcp/mcp.js +117 -4
- package/dist/nodes/nodes.js +10 -7
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/intel-error/intel-error.d.ts +18 -1
- package/dist/shared/intel-error/intel-error.js +18 -1
- package/dist/shared/problem-details/problem-details.d.ts +22 -0
- package/dist/shared/problem-details/problem-details.js +14 -0
- package/package.json +3 -3
package/dist/bundle/bundle.js
CHANGED
|
@@ -7,9 +7,33 @@ import { documentLinkTargets } from "../nodes/document-links/document-links.js";
|
|
|
7
7
|
import { parseCsv } from "../shared/csv/csv.js";
|
|
8
8
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
9
9
|
import { plainTitle } from "../shared/plain-title/plain-title.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* What every bundle leaves out, by decision rather than by accident (#136, ADR-0006). The manifest
|
|
12
|
+
* says so, because a backup that is silent about what it does not hold will be trusted with exactly
|
|
13
|
+
* that.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ ONE list, read by both halves of the round trip: the export writes it into `manifest.json`,
|
|
16
|
+
* and the import answers with it in `BundleImportResult.excluded` (#433). The import does NOT read
|
|
17
|
+
* the word back out of the manifest it was handed — a bundle claiming `excluded: []` would
|
|
18
|
+
* otherwise make the import report that nothing was left behind, and a naked folder carries no
|
|
19
|
+
* manifest to ask at all. Every one of the five holds for every import this build performs: a node
|
|
20
|
+
* arrives with `sequence: 1` and nothing older, an import mints no grants, it starts no runs,
|
|
21
|
+
* nothing it creates is archived, and a published flow's graph is the one that was published.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ `unpublished-flow-changes` is a CONSTANT of the build like the other four, not a report about
|
|
24
|
+
* this particular bundle (#585). It says what a bundle never carries — the edits made to a flow
|
|
25
|
+
* after it was published — in the same way `version-history` says so for a node with a single
|
|
26
|
+
* version. A list that named only the losses this one export happens to suffer would be a different
|
|
27
|
+
* kind of statement, and the import could not make it at all: it answers before it knows what is in
|
|
28
|
+
* the zip, and from a naked folder it never finds out.
|
|
29
|
+
*/
|
|
30
|
+
const Excluded = [
|
|
31
|
+
"version-history",
|
|
32
|
+
"grants",
|
|
33
|
+
"flow-runs",
|
|
34
|
+
"archived-nodes",
|
|
35
|
+
"unpublished-flow-changes",
|
|
36
|
+
];
|
|
13
37
|
/**
|
|
14
38
|
* A title as a file name. Titles are free text and zip paths are not: a separator would move the
|
|
15
39
|
* entry, a control character corrupts the archive listing, and a trailing dot breaks extraction on
|
|
@@ -498,6 +522,27 @@ export function createBundle(deps) {
|
|
|
498
522
|
: { type: "text", load: textLoader(version.contentKey) },
|
|
499
523
|
};
|
|
500
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Which graph of a flow travels: the PUBLISHED one when the flow has one, the draft otherwise.
|
|
527
|
+
* Three call sites read it through this function rather than spelling the fallback out, because
|
|
528
|
+
* it is one decision and not three (#585).
|
|
529
|
+
*
|
|
530
|
+
* ⚠️ For a flow that was published and edited afterwards, this deliberately leaves the newer draft
|
|
531
|
+
* behind — and that is NOT the `version-history` case, which is why `Excluded` names it with a
|
|
532
|
+
* word of its own. The draft is the newest state there is; what decides against it is that the
|
|
533
|
+
* import cannot carry both. It creates rather than restores (ADR-0006): every flow arrives as a
|
|
534
|
+
* draft with one version and `publishedVersionId: null`, so "published v1 plus draft v2" has no
|
|
535
|
+
* shape on the far side, and carrying both would be flow version history — the thing that ADR
|
|
536
|
+
* refuses for exactly the reasons it lists.
|
|
537
|
+
*
|
|
538
|
+
* Of the two single answers the published one wins because it is the one that was IN EFFECT. A
|
|
539
|
+
* draft need never have been validated — publishing is what checks a graph — so moving the draft
|
|
540
|
+
* instead would replace a running process with somebody's unfinished edit, silently, at the one
|
|
541
|
+
* moment nobody can compare against the source any more.
|
|
542
|
+
*/
|
|
543
|
+
function exportedVersionId(flow) {
|
|
544
|
+
return flow.publishedVersionId ?? flow.currentVersionId;
|
|
545
|
+
}
|
|
501
546
|
function plannedFlow(flow, version, directory, used) {
|
|
502
547
|
const path = `${directory}${uniqueName(used, sanitizeName(flow.title), ".json")}`;
|
|
503
548
|
return {
|
|
@@ -547,9 +592,7 @@ export function createBundle(deps) {
|
|
|
547
592
|
if (actor.canReadFlows) {
|
|
548
593
|
const folderIds = new Set(rows.filter((row) => row.node.kind === "folder").map((row) => row.node.id));
|
|
549
594
|
const visible = (await deps.flows.listVisible(asFlowActor(actor))).filter((flow) => flow.parentId === null ? rootId === null : folderIds.has(flow.parentId));
|
|
550
|
-
const versionIds = visible
|
|
551
|
-
.map((flow) => flow.publishedVersionId ?? flow.currentVersionId)
|
|
552
|
-
.filter((id) => id !== null);
|
|
595
|
+
const versionIds = visible.map(exportedVersionId).filter((id) => id !== null);
|
|
553
596
|
for (const version of await deps.flows.getVersions(versionIds)) {
|
|
554
597
|
flowVersions.set(version.id, version);
|
|
555
598
|
}
|
|
@@ -591,7 +634,7 @@ export function createBundle(deps) {
|
|
|
591
634
|
continue;
|
|
592
635
|
}
|
|
593
636
|
if (item.flow !== null) {
|
|
594
|
-
const versionId = item.flow
|
|
637
|
+
const versionId = exportedVersionId(item.flow);
|
|
595
638
|
entries.push(plannedFlow(item.flow, versionId === null ? undefined : flowVersions.get(versionId), directory, used));
|
|
596
639
|
}
|
|
597
640
|
}
|
|
@@ -823,6 +866,9 @@ export function createBundle(deps) {
|
|
|
823
866
|
flows: typeof metadata.flows === "number" ? metadata.flows : 0,
|
|
824
867
|
rootNodeIds: Array.isArray(metadata.rootNodeIds) ? metadata.rootNodeIds : [],
|
|
825
868
|
replayed: true,
|
|
869
|
+
// Not read out of the stored metadata: a replay answers for an import this build performed,
|
|
870
|
+
// and the list is what this build leaves behind rather than something an earlier row said.
|
|
871
|
+
excluded: Excluded,
|
|
826
872
|
});
|
|
827
873
|
}
|
|
828
874
|
return {
|
|
@@ -840,7 +886,7 @@ export function createBundle(deps) {
|
|
|
840
886
|
if (!flow || flow.archivedAt !== null) {
|
|
841
887
|
throw new IntelError(404, "flow_not_found", "Flow was not found");
|
|
842
888
|
}
|
|
843
|
-
const versionId = flow
|
|
889
|
+
const versionId = exportedVersionId(flow);
|
|
844
890
|
const version = versionId === null ? undefined : (await deps.flows.getVersions([versionId]))[0];
|
|
845
891
|
const entries = [plannedFlow(flow, version, "", new Set())];
|
|
846
892
|
// A single flow's bundle is rooted at the flow itself: the manifest names it as the root the
|
|
@@ -1124,7 +1170,13 @@ export function createBundle(deps) {
|
|
|
1124
1170
|
for (const version of versions) {
|
|
1125
1171
|
await deps.indexing.enqueue(version.id);
|
|
1126
1172
|
}
|
|
1127
|
-
return {
|
|
1173
|
+
return {
|
|
1174
|
+
nodes: nodes.length,
|
|
1175
|
+
flows: flows.length,
|
|
1176
|
+
rootNodeIds,
|
|
1177
|
+
replayed: false,
|
|
1178
|
+
excluded: Excluded,
|
|
1179
|
+
};
|
|
1128
1180
|
},
|
|
1129
1181
|
};
|
|
1130
1182
|
}
|
package/dist/flows/flows.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { flowNodeLayer } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import { callersPhrase, namedOrCounted } from "../shared/callers/callers.js";
|
|
2
3
|
import { requireFutureExpiry } from "../shared/grant-expiry/grant-expiry.js";
|
|
3
4
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
4
5
|
import { plainTitle } from "../shared/plain-title/plain-title.js";
|
|
@@ -111,20 +112,28 @@ function toolStepDetail(missing) {
|
|
|
111
112
|
? `You do not reach this tool server in the portal: ${missing.join(", ")}`
|
|
112
113
|
: `You do not reach these tool servers in the portal: ${missing.join(", ")}`;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* ⚠️ How every refusal on this file names a node: by the label the canvas draws, because the id
|
|
117
|
+
* appears nowhere on screen. `Node n-doc is a link and must be attached to a step` sent an author
|
|
118
|
+
* looking for a string the editor never draws, and the way to the cause led through the contract
|
|
119
|
+
* schema instead of through the sentence (#508). The id stays beside it: a graph arrives over MCP
|
|
120
|
+
* as readily as from the canvas, and there the id is what the caller holds.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ One function rather than one spelling per call site (#551): the publish-time refusals name a
|
|
123
|
+
* node too, and two places building the same string by hand is how the two halves of one sentence
|
|
124
|
+
* drift while both keep looking right.
|
|
125
|
+
*/
|
|
126
|
+
function nodeName(node) {
|
|
127
|
+
return `"${node.label}" (${node.id})`;
|
|
128
|
+
}
|
|
114
129
|
export function compileFlow(graph) {
|
|
115
130
|
const nodes = new Map();
|
|
116
|
-
// ⚠️ A refusal names a node the way the canvas does — by its label — because the id appears
|
|
117
|
-
// nowhere on screen. `Node n-doc is a link and must be attached to a step` sent an author looking
|
|
118
|
-
// for a string the editor never draws, and the way to the cause led through the contract schema
|
|
119
|
-
// instead of through the sentence (#508). The id stays beside it: a graph arrives over MCP as
|
|
120
|
-
// readily as from the canvas, and there the id is what the caller holds.
|
|
121
|
-
//
|
|
122
131
|
// ⚠️ It stands ABOVE the loop that fills the map on purpose, so the duplicate-id refusal reaches
|
|
123
132
|
// it too — that one names the node the map already holds, which is the FIRST of the two. An id
|
|
124
133
|
// nothing knows falls back to the bare id, which is what the dangling-edge case wants anyway.
|
|
125
134
|
const named = (id) => {
|
|
126
135
|
const node = nodes.get(id);
|
|
127
|
-
return node ?
|
|
136
|
+
return node ? nodeName(node) : id;
|
|
128
137
|
};
|
|
129
138
|
for (const node of graph.nodes) {
|
|
130
139
|
if (nodes.has(node.id))
|
|
@@ -314,14 +323,14 @@ function decodeCursor(cursor) {
|
|
|
314
323
|
return { createdAt, id };
|
|
315
324
|
}
|
|
316
325
|
// ⚠️ Named where the reader may see it, counted where they may not — the same rule the folder's
|
|
317
|
-
// refusal follows (`
|
|
318
|
-
// flow one cannot see exists (ADR-0004 §3).
|
|
326
|
+
// refusal follows, out of the same place (`callersPhrase`): a refusal must not become a way of
|
|
327
|
+
// learning that a flow one cannot see exists (ADR-0004 §3).
|
|
328
|
+
//
|
|
329
|
+
// ⚠️ The SENTENCE stays its own, and that is deliberate (#593): the folder's says a grant cannot be
|
|
330
|
+
// narrowed yet, this one says a flow cannot be deleted for good. Same facts, different consequence
|
|
331
|
+
// — a single parameterised sentence would save a string and cost each refusal its reason.
|
|
319
332
|
function callersDetail(callers) {
|
|
320
|
-
|
|
321
|
-
const rest = callers.hidden === 0
|
|
322
|
-
? ""
|
|
323
|
-
: `${named ? " and " : ""}${callers.hidden} more flow${callers.hidden === 1 ? "" : "s"} you cannot see`;
|
|
324
|
-
return `Published flows still call this one: ${named}${rest}. Change or unpublish them first.`;
|
|
333
|
+
return `Published flows still call this one: ${callersPhrase(callers)}. Change or unpublish them first.`;
|
|
325
334
|
}
|
|
326
335
|
export function createFlows(deps) {
|
|
327
336
|
async function requireFlow(actor, flowId) {
|
|
@@ -1372,9 +1381,13 @@ export function createFlows(deps) {
|
|
|
1372
1381
|
throw new IntelError(409, "flow_not_archived", "Only an archived flow can be deleted for good. Archive it first.");
|
|
1373
1382
|
}
|
|
1374
1383
|
// A published caller would break at RUN TIME, in front of somebody who did not order this.
|
|
1384
|
+
// ⚠️ The callers travel as DATA beside the sentence (#593, the second half of #448): the
|
|
1385
|
+
// archive shows this refusal, and a German or Spanish screen could otherwise do nothing but
|
|
1386
|
+
// print the English one. The sentence stays for a model over MCP, which has nothing to
|
|
1387
|
+
// formulate with.
|
|
1375
1388
|
const callers = await deps.repository.flowCallers(actor, current.id);
|
|
1376
1389
|
if (callers.visible.length || callers.hidden) {
|
|
1377
|
-
throw new IntelError(409, "flow_in_use_by_flow", callersDetail(callers));
|
|
1390
|
+
throw new IntelError(409, "flow_in_use_by_flow", callersDetail(callers), namedOrCounted(callers));
|
|
1378
1391
|
}
|
|
1379
1392
|
// ⚠️ The one thing a flow has that a node does not: its runs can be the PARENT of runs
|
|
1380
1393
|
// belonging to other flows. Deleting them would take the record of who started those runs
|
|
@@ -1518,9 +1531,18 @@ export function createFlows(deps) {
|
|
|
1518
1531
|
compileFlow(version.graph);
|
|
1519
1532
|
const wanted = [...new Set(treeLinkNodes(version.graph).map(resourceIdOf))];
|
|
1520
1533
|
const reachable = new Set((await reachableNodes(actor, wanted)).map((reference) => reference.id));
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1534
|
+
// ⚠️ Asked over the deduplicated resource ids and REFUSED over the nodes (#551). The resource
|
|
1535
|
+
// id is what the ACL answers about, and it is also the one thing the author cannot look up:
|
|
1536
|
+
// it stands in no place the editor draws, so `Node reference is unavailable: n-01H…` named
|
|
1537
|
+
// the failure after the only participant nobody can find. The link node beside it carries the
|
|
1538
|
+
// label on the canvas, so the refusal names that instead.
|
|
1539
|
+
//
|
|
1540
|
+
// ⚠️ Naming the label reveals nothing the ACL is keeping: it is the author's own text in
|
|
1541
|
+
// their own graph, already on their screen — unlike the run-time refusal, which counts the
|
|
1542
|
+
// documents rather than naming them because there the READER may not see them (#17, #19).
|
|
1543
|
+
for (const node of treeLinkNodes(version.graph)) {
|
|
1544
|
+
if (!reachable.has(resourceIdOf(node))) {
|
|
1545
|
+
throw new IntelError(409, "flow_node_unavailable", `Node ${nodeName(node)} names a document you cannot reach`);
|
|
1524
1546
|
}
|
|
1525
1547
|
}
|
|
1526
1548
|
for (const node of version.graph.nodes) {
|
package/dist/http/http.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Hono } from "hono";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
10
10
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
11
|
-
import { problemDetails as problem } from "../shared/problem-details/problem-details.js";
|
|
11
|
+
import { intelProblem, problemDetails as problem, } from "../shared/problem-details/problem-details.js";
|
|
12
12
|
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
13
13
|
// A query string is a door to the outside like a body is, so what arrives through it is closed
|
|
14
14
|
// rather than tolerated: `z.strictObject` refuses an unknown field, and this is that refusal for the
|
|
@@ -94,7 +94,7 @@ export function createHttp(deps) {
|
|
|
94
94
|
}
|
|
95
95
|
app.onError((error, context) => {
|
|
96
96
|
if (error instanceof IntelError) {
|
|
97
|
-
return context.json(
|
|
97
|
+
return context.json(intelProblem(error), error.status);
|
|
98
98
|
}
|
|
99
99
|
if (error instanceof z.ZodError) {
|
|
100
100
|
return context.json(problem(400, "invalid_request", "Request validation failed", z.prettifyError(error)), 400);
|
|
@@ -440,8 +440,28 @@ export function createHttp(deps) {
|
|
|
440
440
|
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
|
441
441
|
return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
|
|
442
442
|
});
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
/**
|
|
444
|
+
* A single flow as the same bundle shape a node export makes: one entry plus the manifest
|
|
445
|
+
* (#136). `flows/read` like every other read of the graph.
|
|
446
|
+
*
|
|
447
|
+
* ⚠️ Deliberately no `flow_export` tool beside it, and the reason is NOT the one `node_export`
|
|
448
|
+
* carries (#436). There the memory argument decides — a subtree's zip would have to fit in Worker
|
|
449
|
+
* memory twice — and a flow bundle is one entry plus the manifest, so that argument does not
|
|
450
|
+
* reach here.
|
|
451
|
+
*
|
|
452
|
+
* What decides here is that the manifest of ONE entry answers nothing MCP does not already
|
|
453
|
+
* answer. `node_export` is worth a tool because its manifest is the PLAN of a subtree: every
|
|
454
|
+
* readable entry with its path, kind and media type, which is the map for fetching each one. For
|
|
455
|
+
* a single flow that map has one row, whose id the caller already holds — and the substance, the
|
|
456
|
+
* graph, is `flow_version_get` and `flow_graph`, with the record itself in `flow_get`. A tool
|
|
457
|
+
* whose whole answer is two other tools' answers costs bytes in every `tools/list` and makes the
|
|
458
|
+
* other names less reliable as a guide, which `packages/api/CLAUDE.md` treats as the cost it is.
|
|
459
|
+
*
|
|
460
|
+
* ⚠️ This is not the case `.claude/rules/destructive.md` warns about — "a way only the surface
|
|
461
|
+
* has is a way an agent works around". Nothing about a flow is unreachable over MCP; only the zip
|
|
462
|
+
* CONTAINER is HTTP-only, exactly as it is for nodes. `mcp.e2e.ts` pins that this exception is
|
|
463
|
+
* named rather than merely absent.
|
|
464
|
+
*/
|
|
445
465
|
app.get("/flows/:flowId/export", async (context) => {
|
|
446
466
|
const auth = requireCapability(context, "flows", "read");
|
|
447
467
|
const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
|
package/dist/intel/intel.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHttp } from "../http/http.js";
|
|
|
3
3
|
import { handleMcp } from "../mcp/mcp.js";
|
|
4
4
|
import { authorize, authorizeBearer, bearer, } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
5
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
|
-
import { problemDetails } from "../shared/problem-details/problem-details.js";
|
|
6
|
+
import { intelProblem, problemDetails } from "../shared/problem-details/problem-details.js";
|
|
7
7
|
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
8
8
|
export function createIntel(deps) {
|
|
9
9
|
const baseUrl = deps.baseUrl.replace(/\/+$/, "");
|
|
@@ -16,7 +16,7 @@ export function createIntel(deps) {
|
|
|
16
16
|
const app = new Hono();
|
|
17
17
|
app.onError((error, context) => {
|
|
18
18
|
if (error instanceof IntelError) {
|
|
19
|
-
return context.json(
|
|
19
|
+
return context.json(intelProblem(error), error.status);
|
|
20
20
|
}
|
|
21
21
|
// An IntelError is an expected refusal and explains itself; the unknown exception must leave a
|
|
22
22
|
// trace, or the 500 is undiagnosable — the worker answered, so the platform records no
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IdempotencyKey, IntelId } from "@anchrd/intel-contract";
|
|
2
2
|
import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, PurgeFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "@anchrd/intel-contract/flow";
|
|
3
3
|
import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "@anchrd/intel-contract/flow-run";
|
|
4
|
-
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, PurgeNodeInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
5
|
-
import { ListFlowGrantsInput, ListGrantsInput, RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "@anchrd/intel-contract/share";
|
|
4
|
+
import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
|
|
5
|
+
import { ListEffectiveAccessInput, ListFlowEffectiveAccessInput, ListFlowGrantsInput, ListGrantsInput, RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "@anchrd/intel-contract/share";
|
|
6
6
|
import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, GetTableInput, RedefineTableInput, UpdateTableRowsInput, } from "@anchrd/intel-contract/table";
|
|
7
7
|
import { ExecuteToolInput, TestToolInput } from "@anchrd/intel-contract/tool";
|
|
8
8
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -13,6 +13,44 @@ import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
|
13
13
|
function text(value) {
|
|
14
14
|
return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* ⚠️ The one thing a tool error loses on its way out, and the whole reason this `catch` exists
|
|
18
|
+
* (#597).
|
|
19
|
+
*
|
|
20
|
+
* A thrown `IntelError` is turned into a tool error by the SDK's `createToolError`, which keeps
|
|
21
|
+
* `error instanceof Error ? error.message : String(error)` and nothing else — so `callers`, the
|
|
22
|
+
* structured half of a refusal that NAMES flows (#448, #593), never reaches a model over MCP.
|
|
23
|
+
*
|
|
24
|
+
* It arrives here as a SECOND content block. The first one is the sentence, byte for byte what the
|
|
25
|
+
* SDK would have sent, so a reader that only takes text loses nothing: the machine-readable half
|
|
26
|
+
* stands beside the prose rather than instead of it.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ It wraps the handlers whose service can refuse WITH data — `node_grant_revoke` and
|
|
29
|
+
* `flow_purge` today — and deliberately NOT all sixty. For every other refusal there is nothing to
|
|
30
|
+
* carry, and turning each of their errors into JSON would change every tool for the sake of two.
|
|
31
|
+
* What keeps that from becoming a place to forget is where the rule is written down: at the field
|
|
32
|
+
* itself (`IntelError.callers`), in `packages/api/CLAUDE.md`, and as two tests in `mcp.e2e.ts` —
|
|
33
|
+
* whoever attaches `callers` to a new refusal wraps its handler here.
|
|
34
|
+
*/
|
|
35
|
+
async function refusing(run) {
|
|
36
|
+
try {
|
|
37
|
+
return text(await run());
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!(error instanceof IntelError) || !error.callers)
|
|
41
|
+
throw error;
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{ type: "text", text: error.message },
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: JSON.stringify({ code: error.code, callers: error.callers }),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
16
54
|
/**
|
|
17
55
|
* ⚠️ One name per tool, and the second one is gone for good (#149).
|
|
18
56
|
*
|
|
@@ -464,6 +502,34 @@ export async function handleMcp(request, deps) {
|
|
|
464
502
|
openWorldHint: false,
|
|
465
503
|
},
|
|
466
504
|
}, async (input) => text(await deps.nodes.archive(actor, input)));
|
|
505
|
+
/**
|
|
506
|
+
* ⚠️ The look before the irreversible step, and it is on this surface because the step itself
|
|
507
|
+
* is (#586). `node_purge` has stood here since #457 for the reason #436 states — a way only the
|
|
508
|
+
* interface has is a way an agent goes around — but until now the COUNT did not, so an agent
|
|
509
|
+
* could delete for good without being able to see first what went with it. A person in the
|
|
510
|
+
* interface is shown both numbers and then confirms.
|
|
511
|
+
*
|
|
512
|
+
* That is exactly the state `.claude/rules/destructive.md` refuses in its own words: a cascade
|
|
513
|
+
* turns one confirmed click into an unknown number of deletions, and "the question can no
|
|
514
|
+
* longer name what disappears". Here the question could not be ASKED, which is the same gap one
|
|
515
|
+
* step earlier — and the half that was missing was the safe half.
|
|
516
|
+
*
|
|
517
|
+
* ⚠️ It answers about a tree, not about one row: `totalItems` counts the nodes AND the flows
|
|
518
|
+
* beneath it, the same two tables the purge itself walks. `inboundLinks` is the other kind of
|
|
519
|
+
* consequence — mentions elsewhere that will break and are deliberately not refused.
|
|
520
|
+
*/
|
|
521
|
+
server.registerTool("node_purge_preview", {
|
|
522
|
+
title: "Preview a permanent deletion",
|
|
523
|
+
description: "Count what deleting one ARCHIVED node for good would take with it: every item in its subtree, documents and flows alike, and how many documents elsewhere link to it. It changes nothing. Ask before node_purge — afterwards there is no row left to count, and the links that break are not refused, only reported.",
|
|
524
|
+
inputSchema: PurgeNodePreviewInput,
|
|
525
|
+
annotations: {
|
|
526
|
+
title: "Preview a permanent deletion",
|
|
527
|
+
readOnlyHint: true,
|
|
528
|
+
destructiveHint: false,
|
|
529
|
+
idempotentHint: true,
|
|
530
|
+
openWorldHint: false,
|
|
531
|
+
},
|
|
532
|
+
}, async (input) => text(await deps.nodes.purgePreview(actor, input)));
|
|
467
533
|
// ⚠️ It exists on this surface for the reason #436 states: a way only the interface has is a way
|
|
468
534
|
// an agent goes around, by looking for the row in the database. What it may do is bounded by the
|
|
469
535
|
// same rules the interface obeys — only an archived node, only an empty folder, never one a flow
|
|
@@ -498,6 +564,35 @@ export async function handleMcp(request, deps) {
|
|
|
498
564
|
openWorldHint: false,
|
|
499
565
|
},
|
|
500
566
|
}, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
|
|
567
|
+
/**
|
|
568
|
+
* ⚠️ The neighbour above answers a NARROWER question, and that is why this one has to be here
|
|
569
|
+
* (#586). `node_grant_list` reads the rows on this node; this resolves the inheritance — every
|
|
570
|
+
* grant along the folders above plus the owners, who reach it without a grant row existing at
|
|
571
|
+
* all. On the everyday node, whose access comes from the folder it sits in, the first answer is
|
|
572
|
+
* empty and this one is not.
|
|
573
|
+
*
|
|
574
|
+
* Two queries with similar names are two different questions, and `.claude/rules/destructive.md`
|
|
575
|
+
* records what it costs to confuse them: in #457 a refusal asked the one that sounded right and
|
|
576
|
+
* never fired. Here the cost is the same shape and lands on the sharing path — an agent asked
|
|
577
|
+
* "who can see this" reaches for the only name it has, is answered `[]`, and reports that
|
|
578
|
+
* nobody can, about a document the whole organization opens. Silence would not be a missing
|
|
579
|
+
* answer here, it would be a confident wrong one.
|
|
580
|
+
*
|
|
581
|
+
* ⚠️ The list is for reading, never for revoking: only the entries whose `resourceId` is this
|
|
582
|
+
* node can be revoked here, the rest belong to the folder that granted them.
|
|
583
|
+
*/
|
|
584
|
+
server.registerTool("node_effective_access_list", {
|
|
585
|
+
title: "List effective node access",
|
|
586
|
+
description: "List everybody who reaches one node once inheritance is resolved: the grants on it AND on every folder above it, plus the owners, who need no grant. node_grant_list answers the narrower question — the rows sitting on this node alone — and on a node that inherits its access that answer is empty while this one is not. An entry whose resourceId is another node was granted there and can only be revoked there.",
|
|
587
|
+
inputSchema: ListEffectiveAccessInput,
|
|
588
|
+
annotations: {
|
|
589
|
+
title: "List effective node access",
|
|
590
|
+
readOnlyHint: true,
|
|
591
|
+
destructiveHint: false,
|
|
592
|
+
idempotentHint: true,
|
|
593
|
+
openWorldHint: false,
|
|
594
|
+
},
|
|
595
|
+
}, async (input) => text(await deps.nodes.listEffectiveAccess(actor, input.resourceId)));
|
|
501
596
|
server.registerTool("node_grant_create", {
|
|
502
597
|
title: "Grant node access",
|
|
503
598
|
description: "Grant inherited node access to a Gate user, verified email, or the organization.",
|
|
@@ -521,7 +616,7 @@ export async function handleMcp(request, deps) {
|
|
|
521
616
|
idempotentHint: true,
|
|
522
617
|
openWorldHint: false,
|
|
523
618
|
},
|
|
524
|
-
}, async (input) =>
|
|
619
|
+
}, async (input) => await refusing(() => deps.nodes.revokeGrant(actor, input)));
|
|
525
620
|
}
|
|
526
621
|
if (permits(deps.authorization, "flows", "read")) {
|
|
527
622
|
server.registerTool("flow_list", {
|
|
@@ -681,6 +776,24 @@ export async function handleMcp(request, deps) {
|
|
|
681
776
|
openWorldHint: false,
|
|
682
777
|
},
|
|
683
778
|
}, async (input) => text(await deps.flows.listGrants(flowActor, input.flowId)));
|
|
779
|
+
// ⚠️ The flow half of the node tool three groups up, and it is a SECOND tool rather than a
|
|
780
|
+
// widened one because it is a second question (#586): the answer is assembled from `flow_grants`
|
|
781
|
+
// and from the node grants of the folders above, and the owners come from the flow itself as
|
|
782
|
+
// well as from those folders. One tool over a `resourceId` that means either would have to guess
|
|
783
|
+
// which table to ask, and a caller passing the wrong kind would be answered "nobody" instead of
|
|
784
|
+
// being told.
|
|
785
|
+
server.registerTool("flow_effective_access_list", {
|
|
786
|
+
title: "List effective flow access",
|
|
787
|
+
description: "List everybody who reaches one flow once inheritance is resolved: the grants on the flow AND on the folders it is filed under, plus the owners, who need no grant. flow_grant_list answers the narrower question — the rows sitting on the flow itself — and for a flow whose access comes from its folder that answer is empty while this one is not. An entry whose resourceId is a folder was granted there and can only be revoked there.",
|
|
788
|
+
inputSchema: ListFlowEffectiveAccessInput,
|
|
789
|
+
annotations: {
|
|
790
|
+
title: "List effective flow access",
|
|
791
|
+
readOnlyHint: true,
|
|
792
|
+
destructiveHint: false,
|
|
793
|
+
idempotentHint: true,
|
|
794
|
+
openWorldHint: false,
|
|
795
|
+
},
|
|
796
|
+
}, async (input) => text(await deps.flows.listEffectiveAccess(flowActor, input.flowId)));
|
|
684
797
|
server.registerTool("flow_grant_create", {
|
|
685
798
|
title: "Grant flow access",
|
|
686
799
|
description: "Grant access to one flow for a Gate user, verified email, or the organization. The grant reaches this flow alone — not the folder it is filed in, and not the flows it calls; the answer names what it does not cover.",
|
|
@@ -761,7 +874,7 @@ export async function handleMcp(request, deps) {
|
|
|
761
874
|
idempotentHint: false,
|
|
762
875
|
openWorldHint: false,
|
|
763
876
|
},
|
|
764
|
-
}, async (input) =>
|
|
877
|
+
}, async (input) => await refusing(() => deps.flows.purge(flowActor, input)));
|
|
765
878
|
server.registerTool("flow_version_create", {
|
|
766
879
|
title: "Create flow version",
|
|
767
880
|
description: "Append a validated immutable flow graph version with optimistic concurrency.",
|
package/dist/nodes/nodes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TableMediaType } from "@anchrd/intel-contract/table";
|
|
2
|
+
import { callersPhrase, namedOrCounted } from "../shared/callers/callers.js";
|
|
2
3
|
import { encodeCsv, parseCsv } from "../shared/csv/csv.js";
|
|
3
4
|
import { requireFutureExpiry } from "../shared/grant-expiry/grant-expiry.js";
|
|
4
5
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
@@ -28,13 +29,15 @@ function applicableVerbs(kind) {
|
|
|
28
29
|
}
|
|
29
30
|
// ⚠️ The refusal has to be actionable without becoming a directory of the tree. Whoever holds
|
|
30
31
|
// `share` on one folder must not learn the titles of flows they may not see, so the ones they may
|
|
31
|
-
// see are named and the rest are only counted (ADR-0004 §3, and #17's review)
|
|
32
|
+
// see are named and the rest are only counted (ADR-0004 §3, and #17's review) — which is the whole
|
|
33
|
+
// of what `callersPhrase` does, in the one place both refusals read it from.
|
|
34
|
+
//
|
|
35
|
+
// The sentence stays for whoever has nothing to formulate with — a model over MCP reads it as it
|
|
36
|
+
// stands — and `namedOrCounted` carries the same decision as data, so a screen can say it in its
|
|
37
|
+
// own language (#448). Both come out of one place: the split between named and counted is an
|
|
38
|
+
// authorization answer, and a surface that recomputed it would be deciding what it may not.
|
|
32
39
|
function callersDetail(callers) {
|
|
33
|
-
|
|
34
|
-
const rest = callers.hidden === 0
|
|
35
|
-
? ""
|
|
36
|
-
: `${named ? " and " : ""}${callers.hidden} more flow${callers.hidden === 1 ? "" : "s"} you cannot see`;
|
|
37
|
-
return `Flows outside this folder call into it: ${named}${rest}. Change or unpublish them before narrowing the folder.`;
|
|
40
|
+
return `Flows outside this folder call into it: ${callersPhrase(callers)}. Change or unpublish them before narrowing the folder.`;
|
|
38
41
|
}
|
|
39
42
|
// The grantee as the ACL sees them, and as nothing else: an identity with no capability of its own,
|
|
40
43
|
// never `isAdmin`. It answers for Intel's resource ACLs only — whether Gate hands this person
|
|
@@ -1085,7 +1088,7 @@ export function createNodes(deps) {
|
|
|
1085
1088
|
!(await deps.repository.organizationExecuteReaches(input.resourceId, input.grantId))) {
|
|
1086
1089
|
const callers = await deps.externalFlowCallers(actor, input.resourceId);
|
|
1087
1090
|
if (callers.visible.length || callers.hidden) {
|
|
1088
|
-
throw new IntelError(409, "folder_execute_in_use", callersDetail(callers));
|
|
1091
|
+
throw new IntelError(409, "folder_execute_in_use", callersDetail(callers), namedOrCounted(callers));
|
|
1089
1092
|
}
|
|
1090
1093
|
}
|
|
1091
1094
|
return {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NamedOrCounted } from "@anchrd/intel-contract";
|
|
2
|
+
type Callers = {
|
|
3
|
+
visible: string[];
|
|
4
|
+
hidden: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* ⚠️ Named where the reader may see it, counted where they may not (ADR-0004 §3). A refusal has to
|
|
8
|
+
* be actionable without becoming a way of reading the tree: whoever holds one grant on one folder
|
|
9
|
+
* must not learn the titles of flows they are refused directly.
|
|
10
|
+
*
|
|
11
|
+
* This is the middle of both refusals that ask the question — narrowing a folder (`nodes.ts`) and
|
|
12
|
+
* deleting a flow for good (`flows.ts`). ⚠️ **The two SENTENCES around it stay two**, because they
|
|
13
|
+
* say different things to different people: one is about a folder that is about to lose a grant,
|
|
14
|
+
* the other about a flow that is about to be gone. Folding them into one parameterised sentence
|
|
15
|
+
* would buy a shared string and cost the reason each one gives.
|
|
16
|
+
*/
|
|
17
|
+
export declare function callersPhrase(callers: Callers): string;
|
|
18
|
+
/**
|
|
19
|
+
* The same two facts the sentence is built from, in the shape a grant's own warning already uses
|
|
20
|
+
* (`UnreadableNodes`, #448). One question — what may you see, and of how much — deserves one answer
|
|
21
|
+
* shape on every surface, and a screen that has to write the sentence itself needs the facts and
|
|
22
|
+
* not the sentence.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ It reads the SAME argument the phrase above reads. A surface that recomputed the split would
|
|
25
|
+
* be deciding what it may not, and a second decision here would be the one that drifts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function namedOrCounted(callers: Callers): NamedOrCounted;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠️ Named where the reader may see it, counted where they may not (ADR-0004 §3). A refusal has to
|
|
3
|
+
* be actionable without becoming a way of reading the tree: whoever holds one grant on one folder
|
|
4
|
+
* must not learn the titles of flows they are refused directly.
|
|
5
|
+
*
|
|
6
|
+
* This is the middle of both refusals that ask the question — narrowing a folder (`nodes.ts`) and
|
|
7
|
+
* deleting a flow for good (`flows.ts`). ⚠️ **The two SENTENCES around it stay two**, because they
|
|
8
|
+
* say different things to different people: one is about a folder that is about to lose a grant,
|
|
9
|
+
* the other about a flow that is about to be gone. Folding them into one parameterised sentence
|
|
10
|
+
* would buy a shared string and cost the reason each one gives.
|
|
11
|
+
*/
|
|
12
|
+
export function callersPhrase(callers) {
|
|
13
|
+
const named = callers.visible.map((title) => `“${title}”`).join(", ");
|
|
14
|
+
const rest = callers.hidden === 0
|
|
15
|
+
? ""
|
|
16
|
+
: `${named ? " and " : ""}${callers.hidden} more flow${callers.hidden === 1 ? "" : "s"} you cannot see`;
|
|
17
|
+
return `${named}${rest}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The same two facts the sentence is built from, in the shape a grant's own warning already uses
|
|
21
|
+
* (`UnreadableNodes`, #448). One question — what may you see, and of how much — deserves one answer
|
|
22
|
+
* shape on every surface, and a screen that has to write the sentence itself needs the facts and
|
|
23
|
+
* not the sentence.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ It reads the SAME argument the phrase above reads. A surface that recomputed the split would
|
|
26
|
+
* be deciding what it may not, and a second decision here would be the one that drifts.
|
|
27
|
+
*/
|
|
28
|
+
export function namedOrCounted(callers) {
|
|
29
|
+
return { titles: callers.visible, hidden: callers.hidden };
|
|
30
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import type { NamedOrCounted } from "@anchrd/intel-contract";
|
|
1
2
|
export declare class IntelError extends Error {
|
|
2
3
|
readonly status: number;
|
|
3
4
|
readonly code: string;
|
|
4
|
-
|
|
5
|
+
readonly callers?: NamedOrCounted | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* ⚠️ `callers` is what a refusal that NAMES flows carries beside its sentence — today
|
|
8
|
+
* `folder_execute_in_use` (#448) and `flow_in_use_by_flow` (#593). It rides on the error rather
|
|
9
|
+
* than on the route because the split it belongs to is an application-layer one: who may be named
|
|
10
|
+
* and who may only be counted is an authorization answer, and it must be the same over HTTP, over
|
|
11
|
+
* MCP and in the browser. A route that assembled it would be a second place for that rule to live.
|
|
12
|
+
*
|
|
13
|
+
* `message` still carries the finished English sentence — a reader that only takes text must lose
|
|
14
|
+
* nothing — and the field is what lets a screen say it in its own language instead.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ **Setting this field is only half of reaching MCP.** The SDK formats a thrown error through
|
|
17
|
+
* `createToolError`, which keeps `message` and drops everything else, so a tool whose handler is
|
|
18
|
+
* not wrapped in `refusing()` (`mcp/mcp.ts`) answers prose and nothing more (#597). Whoever
|
|
19
|
+
* attaches `callers` to a new refusal wraps that tool's handler in the same change.
|
|
20
|
+
*/
|
|
21
|
+
constructor(status: number, code: string, message: string, callers?: NamedOrCounted | undefined);
|
|
5
22
|
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
export class IntelError extends Error {
|
|
2
2
|
status;
|
|
3
3
|
code;
|
|
4
|
-
|
|
4
|
+
callers;
|
|
5
|
+
/**
|
|
6
|
+
* ⚠️ `callers` is what a refusal that NAMES flows carries beside its sentence — today
|
|
7
|
+
* `folder_execute_in_use` (#448) and `flow_in_use_by_flow` (#593). It rides on the error rather
|
|
8
|
+
* than on the route because the split it belongs to is an application-layer one: who may be named
|
|
9
|
+
* and who may only be counted is an authorization answer, and it must be the same over HTTP, over
|
|
10
|
+
* MCP and in the browser. A route that assembled it would be a second place for that rule to live.
|
|
11
|
+
*
|
|
12
|
+
* `message` still carries the finished English sentence — a reader that only takes text must lose
|
|
13
|
+
* nothing — and the field is what lets a screen say it in its own language instead.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ **Setting this field is only half of reaching MCP.** The SDK formats a thrown error through
|
|
16
|
+
* `createToolError`, which keeps `message` and drops everything else, so a tool whose handler is
|
|
17
|
+
* not wrapped in `refusing()` (`mcp/mcp.ts`) answers prose and nothing more (#597). Whoever
|
|
18
|
+
* attaches `callers` to a new refusal wraps that tool's handler in the same change.
|
|
19
|
+
*/
|
|
20
|
+
constructor(status, code, message, callers) {
|
|
5
21
|
super(message);
|
|
6
22
|
this.status = status;
|
|
7
23
|
this.code = code;
|
|
24
|
+
this.callers = callers;
|
|
8
25
|
this.name = "IntelError";
|
|
9
26
|
}
|
|
10
27
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IntelError } from "../intel-error/intel-error.js";
|
|
1
2
|
export declare function problemDetails(status: number, code: string, title: string, detail?: string): {
|
|
2
3
|
type: string;
|
|
3
4
|
status: number;
|
|
@@ -5,4 +6,25 @@ export declare function problemDetails(status: number, code: string, title: stri
|
|
|
5
6
|
detail?: string | undefined;
|
|
6
7
|
instance?: string | undefined;
|
|
7
8
|
code?: string | undefined;
|
|
9
|
+
callers?: {
|
|
10
|
+
titles: string[];
|
|
11
|
+
hidden: number;
|
|
12
|
+
} | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* One refusal, one document — written here rather than at each `onError`, because two apps turn an
|
|
16
|
+
* `IntelError` into a body and an extension member added at one of them would silently be missing
|
|
17
|
+
* from the other (#448). Whatever the application layer decided to say travels with it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function intelProblem(error: IntelError): {
|
|
20
|
+
type: string;
|
|
21
|
+
status: number;
|
|
22
|
+
title: string;
|
|
23
|
+
detail?: string | undefined;
|
|
24
|
+
instance?: string | undefined;
|
|
25
|
+
code?: string | undefined;
|
|
26
|
+
callers?: {
|
|
27
|
+
titles: string[];
|
|
28
|
+
hidden: number;
|
|
29
|
+
} | undefined;
|
|
8
30
|
};
|
|
@@ -2,3 +2,17 @@ import { ProblemDetails } from "@anchrd/intel-contract";
|
|
|
2
2
|
export function problemDetails(status, code, title, detail) {
|
|
3
3
|
return ProblemDetails.parse({ type: "about:blank", status, code, title, detail });
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* One refusal, one document — written here rather than at each `onError`, because two apps turn an
|
|
7
|
+
* `IntelError` into a body and an extension member added at one of them would silently be missing
|
|
8
|
+
* from the other (#448). Whatever the application layer decided to say travels with it.
|
|
9
|
+
*/
|
|
10
|
+
export function intelProblem(error) {
|
|
11
|
+
return ProblemDetails.parse({
|
|
12
|
+
type: "about:blank",
|
|
13
|
+
status: error.status,
|
|
14
|
+
code: error.code,
|
|
15
|
+
title: error.message,
|
|
16
|
+
callers: error.callers,
|
|
17
|
+
});
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@anchrd/gate-sdk": "^0.
|
|
46
|
-
"@anchrd/intel-contract": "^0.
|
|
45
|
+
"@anchrd/gate-sdk": "^0.20.0",
|
|
46
|
+
"@anchrd/intel-contract": "^0.22.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
49
|
"fflate": "^0.8.3",
|