aontu 0.52.1 → 0.53.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 +88 -0
- package/bin/aontu-mcp.js +4 -0
- package/dist/agentsmd.d.ts +16 -0
- package/dist/agentsmd.js +107 -0
- package/dist/agentsmd.js.map +1 -0
- package/dist/aontu.d.ts +14 -3
- package/dist/aontu.js +96 -4
- package/dist/aontu.js.map +1 -1
- package/dist/cli.d.ts +44 -1
- package/dist/cli.js +2401 -44
- package/dist/cli.js.map +1 -1
- package/dist/ctx.d.ts +13 -0
- package/dist/ctx.js +43 -0
- package/dist/ctx.js.map +1 -1
- package/dist/diff.d.ts +22 -0
- package/dist/diff.js +141 -0
- package/dist/diff.js.map +1 -0
- package/dist/err.d.ts +3 -1
- package/dist/err.js +38 -7
- package/dist/err.js.map +1 -1
- package/dist/graph.d.ts +16 -0
- package/dist/graph.js +73 -0
- package/dist/graph.js.map +1 -0
- package/dist/hcanon.d.ts +3 -0
- package/dist/hcanon.js +146 -0
- package/dist/hcanon.js.map +1 -0
- package/dist/hints.js +167 -4
- package/dist/hints.js.map +1 -1
- package/dist/jsonschema.d.ts +20 -0
- package/dist/jsonschema.js +391 -0
- package/dist/jsonschema.js.map +1 -0
- package/dist/lang.js +512 -69
- package/dist/lang.js.map +1 -1
- package/dist/lsp.d.ts +9 -2
- package/dist/lsp.js +262 -47
- package/dist/lsp.js.map +1 -1
- package/dist/mcp-server.d.ts +20 -0
- package/dist/mcp-server.js +147 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.js +814 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mod-tool.d.ts +58 -0
- package/dist/mod-tool.js +498 -0
- package/dist/mod-tool.js.map +1 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +250 -0
- package/dist/mod.js.map +1 -0
- package/dist/patch.d.ts +44 -0
- package/dist/patch.js +506 -0
- package/dist/patch.js.map +1 -0
- package/dist/provenance.d.ts +40 -0
- package/dist/provenance.js +335 -0
- package/dist/provenance.js.map +1 -0
- package/dist/query.d.ts +27 -0
- package/dist/query.js +294 -0
- package/dist/query.js.map +1 -0
- package/dist/reach.d.ts +14 -0
- package/dist/reach.js +140 -0
- package/dist/reach.js.map +1 -0
- package/dist/relation.d.ts +19 -0
- package/dist/relation.js +305 -0
- package/dist/relation.js.map +1 -0
- package/dist/report-sarif.d.ts +14 -0
- package/dist/report-sarif.js +102 -0
- package/dist/report-sarif.js.map +1 -0
- package/dist/site.d.ts +4 -0
- package/dist/site.js +31 -0
- package/dist/site.js.map +1 -1
- package/dist/std.d.ts +1 -0
- package/dist/std.js +73 -0
- package/dist/std.js.map +1 -0
- package/dist/subsume.d.ts +39 -0
- package/dist/subsume.js +526 -0
- package/dist/subsume.js.map +1 -0
- package/dist/trim.d.ts +19 -0
- package/dist/trim.js +155 -0
- package/dist/trim.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type.d.ts +17 -1
- package/dist/type.js.map +1 -1
- package/dist/unify.d.ts +2 -1
- package/dist/unify.js +253 -36
- package/dist/unify.js.map +1 -1
- package/dist/utility.d.ts +9 -1
- package/dist/utility.js +122 -1
- package/dist/utility.js.map +1 -1
- package/dist/val/AggFuncVal.d.ts +33 -0
- package/dist/val/AggFuncVal.js +202 -0
- package/dist/val/AggFuncVal.js.map +1 -0
- package/dist/val/ArithFuncVal.d.ts +31 -0
- package/dist/val/ArithFuncVal.js +62 -0
- package/dist/val/ArithFuncVal.js.map +1 -0
- package/dist/val/BagVal.d.ts +5 -0
- package/dist/val/BagVal.js +96 -5
- package/dist/val/BagVal.js.map +1 -1
- package/dist/val/CloseFuncVal.js +9 -1
- package/dist/val/CloseFuncVal.js.map +1 -1
- package/dist/val/ConjunctVal.d.ts +1 -1
- package/dist/val/ConjunctVal.js +19 -0
- package/dist/val/ConjunctVal.js.map +1 -1
- package/dist/val/ConstraintVal.d.ts +7 -1
- package/dist/val/ConstraintVal.js +490 -40
- package/dist/val/ConstraintVal.js.map +1 -1
- package/dist/val/CopyFuncVal.d.ts +1 -2
- package/dist/val/CopyFuncVal.js +7 -0
- package/dist/val/CopyFuncVal.js.map +1 -1
- package/dist/val/Decimal.d.ts +1 -0
- package/dist/val/Decimal.js +13 -0
- package/dist/val/Decimal.js.map +1 -1
- package/dist/val/DeprecateFuncVal.d.ts +11 -0
- package/dist/val/DeprecateFuncVal.js +47 -0
- package/dist/val/DeprecateFuncVal.js.map +1 -0
- package/dist/val/DisjunctVal.js +130 -21
- package/dist/val/DisjunctVal.js.map +1 -1
- package/dist/val/EachFuncVal.d.ts +15 -0
- package/dist/val/EachFuncVal.js +75 -0
- package/dist/val/EachFuncVal.js.map +1 -0
- package/dist/val/ExpectVal.js +27 -4
- package/dist/val/ExpectVal.js.map +1 -1
- package/dist/val/FeatureVal.js +1 -1
- package/dist/val/FeatureVal.js.map +1 -1
- package/dist/val/FilterFuncVal.d.ts +15 -0
- package/dist/val/FilterFuncVal.js +91 -0
- package/dist/val/FilterFuncVal.js.map +1 -0
- package/dist/val/FuncBaseVal.d.ts +6 -1
- package/dist/val/FuncBaseVal.js +171 -1
- package/dist/val/FuncBaseVal.js.map +1 -1
- package/dist/val/HideFuncVal.js.map +1 -1
- package/dist/val/IdFuncVal.d.ts +13 -0
- package/dist/val/IdFuncVal.js +54 -0
- package/dist/val/IdFuncVal.js.map +1 -0
- package/dist/val/JunctionVal.js +7 -1
- package/dist/val/JunctionVal.js.map +1 -1
- package/dist/val/KeyFuncVal.d.ts +1 -1
- package/dist/val/KeyFuncVal.js +38 -30
- package/dist/val/KeyFuncVal.js.map +1 -1
- package/dist/val/ListVal.js +101 -14
- package/dist/val/ListVal.js.map +1 -1
- package/dist/val/LowerFuncVal.js.map +1 -1
- package/dist/val/MapVal.js +93 -8
- package/dist/val/MapVal.js.map +1 -1
- package/dist/val/MatchFuncVal.d.ts +15 -0
- package/dist/val/MatchFuncVal.js +107 -0
- package/dist/val/MatchFuncVal.js.map +1 -0
- package/dist/val/MoveFuncVal.js.map +1 -1
- package/dist/val/NilVal.js +24 -0
- package/dist/val/NilVal.js.map +1 -1
- package/dist/val/OpBaseVal.d.ts +1 -1
- package/dist/val/OpBaseVal.js +19 -1
- package/dist/val/OpBaseVal.js.map +1 -1
- package/dist/val/OpenFuncVal.js +4 -1
- package/dist/val/OpenFuncVal.js.map +1 -1
- package/dist/val/PackFuncVal.d.ts +15 -0
- package/dist/val/PackFuncVal.js +108 -0
- package/dist/val/PackFuncVal.js.map +1 -0
- package/dist/val/PathFuncVal.js.map +1 -1
- package/dist/val/PlaceVal.d.ts +13 -0
- package/dist/val/PlaceVal.js +131 -0
- package/dist/val/PlaceVal.js.map +1 -0
- package/dist/val/PlusOpVal.js +11 -2
- package/dist/val/PlusOpVal.js.map +1 -1
- package/dist/val/PrefFuncVal.js.map +1 -1
- package/dist/val/PrefVal.d.ts +2 -2
- package/dist/val/PrefVal.js +78 -23
- package/dist/val/PrefVal.js.map +1 -1
- package/dist/val/RefVal.d.ts +1 -1
- package/dist/val/RefVal.js +89 -7
- package/dist/val/RefVal.js.map +1 -1
- package/dist/val/ReferFuncVal.d.ts +36 -0
- package/dist/val/ReferFuncVal.js +303 -0
- package/dist/val/ReferFuncVal.js.map +1 -0
- package/dist/val/ScalarKindVal.d.ts +1 -2
- package/dist/val/ScalarKindVal.js +0 -11
- package/dist/val/ScalarKindVal.js.map +1 -1
- package/dist/val/TopVal.js.map +1 -1
- package/dist/val/TypeFuncVal.js.map +1 -1
- package/dist/val/UpperFuncVal.js.map +1 -1
- package/dist/val/Val.d.ts +8 -1
- package/dist/val/Val.js +150 -1
- package/dist/val/Val.js.map +1 -1
- package/dist/val/VarVal.js.map +1 -1
- package/dist/val/arith.d.ts +6 -0
- package/dist/val/arith.js +170 -0
- package/dist/val/arith.js.map +1 -0
- package/dist/vet.d.ts +45 -0
- package/dist/vet.js +776 -0
- package/dist/vet.js.map +1 -0
- package/dist/walk.d.ts +2 -0
- package/dist/walk.js +91 -0
- package/dist/walk.js.map +1 -0
- package/grammar/aontu.gbnf +130 -0
- package/grammar/aontu.lark +113 -0
- package/package.json +21 -6
- package/skill/SKILL.md +37 -0
- package/skill/error-codes.md +62 -0
- package/skill/examples.md +99 -0
- package/skill/grammar-card.md +57 -0
- package/src/agentsmd.ts +135 -0
- package/src/aontu.ts +135 -4
- package/src/cli.ts +2858 -71
- package/src/ctx.ts +73 -1
- package/src/diff.ts +196 -0
- package/src/err.ts +42 -7
- package/src/graph.ts +135 -0
- package/src/hcanon.ts +169 -0
- package/src/hints.ts +208 -4
- package/src/jsonschema.ts +511 -0
- package/src/lang.ts +570 -70
- package/src/lsp.ts +281 -48
- package/src/mcp-server.ts +187 -0
- package/src/mcp.ts +993 -0
- package/src/mod-tool.ts +679 -0
- package/src/mod.ts +344 -0
- package/src/patch.ts +624 -0
- package/src/provenance.ts +430 -0
- package/src/query.ts +379 -0
- package/src/reach.ts +184 -0
- package/src/relation.ts +395 -0
- package/src/report-sarif.ts +137 -0
- package/src/site.ts +36 -1
- package/src/std.ts +73 -0
- package/src/subsume.ts +690 -0
- package/src/trim.ts +195 -0
- package/src/tsconfig.json +10 -4
- package/src/type.ts +51 -2
- package/src/unify.ts +274 -36
- package/src/utility.ts +139 -1
- package/src/val/AggFuncVal.ts +319 -0
- package/src/val/ArithFuncVal.ts +108 -0
- package/src/val/BagVal.ts +101 -4
- package/src/val/CloseFuncVal.ts +9 -1
- package/src/val/ConjunctVal.ts +20 -0
- package/src/val/ConstraintVal.ts +542 -43
- package/src/val/CopyFuncVal.ts +7 -1
- package/src/val/Decimal.ts +15 -0
- package/src/val/DeprecateFuncVal.ts +84 -0
- package/src/val/DisjunctVal.ts +139 -28
- package/src/val/EachFuncVal.ts +133 -0
- package/src/val/ExpectVal.ts +28 -6
- package/src/val/FeatureVal.ts +1 -1
- package/src/val/FilterFuncVal.ts +154 -0
- package/src/val/FuncBaseVal.ts +192 -1
- package/src/val/HideFuncVal.ts +0 -2
- package/src/val/IdFuncVal.ts +91 -0
- package/src/val/JunctionVal.ts +7 -1
- package/src/val/KeyFuncVal.ts +39 -35
- package/src/val/ListVal.ts +108 -15
- package/src/val/LowerFuncVal.ts +0 -1
- package/src/val/MapVal.ts +99 -8
- package/src/val/MatchFuncVal.ts +176 -0
- package/src/val/MoveFuncVal.ts +0 -2
- package/src/val/NilVal.ts +25 -0
- package/src/val/OpBaseVal.ts +20 -1
- package/src/val/OpenFuncVal.ts +4 -2
- package/src/val/PackFuncVal.ts +175 -0
- package/src/val/PathFuncVal.ts +0 -1
- package/src/val/PlaceVal.ts +193 -0
- package/src/val/PlusOpVal.ts +11 -2
- package/src/val/PrefFuncVal.ts +0 -1
- package/src/val/PrefVal.ts +79 -36
- package/src/val/RefVal.ts +91 -8
- package/src/val/ReferFuncVal.ts +387 -0
- package/src/val/ScalarKindVal.ts +0 -13
- package/src/val/TopVal.ts +0 -1
- package/src/val/TypeFuncVal.ts +0 -2
- package/src/val/UpperFuncVal.ts +0 -1
- package/src/val/Val.ts +205 -2
- package/src/val/VarVal.ts +0 -1
- package/src/val/arith.ts +316 -0
- package/src/vet.ts +992 -0
- package/src/walk.ts +99 -0
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
/* Copyright (c) 2025 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// THE MCP TOOL LIBRARY (G7 phase 6,
|
|
4
|
+
// docs/capability-review/g7-machine-access.md; completed to the full
|
|
5
|
+
// CLI verb surface by the use-case review's MCP recommendation,
|
|
6
|
+
// use-cases/SUPPORT.md): the verbs an agent calls, over the Model
|
|
7
|
+
// Context Protocol, as a transport-free library. The split follows
|
|
8
|
+
// the LSP's (docs/lsp.md): this file is the protocol and the tools,
|
|
9
|
+
// ts/src/mcp-server.ts is stdio and nothing else, and the whole thing
|
|
10
|
+
// is testable without a socket.
|
|
11
|
+
//
|
|
12
|
+
// Every tool returns THE SAME JSON CONTRACT THE CLI PRINTS. That is
|
|
13
|
+
// the point of the surface: an agent that has read `aontu vet
|
|
14
|
+
// --format json` output knows what the `vet` tool answers, and a
|
|
15
|
+
// report copied from one to the other is the same object. The tools
|
|
16
|
+
// add no vocabulary of their own.
|
|
17
|
+
//
|
|
18
|
+
// The server evaluates under a CONFINED resolver (G5, docs/trust.md):
|
|
19
|
+
// a caller hands source text, and text that could reach out through
|
|
20
|
+
// `@"..."` is exactly what a server must not run unconfined. By
|
|
21
|
+
// default every include is denied; a server started with
|
|
22
|
+
// `--root <dir>` (ts/src/mcp-server.ts) resolves includes confined
|
|
23
|
+
// below that root instead — the CLI's `--trust root:<dir>` posture —
|
|
24
|
+
// and lets every document argument arrive as a `<name>Path` file
|
|
25
|
+
// under the same root. The package-resolver leg is never enabled
|
|
26
|
+
// here.
|
|
27
|
+
|
|
28
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
29
|
+
import {
|
|
30
|
+
basename as pathBasename,
|
|
31
|
+
dirname as pathDirname,
|
|
32
|
+
join as pathJoin,
|
|
33
|
+
resolve as pathResolve,
|
|
34
|
+
sep as pathSep,
|
|
35
|
+
} from 'node:path'
|
|
36
|
+
|
|
37
|
+
import { Aontu } from './aontu'
|
|
38
|
+
import type { TrustOptions } from './type'
|
|
39
|
+
import { vet } from './vet'
|
|
40
|
+
import type { VetFinding } from './vet'
|
|
41
|
+
import { get, why, evalFailure } from './query'
|
|
42
|
+
import { diff } from './diff'
|
|
43
|
+
import { canonHash, hcanon } from './hcanon'
|
|
44
|
+
import { cmpCodePoint } from './keyorder'
|
|
45
|
+
import { subsume } from './subsume'
|
|
46
|
+
import type { SubsumeVerdict } from './subsume'
|
|
47
|
+
import { trimCheck } from './trim'
|
|
48
|
+
import { jsonSchema } from './jsonschema'
|
|
49
|
+
import { relationCheck } from './relation'
|
|
50
|
+
import { reachCheck } from './reach'
|
|
51
|
+
import { patch } from './patch'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export const MCP_PROTOCOL = '2024-11-05'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export type McpRequest = {
|
|
58
|
+
id?: number | string | null
|
|
59
|
+
jsonrpc?: string
|
|
60
|
+
method?: string
|
|
61
|
+
params?: any
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type McpResponse = {
|
|
65
|
+
error?: { code: number, message: string }
|
|
66
|
+
id: number | string | null
|
|
67
|
+
jsonrpc: '2.0'
|
|
68
|
+
result?: any
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// JSON-RPC's own codes, the three a server this small can raise.
|
|
73
|
+
const PARSE_ERROR = -32700
|
|
74
|
+
const METHOD_NOT_FOUND = -32601
|
|
75
|
+
const INVALID_PARAMS = -32602
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export type ToolDef = {
|
|
79
|
+
name: string
|
|
80
|
+
description: string
|
|
81
|
+
properties: Record<
|
|
82
|
+
string, { type: string, description: string, items?: any }>
|
|
83
|
+
required: string[]
|
|
84
|
+
// The properties that carry DOCUMENT TEXT. Each one gains a
|
|
85
|
+
// `<name>Path` file alternative when the server runs with --root
|
|
86
|
+
// (toolList), and each one is what the confined pre-parse below
|
|
87
|
+
// covers for a tool that declares `refuse`.
|
|
88
|
+
docs?: string[]
|
|
89
|
+
// Argument validation beyond "required and a string": the message
|
|
90
|
+
// for a call that could not be made, or undefined for a good call.
|
|
91
|
+
check?: (a: any) => string | undefined
|
|
92
|
+
// ENGINES THAT TAKE NO TRUST PROFILE (subsume, trimCheck,
|
|
93
|
+
// relationCheck, patch) build their own evaluators, so the served
|
|
94
|
+
// profile cannot ride into them as an argument the way it rides
|
|
95
|
+
// into vet or get. Their documents are PRE-PARSED under the profile
|
|
96
|
+
// by callTool instead (confinedParseFailure below), and `refuse` is
|
|
97
|
+
// the tool's own error report for a document that fails it. A tool
|
|
98
|
+
// whose engine takes `trust` directly declares no `refuse`.
|
|
99
|
+
refuse?: (
|
|
100
|
+
a: any, finding: VetFinding, trust: TrustOptions,
|
|
101
|
+
paths: Record<string, string>) => any
|
|
102
|
+
run: (
|
|
103
|
+
a: any, trust: TrustOptions, paths: Record<string, string>) => any
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// The trust profile a served evaluation runs under: no includes at
|
|
108
|
+
// all — or, when the server was started with --root, includes
|
|
109
|
+
// realpath-confined below that root (the CLI's `--trust root:<dir>`
|
|
110
|
+
// semantics; docs/trust.md). The package-resolver leg is enabled by
|
|
111
|
+
// neither.
|
|
112
|
+
//
|
|
113
|
+
// The profile is INJECTED into every tool by callTool rather than
|
|
114
|
+
// applied by each tool for itself. That is deliberate: four of the six
|
|
115
|
+
// original tools once called the library with no profile at all, so a
|
|
116
|
+
// served `@"x.js"` was require()d in the server process, while the
|
|
117
|
+
// module header claimed confinement. A tool that must remember to
|
|
118
|
+
// confine itself is a tool that eventually forgets, and the forgetting
|
|
119
|
+
// is silent. With the profile arriving as an argument, a tool cannot
|
|
120
|
+
// run unconfined without visibly discarding it.
|
|
121
|
+
export function servedTrust(root?: string): TrustOptions {
|
|
122
|
+
return null == root
|
|
123
|
+
? { include: 'none' }
|
|
124
|
+
: { include: { root } }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function served(trust: TrustOptions): Aontu {
|
|
128
|
+
return new Aontu({ trust } as any)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// DOES THIS DOCUMENT STAND UP UNDER THE SERVED PROFILE — or the
|
|
133
|
+
// finding that says why not. This is the confinement gate for the
|
|
134
|
+
// engines that take no trust profile, and parse is the whole include
|
|
135
|
+
// story: `@"..."` resolves at parse time (ts/src/lang.ts), so a
|
|
136
|
+
// document whose confined parse is clean either has no includes at
|
|
137
|
+
// all (capability 'none') or resolves every one below the root — and
|
|
138
|
+
// an engine that then re-resolves the same closure under the default
|
|
139
|
+
// profile reads exactly the files the confined parse proved in
|
|
140
|
+
// bounds. A parse that fails for any reason refuses the call: an
|
|
141
|
+
// engine's own answer for a document this profile cannot read is not
|
|
142
|
+
// an answer this server may compute.
|
|
143
|
+
export function confinedParseFailure(
|
|
144
|
+
src: string, trust: TrustOptions, path?: string
|
|
145
|
+
): VetFinding | undefined {
|
|
146
|
+
const aontu = served(trust)
|
|
147
|
+
const ctx = aontu.ctx({ collect: true })
|
|
148
|
+
aontu.parse(src, null == path ? undefined : { path }, ctx)
|
|
149
|
+
return 0 < ctx.err.length ? evalFailure(ctx) : undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
// Confinement is realpath-then-prefix-check, mirroring the include
|
|
154
|
+
// resolver's own rule (ts/src/lang.ts, docs/trust.md): the file's
|
|
155
|
+
// real path must sit below the root's real path, so a symlink inside
|
|
156
|
+
// the root pointing outside it is an escape, not a loophole.
|
|
157
|
+
//
|
|
158
|
+
// A path that does not (fully) exist cannot be realpath'd whole, and
|
|
159
|
+
// falling back to the LEXICAL form compares apples to oranges when the
|
|
160
|
+
// root itself sits behind a symlink -- on macOS a root under /var
|
|
161
|
+
// realpaths to /private/var, so a merely-missing file inside it read
|
|
162
|
+
// as an escape instead of "cannot read" (the CI failure that bought
|
|
163
|
+
// this comment). Realpath the deepest EXISTING ancestor and re-attach
|
|
164
|
+
// the rest, so both sides of the prefix check are in real coordinates.
|
|
165
|
+
function realpathOf(p: string): string {
|
|
166
|
+
try {
|
|
167
|
+
return realpathSync(p)
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
const parent = pathDirname(p)
|
|
171
|
+
if (parent === p) {
|
|
172
|
+
return p
|
|
173
|
+
}
|
|
174
|
+
return pathJoin(realpathOf(parent), pathBasename(p))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function outsideRoot(root: string, full: string): boolean {
|
|
179
|
+
const rootReal = realpathOf(root)
|
|
180
|
+
const fullReal = realpathOf(full)
|
|
181
|
+
return fullReal !== rootReal && !fullReal.startsWith(rootReal + pathSep)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
const TOOLS: ToolDef[] = [
|
|
186
|
+
{
|
|
187
|
+
name: 'vet',
|
|
188
|
+
description:
|
|
189
|
+
'Validate a data document against a schema document. Returns the ' +
|
|
190
|
+
'vet report: verdict (valid | invalid | incomplete | error), and ' +
|
|
191
|
+
'findings with codes, paths, sites and a repair hint. A site ' +
|
|
192
|
+
'names the file whose text it excerpts, so its row and column ' +
|
|
193
|
+
'are safe to edit at even when the schema loads other files.',
|
|
194
|
+
properties: {
|
|
195
|
+
schema: { type: 'string', description: 'The schema document' },
|
|
196
|
+
data: { type: 'string', description: 'The data document' },
|
|
197
|
+
at: { type: 'string', description: 'Validate at this path ($.a.b)' },
|
|
198
|
+
},
|
|
199
|
+
required: ['schema', 'data'],
|
|
200
|
+
docs: ['schema', 'data'],
|
|
201
|
+
run: (a, trust, paths) => vet(str(a.schema), str(a.data), {
|
|
202
|
+
...(null == a.at ? {} : { at: str(a.at) }),
|
|
203
|
+
schemaPath: paths.schema,
|
|
204
|
+
dataPath: paths.data,
|
|
205
|
+
schemaUrl: paths.schema,
|
|
206
|
+
dataUrl: paths.data,
|
|
207
|
+
trust,
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'get',
|
|
212
|
+
description:
|
|
213
|
+
'Select one node of an evaluated document by path and render it: ' +
|
|
214
|
+
'generated JSON by default, or the canon, types, keys or ' +
|
|
215
|
+
'depth-elided view. A view other than json is a valid Aontu ' +
|
|
216
|
+
'document that subsumes the truth it summarises.',
|
|
217
|
+
properties: {
|
|
218
|
+
src: { type: 'string', description: 'The document' },
|
|
219
|
+
path: { type: 'string', description: 'The path ($.a.b)' },
|
|
220
|
+
view: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'json (default), canon, types or keys',
|
|
223
|
+
},
|
|
224
|
+
depth: {
|
|
225
|
+
type: 'number',
|
|
226
|
+
description: 'Structure to this depth; deeper nodes render as top',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ['src', 'path'],
|
|
230
|
+
docs: ['src'],
|
|
231
|
+
run: (a, trust, paths) => get(str(a.src), str(a.path), {
|
|
232
|
+
view: a.view,
|
|
233
|
+
depth: 'number' === typeof a.depth ? a.depth : undefined,
|
|
234
|
+
path: paths.src,
|
|
235
|
+
trust,
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'why',
|
|
240
|
+
description:
|
|
241
|
+
'Explain the value at a path: every contribution that met there, ' +
|
|
242
|
+
'in source order, with its role (literal, spread, ref, pref) and ' +
|
|
243
|
+
'the site it was written at.',
|
|
244
|
+
properties: {
|
|
245
|
+
src: { type: 'string', description: 'The document' },
|
|
246
|
+
path: { type: 'string', description: 'The path ($.a.b)' },
|
|
247
|
+
},
|
|
248
|
+
required: ['src', 'path'],
|
|
249
|
+
docs: ['src'],
|
|
250
|
+
run: (a, trust, paths) =>
|
|
251
|
+
why(str(a.src), str(a.path), { path: paths.src, trust }),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'diff',
|
|
255
|
+
description:
|
|
256
|
+
'What changed, at which paths, between two documents. Compares ' +
|
|
257
|
+
'the hash form, so reformatting is not a change and closing a ' +
|
|
258
|
+
'map is. Whether a change is BREAKING is the breaking verb\'s ' +
|
|
259
|
+
'question, not this one.',
|
|
260
|
+
properties: {
|
|
261
|
+
left: { type: 'string', description: 'The earlier document' },
|
|
262
|
+
right: { type: 'string', description: 'The later document' },
|
|
263
|
+
at: { type: 'string', description: 'Compare at this path ($.a.b)' },
|
|
264
|
+
},
|
|
265
|
+
required: ['left', 'right'],
|
|
266
|
+
docs: ['left', 'right'],
|
|
267
|
+
run: (a, trust, paths) => diff(str(a.left), str(a.right), {
|
|
268
|
+
...(null == a.at ? {} : { at: str(a.at) }),
|
|
269
|
+
leftPath: paths.left,
|
|
270
|
+
rightPath: paths.right,
|
|
271
|
+
trust,
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'canon',
|
|
276
|
+
description:
|
|
277
|
+
'Normalise a document to its canonical form: the deterministic ' +
|
|
278
|
+
'text two documents that mean the same thing share.',
|
|
279
|
+
properties: {
|
|
280
|
+
src: { type: 'string', description: 'The document' },
|
|
281
|
+
},
|
|
282
|
+
required: ['src'],
|
|
283
|
+
docs: ['src'],
|
|
284
|
+
run: (a, trust, paths) => canonOf(str(a.src), trust, paths.src),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'summary',
|
|
288
|
+
description:
|
|
289
|
+
'A document at a glance: its canon-hash pin, its root keys, and ' +
|
|
290
|
+
'the shape of its top tier. The first tier of progressive ' +
|
|
291
|
+
'disclosure — expand by calling get with a path.',
|
|
292
|
+
properties: {
|
|
293
|
+
src: { type: 'string', description: 'The document' },
|
|
294
|
+
},
|
|
295
|
+
required: ['src'],
|
|
296
|
+
docs: ['src'],
|
|
297
|
+
run: (a, trust, paths) => summaryOf(str(a.src), trust, paths.src),
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// The evolution and change verbs (the use-case review's "MCP is a
|
|
301
|
+
// read-only subset" gap, use-cases/09-agent-tools/README.md gap 11).
|
|
302
|
+
|
|
303
|
+
{
|
|
304
|
+
name: 'subsume',
|
|
305
|
+
description:
|
|
306
|
+
'Does the general document admit every instance the specific ' +
|
|
307
|
+
'one admits? Returns the subsume report: verdict (subsumes | ' +
|
|
308
|
+
'does_not_subsume | undecided | error) and compat findings, ' +
|
|
309
|
+
'each with a witness at the path that narrowed.',
|
|
310
|
+
properties: {
|
|
311
|
+
general: { type: 'string', description: 'The general document' },
|
|
312
|
+
specific: { type: 'string', description: 'The specific document' },
|
|
313
|
+
profile: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'values, defaults (default) or gen',
|
|
316
|
+
},
|
|
317
|
+
at: { type: 'string', description: 'Compare at this path ($.a.b)' },
|
|
318
|
+
},
|
|
319
|
+
required: ['general', 'specific'],
|
|
320
|
+
docs: ['general', 'specific'],
|
|
321
|
+
check: (a) => null == a.profile || 'values' === a.profile ||
|
|
322
|
+
'defaults' === a.profile || 'gen' === a.profile
|
|
323
|
+
? undefined : 'profile needs values, defaults or gen',
|
|
324
|
+
refuse: (_a, finding) => ({ verdict: 'error', findings: [finding] }),
|
|
325
|
+
run: (a, _trust, paths) => subsume(str(a.general), str(a.specific), {
|
|
326
|
+
...(null == a.profile ? {} : { profile: a.profile }),
|
|
327
|
+
...(null == a.at ? {} : { at: str(a.at) }),
|
|
328
|
+
generalUrl: paths.general,
|
|
329
|
+
specificUrl: paths.specific,
|
|
330
|
+
generalPath: paths.general,
|
|
331
|
+
specificPath: paths.specific,
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'breaking',
|
|
336
|
+
description:
|
|
337
|
+
'Is the new version of a document a breaking change against the ' +
|
|
338
|
+
'old one? Wraps subsume in the CLI\'s policy logic: the mode ' +
|
|
339
|
+
'argument, else the document\'s own $.aontu_policy.compat, else ' +
|
|
340
|
+
'backward. Returns verdict (compatible | breaking | undecided | ' +
|
|
341
|
+
'error), the mode checked, and the findings.',
|
|
342
|
+
properties: {
|
|
343
|
+
old: { type: 'string', description: 'The old (published) version' },
|
|
344
|
+
new: { type: 'string', description: 'The new (proposed) version' },
|
|
345
|
+
mode: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'backward, forward or full; overrides the ' +
|
|
348
|
+
'document\'s own $.aontu_policy.compat declaration',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ['old', 'new'],
|
|
352
|
+
docs: ['old', 'new'],
|
|
353
|
+
check: (a) => null == a.mode || 'backward' === a.mode ||
|
|
354
|
+
'forward' === a.mode || 'full' === a.mode
|
|
355
|
+
? undefined : 'mode needs backward, forward or full',
|
|
356
|
+
refuse: (a, finding, trust, paths) => ({
|
|
357
|
+
verdict: 'error',
|
|
358
|
+
mode: breakingMode(a, trust, paths),
|
|
359
|
+
findings: [finding],
|
|
360
|
+
}),
|
|
361
|
+
run: (a, trust, paths) => breakingOf(a, trust, paths),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'set',
|
|
365
|
+
description:
|
|
366
|
+
'Change values by appending to an overlay document (or, with ' +
|
|
367
|
+
'inPlace, rewriting a pinned literal where that is provably ' +
|
|
368
|
+
'safe). Returns the vet-class report plus the NEW OVERLAY TEXT: ' +
|
|
369
|
+
'this server never writes files, so the caller owns the write.',
|
|
370
|
+
properties: {
|
|
371
|
+
entry: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
description: 'The entry document (the truth the overlay must ' +
|
|
374
|
+
'hold against)',
|
|
375
|
+
},
|
|
376
|
+
overlay: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
description: 'The overlay document as it stands (may be empty)',
|
|
379
|
+
},
|
|
380
|
+
assignments: {
|
|
381
|
+
type: 'array',
|
|
382
|
+
items: {
|
|
383
|
+
type: 'object',
|
|
384
|
+
properties: {
|
|
385
|
+
path: { type: 'string', description: 'The path ($.a.b)' },
|
|
386
|
+
value: {
|
|
387
|
+
type: 'string',
|
|
388
|
+
description: 'The value, as Aontu source text',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
required: ['path', 'value'],
|
|
392
|
+
},
|
|
393
|
+
description: 'The assignments to apply, in order',
|
|
394
|
+
},
|
|
395
|
+
inPlace: {
|
|
396
|
+
type: 'boolean',
|
|
397
|
+
description: 'Rewrite a pinned literal where it was written, ' +
|
|
398
|
+
'where provably safe; otherwise append as usual',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
required: ['entry', 'overlay', 'assignments'],
|
|
402
|
+
docs: ['entry', 'overlay'],
|
|
403
|
+
check: checkAssignments,
|
|
404
|
+
refuse: (a, finding) => setError(str(a.overlay), finding),
|
|
405
|
+
run: (a, trust, paths) => setOf(a, trust, paths),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: 'relations',
|
|
409
|
+
description:
|
|
410
|
+
'Check the declared relations of a finished model: acyclicity ' +
|
|
411
|
+
'and inverse consistency over the entity edge set. Returns ' +
|
|
412
|
+
'verdict (pass | fail | error) and relation findings.',
|
|
413
|
+
properties: {
|
|
414
|
+
source: { type: 'string', description: 'The document' },
|
|
415
|
+
},
|
|
416
|
+
required: ['source'],
|
|
417
|
+
docs: ['source'],
|
|
418
|
+
// The pre-parse finding rides `errors`, exactly where the engine
|
|
419
|
+
// puts its own reason for a document that does not stand up
|
|
420
|
+
// (ts/src/relation.ts, the review's finding F). NOT `findings`:
|
|
421
|
+
// RelationFinding is its own vocabulary (code, relation, at,
|
|
422
|
+
// detail) and a document with no graph has no graph findings.
|
|
423
|
+
refuse: (_a, finding) =>
|
|
424
|
+
({ verdict: 'error', findings: [], errors: [finding] }),
|
|
425
|
+
run: (a, _trust, paths) =>
|
|
426
|
+
relationCheck(str(a.source), { path: paths.source }),
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'reaches',
|
|
430
|
+
description:
|
|
431
|
+
'Ask whether one entity reaches another over the entity graph, ' +
|
|
432
|
+
'at any remove — the closure question `relations` cannot ask one ' +
|
|
433
|
+
'edge at a time (blast radius, containment). Returns verdict ' +
|
|
434
|
+
'(reaches | unreachable | error) and, when it reaches, a shortest ' +
|
|
435
|
+
'path. Transitive, not reflexive: an entity reaches itself only ' +
|
|
436
|
+
'through a cycle.',
|
|
437
|
+
properties: {
|
|
438
|
+
source: { type: 'string', description: 'The document' },
|
|
439
|
+
from: { type: 'string', description: 'The entity to start at' },
|
|
440
|
+
to: { type: 'string', description: 'The entity to look for' },
|
|
441
|
+
relation: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
description: 'Follow only edges under this relation (optional)',
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
required: ['source', 'from', 'to'],
|
|
447
|
+
docs: ['source'],
|
|
448
|
+
refuse: (_a, finding) => ({ verdict: 'error', errors: [finding] }),
|
|
449
|
+
run: (a, _trust, paths) =>
|
|
450
|
+
reachCheck(str(a.source), str(a.from), str(a.to), {
|
|
451
|
+
path: paths.source,
|
|
452
|
+
relation: null == a.relation ? undefined : str(a.relation),
|
|
453
|
+
}),
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: 'hash',
|
|
457
|
+
description:
|
|
458
|
+
'The canon-hash pin of a document: "aon1-" + ' +
|
|
459
|
+
'base64url(SHA-256(hash form)). Survives reformatting; moves on ' +
|
|
460
|
+
'any change of meaning. Pass form: true for the hash form text ' +
|
|
461
|
+
'the pin digests.',
|
|
462
|
+
properties: {
|
|
463
|
+
source: { type: 'string', description: 'The document' },
|
|
464
|
+
form: {
|
|
465
|
+
type: 'boolean',
|
|
466
|
+
description: 'Include the hash form text as well',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
required: ['source'],
|
|
470
|
+
docs: ['source'],
|
|
471
|
+
run: (a, trust, paths) =>
|
|
472
|
+
hashOf(str(a.source), true === a.form, trust, paths.source),
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'trim',
|
|
476
|
+
description:
|
|
477
|
+
'Report redundant map entries — entries whose removal leaves ' +
|
|
478
|
+
'the evaluated result unchanged, the spread-implied case ' +
|
|
479
|
+
'included — as paths. Report-only, the CLI\'s trim --check. ' +
|
|
480
|
+
'Returns verdict (clean | redundant | error).',
|
|
481
|
+
properties: {
|
|
482
|
+
source: { type: 'string', description: 'The document' },
|
|
483
|
+
},
|
|
484
|
+
required: ['source'],
|
|
485
|
+
docs: ['source'],
|
|
486
|
+
// The pre-parse finding rides `errors`, exactly where the engine
|
|
487
|
+
// puts its own reason for a document that does not stand up
|
|
488
|
+
// (ts/src/trim.ts, the review's finding F).
|
|
489
|
+
refuse: (_a, finding) =>
|
|
490
|
+
({ verdict: 'error', redundant: [], errors: [finding] }),
|
|
491
|
+
run: (a, _trust, paths) =>
|
|
492
|
+
trimCheck(str(a.source), { path: paths.source }),
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'jsonschema',
|
|
496
|
+
description:
|
|
497
|
+
'Export a document as a JSON Schema (draft 2020-12), and say ' +
|
|
498
|
+
'what could not be carried. This is the bridge to a ' +
|
|
499
|
+
'structured-output API, which constrains generation to JSON ' +
|
|
500
|
+
'Schema and to nothing else -- and to an MCP tool\'s own ' +
|
|
501
|
+
'inputSchema, which the protocol requires to be one. Returns ' +
|
|
502
|
+
'verdict (ok | lossy | error), the schema, and a `lossy` list ' +
|
|
503
|
+
'naming every construct the schema could not say and what it ' +
|
|
504
|
+
'says instead. A lossy schema admits MORE than the model does, ' +
|
|
505
|
+
'so vet the result against the model rather than trusting the ' +
|
|
506
|
+
'schema alone.',
|
|
507
|
+
properties: {
|
|
508
|
+
source: { type: 'string', description: 'The document' },
|
|
509
|
+
at: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
description: 'Export this path of the document ($.a.b)',
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
required: ['source'],
|
|
515
|
+
docs: ['source'],
|
|
516
|
+
refuse: (_a, finding) =>
|
|
517
|
+
({ verdict: 'error', schema: {}, lossy: [], errors: [finding] }),
|
|
518
|
+
run: (a, _trust, paths) =>
|
|
519
|
+
jsonSchema(str(a.source), {
|
|
520
|
+
at: null == a.at ? undefined : str(a.at), path: paths.source,
|
|
521
|
+
}),
|
|
522
|
+
},
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
function str(v: any): string {
|
|
527
|
+
return 'string' === typeof v ? v : ''
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
function canonOf(src: string, trust: TrustOptions, path?: string): any {
|
|
532
|
+
const aontu = served(trust)
|
|
533
|
+
const ctx = aontu.ctx({ collect: true })
|
|
534
|
+
const v: any = aontu.unify(
|
|
535
|
+
src, null == path ? undefined : { path }, ctx)
|
|
536
|
+
if (0 < ctx.err.length) {
|
|
537
|
+
return { ok: false, canon: '', findings: [evalFailure(ctx)] }
|
|
538
|
+
}
|
|
539
|
+
return { ok: true, canon: v.canon, findings: [] }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
function summaryOf(src: string, trust: TrustOptions, path?: string): any {
|
|
544
|
+
const aontu = served(trust)
|
|
545
|
+
const ctx = aontu.ctx({ collect: true })
|
|
546
|
+
const v: any = aontu.unify(
|
|
547
|
+
src, null == path ? undefined : { path }, ctx)
|
|
548
|
+
if (0 < ctx.err.length) {
|
|
549
|
+
return {
|
|
550
|
+
ok: false, hash: '', keys: [], shape: '', findings: [evalFailure(ctx)],
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const keys = true === v.isMap ? Object.keys(v.peg).sort(cmpCodePoint) : []
|
|
554
|
+
return {
|
|
555
|
+
ok: true,
|
|
556
|
+
hash: canonHash(v),
|
|
557
|
+
keys,
|
|
558
|
+
// The top tier only: every key, with its subtree elided to `top`.
|
|
559
|
+
shape: get(src, '$', { view: 'types', depth: 2, path, trust }).out,
|
|
560
|
+
findings: [],
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
// The pin, computed under the served profile — this engine is the
|
|
566
|
+
// evaluation itself, so the profile rides in directly and no pre-parse
|
|
567
|
+
// is needed. The error answer follows canonOf's shape; the CLI prints
|
|
568
|
+
// nothing but a message there (ts/src/cli.ts runHash), so this shape
|
|
569
|
+
// is the served superset of it.
|
|
570
|
+
function hashOf(
|
|
571
|
+
src: string, form: boolean, trust: TrustOptions, path?: string): any {
|
|
572
|
+
const aontu = served(trust)
|
|
573
|
+
const ctx = aontu.ctx({ collect: true })
|
|
574
|
+
const v: any = aontu.unify(
|
|
575
|
+
src, null == path ? undefined : { path }, ctx)
|
|
576
|
+
if (0 < ctx.err.length || null == v || true === v.isNil) {
|
|
577
|
+
return { ok: false, hash: '', findings: [evalFailure(ctx)] }
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
hash: canonHash(v),
|
|
582
|
+
...(form ? { form: hcanon(v) } : {}),
|
|
583
|
+
findings: [],
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
// ---------------------------------------------------------------------
|
|
589
|
+
// THE BREAKING POLICY WRAPPER. Re-implemented from ts/src/cli.ts
|
|
590
|
+
// (runBreaking and policyCompat) rather than extracted from it: cli.ts
|
|
591
|
+
// owns the process-facing halves — files, git revisions, exit codes,
|
|
592
|
+
// rendering — that a served tool must not import, and the policy
|
|
593
|
+
// itself (which mode applies, which side is general, how verdicts
|
|
594
|
+
// aggregate) is small enough that a shared home would be all seam.
|
|
595
|
+
// cli.ts is the reference; a change there is a change here.
|
|
596
|
+
|
|
597
|
+
type BreakingMode = 'backward' | 'forward' | 'full' | 'none'
|
|
598
|
+
|
|
599
|
+
// Verdict aggregation: an error anywhere makes the run an error;
|
|
600
|
+
// otherwise a witness anywhere makes it breaking; otherwise an open
|
|
601
|
+
// question anywhere leaves it undecided.
|
|
602
|
+
const BREAKING_RANK: Record<SubsumeVerdict, number> = {
|
|
603
|
+
subsumes: 0,
|
|
604
|
+
undecided: 1,
|
|
605
|
+
does_not_subsume: 2,
|
|
606
|
+
error: 3,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const BREAKING_VERDICT: Record<SubsumeVerdict, string> = {
|
|
610
|
+
subsumes: 'compatible',
|
|
611
|
+
does_not_subsume: 'breaking',
|
|
612
|
+
undecided: 'undecided',
|
|
613
|
+
error: 'error',
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
// The document's own compatibility declaration: `$.aontu_policy.compat`,
|
|
618
|
+
// a disjunction whose default is the declared mode. Undefined when the
|
|
619
|
+
// key is absent or does not spell a mode. The one departure from the
|
|
620
|
+
// cli.ts original: the read runs CONFINED, because here the document
|
|
621
|
+
// came from a caller.
|
|
622
|
+
function policyCompatOf(
|
|
623
|
+
newSrc: string, trust: TrustOptions, path?: string
|
|
624
|
+
): BreakingMode | undefined {
|
|
625
|
+
const aontu = served(trust)
|
|
626
|
+
const ctx = aontu.ctx({ collect: true })
|
|
627
|
+
const v: any = aontu.unify(
|
|
628
|
+
newSrc, null == path ? undefined : { path }, ctx)
|
|
629
|
+
if (0 < ctx.err.length || true === v?.isNil) {
|
|
630
|
+
return undefined
|
|
631
|
+
}
|
|
632
|
+
let compat: any = v?.peg?.aontu_policy?.peg?.compat
|
|
633
|
+
if (null == compat) {
|
|
634
|
+
return undefined
|
|
635
|
+
}
|
|
636
|
+
if (true === compat.isDisjunct && Array.isArray(compat.peg)) {
|
|
637
|
+
compat = compat.peg.find((m: any) => true === m?.isPref) ?? compat.peg[0]
|
|
638
|
+
}
|
|
639
|
+
if (true === compat.isPref) {
|
|
640
|
+
compat = compat.peg
|
|
641
|
+
}
|
|
642
|
+
const m = true === compat?.isString ? compat.peg : undefined
|
|
643
|
+
return 'backward' === m || 'forward' === m || 'full' === m || 'none' === m
|
|
644
|
+
? m : undefined
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
// The declared mode: the mode argument overrides the document's own
|
|
649
|
+
// policy; neither means backward (v1-valid documents stay valid).
|
|
650
|
+
function breakingMode(
|
|
651
|
+
a: any, trust: TrustOptions, paths: Record<string, string>
|
|
652
|
+
): BreakingMode {
|
|
653
|
+
return a.mode ?? policyCompatOf(str(a.new), trust, paths.new) ?? 'backward'
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
function breakingOf(
|
|
658
|
+
a: any, trust: TrustOptions, paths: Record<string, string>): any {
|
|
659
|
+
const mode = breakingMode(a, trust, paths)
|
|
660
|
+
|
|
661
|
+
if ('none' === mode) {
|
|
662
|
+
// The document declares no compatibility promise: nothing to check.
|
|
663
|
+
return { verdict: 'compatible', mode, findings: [] }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const sides = {
|
|
667
|
+
old: { src: str(a.old), url: paths.old ?? 'old', path: paths.old },
|
|
668
|
+
new: { src: str(a.new), url: paths.new ?? 'new', path: paths.new },
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// backward: the NEW document is the general side — every old
|
|
672
|
+
// instance must still be admitted. forward: the old one is.
|
|
673
|
+
const checks: Array<{
|
|
674
|
+
general: typeof sides.old, specific: typeof sides.old
|
|
675
|
+
}> = []
|
|
676
|
+
if ('backward' === mode || 'full' === mode) {
|
|
677
|
+
checks.push({ general: sides.new, specific: sides.old })
|
|
678
|
+
}
|
|
679
|
+
if ('forward' === mode || 'full' === mode) {
|
|
680
|
+
checks.push({ general: sides.old, specific: sides.new })
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let worst: SubsumeVerdict = 'subsumes'
|
|
684
|
+
const findings: VetFinding[] = []
|
|
685
|
+
for (const check of checks) {
|
|
686
|
+
const report = subsume(check.general.src, check.specific.src, {
|
|
687
|
+
generalUrl: check.general.url,
|
|
688
|
+
specificUrl: check.specific.url,
|
|
689
|
+
generalPath: check.general.path,
|
|
690
|
+
specificPath: check.specific.path,
|
|
691
|
+
})
|
|
692
|
+
if (BREAKING_RANK[worst] < BREAKING_RANK[report.verdict]) {
|
|
693
|
+
worst = report.verdict
|
|
694
|
+
}
|
|
695
|
+
findings.push(...report.findings)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return { verdict: BREAKING_VERDICT[worst], mode, findings }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
// ---------------------------------------------------------------------
|
|
703
|
+
// The set tool: the CLI's `set` verb minus the filesystem — the patch
|
|
704
|
+
// engine (ts/src/patch.ts) already answers with the new overlay text,
|
|
705
|
+
// and the caller owns the write.
|
|
706
|
+
|
|
707
|
+
// The assignments arrive structured ({path, value}) rather than as the
|
|
708
|
+
// CLI's `<path>=<value>` spelling, and are re-joined for the engine's
|
|
709
|
+
// parseAssignment — so the path must not smuggle a `=` that would move
|
|
710
|
+
// the split.
|
|
711
|
+
function checkAssignments(a: any): string | undefined {
|
|
712
|
+
if (0 === a.assignments.length) {
|
|
713
|
+
return 'assignments needs at least one {path, value}'
|
|
714
|
+
}
|
|
715
|
+
for (const x of a.assignments) {
|
|
716
|
+
if ('string' !== typeof x?.path || '' === x.path.trim() ||
|
|
717
|
+
'string' !== typeof x?.value || '' === x.value.trim()) {
|
|
718
|
+
return 'each assignment needs a path and a value, both strings'
|
|
719
|
+
}
|
|
720
|
+
if (x.path.includes('=')) {
|
|
721
|
+
return `assignment path may not contain "=": ${x.path}`
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return undefined
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
function setError(overlay: string, finding: VetFinding): any {
|
|
729
|
+
return {
|
|
730
|
+
overlay,
|
|
731
|
+
appended: [],
|
|
732
|
+
replaced: [],
|
|
733
|
+
verdict: 'error',
|
|
734
|
+
findings: [finding],
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
function setOf(
|
|
740
|
+
a: any, trust: TrustOptions, paths: Record<string, string>): any {
|
|
741
|
+
// THE ASSIGNMENT VALUES ARE DOCUMENTS TOO: each one is appended (or
|
|
742
|
+
// spliced) into the overlay and evaluated there by the engine's
|
|
743
|
+
// final vet, so a value that smuggles an include — or a newline and
|
|
744
|
+
// then an include — gets the same confined pre-parse as the
|
|
745
|
+
// documents themselves, wrapped exactly as the engine's own
|
|
746
|
+
// spanValue wraps a fragment (ts/src/patch.ts).
|
|
747
|
+
for (const x of a.assignments) {
|
|
748
|
+
const denied = confinedParseFailure('v: ' + x.value, trust, paths.overlay)
|
|
749
|
+
if (null != denied) {
|
|
750
|
+
return setError(str(a.overlay), denied)
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return patch(str(a.entry), str(a.overlay),
|
|
755
|
+
a.assignments.map((x: any) => x.path + '=' + x.value),
|
|
756
|
+
{
|
|
757
|
+
entryPath: paths.entry,
|
|
758
|
+
overlayPath: paths.overlay,
|
|
759
|
+
inPlace: true === a.inPlace,
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
// ---------------------------------------------------------------------
|
|
765
|
+
// The tool list as MCP spells it: a name, a description, and a JSON
|
|
766
|
+
// Schema for the arguments. With a served root, every document
|
|
767
|
+
// property gains its `<name>Path` file alternative — and comes OFF the
|
|
768
|
+
// `required` list, because JSON Schema's `required` cannot say "one of
|
|
769
|
+
// the two"; callTool's own argument check still refuses a call that
|
|
770
|
+
// carries neither.
|
|
771
|
+
export function toolList(root?: string): any[] {
|
|
772
|
+
return TOOLS.map((t) => {
|
|
773
|
+
const properties: Record<string, any> = { ...t.properties }
|
|
774
|
+
let required = t.required
|
|
775
|
+
if (null != root && null != t.docs) {
|
|
776
|
+
for (const doc of t.docs) {
|
|
777
|
+
properties[doc + 'Path'] = {
|
|
778
|
+
type: 'string',
|
|
779
|
+
description: `File below the server's --root, read as ` +
|
|
780
|
+
`\`${doc}\`; an alternative to inline \`${doc}\` text`,
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
required = t.required.filter(
|
|
784
|
+
(r) => !(t.docs as string[]).includes(r))
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
name: t.name,
|
|
788
|
+
description: t.description,
|
|
789
|
+
inputSchema: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
properties,
|
|
792
|
+
required,
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
function refusal(text: string): any {
|
|
800
|
+
return { content: [{ type: 'text', text }], isError: true }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
// One tool call. A tool that REFUSES (an invalid document, a path that
|
|
805
|
+
// names nothing, a document the served profile cannot read) is not a
|
|
806
|
+
// protocol error: it answers with its own report and `isError` false,
|
|
807
|
+
// because the report IS the answer the agent asked for. `isError` is
|
|
808
|
+
// reserved for a call that could not be made at all — an unknown tool,
|
|
809
|
+
// a missing or malformed argument, a file argument the server cannot
|
|
810
|
+
// serve.
|
|
811
|
+
// `opts.tools` is injectable for the same reason the watch loop's
|
|
812
|
+
// waiter is: the catch below is defensive code no document reaches --
|
|
813
|
+
// every verb answers with a report rather than throwing -- and code
|
|
814
|
+
// the suite cannot execute is code the ADR-002 floor cannot hold.
|
|
815
|
+
export function callTool(
|
|
816
|
+
name: string, args: any,
|
|
817
|
+
opts?: { root?: string, tools?: ToolDef[] }): any {
|
|
818
|
+
const root = opts?.root
|
|
819
|
+
const tools = opts?.tools ?? TOOLS
|
|
820
|
+
const tool = tools.find((t) => t.name === name)
|
|
821
|
+
if (null == tool) {
|
|
822
|
+
return refusal(`no such tool: ${name}`)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const a: any = { ...(args ?? {}) }
|
|
826
|
+
const paths: Record<string, string> = {}
|
|
827
|
+
|
|
828
|
+
// THE FILE ALTERNATIVES (--root). A document that did not arrive as
|
|
829
|
+
// inline text may arrive as a `<name>Path` file — served only when
|
|
830
|
+
// the operator granted a root at startup, confined below it by the
|
|
831
|
+
// same realpath rule the include resolver applies, and recorded in
|
|
832
|
+
// `paths` so the engine resolves the file's own relative includes
|
|
833
|
+
// from its directory (the CLI's rule for a named file). Inline text
|
|
834
|
+
// wins when a caller sends both.
|
|
835
|
+
for (const doc of tool.docs ?? []) {
|
|
836
|
+
if ('string' === typeof a[doc]) {
|
|
837
|
+
continue
|
|
838
|
+
}
|
|
839
|
+
const rel = a[doc + 'Path']
|
|
840
|
+
if ('string' !== typeof rel) {
|
|
841
|
+
continue
|
|
842
|
+
}
|
|
843
|
+
if (null == root) {
|
|
844
|
+
return refusal(
|
|
845
|
+
`tool ${name}: ${doc}Path needs a server started with ` +
|
|
846
|
+
`--root <dir>; pass ${doc} as document text instead`)
|
|
847
|
+
}
|
|
848
|
+
const full = pathResolve(root, rel)
|
|
849
|
+
if (outsideRoot(root, full)) {
|
|
850
|
+
return refusal(
|
|
851
|
+
`tool ${name}: ${doc}Path escapes the server root: ${rel}`)
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
a[doc] = readFileSync(full, 'utf8')
|
|
855
|
+
}
|
|
856
|
+
catch (e: any) {
|
|
857
|
+
return refusal(
|
|
858
|
+
`tool ${name}: cannot read ${doc}Path ${rel}: ${e?.message ?? e}`)
|
|
859
|
+
}
|
|
860
|
+
paths[doc] = full
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (const req of tool.required) {
|
|
864
|
+
const kind = tool.properties[req]?.type
|
|
865
|
+
const okv = 'array' === kind
|
|
866
|
+
? Array.isArray(a[req])
|
|
867
|
+
: 'string' === typeof a[req]
|
|
868
|
+
if (!okv) {
|
|
869
|
+
const alt = null != root && (tool.docs ?? []).includes(req)
|
|
870
|
+
? ` (or ${req}Path)` : ''
|
|
871
|
+
return refusal(
|
|
872
|
+
`tool ${name} needs ${'array' === kind ? 'an array' : 'a string'}` +
|
|
873
|
+
` argument: ${req}${alt}`)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const bad = null == tool.check ? undefined : tool.check(a)
|
|
878
|
+
if (null != bad) {
|
|
879
|
+
return refusal(`tool ${name}: ${bad}`)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// The served profile is supplied HERE, once, for every tool.
|
|
883
|
+
// A tool never chooses its own confinement.
|
|
884
|
+
const trust = servedTrust(root)
|
|
885
|
+
let out: any
|
|
886
|
+
try {
|
|
887
|
+
// The confined pre-parse, for the engines that cannot take the
|
|
888
|
+
// profile themselves (see ToolDef.refuse).
|
|
889
|
+
if (null != tool.refuse) {
|
|
890
|
+
for (const doc of tool.docs ?? []) {
|
|
891
|
+
if ('string' !== typeof a[doc]) {
|
|
892
|
+
continue
|
|
893
|
+
}
|
|
894
|
+
const denied = confinedParseFailure(a[doc], trust, paths[doc])
|
|
895
|
+
if (null != denied) {
|
|
896
|
+
out = tool.refuse(a, denied, trust, paths)
|
|
897
|
+
break
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (undefined === out) {
|
|
902
|
+
out = tool.run(a, trust, paths)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch (e: any) {
|
|
906
|
+
// A tool that throws is a call that could not be made, which is
|
|
907
|
+
// what isError means -- and it must not take the server process
|
|
908
|
+
// down with it: a stdio server serves one client for a whole
|
|
909
|
+
// session, so an unhandled throw on one document loses every
|
|
910
|
+
// later call too.
|
|
911
|
+
return refusal(`tool ${name} failed: ${e?.message ?? e}`)
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
915
|
+
isError: false,
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
// What a connecting client is told once, at the handshake: which
|
|
921
|
+
// confinement mode this server is in. The --root capability is a
|
|
922
|
+
// startup grant, not a per-call negotiation, so initialize is where a
|
|
923
|
+
// caller learns whether `<name>Path` arguments are served.
|
|
924
|
+
export function serverInstructions(root?: string): string {
|
|
925
|
+
return 'Aontu tools take document TEXT arguments and answer the ' +
|
|
926
|
+
'same JSON reports the aontu CLI prints with --format json. ' +
|
|
927
|
+
(null == root
|
|
928
|
+
? 'Evaluation is confined: includes (@"...") are denied, and ' +
|
|
929
|
+
'file-path arguments (schemaPath, srcPath, sourcePath, ...) ' +
|
|
930
|
+
'are refused. Start the server with --root <dir> to serve ' +
|
|
931
|
+
'both, confined below that directory.'
|
|
932
|
+
: `This server was started with --root ${root}: every document ` +
|
|
933
|
+
'argument also accepts a <name>Path alternative naming a file ' +
|
|
934
|
+
'below that root, and includes (@"...") resolve confined to it.')
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
// Handle one JSON-RPC message. Returns undefined for a NOTIFICATION
|
|
939
|
+
// (no id): MCP sends `notifications/initialized`, and answering a
|
|
940
|
+
// notification is a protocol error in the other direction.
|
|
941
|
+
export function handle(
|
|
942
|
+
msg: McpRequest, version: string, root?: string
|
|
943
|
+
): McpResponse | undefined {
|
|
944
|
+
const id = msg.id ?? null
|
|
945
|
+
|
|
946
|
+
if (null == msg.id) {
|
|
947
|
+
return undefined
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
switch (msg.method) {
|
|
951
|
+
case 'initialize':
|
|
952
|
+
return ok(id, {
|
|
953
|
+
protocolVersion: MCP_PROTOCOL,
|
|
954
|
+
capabilities: { tools: {} },
|
|
955
|
+
serverInfo: { name: 'aontu', version },
|
|
956
|
+
instructions: serverInstructions(root),
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
case 'ping':
|
|
960
|
+
return ok(id, {})
|
|
961
|
+
|
|
962
|
+
case 'tools/list':
|
|
963
|
+
return ok(id, { tools: toolList(root) })
|
|
964
|
+
|
|
965
|
+
case 'tools/call': {
|
|
966
|
+
const name = msg.params?.name
|
|
967
|
+
if ('string' !== typeof name) {
|
|
968
|
+
return err(id, INVALID_PARAMS, 'tools/call needs a tool name')
|
|
969
|
+
}
|
|
970
|
+
return ok(id, callTool(name, msg.params?.arguments ?? {}, { root }))
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
default:
|
|
974
|
+
return err(id, METHOD_NOT_FOUND, `no such method: ${msg.method}`)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
// A message that did not decode at all.
|
|
980
|
+
export function parseError(): McpResponse {
|
|
981
|
+
return err(null, PARSE_ERROR, 'invalid JSON')
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
function ok(id: number | string | null, result: any): McpResponse {
|
|
986
|
+
return { jsonrpc: '2.0', id, result }
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
function err(
|
|
991
|
+
id: number | string | null, code: number, message: string): McpResponse {
|
|
992
|
+
return { jsonrpc: '2.0', id, error: { code, message } }
|
|
993
|
+
}
|