@m6d/cortex-cli 0.12.1 → 0.14.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 +193 -128
- package/package.json +16 -5
- package/src/cli.ts +19 -12
- package/src/commands/apply.ts +186 -0
- package/src/commands/{cc.ts → management.ts} +173 -112
- package/src/commands/new.ts +83 -84
- package/src/commands/setup.ts +610 -0
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +973 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +349 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +67 -33
- package/src/{cc → management}/client.ts +33 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +10 -8
- package/src/scaffold/files.ts +448 -284
- package/src/scaffold/ports.ts +12 -24
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +6 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +1 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +44 -0
- package/src/ui/prompts.ts +24 -38
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -62
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /package/src/{cc → management}/output.ts +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { manifestSchema } from "../manifest/index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Imports a manifest file, TypeScript or JSON (Bun imports both), and checks
|
|
7
|
+
* its default export against the contract: `written` is the export as
|
|
8
|
+
* written, for a caller that lets Cortex fill its own defaults, and
|
|
9
|
+
* `parsed` the check. Throws when the file does not import or exports
|
|
10
|
+
* nothing. Kept out of the contract's module, which the browser imports too.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadManifestFile(path: string) {
|
|
13
|
+
const module = (await import(pathToFileURL(path).href)) as { default?: unknown };
|
|
14
|
+
if (module.default === undefined) throw new Error("The module has no default export.");
|
|
15
|
+
return { written: module.default, parsed: manifestSchema.safeParse(module.default) };
|
|
16
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* CommonMark fence tracking shared by the prompt rich-text pipeline, the
|
|
3
|
-
* widget's streamed-message segmenter, and
|
|
3
|
+
* widget's streamed-message segmenter, and Cortex's report stand-ins.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** A fence delimiter of three or more backticks or tildes, indented at most three spaces, and the rest of its line. */
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// cspell:ignore fefe fff
|
|
2
|
+
import { RE2JS } from "re2js";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* A tool input schema's `pattern`: a regular expression JavaScript reads with
|
|
6
|
+
* no flags, which Cortex matches on RE2 so a value is checked in time linear
|
|
7
|
+
* in its length. A pattern RE2 cannot match as JavaScript reads it is refused
|
|
8
|
+
* by Cortex and left out of what the TypeScript SDK exports.
|
|
9
|
+
*
|
|
10
|
+
* RE2 matches code points where JavaScript matches UTF-16 units, so the
|
|
11
|
+
* pattern and every input lift each surrogate unit into a private-use plane,
|
|
12
|
+
* where it is one code point: RE2 then reads the units JavaScript reads.
|
|
13
|
+
* Nothing collides there, since a real character in that plane is astral,
|
|
14
|
+
* arrives as two surrogate units and is lifted in turn.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What one escape or character in a class stands for, spelled as the inside
|
|
19
|
+
* of an RE2 class: a single character carries its UTF-16 unit, and an
|
|
20
|
+
* unescaped `-` is marked as one that may make a range.
|
|
21
|
+
*/
|
|
22
|
+
type Atom = { body: string; char?: number; dash?: boolean };
|
|
23
|
+
|
|
24
|
+
/** JavaScript's `\s`, its WhiteSpace and LineTerminator characters, as the inside of an RE2 class. */
|
|
25
|
+
const SPACE =
|
|
26
|
+
"\\x{9}-\\x{d}\\x{20}\\x{a0}\\x{1680}\\x{2000}-\\x{200a}\\x{2028}\\x{2029}\\x{202f}\\x{205f}\\x{3000}\\x{feff}";
|
|
27
|
+
/** Every character `SPACE` leaves out: JavaScript's `\S`. */
|
|
28
|
+
const NON_SPACE =
|
|
29
|
+
"\\x{0}-\\x{8}\\x{e}-\\x{1f}\\x{21}-\\x{9f}\\x{a1}-\\x{167f}\\x{1681}-\\x{1fff}\\x{200b}-\\x{2027}" +
|
|
30
|
+
"\\x{202a}-\\x{202e}\\x{2030}-\\x{205e}\\x{2060}-\\x{2fff}\\x{3001}-\\x{fefe}\\x{ff00}-\\x{10ffff}";
|
|
31
|
+
/** The set escapes as JavaScript reads them; RE2's own `\d` and `\w` match, its `\s` holds only ASCII. */
|
|
32
|
+
const SET_ESCAPES = new Map([
|
|
33
|
+
["d", "\\d"],
|
|
34
|
+
["D", "\\D"],
|
|
35
|
+
["w", "\\w"],
|
|
36
|
+
["W", "\\W"],
|
|
37
|
+
["s", SPACE],
|
|
38
|
+
["S", NON_SPACE],
|
|
39
|
+
]);
|
|
40
|
+
/** JavaScript's `.`: every character but a line terminator, where RE2's leaves out only `\n`. */
|
|
41
|
+
const DOT = "[^\\x{a}\\x{d}\\x{2028}\\x{2029}]";
|
|
42
|
+
const CONTROL_ESCAPES = new Map([
|
|
43
|
+
["f", 0x0c],
|
|
44
|
+
["n", 0x0a],
|
|
45
|
+
["r", 0x0d],
|
|
46
|
+
["t", 0x09],
|
|
47
|
+
["v", 0x0b],
|
|
48
|
+
]);
|
|
49
|
+
const QUANTIFIER = /\{\d+(?:,\d*)?\}/y;
|
|
50
|
+
const SURROGATE = /[\uD800-\uDFFF]/g;
|
|
51
|
+
/** Where surrogate unit `0xd800 + n` is lifted: code point `0xf0000 + n`. */
|
|
52
|
+
const LIFTED_SURROGATES = 0xf0000;
|
|
53
|
+
const HEX = /[0-9A-Fa-f]+/y;
|
|
54
|
+
/** What JavaScript's `[^]` and `[]` hold: every character, and none. */
|
|
55
|
+
const ANY = "[\\x{0}-\\x{10ffff}]";
|
|
56
|
+
const NONE = "[^\\x{0}-\\x{10ffff}]";
|
|
57
|
+
|
|
58
|
+
/** A thrown error: the pattern holds what RE2 cannot match as JavaScript reads it. */
|
|
59
|
+
function unsupported(what: string): never {
|
|
60
|
+
throw new Error(`a pattern with ${what}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A UTF-16 unit as the one code point RE2 reads: a surrogate lifted, any other unit itself. */
|
|
64
|
+
function lifted(unit: number) {
|
|
65
|
+
return unit >= 0xd800 && unit <= 0xdfff ? LIFTED_SURROGATES + unit - 0xd800 : unit;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `input` with every surrogate unit lifted, one code point per UTF-16 unit. */
|
|
69
|
+
function liftedText(input: string) {
|
|
70
|
+
return input.replace(SURROGATE, (unit) => String.fromCodePoint(lifted(unit.charCodeAt(0))));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One UTF-16 unit as RE2 reads it literally, inside a class or out. */
|
|
74
|
+
function literal(unit: number) {
|
|
75
|
+
const char = lifted(unit);
|
|
76
|
+
const spelled = String.fromCodePoint(char);
|
|
77
|
+
return /^[A-Za-z0-9]$/.test(spelled) ? spelled : `\\x{${char.toString(16)}}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The units from `from` to `to` as the inside of an RE2 class: the part
|
|
82
|
+
* among the surrogates, lifted, apart from the parts below and above it.
|
|
83
|
+
*/
|
|
84
|
+
function unitRange(from: number, to: number) {
|
|
85
|
+
return (
|
|
86
|
+
[
|
|
87
|
+
[from, Math.min(to, 0xd7ff)],
|
|
88
|
+
[Math.max(from, 0xd800), Math.min(to, 0xdfff)],
|
|
89
|
+
[Math.max(from, 0xe000), to],
|
|
90
|
+
] satisfies Array<[number, number]>
|
|
91
|
+
)
|
|
92
|
+
.filter(([low, high]) => low <= high)
|
|
93
|
+
.map(([low, high]) => `${literal(low)}-${literal(high)}`)
|
|
94
|
+
.join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function character(char: number, dash = false) {
|
|
98
|
+
return { body: literal(char), char, dash };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A class's atoms as the inside of an RE2 class: an unescaped dash between
|
|
103
|
+
* two characters makes a range, and beside a set escape it is a character,
|
|
104
|
+
* as JavaScript reads them.
|
|
105
|
+
*/
|
|
106
|
+
function classBody(atoms: Atom[]) {
|
|
107
|
+
let body = "";
|
|
108
|
+
for (let index = 0; index < atoms.length; index += 1) {
|
|
109
|
+
const [from, dash, to] = atoms.slice(index, index + 3);
|
|
110
|
+
if (!from) break;
|
|
111
|
+
if (!dash?.dash || !to) {
|
|
112
|
+
body += from.body;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
body +=
|
|
116
|
+
from.char !== undefined && to.char !== undefined
|
|
117
|
+
? unitRange(from.char, to.char)
|
|
118
|
+
: `${from.body}${dash.body}${to.body}`;
|
|
119
|
+
index += 2;
|
|
120
|
+
}
|
|
121
|
+
return body;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A pattern read left to right as JavaScript reads it with no flags (Annex B
|
|
126
|
+
* included), written in RE2's syntax; throws on a lookaround, a
|
|
127
|
+
* backreference, a legacy octal escape, a group modifier or a `\c` without
|
|
128
|
+
* its letter. It reads UTF-16 units, as JavaScript does. Groups do not
|
|
129
|
+
* capture: nothing reads them.
|
|
130
|
+
*/
|
|
131
|
+
class Translation {
|
|
132
|
+
private at = 0;
|
|
133
|
+
|
|
134
|
+
public constructor(private readonly pattern: string) {}
|
|
135
|
+
|
|
136
|
+
public spell() {
|
|
137
|
+
let re2 = "";
|
|
138
|
+
while (this.at < this.pattern.length) re2 += this.term();
|
|
139
|
+
return re2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private term() {
|
|
143
|
+
const char = this.pattern[this.at] ?? "";
|
|
144
|
+
if (char === "\\") return this.escapeOutsideClass();
|
|
145
|
+
if (char === "[") return this.characterClass();
|
|
146
|
+
if (char === "(") return this.group();
|
|
147
|
+
if (char === "{") return this.brace();
|
|
148
|
+
if (!"|)^$*+?.".includes(char)) return literal(this.unit());
|
|
149
|
+
this.at += 1;
|
|
150
|
+
return char === "." ? DOT : char;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private escapeOutsideClass() {
|
|
154
|
+
this.at += 1;
|
|
155
|
+
const assertion = this.pattern[this.at];
|
|
156
|
+
if (assertion === "b" || assertion === "B") {
|
|
157
|
+
this.at += 1;
|
|
158
|
+
return `\\${assertion}`;
|
|
159
|
+
}
|
|
160
|
+
const atom = this.escape(false);
|
|
161
|
+
return "char" in atom ? atom.body : `[${atom.body}]`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The escape after a backslash, which `at` is past; in a class `\b` is a backspace. */
|
|
165
|
+
private escape(insideClass: boolean) {
|
|
166
|
+
const next = this.pattern[this.at] ?? unsupported("a trailing backslash");
|
|
167
|
+
this.at += 1;
|
|
168
|
+
const set = SET_ESCAPES.get(next);
|
|
169
|
+
if (set) return { body: set };
|
|
170
|
+
const control = insideClass && next === "b" ? 0x08 : CONTROL_ESCAPES.get(next);
|
|
171
|
+
if (control !== undefined) return character(control);
|
|
172
|
+
if (next === "c") return character(this.controlLetter(insideClass));
|
|
173
|
+
if (next === "x" || next === "u") {
|
|
174
|
+
return character(this.hex(next === "x" ? 2 : 4) ?? next.charCodeAt(0));
|
|
175
|
+
}
|
|
176
|
+
if (next === "0" && !/^\d$/.test(this.pattern[this.at] ?? "")) return character(0);
|
|
177
|
+
if (/^[\dk]$/.test(next)) unsupported("a backreference or an octal escape");
|
|
178
|
+
this.at -= 1;
|
|
179
|
+
return character(this.unit());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The control character a `\c` names: a letter, or in a class also a digit or `_`. */
|
|
183
|
+
private controlLetter(insideClass: boolean) {
|
|
184
|
+
const letter = this.pattern[this.at] ?? "";
|
|
185
|
+
if (!(insideClass ? /^\w$/ : /^[A-Za-z]$/).test(letter)) {
|
|
186
|
+
unsupported("a \\c without its letter");
|
|
187
|
+
}
|
|
188
|
+
this.at += 1;
|
|
189
|
+
return letter.charCodeAt(0) % 32;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `length` hex digits at `at` as a UTF-16 unit, consumed; undefined when fewer follow. */
|
|
193
|
+
private hex(length: number) {
|
|
194
|
+
HEX.lastIndex = this.at;
|
|
195
|
+
const digits = HEX.exec(this.pattern)?.[0] ?? "";
|
|
196
|
+
if (digits.length < length) return undefined;
|
|
197
|
+
this.at += length;
|
|
198
|
+
return Number.parseInt(digits.slice(0, length), 16);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private unit() {
|
|
202
|
+
const unit = this.pattern.charCodeAt(this.at);
|
|
203
|
+
if (Number.isNaN(unit)) unsupported("a pattern cut short");
|
|
204
|
+
this.at += 1;
|
|
205
|
+
return unit;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A class, `at` on its `[`. In JavaScript a `]` always closes one, so
|
|
210
|
+
* `[]` holds nothing and `[^]` everything.
|
|
211
|
+
*/
|
|
212
|
+
private characterClass() {
|
|
213
|
+
this.at += 1;
|
|
214
|
+
const negated = this.pattern[this.at] === "^";
|
|
215
|
+
if (negated) this.at += 1;
|
|
216
|
+
const atoms: Atom[] = [];
|
|
217
|
+
while (this.pattern[this.at] !== "]") {
|
|
218
|
+
if (this.at >= this.pattern.length) unsupported("an unclosed class");
|
|
219
|
+
atoms.push(this.classAtom());
|
|
220
|
+
}
|
|
221
|
+
this.at += 1;
|
|
222
|
+
const body = classBody(atoms);
|
|
223
|
+
if (!body) return negated ? ANY : NONE;
|
|
224
|
+
return `[${negated ? "^" : ""}${body}]`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private classAtom() {
|
|
228
|
+
const char = this.pattern[this.at];
|
|
229
|
+
this.at += 1;
|
|
230
|
+
if (char === "\\") return this.escape(true);
|
|
231
|
+
if (char === "-") return character(0x2d, true);
|
|
232
|
+
this.at -= 1;
|
|
233
|
+
return character(this.unit());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A group's opening as RE2 reads it: every group, named or not, without a capture. */
|
|
237
|
+
private group() {
|
|
238
|
+
const opening = this.pattern.slice(this.at, this.at + 4);
|
|
239
|
+
if (/^\(\?<[^=!]/.test(opening)) {
|
|
240
|
+
const name = this.pattern.indexOf(">", this.at);
|
|
241
|
+
if (name < 0) unsupported("an unclosed group name");
|
|
242
|
+
this.at = name + 1;
|
|
243
|
+
} else if (opening.startsWith("(?")) {
|
|
244
|
+
if (!opening.startsWith("(?:")) unsupported("a lookaround or a group modifier");
|
|
245
|
+
this.at += 3;
|
|
246
|
+
} else {
|
|
247
|
+
this.at += 1;
|
|
248
|
+
}
|
|
249
|
+
return "(?:";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** A quantifier, or a brace JavaScript reads as a character because it opens none. */
|
|
253
|
+
private brace() {
|
|
254
|
+
QUANTIFIER.lastIndex = this.at;
|
|
255
|
+
const quantifier = QUANTIFIER.exec(this.pattern)?.[0];
|
|
256
|
+
this.at += quantifier?.length ?? 1;
|
|
257
|
+
return quantifier ?? literal(0x7b);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A matcher for a pattern JavaScript compiles, meaning what the pattern
|
|
263
|
+
* means to JavaScript with no flags, UTF-16 unit for unit; undefined when
|
|
264
|
+
* RE2 cannot match it so, as for a lookaround, a backreference or a
|
|
265
|
+
* repetition past 1000. Its `test` runs on RE2, in time linear in the input.
|
|
266
|
+
* It never calls `new RegExp`, so it works while a caller has replaced that
|
|
267
|
+
* global.
|
|
268
|
+
*/
|
|
269
|
+
export function linearPattern(pattern: string) {
|
|
270
|
+
try {
|
|
271
|
+
const re2 = RE2JS.compile(new Translation(pattern).spell());
|
|
272
|
+
return { test: (input: string) => re2.test(liftedText(input)) };
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { REQUEST_LOCATION_TOOL } from "../client-tools/index";
|
|
3
|
+
import { backendErrorSchema } from "../errors/index";
|
|
4
|
+
import { pendingApprovalSchema, TRIGGER_ANSWER_TOOL_NAME } from "../triggers/index";
|
|
5
|
+
import { isRecord, unescapePointerToken } from "../json/index";
|
|
6
|
+
import { APPROVAL_TITLE_FIELD, LOCALES } from "../wire/index";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The server protocol: what Cortex calls on a customer's backend. `GET /`
|
|
10
|
+
* answers the manifest (the variables it fills, the native tools it runs,
|
|
11
|
+
* whether it verifies end-user tokens, the events it hears); the other
|
|
12
|
+
* routes are the calls behind them. Every request carries the server key as
|
|
13
|
+
* its bearer, and a run-scoped one also the run's token in
|
|
14
|
+
* `END_USER_TOKEN_HEADER` when the run has one. A refusal on any route
|
|
15
|
+
* answers in the backend error envelope.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The run's end-user token, which Cortex forwards verbatim to a tool's backend, a dataset host or the agent's server. */
|
|
19
|
+
export const END_USER_TOKEN_HEADER = "X-End-User-Token";
|
|
20
|
+
|
|
21
|
+
/** The agent slug Cortex's routes name an agent by. */
|
|
22
|
+
export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
23
|
+
export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
|
|
24
|
+
|
|
25
|
+
/** An end user's id, as wide as Cortex stores one. */
|
|
26
|
+
export const userIdSchema = z.string().max(255);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An ASCII JavaScript identifier: what a tool name (a `tools.<name>()`
|
|
30
|
+
* sandbox binding) and a variable name must be, and what a rendered
|
|
31
|
+
* signature prints bare rather than quoted.
|
|
32
|
+
*/
|
|
33
|
+
export const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
34
|
+
|
|
35
|
+
export const localeSchema = z.enum(LOCALES);
|
|
36
|
+
|
|
37
|
+
/** The value one thread's variable holds: what its server fills, or an operator sets on a test thread. */
|
|
38
|
+
const variableValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
39
|
+
|
|
40
|
+
/** One thread's variable values, by name. */
|
|
41
|
+
export const variableValuesSchema = z.record(z.string(), variableValueSchema);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A variable an agent's text and tool inputs may read: one its server
|
|
45
|
+
* declares, or one Cortex computes. The console offers exactly these, in
|
|
46
|
+
* prompts and as tool input sources alike.
|
|
47
|
+
*/
|
|
48
|
+
export const variableDeclarationSchema = z.object({
|
|
49
|
+
name: z.string().regex(IDENTIFIER_PATTERN).max(64),
|
|
50
|
+
description: z.string().max(500),
|
|
51
|
+
example: z.string().max(200),
|
|
52
|
+
/** The type its value takes. */
|
|
53
|
+
type: z.enum(["string", "number", "boolean"]),
|
|
54
|
+
/** Computed by Cortex for every thread; no server can set it. */
|
|
55
|
+
computed: z.boolean(),
|
|
56
|
+
});
|
|
57
|
+
export type VariableDeclaration = z.infer<typeof variableDeclarationSchema>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The variables Cortex computes for every thread, listed first among a
|
|
61
|
+
* server's variables so the console offers them like any declared one.
|
|
62
|
+
*/
|
|
63
|
+
export const BUILT_IN_VARIABLES = [
|
|
64
|
+
{
|
|
65
|
+
name: "utcTime",
|
|
66
|
+
description: "Current time in UTC (ISO 8601)",
|
|
67
|
+
example: "2026-09-12T08:30:00Z",
|
|
68
|
+
type: "string",
|
|
69
|
+
computed: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "locale",
|
|
73
|
+
description: "Resolved locale of the session",
|
|
74
|
+
example: "ar",
|
|
75
|
+
type: "string",
|
|
76
|
+
computed: true,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "timezone",
|
|
80
|
+
description: "The user's IANA time zone, as the widget reports it",
|
|
81
|
+
example: "Asia/Riyadh",
|
|
82
|
+
type: "string",
|
|
83
|
+
computed: true,
|
|
84
|
+
},
|
|
85
|
+
] as const satisfies ReadonlyArray<VariableDeclaration>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The names of Cortex's built-in tools. No other tool may take one: the
|
|
89
|
+
* model would see two tools under one name. The .NET SDK holds its own copy,
|
|
90
|
+
* checked against `reserved-names.fixtures.json`.
|
|
91
|
+
*/
|
|
92
|
+
export const BUILT_IN_TOOL_NAMES = [
|
|
93
|
+
"describeDataset",
|
|
94
|
+
"executeCode",
|
|
95
|
+
"fetchImage",
|
|
96
|
+
"loadSkill",
|
|
97
|
+
"queryDataset",
|
|
98
|
+
"readAttachment",
|
|
99
|
+
"readReport",
|
|
100
|
+
"searchKnowledge",
|
|
101
|
+
"searchServices",
|
|
102
|
+
"searchThreadHistory",
|
|
103
|
+
"searchTools",
|
|
104
|
+
REQUEST_LOCATION_TOOL,
|
|
105
|
+
TRIGGER_ANSWER_TOOL_NAME,
|
|
106
|
+
] as const;
|
|
107
|
+
|
|
108
|
+
export function isBuiltInToolName(name: string) {
|
|
109
|
+
return BUILT_IN_TOOL_NAMES.some((builtIn) => builtIn === name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The longest name a tool takes, a server's or one defined in Cortex. */
|
|
113
|
+
export const TOOL_NAME_MAX_LENGTH = 64;
|
|
114
|
+
|
|
115
|
+
/** A tool name, a server's or one defined in Cortex: an identifier no built-in tool holds. */
|
|
116
|
+
const declaredToolNameSchema = z
|
|
117
|
+
.string()
|
|
118
|
+
.regex(IDENTIFIER_PATTERN)
|
|
119
|
+
.max(TOOL_NAME_MAX_LENGTH)
|
|
120
|
+
.refine((name) => !isBuiltInToolName(name), "the name of a built-in tool");
|
|
121
|
+
|
|
122
|
+
const jsonSchemaSchema = z.record(z.string(), z.unknown());
|
|
123
|
+
|
|
124
|
+
/** A variable the server fills for its agents' threads; the built-in ones are Cortex's. */
|
|
125
|
+
const serverVariableSchema = z.object({
|
|
126
|
+
name: z
|
|
127
|
+
.string()
|
|
128
|
+
.regex(IDENTIFIER_PATTERN)
|
|
129
|
+
.max(64)
|
|
130
|
+
.refine(
|
|
131
|
+
(name) => !BUILT_IN_VARIABLES.some((builtIn) => builtIn.name === name),
|
|
132
|
+
"the name of a built-in variable",
|
|
133
|
+
),
|
|
134
|
+
description: z.string().max(500),
|
|
135
|
+
example: z.string().max(200),
|
|
136
|
+
type: variableDeclarationSchema.shape.type,
|
|
137
|
+
});
|
|
138
|
+
export type ServerVariable = z.infer<typeof serverVariableSchema>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A tool the server runs. Its input is an object, the `input` its calls
|
|
142
|
+
* carry. `requiresApproval` parks every call for the end user's approval;
|
|
143
|
+
* such a tool's input cannot declare `APPROVAL_TITLE_FIELD`, which the
|
|
144
|
+
* model fills for the approval card.
|
|
145
|
+
*/
|
|
146
|
+
export const serverToolSchema = z.object({
|
|
147
|
+
name: declaredToolNameSchema,
|
|
148
|
+
description: z.string().trim().min(1).max(500),
|
|
149
|
+
inputSchema: jsonSchemaSchema.refine(
|
|
150
|
+
(schema) => schema.type === "object",
|
|
151
|
+
"a tool's input is an object",
|
|
152
|
+
),
|
|
153
|
+
outputSchema: jsonSchemaSchema.optional(),
|
|
154
|
+
readOnly: z.boolean(),
|
|
155
|
+
requiresApproval: z.boolean().optional(),
|
|
156
|
+
});
|
|
157
|
+
export type ServerTool = z.infer<typeof serverToolSchema>;
|
|
158
|
+
|
|
159
|
+
/** The events a server hears at `POST /events/:event`, and a webhook delivers. */
|
|
160
|
+
export const SERVER_EVENTS = ["approval_requested"] as const;
|
|
161
|
+
|
|
162
|
+
/** The header a webhook delivery names its event in, so a receiver tells events apart without parsing the body. */
|
|
163
|
+
export const EVENT_HEADER = "X-Cortex-Event";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A run parked on server tools that need the end user's approval, as every
|
|
167
|
+
* receiver hears of it: a webhook's body and the agent's server's event are
|
|
168
|
+
* this one object. `firingId` and `triggerName` are set when a trigger
|
|
169
|
+
* started the run, which is what a receiver answering through the firing
|
|
170
|
+
* route needs. Every parked call is named; one whose input would carry the
|
|
171
|
+
* event past `SERVER_REQUEST_MAX_BYTES` comes without its input.
|
|
172
|
+
*/
|
|
173
|
+
export const approvalRequestedEventSchema = z.object({
|
|
174
|
+
event: z.literal("approval_requested"),
|
|
175
|
+
agentSlug: agentSlugSchema,
|
|
176
|
+
agentId: z.uuid(),
|
|
177
|
+
threadId: z.string(),
|
|
178
|
+
runId: z.string(),
|
|
179
|
+
userId: z.string(),
|
|
180
|
+
firingId: z.uuid().optional(),
|
|
181
|
+
triggerName: z.string().optional(),
|
|
182
|
+
approvals: z.array(pendingApprovalSchema.partial({ input: true })),
|
|
183
|
+
occurredAt: z.iso.datetime(),
|
|
184
|
+
});
|
|
185
|
+
export type ApprovalRequestedEvent = z.infer<typeof approvalRequestedEventSchema>;
|
|
186
|
+
|
|
187
|
+
/** A JSON Pointer token as a key: percent-decoded, then unescaped; undefined when its encoding is broken. */
|
|
188
|
+
function pointerKey(token: string) {
|
|
189
|
+
try {
|
|
190
|
+
return unescapePointerToken(decodeURIComponent(token));
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** What a local `$ref` (`#/$defs/order`) points at in `root`, read as the input compiler reads it; undefined for any other. */
|
|
197
|
+
function pointedAt(root: unknown, ref: string) {
|
|
198
|
+
if (!ref.startsWith("#")) return undefined;
|
|
199
|
+
let node = root;
|
|
200
|
+
for (const token of ref.slice(1).split("/").slice(1)) {
|
|
201
|
+
const key = pointerKey(token);
|
|
202
|
+
node =
|
|
203
|
+
key !== undefined && (isRecord(node) || Array.isArray(node))
|
|
204
|
+
? new Map<string, unknown>(Object.entries(node)).get(key)
|
|
205
|
+
: undefined;
|
|
206
|
+
}
|
|
207
|
+
return node;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether one schema node names `APPROVAL_TITLE_FIELD` as a property or a required field. */
|
|
211
|
+
function namesApprovalTitle(node: Record<string, unknown>) {
|
|
212
|
+
const required: unknown = node.required;
|
|
213
|
+
return (
|
|
214
|
+
(isRecord(node.properties) && Object.hasOwn(node.properties, APPROVAL_TITLE_FIELD)) ||
|
|
215
|
+
(Array.isArray(required) && required.includes(APPROVAL_TITLE_FIELD))
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** What describes the same object as `node`: its `allOf`, `anyOf` and `oneOf` members, and what its local `$ref` points at in `root`. */
|
|
220
|
+
function sameObjectSchemas(node: Record<string, unknown>, root: unknown) {
|
|
221
|
+
const schemas: unknown[] = typeof node.$ref === "string" ? [pointedAt(root, node.$ref)] : [];
|
|
222
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
223
|
+
const members: unknown = node[keyword];
|
|
224
|
+
if (!Array.isArray(members)) continue;
|
|
225
|
+
const listed: unknown[] = members;
|
|
226
|
+
schemas.push(...listed);
|
|
227
|
+
}
|
|
228
|
+
return schemas;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Every node that describes the same object as an input schema's root: the
|
|
233
|
+
* root, and what `sameObjectSchemas` reaches from it at any depth, each once.
|
|
234
|
+
*/
|
|
235
|
+
function sameObjectNodes(inputSchema: unknown) {
|
|
236
|
+
const nodes = new Array<Record<string, unknown>>();
|
|
237
|
+
const pending = [inputSchema];
|
|
238
|
+
while (pending.length > 0) {
|
|
239
|
+
const node = pending.pop();
|
|
240
|
+
if (!isRecord(node) || nodes.includes(node)) continue;
|
|
241
|
+
nodes.push(node);
|
|
242
|
+
pending.push(...sameObjectSchemas(node, inputSchema));
|
|
243
|
+
}
|
|
244
|
+
return nodes;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Whether one schema node is closed to properties it does not declare. */
|
|
248
|
+
function closed(node: Record<string, unknown>) {
|
|
249
|
+
return node.additionalProperties === false || node.unevaluatedProperties === false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether an input schema leaves no room for `APPROVAL_TITLE_FIELD`, which
|
|
254
|
+
* Cortex adds to the root of a tool that needs approval: a node describing
|
|
255
|
+
* the input names it, or one besides the root is closed to it, so a strict
|
|
256
|
+
* reading of the schema could not take the title.
|
|
257
|
+
*/
|
|
258
|
+
export function blocksApprovalTitle(inputSchema: unknown) {
|
|
259
|
+
return sameObjectNodes(inputSchema).some(
|
|
260
|
+
(node) => namesApprovalTitle(node) || (node !== inputSchema && closed(node)),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Refuses, under `path`, a tool that needs approval whose input `blocksApprovalTitle`. */
|
|
265
|
+
export function refuseBlockedApprovalTitle(
|
|
266
|
+
tool: { requiresApproval?: boolean; inputSchema: unknown },
|
|
267
|
+
context: z.RefinementCtx,
|
|
268
|
+
path: Array<string | number>,
|
|
269
|
+
) {
|
|
270
|
+
if (!tool.requiresApproval || !blocksApprovalTitle(tool.inputSchema)) return;
|
|
271
|
+
context.addIssue({
|
|
272
|
+
code: "custom",
|
|
273
|
+
path: [...path, "inputSchema"],
|
|
274
|
+
message: `a tool that needs approval cannot take an input named ${APPROVAL_TITLE_FIELD}, nor one a closed sub-schema describes: Cortex adds the approval card's title to it`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
|
|
279
|
+
return new Set(entries.map((entry) => entry.name)).size === entries.length;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `GET /`: everything the server offers, keyed by `version`, which changes whenever anything else does. */
|
|
283
|
+
export const serverManifestSchema = z.object({
|
|
284
|
+
version: z.string().min(1).max(128),
|
|
285
|
+
variables: z.array(serverVariableSchema).max(100).refine(uniqueNames, "variable names repeat"),
|
|
286
|
+
tools: z
|
|
287
|
+
.array(serverToolSchema)
|
|
288
|
+
.max(500)
|
|
289
|
+
.refine(uniqueNames, "tool names repeat")
|
|
290
|
+
.superRefine((tools, context) => {
|
|
291
|
+
tools.forEach((tool, index) => refuseBlockedApprovalTitle(tool, context, [index]));
|
|
292
|
+
}),
|
|
293
|
+
verify: z.boolean(),
|
|
294
|
+
events: z.array(z.enum(SERVER_EVENTS)),
|
|
295
|
+
});
|
|
296
|
+
export type ServerManifest = z.infer<typeof serverManifestSchema>;
|
|
297
|
+
|
|
298
|
+
/** The agent a call is for. */
|
|
299
|
+
const agentSchema = z.object({ id: z.uuid(), slug: agentSlugSchema });
|
|
300
|
+
|
|
301
|
+
/** The end user a run acts for, as their token was verified; null for a trigger's run. */
|
|
302
|
+
const serverUserSchema = z
|
|
303
|
+
.object({ id: userIdSchema, claims: z.record(z.string(), z.unknown()) })
|
|
304
|
+
.nullable();
|
|
305
|
+
|
|
306
|
+
/** `POST /variables`: the values one thread runs with, asked once per thread. */
|
|
307
|
+
export const variablesRequestSchema = z.object({
|
|
308
|
+
agent: agentSchema,
|
|
309
|
+
thread: z.object({ id: z.string() }),
|
|
310
|
+
user: serverUserSchema,
|
|
311
|
+
trigger: z.object({ name: z.string() }).optional(),
|
|
312
|
+
});
|
|
313
|
+
export type VariablesRequest = z.infer<typeof variablesRequestSchema>;
|
|
314
|
+
|
|
315
|
+
export const variablesResponseSchema = z.object({ values: variableValuesSchema });
|
|
316
|
+
export type VariablesResponse = z.infer<typeof variablesResponseSchema>;
|
|
317
|
+
|
|
318
|
+
/** `POST /tools/:name`: one call, with the variables it runs with. */
|
|
319
|
+
export const toolRequestSchema = z.object({
|
|
320
|
+
input: z.record(z.string(), z.unknown()),
|
|
321
|
+
context: variablesRequestSchema.pick({ agent: true, thread: true, user: true }).extend({
|
|
322
|
+
variables: z.record(z.string(), z.unknown()),
|
|
323
|
+
}),
|
|
324
|
+
});
|
|
325
|
+
export type ToolRequest = z.infer<typeof toolRequestSchema>;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The largest request body a server takes, and so the largest one Cortex
|
|
329
|
+
* sends: room for a tool call's input of up to 256 KiB with the variables
|
|
330
|
+
* and claims it carries.
|
|
331
|
+
*/
|
|
332
|
+
export const SERVER_REQUEST_MAX_BYTES = 4 * 1024 * 1024;
|
|
333
|
+
|
|
334
|
+
/** What a tool call answers: its output, or a refusal in the backend error envelope. */
|
|
335
|
+
export const toolResponseSchema = z.union([backendErrorSchema, z.object({ output: z.json() })]);
|
|
336
|
+
|
|
337
|
+
/** `POST /verify`: whose end-user token this is; `sub: null` refuses it. */
|
|
338
|
+
export const verifyRequestSchema = z.object({ token: z.string().min(1) });
|
|
339
|
+
export type VerifyRequest = z.infer<typeof verifyRequestSchema>;
|
|
340
|
+
export const verifyResponseSchema = z.union([
|
|
341
|
+
z.object({ sub: userIdSchema.min(1), claims: z.record(z.string(), z.unknown()) }),
|
|
342
|
+
z.object({ sub: z.null() }),
|
|
343
|
+
]);
|
|
344
|
+
export type VerifyResponse = z.infer<typeof verifyResponseSchema>;
|
|
345
|
+
|
|
346
|
+
/** `POST /events/:event`: the body of each event a manifest subscribes to. */
|
|
347
|
+
export const serverEventSchemas = {
|
|
348
|
+
approval_requested: approvalRequestedEventSchema,
|
|
349
|
+
} as const satisfies Record<(typeof SERVER_EVENTS)[number], z.ZodType>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tools": [
|
|
3
|
+
"describeDataset",
|
|
4
|
+
"executeCode",
|
|
5
|
+
"fetchImage",
|
|
6
|
+
"loadSkill",
|
|
7
|
+
"queryDataset",
|
|
8
|
+
"readAttachment",
|
|
9
|
+
"readReport",
|
|
10
|
+
"searchKnowledge",
|
|
11
|
+
"searchServices",
|
|
12
|
+
"searchThreadHistory",
|
|
13
|
+
"searchTools",
|
|
14
|
+
"requestLocation",
|
|
15
|
+
"submitAnswer"
|
|
16
|
+
],
|
|
17
|
+
"variables": ["utcTime", "locale", "timezone"]
|
|
18
|
+
}
|