@m6d/cortex-cli 0.16.0 → 0.18.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 +40 -6
- package/package.json +1 -1
- package/src/commands/apply.ts +16 -6
- package/src/commands/setup.ts +7 -1
- package/src/contracts/datasets/dataset-sql.ts +65 -28
- package/src/contracts/manifest/index.ts +427 -82
- package/src/contracts/manifest-file/index.ts +58 -6
- package/src/contracts/wire/index.ts +25 -4
- package/src/scaffold/files.ts +4 -4
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ after; without it the command says so and names the command. Fields the API
|
|
|
135
135
|
marks live (`x-live-fields`) take effect without a publish, from agents'
|
|
136
136
|
next turn, and the command names them: a trigger's
|
|
137
137
|
`credentialName`, `fireCredentialName` and model, say, or a dataset host's
|
|
138
|
-
`url`, `
|
|
138
|
+
`url`, `headers` and `timeoutMs`. `cortex datasetHosts sync <id>` reads a host's manifest, and a
|
|
139
139
|
draft host publishes only after `datasetHosts test` reached it once.
|
|
140
140
|
|
|
141
141
|
A procedure the API marks destructive (`x-destructive`: delete, publish, rotate, toggle and the
|
|
@@ -200,7 +200,7 @@ openapi: [
|
|
|
200
200
|
{
|
|
201
201
|
name: "shop",
|
|
202
202
|
url: "https://shop.example.com/openapi.json",
|
|
203
|
-
|
|
203
|
+
headers: [{ header: "Authorization", prefix: "Bearer ", from: { kind: "runToken" } }],
|
|
204
204
|
// For an API that wraps every answer: where each one keeps its result.
|
|
205
205
|
// resultPath: "/data",
|
|
206
206
|
pinned: ["findOrder"],
|
|
@@ -211,10 +211,11 @@ openapi: [
|
|
|
211
211
|
],
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
Every apply reads the document first, without the source's
|
|
214
|
+
Every apply reads the document first, without the source's headers. An
|
|
215
215
|
operation it adds is created, a changed one updated, and a dropped one
|
|
216
|
-
deleted. A source
|
|
217
|
-
|
|
216
|
+
deleted. A source whose headers carry a credential or the run token is read
|
|
217
|
+
only over https, or from a host the deployment allows, since the document
|
|
218
|
+
says where they go. A source needs the `tools` write permission even when nothing
|
|
218
219
|
changes. Each tool is named after its `operationId`, made an identifier of at
|
|
219
220
|
most 64 characters; an operation Cortex cannot import is left
|
|
220
221
|
out. Two operations that come out with one name are told apart by a number
|
|
@@ -224,6 +225,39 @@ or any name such a collision decided (`ambiguous_tool`), since a path the
|
|
|
224
225
|
document adds or drops could move it to another endpoint. It also fails when an
|
|
225
226
|
operation's name is already a listed tool's or another source's.
|
|
226
227
|
|
|
228
|
+
A collection lists its documents. Each is inline `text`, a file `path`
|
|
229
|
+
relative to the manifest file, or a `url` Cortex reads on every apply, with no
|
|
230
|
+
credentials:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
collections: [
|
|
234
|
+
{
|
|
235
|
+
name: "Policies",
|
|
236
|
+
audience: { agents: ["support"] },
|
|
237
|
+
documents: [
|
|
238
|
+
{ name: "faq.md", text: "# FAQ\n\nReturns take 30 days." },
|
|
239
|
+
{ path: "./docs/returns.pdf" },
|
|
240
|
+
{ url: "https://shop.example.com/policies/shipping.html" },
|
|
241
|
+
],
|
|
242
|
+
// overridable: ["documents"],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A document's name sets its type: `.md`, `.txt`, `.html`, `.htm`, `.pdf` or
|
|
248
|
+
`.docx`. A path or URL document is named after its file unless `name` says
|
|
249
|
+
otherwise. Each is at most 10 MB. `cortex apply` reads paths and sends their
|
|
250
|
+
bytes, so a request carries every inline and path document again; Cortex
|
|
251
|
+
stores only what changed. A document with new bytes replaces the old one,
|
|
252
|
+
which keeps answering until the new one is processed, and one the manifest
|
|
253
|
+
drops is retired.
|
|
254
|
+
|
|
255
|
+
Listing `documents` makes the manifest own them: an apply retires a document
|
|
256
|
+
uploaded in the console, the first apply after upgrading included, unless
|
|
257
|
+
the collection offers `documents` in `overridable`. Offered, a document
|
|
258
|
+
uploaded or removed in Cortex stays so until the manifest changes that same
|
|
259
|
+
document, which `cortex apply` then asks about.
|
|
260
|
+
|
|
227
261
|
The manifest is checked against the contract before anything is sent, then
|
|
228
262
|
sent as written, so Cortex fills its own defaults. It goes to the
|
|
229
263
|
management API (`manifests/sync`, or `manifests/preview` with `--dry-run`,
|
|
@@ -240,7 +274,7 @@ question, and is required with no terminal to ask on. `--dry-run` never asks.
|
|
|
240
274
|
References between entries (mentions, service scopes, which role a
|
|
241
275
|
credential or secret plays) are judged on the state the apply leaves, so
|
|
242
276
|
the order the manifest lists them in does not matter. A dataset host's
|
|
243
|
-
|
|
277
|
+
headers are the exception: its catalog is read with them before the
|
|
244
278
|
apply, so they are judged on the deployment as it stands. A credential a
|
|
245
279
|
trigger fires with cannot go to a host in the same apply that drops
|
|
246
280
|
it; drop it first, then apply again.
|
package/package.json
CHANGED
package/src/commands/apply.ts
CHANGED
|
@@ -202,9 +202,14 @@ async function sendManifest(options: {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/** The field both sides changed, and its part when the field is judged part by part. */
|
|
206
|
+
function conflictField({ field, key }: Conflict) {
|
|
207
|
+
return key === undefined ? field : `${field} (${key})`;
|
|
208
|
+
}
|
|
209
|
+
|
|
205
210
|
/** How a conflict reads: its entry and the field both sides changed. */
|
|
206
|
-
function conflictLabel(
|
|
207
|
-
return `${kind} ${name}: ${
|
|
211
|
+
function conflictLabel(conflict: Conflict) {
|
|
212
|
+
return `${conflict.kind} ${conflict.name}: ${conflictField(conflict)} changed in both places`;
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
/** Ends a run that does not ask, naming every conflict and how to run `command` again to answer them. */
|
|
@@ -250,8 +255,9 @@ export function findManifest(path: string, command?: string) {
|
|
|
250
255
|
|
|
251
256
|
/**
|
|
252
257
|
* The file's default export, checked against the manifest contract here so
|
|
253
|
-
* a mistake is named before anything is sent, then sent as written
|
|
254
|
-
*
|
|
258
|
+
* a mistake is named before anything is sent, then sent as written, each
|
|
259
|
+
* document `path` read from beside the file: Cortex fills its own defaults,
|
|
260
|
+
* which may be newer than this CLI's.
|
|
255
261
|
*/
|
|
256
262
|
export async function loadManifest(path: string, verbose: boolean) {
|
|
257
263
|
const { written, parsed } = await loadManifestFile(path).catch((error: unknown) =>
|
|
@@ -288,8 +294,12 @@ function printChanges(
|
|
|
288
294
|
rows.set(kind, [...(rows.get(kind) ?? []), ...marked]);
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
|
-
for (const
|
|
292
|
-
|
|
297
|
+
for (const conflict of conflicts) {
|
|
298
|
+
const { kind, name } = conflict;
|
|
299
|
+
rows.set(kind, [
|
|
300
|
+
...(rows.get(kind) ?? []),
|
|
301
|
+
`! ${name} ${conflictField(conflict)} changed in both places`,
|
|
302
|
+
]);
|
|
293
303
|
}
|
|
294
304
|
for (const [kind, lines] of rows) {
|
|
295
305
|
for (const [index, line] of lines.entries()) {
|
package/src/commands/setup.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
normalizeHost,
|
|
13
13
|
scrubCredentialStrings,
|
|
14
14
|
} from "@cortex/contracts/egress-policy";
|
|
15
|
-
import { MODEL_PROVIDER_LIMITS, NAME_MAX_LENGTH } from "@cortex/contracts/manifest";
|
|
15
|
+
import { MODEL_PROVIDER_LIMITS, NAME_MAX_LENGTH, SETTING_BOUNDS } from "@cortex/contracts/manifest";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
|
|
18
18
|
import {
|
|
@@ -91,6 +91,8 @@ const serverSchema = namedSchema.extend({
|
|
|
91
91
|
modelProvider: z.string().nullable(),
|
|
92
92
|
modelName: z.string().nullable(),
|
|
93
93
|
modelEffort: z.string().nullable(),
|
|
94
|
+
signInChecksPerMinute: z.number(),
|
|
95
|
+
triggerChecksPerMinute: z.number(),
|
|
94
96
|
});
|
|
95
97
|
const chatSchema = z.object({ choices: z.array(z.object({ message: z.object({}) })).min(1) });
|
|
96
98
|
const embeddingsSchema = z.object({
|
|
@@ -388,6 +390,8 @@ async function ensureServer(
|
|
|
388
390
|
url,
|
|
389
391
|
keySecretName,
|
|
390
392
|
...model,
|
|
393
|
+
signInChecksPerMinute: SETTING_BOUNDS.server.signInChecksPerMinute.default,
|
|
394
|
+
triggerChecksPerMinute: SETTING_BOUNDS.server.triggerChecksPerMinute.default,
|
|
391
395
|
}),
|
|
392
396
|
);
|
|
393
397
|
report.input("Server", `${name} · created`);
|
|
@@ -413,6 +417,8 @@ async function ensureServer(
|
|
|
413
417
|
modelName: server.modelName,
|
|
414
418
|
modelEffort: server.modelEffort,
|
|
415
419
|
}),
|
|
420
|
+
signInChecksPerMinute: server.signInChecksPerMinute,
|
|
421
|
+
triggerChecksPerMinute: server.triggerChecksPerMinute,
|
|
416
422
|
}),
|
|
417
423
|
);
|
|
418
424
|
report.input("Server", `${name} · updated`);
|
|
@@ -57,8 +57,13 @@ type ParsedDatasetSql = {
|
|
|
57
57
|
/** A refusal the model can act on. */
|
|
58
58
|
export class DatasetSqlError extends Error {}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* `upper` is a word's keyword spelling. A quoted identifier and a word after a
|
|
62
|
+
* dot (`plans.from`) have none (`""`), so no keyword check matches them and
|
|
63
|
+
* they always read as names.
|
|
64
|
+
*/
|
|
60
65
|
type Token =
|
|
61
|
-
| { kind: "word"; value: string; upper: string
|
|
66
|
+
| { kind: "word"; value: string; upper: string }
|
|
62
67
|
| { kind: "string"; value: string }
|
|
63
68
|
| { kind: "number"; value: number }
|
|
64
69
|
| { kind: "punct"; value: string }
|
|
@@ -84,6 +89,14 @@ function stringLiteral(sql: string, start: number) {
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
/** Whether `previous` ends a value, so a minus after it subtracts; after a keyword or an operator it signs the number. */
|
|
93
|
+
function endsValue(previous: Token | undefined) {
|
|
94
|
+
if (previous === undefined) return false;
|
|
95
|
+
if (previous.kind === "punct") return previous.value === ")";
|
|
96
|
+
if (previous.kind !== "word") return true;
|
|
97
|
+
return ["END", "NULL", "TRUE", "FALSE"].includes(previous.upper) || isName(previous);
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/** Reads the token at `i`: a quoted identifier, a number (negative only where a value is expected), a word or punctuation. */
|
|
88
101
|
function lexeme(sql: string, i: number, previous: Token | undefined) {
|
|
89
102
|
const char = sql[i]!;
|
|
@@ -92,27 +105,13 @@ function lexeme(sql: string, i: number, previous: Token | undefined) {
|
|
|
92
105
|
if (close === -1) throw new DatasetSqlError("A quoted identifier is not closed.");
|
|
93
106
|
const value = sql.slice(i + 1, close);
|
|
94
107
|
return {
|
|
95
|
-
token: {
|
|
96
|
-
kind: "word",
|
|
97
|
-
value,
|
|
98
|
-
upper: value.toUpperCase(),
|
|
99
|
-
quoted: true,
|
|
100
|
-
} satisfies Token,
|
|
108
|
+
token: { kind: "word", value, upper: "" } satisfies Token,
|
|
101
109
|
next: close + 1,
|
|
102
110
|
};
|
|
103
111
|
}
|
|
104
112
|
const rest = sql.slice(i);
|
|
105
113
|
const number = /^-?\d+(\.\d+)?/.exec(rest);
|
|
106
|
-
|
|
107
|
-
const afterValue =
|
|
108
|
-
previous !== undefined &&
|
|
109
|
-
(previous.kind === "punct"
|
|
110
|
-
? previous.value === ")"
|
|
111
|
-
: previous.kind === "word"
|
|
112
|
-
? ["END", "NULL", "TRUE", "FALSE"].includes(previous.upper) || isName(previous)
|
|
113
|
-
: true);
|
|
114
|
-
const signIsOperator = char === "-" && afterValue;
|
|
115
|
-
if (number && !signIsOperator) {
|
|
114
|
+
if (number && !(char === "-" && endsValue(previous))) {
|
|
116
115
|
return {
|
|
117
116
|
token: { kind: "number", value: Number(number[0]) } satisfies Token,
|
|
118
117
|
next: i + number[0].length,
|
|
@@ -120,8 +119,13 @@ function lexeme(sql: string, i: number, previous: Token | undefined) {
|
|
|
120
119
|
}
|
|
121
120
|
const word = /^[A-Za-z_][A-Za-z0-9_]*/.exec(rest);
|
|
122
121
|
if (word) {
|
|
122
|
+
const qualified = previous?.kind === "punct" && previous.value === ".";
|
|
123
123
|
return {
|
|
124
|
-
token: {
|
|
124
|
+
token: {
|
|
125
|
+
kind: "word",
|
|
126
|
+
value: word[0],
|
|
127
|
+
upper: qualified ? "" : word[0].toUpperCase(),
|
|
128
|
+
} satisfies Token,
|
|
125
129
|
next: i + word[0].length,
|
|
126
130
|
};
|
|
127
131
|
}
|
|
@@ -338,12 +342,35 @@ const RESERVED = [
|
|
|
338
342
|
"END",
|
|
339
343
|
];
|
|
340
344
|
|
|
345
|
+
/** Whether a bare word reads as a keyword rather than a name. */
|
|
346
|
+
function isKeyword(upper: string) {
|
|
347
|
+
return RESERVED.includes(upper) || upper in REFUSED_CLAUSES;
|
|
348
|
+
}
|
|
349
|
+
|
|
341
350
|
function isName(token: Token): token is Token & { kind: "word" } {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
return token.kind === "word" && !isKeyword(token.upper);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Words that bare read as a value rather than a column. */
|
|
355
|
+
const VALUE_WORDS = ["TRUE", "FALSE", "CURRENT_DATE", "CURRENT_TIMESTAMP"];
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* A column name as a query writes it: double quoted when bare it would read
|
|
359
|
+
* as a keyword or a value (`"from"`), so what the model is shown is what it
|
|
360
|
+
* can send.
|
|
361
|
+
*/
|
|
362
|
+
export function sqlColumnName(name: string) {
|
|
363
|
+
const upper = name.toUpperCase();
|
|
364
|
+
return isKeyword(upper) || VALUE_WORDS.includes(upper) ? `"${name}"` : name;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** `message`, and for a keyword where a column belongs, how to write a column spelled like it. */
|
|
368
|
+
function keywordError(token: Token, message: string) {
|
|
369
|
+
const hint =
|
|
370
|
+
token.kind === "word" && isKeyword(token.upper)
|
|
371
|
+
? ` ${token.value} is a SQL keyword: write a column of that name as "${token.value}" or dataset.${token.value}.`
|
|
372
|
+
: "";
|
|
373
|
+
return new DatasetSqlError(`${message}${hint}`);
|
|
347
374
|
}
|
|
348
375
|
|
|
349
376
|
/** A unit written as `'month'`, `month` or `MONTH`. */
|
|
@@ -971,11 +998,21 @@ class Parser {
|
|
|
971
998
|
);
|
|
972
999
|
}
|
|
973
1000
|
const op = (operator.value === "<>" ? "!=" : operator.value) as DatasetFilterOp;
|
|
1001
|
+
const message = `"${shown} ${operator.value}" needs a value on the right.`;
|
|
974
1002
|
if (this.startsTerm()) {
|
|
975
|
-
return {
|
|
1003
|
+
return {
|
|
1004
|
+
kind: "cond",
|
|
1005
|
+
left,
|
|
1006
|
+
op,
|
|
1007
|
+
value: { term: this.term(message) },
|
|
1008
|
+
} satisfies Predicate;
|
|
1009
|
+
}
|
|
1010
|
+
// A keyword here may be a column spelled like one (`x > from`); NULL keeps its own refusal.
|
|
1011
|
+
const next = this.peek();
|
|
1012
|
+
if (next.kind === "word" && next.upper !== "NULL" && isKeyword(next.upper)) {
|
|
1013
|
+
throw keywordError(next, message);
|
|
976
1014
|
}
|
|
977
|
-
|
|
978
|
-
return { kind: "cond", left, op, value } satisfies Predicate;
|
|
1015
|
+
return { kind: "cond", left, op, value: this.literal(message) } satisfies Predicate;
|
|
979
1016
|
}
|
|
980
1017
|
|
|
981
1018
|
/** Whether the next tokens are a value expression rather than a literal: a name, `(`, CASE, or a number followed by an operator. */
|
|
@@ -1174,10 +1211,10 @@ class Parser {
|
|
|
1174
1211
|
return count.value;
|
|
1175
1212
|
}
|
|
1176
1213
|
|
|
1177
|
-
/** `a.b` as one reference; a bare keyword in reference position gets the caller's message. */
|
|
1214
|
+
/** `a.b` as one reference; a bare keyword in reference position gets the caller's message (`keywordError`). */
|
|
1178
1215
|
private reference(message: string) {
|
|
1179
1216
|
const token = this.peek();
|
|
1180
|
-
if (!isName(token)) throw
|
|
1217
|
+
if (!isName(token)) throw keywordError(token, message);
|
|
1181
1218
|
this.take();
|
|
1182
1219
|
let ref = token.value;
|
|
1183
1220
|
while (this.optionalPunct(".")) {
|
|
@@ -70,13 +70,6 @@ export const SETTING_BOUNDS = {
|
|
|
70
70
|
tool: {
|
|
71
71
|
timeoutMs: { min: 100, max: 120_000, default: 30_000 },
|
|
72
72
|
maxResponseBytes: { min: 1_024, max: 1_048_576, default: 262_144 },
|
|
73
|
-
rateLimitRequests: { min: 1, max: 10_000, default: 60 },
|
|
74
|
-
rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
|
|
75
|
-
},
|
|
76
|
-
agent: {
|
|
77
|
-
rateLimitRequests: { min: 1, max: 10_000, default: 60 },
|
|
78
|
-
rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
|
|
79
|
-
dailyExecutionQuota: { min: 1, max: 1_000_000, default: 10_000 },
|
|
80
73
|
},
|
|
81
74
|
/** How much of the conversation the agent's model sees. */
|
|
82
75
|
agentContext: {
|
|
@@ -95,6 +88,18 @@ export const SETTING_BOUNDS = {
|
|
|
95
88
|
trigger: { disableAfterFailures: { min: 1, max: 1_000, default: 5 } },
|
|
96
89
|
datasetHost: { timeoutMs: { min: 100, max: 120_000, default: 30_000 } },
|
|
97
90
|
webhook: { timeoutMs: { min: 1_000, max: 30_000, default: 10_000 } },
|
|
91
|
+
/** The score, a fraction, a new visitor of an anonymous agent needs to get an id. */
|
|
92
|
+
recaptcha: { minScore: { min: 0.1, max: 0.9, default: 0.5 } },
|
|
93
|
+
/**
|
|
94
|
+
* How often Cortex checks a caller for the server, per minute: a sign-in
|
|
95
|
+
* (a new end-user token it asks the server to verify, or a visitor one
|
|
96
|
+
* of its anonymous agents gives an id), or a trigger's secret. Anyone
|
|
97
|
+
* can try either, so each bounds what strangers cost it.
|
|
98
|
+
*/
|
|
99
|
+
server: {
|
|
100
|
+
signInChecksPerMinute: { min: 1, max: 100_000, default: 600 },
|
|
101
|
+
triggerChecksPerMinute: { min: 1, max: 10_000, default: 60 },
|
|
102
|
+
},
|
|
98
103
|
} as const satisfies Record<string, Record<string, Bounds>>;
|
|
99
104
|
|
|
100
105
|
/** An integer within `bounds`, their default when left out. */
|
|
@@ -112,14 +117,68 @@ export function settingDefaults<TBounds extends Record<string, Bounds>>(bounds:
|
|
|
112
117
|
) as SettingValues<TBounds>;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
type RateLimitBounds = { max: number; allUsers: number | null; perUser: number | null };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Every rate limit an author sets, in the console and in a manifest alike,
|
|
124
|
+
* per minute except an agent's tokens, per UTC day. Each holds for all of an
|
|
125
|
+
* entry's users together and for each user alone, from 1 to `max`, or not
|
|
126
|
+
* at all (null); a manifest that leaves one out gets the default.
|
|
127
|
+
*/
|
|
128
|
+
export const RATE_LIMITS = {
|
|
129
|
+
agent: {
|
|
130
|
+
messagesPerMinute: { max: 10_000, allUsers: 600, perUser: 20 },
|
|
131
|
+
tokensPerDay: { max: 1_000_000_000_000, allUsers: null, perUser: 1_000_000 },
|
|
132
|
+
},
|
|
133
|
+
tool: { callsPerMinute: { max: 10_000, allUsers: 60, perUser: 20 } },
|
|
134
|
+
datasetHost: { queriesPerMinute: { max: 10_000, allUsers: 60, perUser: 20 } },
|
|
135
|
+
} as const satisfies Record<string, Record<string, RateLimitBounds>>;
|
|
136
|
+
|
|
137
|
+
/** Whom a rate limit holds for: all of an entry's users together, or each user alone. */
|
|
138
|
+
export const RATE_LIMIT_USERS = ["allUsers", "perUser"] as const;
|
|
139
|
+
export type RateLimitUsers = (typeof RATE_LIMIT_USERS)[number];
|
|
140
|
+
|
|
141
|
+
/** A group of `RATE_LIMITS`, one value per limit for all users and for each user. */
|
|
142
|
+
export function mapRateLimits<TBounds extends Record<string, RateLimitBounds>, TValue>(
|
|
143
|
+
bounds: TBounds,
|
|
144
|
+
value: (bound: RateLimitBounds, users: RateLimitUsers) => TValue,
|
|
145
|
+
) {
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
RATE_LIMIT_USERS.map((users) => [
|
|
148
|
+
users,
|
|
149
|
+
Object.fromEntries(
|
|
150
|
+
Object.entries(bounds).map(([name, bound]) => [name, value(bound, users)]),
|
|
151
|
+
),
|
|
152
|
+
]),
|
|
153
|
+
) as Record<RateLimitUsers, { [Name in keyof TBounds]: TValue }>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** An entry's rate limits as its row holds them; null lifts one. */
|
|
157
|
+
export type RateLimits<TBounds> = Record<
|
|
158
|
+
RateLimitUsers,
|
|
159
|
+
{ [Name in keyof TBounds]: number | null }
|
|
160
|
+
>;
|
|
161
|
+
|
|
162
|
+
/** Every limit of a group at its default, as a row that sets none stores them. */
|
|
163
|
+
export function rateLimitDefaults<TBounds extends Record<string, RateLimitBounds>>(
|
|
164
|
+
bounds: TBounds,
|
|
165
|
+
) {
|
|
166
|
+
return mapRateLimits(bounds, (bound, users) => bound[users]) satisfies RateLimits<TBounds>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** One rate limit as a manifest writes it: null lifts it, and left out it takes its default. */
|
|
170
|
+
function rateLimit(bound: RateLimitBounds, users: RateLimitUsers) {
|
|
171
|
+
return z.number().int().min(1).max(bound.max).nullable().default(bound[users]);
|
|
172
|
+
}
|
|
173
|
+
|
|
115
174
|
/**
|
|
116
175
|
* The fields of an entry users may change in Cortex after an apply, each
|
|
117
176
|
* once, from the ones `fields` allows; none when left out. Every other field
|
|
118
177
|
* is the manifest's alone. An apply keeps a live change to a listed field
|
|
119
178
|
* unless the manifest changed that field too (a conflict, see
|
|
120
179
|
* `manifestApplyInputSchema`). Identity, what is fixed at creation, the
|
|
121
|
-
* audience,
|
|
122
|
-
*
|
|
180
|
+
* audience, and whatever an apply checks against the manifest's own value
|
|
181
|
+
* are never offered.
|
|
123
182
|
*/
|
|
124
183
|
function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
|
|
125
184
|
return z
|
|
@@ -359,19 +418,25 @@ export function confinesEndpointParams(url: string) {
|
|
|
359
418
|
const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
|
|
360
419
|
|
|
361
420
|
/**
|
|
362
|
-
*
|
|
363
|
-
* characters, and how many headers at most
|
|
364
|
-
*
|
|
421
|
+
* The headers a call carries: a header name and the prefix before the value
|
|
422
|
+
* in characters, and how many headers at most. The console caps its header
|
|
423
|
+
* rows at the same.
|
|
365
424
|
*/
|
|
366
|
-
export const
|
|
425
|
+
export const HEADER_LIMITS = { headerLength: 120, prefixLength: 40, headers: 8 } as const;
|
|
367
426
|
|
|
368
427
|
/** An HTTP header field name (RFC 9110 token). */
|
|
369
428
|
export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A header value fetch sends: visible ASCII, tab, space and obs-text.
|
|
432
|
+
* Control characters, and any character past one byte, make fetch throw.
|
|
433
|
+
*/
|
|
434
|
+
export const HEADER_VALUE_PATTERN = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
370
435
|
const headerNameSchema = z
|
|
371
436
|
.string()
|
|
372
437
|
.trim()
|
|
373
438
|
.min(1)
|
|
374
|
-
.max(
|
|
439
|
+
.max(HEADER_LIMITS.headerLength)
|
|
375
440
|
.regex(HEADER_NAME_PATTERN);
|
|
376
441
|
|
|
377
442
|
/** Headers Cortex or fetch fills on every request. Lower-cased. */
|
|
@@ -388,20 +453,20 @@ export const transportForbiddenHeaders = [
|
|
|
388
453
|
"proxy-authorization",
|
|
389
454
|
] as const;
|
|
390
455
|
|
|
391
|
-
/** Headers the MCP transport sets on its own requests: no MCP server's
|
|
456
|
+
/** Headers the MCP transport sets on its own requests: no header an MCP server's entry declares travels under one. Lower-cased. */
|
|
392
457
|
export const mcpTransportHeaders = new Set<string>([
|
|
393
458
|
"mcp-session-id",
|
|
394
459
|
"mcp-protocol-version",
|
|
395
460
|
"last-event-id",
|
|
396
461
|
]);
|
|
397
462
|
|
|
398
|
-
/** Headers no
|
|
463
|
+
/** Headers no declared header or field may travel under; test lower-cased names. */
|
|
399
464
|
export const transportOwnedHeaders = new Set<string>([
|
|
400
465
|
...transportSetHeaders,
|
|
401
466
|
...transportForbiddenHeaders,
|
|
402
467
|
]);
|
|
403
468
|
|
|
404
|
-
/** The index of each entry
|
|
469
|
+
/** The index of each entry declaring a header an earlier entry already declares, compared case-insensitively. */
|
|
405
470
|
function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
|
|
406
471
|
const seen = new Set<string>();
|
|
407
472
|
return entries.flatMap((entry, index) => {
|
|
@@ -423,18 +488,18 @@ const presentedHeaderSchema = headerNameSchema.refine(
|
|
|
423
488
|
);
|
|
424
489
|
|
|
425
490
|
/**
|
|
426
|
-
*
|
|
427
|
-
* it (`"Bearer "`, say), and the
|
|
428
|
-
* it, at most `
|
|
429
|
-
*
|
|
491
|
+
* The headers every call carries: each entry a header, what precedes the
|
|
492
|
+
* value in it (`"Bearer "`, say), and where the value comes from, as `entry`
|
|
493
|
+
* validates it, at most `HEADER_LIMITS.headers` of them. A header is
|
|
494
|
+
* declared once; `repeated` words the refusal of a later one.
|
|
430
495
|
*/
|
|
431
|
-
export function
|
|
496
|
+
export function headerListSchema<TEntry extends z.ZodType<{ header: string }>>(
|
|
432
497
|
entry: TEntry,
|
|
433
498
|
repeated: () => string,
|
|
434
499
|
) {
|
|
435
500
|
return z
|
|
436
501
|
.array(entry)
|
|
437
|
-
.max(
|
|
502
|
+
.max(HEADER_LIMITS.headers)
|
|
438
503
|
.superRefine((entries, context) => {
|
|
439
504
|
for (const index of repeatedHeaders(entries)) {
|
|
440
505
|
context.addIssue({
|
|
@@ -446,23 +511,71 @@ export function presentationsSchema<TEntry extends z.ZodType<{ header: string }>
|
|
|
446
511
|
});
|
|
447
512
|
}
|
|
448
513
|
|
|
449
|
-
/**
|
|
450
|
-
|
|
514
|
+
/** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`). */
|
|
515
|
+
const variableSourceSchema = z.object({
|
|
516
|
+
kind: z.literal("variable"),
|
|
517
|
+
name: z.string().trim().min(1).max(64),
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
/** A credential Cortex holds or acquires, by name. */
|
|
521
|
+
const credentialSourceSchema = z.object({ kind: z.literal("credential"), name: nameSchema });
|
|
522
|
+
|
|
523
|
+
/** A value written as is. */
|
|
524
|
+
const constantHeaderSourceSchema = z.object({
|
|
525
|
+
kind: z.literal("constant"),
|
|
526
|
+
value: z.string().max(4_000),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Where a header's value comes from: a credential, the run's own token (the
|
|
531
|
+
* end user's in a chat, the trigger's in a firing), a variable, or a
|
|
532
|
+
* constant. A run token or a variable needs a run; a call without one (a
|
|
533
|
+
* webhook delivery, a catalog read) presents only credentials and constants.
|
|
534
|
+
*/
|
|
535
|
+
export const headerSourceSchema = z.discriminatedUnion("kind", [
|
|
536
|
+
credentialSourceSchema,
|
|
537
|
+
z.object({ kind: z.literal("runToken") }),
|
|
538
|
+
variableSourceSchema,
|
|
539
|
+
constantHeaderSourceSchema,
|
|
540
|
+
]);
|
|
541
|
+
export type HeaderSource = z.infer<typeof headerSourceSchema>;
|
|
542
|
+
|
|
543
|
+
/** The sources a call without a run can present. */
|
|
544
|
+
export const runlessHeaderSourceSchema = z.discriminatedUnion("kind", [
|
|
545
|
+
credentialSourceSchema,
|
|
546
|
+
constantHeaderSourceSchema,
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* The headers a manifest entry declares: no unknown key, an omitted prefix
|
|
551
|
+
* or list empty, and a prefix or constant only of what a header can carry
|
|
552
|
+
* (`HEADER_VALUE_PATTERN`).
|
|
553
|
+
*/
|
|
554
|
+
function declaredHeadersSchema<TSource extends HeaderSource>(
|
|
451
555
|
header: z.ZodString,
|
|
452
|
-
|
|
556
|
+
from: z.ZodType<TSource, TSource>,
|
|
453
557
|
) {
|
|
454
|
-
return
|
|
455
|
-
z
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
558
|
+
return headerListSchema(
|
|
559
|
+
z
|
|
560
|
+
.strictObject({
|
|
561
|
+
header,
|
|
562
|
+
prefix: z
|
|
563
|
+
.string()
|
|
564
|
+
.max(HEADER_LIMITS.prefixLength)
|
|
565
|
+
.regex(HEADER_VALUE_PATTERN, "a prefix a header cannot carry")
|
|
566
|
+
.default(""),
|
|
567
|
+
from,
|
|
568
|
+
})
|
|
569
|
+
.refine(
|
|
570
|
+
(entry) =>
|
|
571
|
+
entry.from.kind !== "constant" || HEADER_VALUE_PATTERN.test(entry.from.value),
|
|
572
|
+
{ path: ["from", "value"], message: "a value a header cannot carry" },
|
|
573
|
+
),
|
|
574
|
+
() => "a header declared twice",
|
|
461
575
|
).default([]);
|
|
462
576
|
}
|
|
463
577
|
|
|
464
|
-
|
|
465
|
-
const credentialsSchema = declaredPresentationsSchema(presentedHeaderSchema, nameSchema.nullable());
|
|
578
|
+
const headersSchema = declaredHeadersSchema(presentedHeaderSchema, headerSourceSchema);
|
|
466
579
|
|
|
467
580
|
/**
|
|
468
581
|
* Where Cortex fills a top-level scalar input field from, out of the model's
|
|
@@ -470,7 +583,7 @@ const credentialsSchema = declaredPresentationsSchema(presentedHeaderSchema, nam
|
|
|
470
583
|
* a constant, or the end user's bearer.
|
|
471
584
|
*/
|
|
472
585
|
export const fieldSourceSchema = z.discriminatedUnion("kind", [
|
|
473
|
-
|
|
586
|
+
variableSourceSchema,
|
|
474
587
|
z.object({ kind: z.literal("secret"), name: nameSchema }),
|
|
475
588
|
z.object({
|
|
476
589
|
kind: z.literal("constant"),
|
|
@@ -806,6 +919,7 @@ const localizedTextSchema = z.record(
|
|
|
806
919
|
);
|
|
807
920
|
|
|
808
921
|
const { tool: toolBounds } = SETTING_BOUNDS;
|
|
922
|
+
const { tool: toolRateLimits, datasetHost: datasetHostRateLimits } = RATE_LIMITS;
|
|
809
923
|
|
|
810
924
|
/** A `type` tool's input schema, its fields bound only as `TOOL_TYPES` lets the type: filled from a source, placed in the request. */
|
|
811
925
|
function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
|
|
@@ -816,30 +930,47 @@ function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
|
|
|
816
930
|
]).default(EMPTY_OBJECT);
|
|
817
931
|
}
|
|
818
932
|
|
|
819
|
-
/**
|
|
933
|
+
/** What a live write may change on a manifest's tool, and keep through the next apply. */
|
|
934
|
+
const TOOL_OVERRIDABLES = [
|
|
935
|
+
"description",
|
|
936
|
+
"timeoutMs",
|
|
937
|
+
"maxResponseBytes",
|
|
938
|
+
"rateLimits",
|
|
939
|
+
"pinned",
|
|
940
|
+
"tags",
|
|
941
|
+
] as const;
|
|
942
|
+
|
|
943
|
+
/** The same for a tool that calls an endpoint, whose headers are its own. */
|
|
944
|
+
const CALLED_TOOL_OVERRIDABLES = [...TOOL_OVERRIDABLES, "headers"] as const;
|
|
945
|
+
|
|
946
|
+
/** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, headers, approval or bound fields. */
|
|
820
947
|
const toolBaseSchema = z.strictObject({
|
|
821
948
|
...serverToolSchema.pick({ name: true, description: true }).shape,
|
|
822
949
|
inputSchema: toolInputSchemaOf("client"),
|
|
823
950
|
outputSchema: objectSchema.default(EMPTY_OBJECT),
|
|
824
951
|
timeoutMs: setting(toolBounds.timeoutMs),
|
|
825
952
|
maxResponseBytes: setting(toolBounds.maxResponseBytes),
|
|
826
|
-
|
|
827
|
-
|
|
953
|
+
rateLimits: z
|
|
954
|
+
.strictObject({
|
|
955
|
+
allUsers: z
|
|
956
|
+
.strictObject({
|
|
957
|
+
callsPerMinute: rateLimit(toolRateLimits.callsPerMinute, "allUsers"),
|
|
958
|
+
})
|
|
959
|
+
.prefault({}),
|
|
960
|
+
perUser: z
|
|
961
|
+
.strictObject({
|
|
962
|
+
callsPerMinute: rateLimit(toolRateLimits.callsPerMinute, "perUser"),
|
|
963
|
+
})
|
|
964
|
+
.prefault({}),
|
|
965
|
+
})
|
|
966
|
+
.prefault({}),
|
|
828
967
|
/** Trims the upstream's answer to what the output schema declares. */
|
|
829
968
|
projectOutput: z.boolean().default(false),
|
|
830
969
|
/** Listed in every turn of every agent the tool reaches, whatever the question. */
|
|
831
970
|
pinned: z.boolean().default(false),
|
|
832
971
|
tags: toolTagsSchema.default([]),
|
|
833
972
|
audience: audienceSchema,
|
|
834
|
-
overridable: overridable(
|
|
835
|
-
"description",
|
|
836
|
-
"timeoutMs",
|
|
837
|
-
"maxResponseBytes",
|
|
838
|
-
"rateLimitRequests",
|
|
839
|
-
"rateLimitWindowSeconds",
|
|
840
|
-
"pinned",
|
|
841
|
-
"tags",
|
|
842
|
-
]),
|
|
973
|
+
overridable: overridable(TOOL_OVERRIDABLES),
|
|
843
974
|
});
|
|
844
975
|
|
|
845
976
|
/** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
|
|
@@ -848,8 +979,9 @@ const calledToolSchema = toolBaseSchema.extend({
|
|
|
848
979
|
confinesEndpointParams,
|
|
849
980
|
"a {parameter} outside the path or query",
|
|
850
981
|
),
|
|
851
|
-
|
|
982
|
+
headers: headersSchema,
|
|
852
983
|
requiresApproval: z.boolean().default(false),
|
|
984
|
+
overridable: overridable(CALLED_TOOL_OVERRIDABLES),
|
|
853
985
|
});
|
|
854
986
|
|
|
855
987
|
const manifestToolSchema = z
|
|
@@ -891,16 +1023,16 @@ const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]
|
|
|
891
1023
|
* An OpenAPI document whose operations become HTTP tools this manifest owns.
|
|
892
1024
|
* Each apply reads it, imports every operation Cortex supports, keeps those
|
|
893
1025
|
* tools in step with the document and deletes the ones it no longer has.
|
|
894
|
-
* Every tool
|
|
1026
|
+
* Every tool carries the source's `headers`. A GET call runs at once; any other
|
|
895
1027
|
* method waits for the end user's approval, unless `approval` names the
|
|
896
1028
|
* tool the other way.
|
|
897
1029
|
*/
|
|
898
1030
|
const manifestOpenApiSchema = z.strictObject({
|
|
899
1031
|
/** Names the source in what an apply reports. */
|
|
900
1032
|
name: nameSchema,
|
|
901
|
-
/** Where the document is read, with no
|
|
1033
|
+
/** Where the document is read, with no headers: OpenAPI 3 or Swagger 2, JSON or YAML. */
|
|
902
1034
|
url: upstreamUrlSchema,
|
|
903
|
-
|
|
1035
|
+
headers: headersSchema,
|
|
904
1036
|
/** Where every operation's successful response keeps its result, when the API wraps each answer. */
|
|
905
1037
|
resultPath: jsonPointerSchema.optional(),
|
|
906
1038
|
/** Tools listed in every turn of every agent they reach, whatever the question. */
|
|
@@ -913,6 +1045,8 @@ const manifestOpenApiSchema = z.strictObject({
|
|
|
913
1045
|
"a tool both never and always needs approval",
|
|
914
1046
|
),
|
|
915
1047
|
audience: audienceSchema,
|
|
1048
|
+
/** What a live write may change on each of the source's tools, and keep through the next apply. */
|
|
1049
|
+
overridable: overridable(CALLED_TOOL_OVERRIDABLES),
|
|
916
1050
|
});
|
|
917
1051
|
export type ManifestOpenApi = z.input<typeof manifestOpenApiSchema>;
|
|
918
1052
|
|
|
@@ -1014,14 +1148,34 @@ const manifestDatasetHostSchema = z
|
|
|
1014
1148
|
.strictObject({
|
|
1015
1149
|
name: datasetIdentifierSchema,
|
|
1016
1150
|
url: upstreamUrlSchema,
|
|
1017
|
-
|
|
1151
|
+
headers: headersSchema,
|
|
1018
1152
|
timeoutMs: setting(SETTING_BOUNDS.datasetHost.timeoutMs),
|
|
1153
|
+
rateLimits: z
|
|
1154
|
+
.strictObject({
|
|
1155
|
+
allUsers: z
|
|
1156
|
+
.strictObject({
|
|
1157
|
+
queriesPerMinute: rateLimit(
|
|
1158
|
+
datasetHostRateLimits.queriesPerMinute,
|
|
1159
|
+
"allUsers",
|
|
1160
|
+
),
|
|
1161
|
+
})
|
|
1162
|
+
.prefault({}),
|
|
1163
|
+
perUser: z
|
|
1164
|
+
.strictObject({
|
|
1165
|
+
queriesPerMinute: rateLimit(
|
|
1166
|
+
datasetHostRateLimits.queriesPerMinute,
|
|
1167
|
+
"perUser",
|
|
1168
|
+
),
|
|
1169
|
+
})
|
|
1170
|
+
.prefault({}),
|
|
1171
|
+
})
|
|
1172
|
+
.prefault({}),
|
|
1019
1173
|
/** The datasets the host serves that agents may query. */
|
|
1020
1174
|
expose: z.array(datasetIdentifierSchema).min(1).max(500),
|
|
1021
1175
|
/** Among those, the ones listed in every turn, whatever the question. */
|
|
1022
1176
|
pinned: z.array(datasetIdentifierSchema).max(50).default([]),
|
|
1023
1177
|
audience: audienceSchema,
|
|
1024
|
-
overridable: overridable(["timeoutMs"]),
|
|
1178
|
+
overridable: overridable(["timeoutMs", "rateLimits", "headers"]),
|
|
1025
1179
|
})
|
|
1026
1180
|
.superRefine((host, context) => {
|
|
1027
1181
|
const unexposed = unexposedPins(host);
|
|
@@ -1051,60 +1205,201 @@ const manifestMcpServerSchema = z.strictObject({
|
|
|
1051
1205
|
/** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
|
|
1052
1206
|
name: identifierSchema,
|
|
1053
1207
|
url: upstreamUrlSchema,
|
|
1054
|
-
|
|
1208
|
+
headers: declaredHeadersSchema(
|
|
1055
1209
|
presentedHeaderSchema.refine(
|
|
1056
1210
|
(name) => !mcpTransportHeaders.has(name.toLowerCase()),
|
|
1057
1211
|
"a header the MCP transport sets",
|
|
1058
1212
|
),
|
|
1059
|
-
|
|
1213
|
+
headerSourceSchema,
|
|
1060
1214
|
),
|
|
1061
1215
|
/** The server's own tool names to offer; empty offers every tool. */
|
|
1062
1216
|
include: mcpToolNamesSchema.default([]),
|
|
1063
1217
|
exclude: mcpToolNamesSchema.default([]),
|
|
1064
1218
|
audience: audienceSchema,
|
|
1065
|
-
overridable: overridable(["include", "exclude"]),
|
|
1219
|
+
overridable: overridable(["include", "exclude", "headers"]),
|
|
1066
1220
|
});
|
|
1067
1221
|
export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
|
|
1068
1222
|
|
|
1069
1223
|
/**
|
|
1070
1224
|
* Delivers its event to a receiver while it is declared. A delivery carries
|
|
1071
|
-
* no run, so
|
|
1072
|
-
* header that names the event.
|
|
1225
|
+
* no run, so its headers come from credentials and constants, and none
|
|
1226
|
+
* travels under the header that names the event.
|
|
1073
1227
|
*/
|
|
1074
1228
|
const manifestWebhookSchema = z.strictObject({
|
|
1075
1229
|
name: slugSchema,
|
|
1076
1230
|
description: z.string().trim().max(500).default(""),
|
|
1077
1231
|
event: z.enum(SERVER_EVENTS),
|
|
1078
1232
|
url: upstreamUrlSchema,
|
|
1079
|
-
|
|
1233
|
+
headers: declaredHeadersSchema(
|
|
1080
1234
|
presentedHeaderSchema.refine(
|
|
1081
1235
|
(name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
|
|
1082
1236
|
"the header that names the event",
|
|
1083
1237
|
),
|
|
1084
|
-
|
|
1238
|
+
runlessHeaderSourceSchema,
|
|
1085
1239
|
),
|
|
1086
1240
|
timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
|
|
1087
1241
|
audience: audienceSchema,
|
|
1088
|
-
overridable: overridable(["description", "timeoutMs"]),
|
|
1242
|
+
overridable: overridable(["description", "timeoutMs", "headers"]),
|
|
1089
1243
|
});
|
|
1090
1244
|
export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
|
|
1091
1245
|
|
|
1092
|
-
/**
|
|
1093
|
-
|
|
1246
|
+
/**
|
|
1247
|
+
* A collection's name and description in characters, and how many documents
|
|
1248
|
+
* a manifest lists in one. The console caps its editor at the same.
|
|
1249
|
+
*/
|
|
1250
|
+
export const COLLECTION_LIMITS = {
|
|
1251
|
+
nameLength: 80,
|
|
1252
|
+
descriptionLength: 2_000,
|
|
1253
|
+
documents: 100,
|
|
1254
|
+
} as const;
|
|
1255
|
+
|
|
1256
|
+
/** A knowledge document's name in characters and its size in bytes, however it arrives. */
|
|
1257
|
+
export const DOCUMENT_LIMITS = { nameLength: 255, bytes: 10 * 1024 * 1024 } as const;
|
|
1258
|
+
|
|
1259
|
+
export const DOCX_MIME_TYPE =
|
|
1260
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
1261
|
+
|
|
1262
|
+
/** The content types Cortex parses a knowledge document as. */
|
|
1263
|
+
export const DOCUMENT_MIME_TYPES = [
|
|
1264
|
+
"application/pdf",
|
|
1265
|
+
DOCX_MIME_TYPE,
|
|
1266
|
+
"text/markdown",
|
|
1267
|
+
"text/plain",
|
|
1268
|
+
"text/html",
|
|
1269
|
+
] as const;
|
|
1270
|
+
export type DocumentMimeType = (typeof DOCUMENT_MIME_TYPES)[number];
|
|
1271
|
+
|
|
1272
|
+
const mimeTypeByExtension = new Map<string, DocumentMimeType>([
|
|
1273
|
+
["md", "text/markdown"],
|
|
1274
|
+
["txt", "text/plain"],
|
|
1275
|
+
["html", "text/html"],
|
|
1276
|
+
["htm", "text/html"],
|
|
1277
|
+
["pdf", "application/pdf"],
|
|
1278
|
+
["docx", DOCX_MIME_TYPE],
|
|
1279
|
+
]);
|
|
1094
1280
|
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
name
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1281
|
+
/** The content type a document is parsed as, from the extension its name ends in; none for any other name. */
|
|
1282
|
+
export function documentType(name: string) {
|
|
1283
|
+
const dot = name.lastIndexOf(".");
|
|
1284
|
+
return dot === -1 ? undefined : mimeTypeByExtension.get(name.slice(dot + 1).toLowerCase());
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** How many bytes a base64 string decodes to. */
|
|
1288
|
+
function base64Bytes(data: string) {
|
|
1289
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
1290
|
+
return (data.length / 4) * 3 - padding;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const documentNameSchema = z.string().trim().min(1).max(DOCUMENT_LIMITS.nameLength);
|
|
1294
|
+
|
|
1295
|
+
/** A document written in the manifest itself; its name's extension says how it is parsed. */
|
|
1296
|
+
const textDocumentSchema = z.strictObject({
|
|
1297
|
+
name: documentNameSchema,
|
|
1298
|
+
text: z
|
|
1299
|
+
.string()
|
|
1300
|
+
.min(1)
|
|
1301
|
+
.refine(
|
|
1302
|
+
(text) => new TextEncoder().encode(text).byteLength <= DOCUMENT_LIMITS.bytes,
|
|
1303
|
+
`larger than ${DOCUMENT_LIMITS.bytes} bytes`,
|
|
1304
|
+
),
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
/** A file `cortex apply` reads, its path relative to the manifest file; named after the file unless `name` says otherwise. */
|
|
1308
|
+
const pathDocumentSchema = z.strictObject({
|
|
1309
|
+
path: z.string().trim().min(1),
|
|
1310
|
+
name: documentNameSchema.optional(),
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
/** A file Cortex reads on every apply, with no credentials; named after the URL's last segment unless `name` says otherwise. */
|
|
1314
|
+
const urlDocumentSchema = z.strictObject({
|
|
1315
|
+
url: upstreamUrlSchema,
|
|
1316
|
+
name: documentNameSchema.optional(),
|
|
1105
1317
|
});
|
|
1318
|
+
|
|
1319
|
+
/** A file's bytes in base64, as `cortex apply` sends a `path`. */
|
|
1320
|
+
const dataDocumentSchema = z.strictObject({
|
|
1321
|
+
name: documentNameSchema,
|
|
1322
|
+
data: z
|
|
1323
|
+
.base64()
|
|
1324
|
+
.refine(
|
|
1325
|
+
(data) => base64Bytes(data) <= DOCUMENT_LIMITS.bytes,
|
|
1326
|
+
`larger than ${DOCUMENT_LIMITS.bytes} bytes`,
|
|
1327
|
+
),
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
/** The name a document goes by: its own, else its file's or its URL's last segment. */
|
|
1331
|
+
export function documentName(document: { name?: string; path?: string; url?: string }) {
|
|
1332
|
+
if (document.name !== undefined) return document.name;
|
|
1333
|
+
if (document.path !== undefined) return document.path.split(/[/\\]/).pop() ?? "";
|
|
1334
|
+
const segment = new URL(document.url ?? "", "https://x").pathname.split("/").pop() ?? "";
|
|
1335
|
+
try {
|
|
1336
|
+
return decodeURIComponent(segment);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return segment;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** A collection's documents: each name once, and each a name whose extension Cortex parses. */
|
|
1343
|
+
function documentsSchema<TDocument extends z.ZodType<Parameters<typeof documentName>[0]>>(
|
|
1344
|
+
document: TDocument,
|
|
1345
|
+
) {
|
|
1346
|
+
return z
|
|
1347
|
+
.array(document)
|
|
1348
|
+
.max(COLLECTION_LIMITS.documents)
|
|
1349
|
+
.superRefine((documents, ctx) => {
|
|
1350
|
+
const names = documents.map(documentName);
|
|
1351
|
+
names.forEach((name, index) => {
|
|
1352
|
+
if (documentType(name) === undefined) {
|
|
1353
|
+
ctx.addIssue({
|
|
1354
|
+
code: "custom",
|
|
1355
|
+
path: [index, "name"],
|
|
1356
|
+
message: "a document's name ends in .md, .txt, .html, .htm, .pdf or .docx",
|
|
1357
|
+
});
|
|
1358
|
+
} else if (names.indexOf(name) !== index) {
|
|
1359
|
+
ctx.addIssue({
|
|
1360
|
+
code: "custom",
|
|
1361
|
+
path: [index, "name"],
|
|
1362
|
+
message: "a name repeats",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
})
|
|
1367
|
+
.default([]);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Holds documents. Its `documents` are the manifest's: an apply stores what
|
|
1372
|
+
* is new or changed and retires what it no longer lists, unless
|
|
1373
|
+
* `overridable` offers them, and then a document added or removed in
|
|
1374
|
+
* Cortex stays so until the manifest changes that document too.
|
|
1375
|
+
*/
|
|
1376
|
+
function collectionSchema<TDocument extends z.ZodType<Parameters<typeof documentName>[0]>>(
|
|
1377
|
+
document: TDocument,
|
|
1378
|
+
) {
|
|
1379
|
+
return z.strictObject({
|
|
1380
|
+
name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
|
|
1381
|
+
description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
|
|
1382
|
+
/** The key of the service every document in the collection belongs to. */
|
|
1383
|
+
service: slugSchema.optional(),
|
|
1384
|
+
/** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
|
|
1385
|
+
citable: z.boolean().default(true),
|
|
1386
|
+
documents: documentsSchema(document),
|
|
1387
|
+
audience: audienceSchema,
|
|
1388
|
+
overridable: overridable(["description", "documents"]),
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const manifestDocumentSchema = z.union([textDocumentSchema, pathDocumentSchema, urlDocumentSchema]);
|
|
1393
|
+
export type ManifestDocument = z.input<typeof manifestDocumentSchema>;
|
|
1394
|
+
|
|
1395
|
+
const manifestCollectionSchema = collectionSchema(manifestDocumentSchema);
|
|
1106
1396
|
export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
|
|
1107
1397
|
|
|
1398
|
+
/** A collection as Cortex receives it: each `path` read into `data`. */
|
|
1399
|
+
const resolvedCollectionSchema = collectionSchema(
|
|
1400
|
+
z.union([textDocumentSchema, dataDocumentSchema, urlDocumentSchema]),
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1108
1403
|
export const AUTH_JWT_SECRET_ALGORITHMS = ["HS256", "HS384", "HS512"] as const;
|
|
1109
1404
|
|
|
1110
1405
|
const issuerSchema = z.string().trim().min(1).max(2048);
|
|
@@ -1131,11 +1426,32 @@ export const agentAuthSchema = z.discriminatedUnion("kind", [
|
|
|
1131
1426
|
issuer: issuerSchema,
|
|
1132
1427
|
audience: tokenAudienceSchema,
|
|
1133
1428
|
}),
|
|
1134
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Each visitor gets an id of their own from Cortex, kept in their
|
|
1431
|
+
* browser. With `recaptcha`, reCAPTCHA Enterprise scores each new
|
|
1432
|
+
* visitor first, and one below `minScore` gets none.
|
|
1433
|
+
*/
|
|
1434
|
+
z.strictObject({
|
|
1435
|
+
kind: z.literal("anonymous"),
|
|
1436
|
+
recaptcha: z
|
|
1437
|
+
.strictObject({
|
|
1438
|
+
siteKey: z.string().trim().min(1).max(200),
|
|
1439
|
+
projectId: z.string().trim().min(1).max(200),
|
|
1440
|
+
/** The secret holding a Google Cloud API key allowed to create assessments. */
|
|
1441
|
+
secret: nameSchema,
|
|
1442
|
+
minScore: z
|
|
1443
|
+
.number()
|
|
1444
|
+
.min(SETTING_BOUNDS.recaptcha.minScore.min)
|
|
1445
|
+
.max(SETTING_BOUNDS.recaptcha.minScore.max)
|
|
1446
|
+
.default(SETTING_BOUNDS.recaptcha.minScore.default),
|
|
1447
|
+
})
|
|
1448
|
+
.optional(),
|
|
1449
|
+
}),
|
|
1135
1450
|
z.strictObject({ kind: z.literal("server") }),
|
|
1136
1451
|
]);
|
|
1137
1452
|
|
|
1138
|
-
const {
|
|
1453
|
+
const { agentContext, agentLimits } = SETTING_BOUNDS;
|
|
1454
|
+
const { agent: agentRateLimits } = RATE_LIMITS;
|
|
1139
1455
|
|
|
1140
1456
|
/**
|
|
1141
1457
|
* How the agent runs; every setting left out keeps its default. The
|
|
@@ -1160,9 +1476,22 @@ export const agentSettingsSchema = z.strictObject({
|
|
|
1160
1476
|
/** Off, end users cannot message the agent: only its triggers reach it. */
|
|
1161
1477
|
messageTrigger: z.boolean().default(true),
|
|
1162
1478
|
defaultLocale: localeSchema.default(BASE_LOCALE),
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1479
|
+
rateLimits: z
|
|
1480
|
+
.strictObject({
|
|
1481
|
+
allUsers: z
|
|
1482
|
+
.strictObject({
|
|
1483
|
+
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
|
|
1484
|
+
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
|
|
1485
|
+
})
|
|
1486
|
+
.prefault({}),
|
|
1487
|
+
perUser: z
|
|
1488
|
+
.strictObject({
|
|
1489
|
+
messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
|
|
1490
|
+
tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
|
|
1491
|
+
})
|
|
1492
|
+
.prefault({}),
|
|
1493
|
+
})
|
|
1494
|
+
.prefault({}),
|
|
1166
1495
|
context: z
|
|
1167
1496
|
.strictObject({
|
|
1168
1497
|
maxContextTokens: setting(agentContext.maxContextTokens),
|
|
@@ -1289,6 +1618,15 @@ export const manifestSchema = z.strictObject({
|
|
|
1289
1618
|
/** A manifest as written: every field with a default may be left out. */
|
|
1290
1619
|
export type Manifest = z.input<typeof manifestSchema>;
|
|
1291
1620
|
|
|
1621
|
+
/**
|
|
1622
|
+
* A manifest as Cortex receives it: the one written, each document `path`
|
|
1623
|
+
* read into `data` by `cortex apply`, since Cortex cannot read the files
|
|
1624
|
+
* beside it.
|
|
1625
|
+
*/
|
|
1626
|
+
export const resolvedManifestSchema = manifestSchema.extend({
|
|
1627
|
+
collections: listOf(MANIFEST_NAME_FIELDS.collections, resolvedCollectionSchema),
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1292
1630
|
const manifestKindSchema = z.enum(MANIFEST_KINDS);
|
|
1293
1631
|
|
|
1294
1632
|
/**
|
|
@@ -1304,11 +1642,17 @@ export const manifestChangesSchema = z.object({
|
|
|
1304
1642
|
delete: namesByKindSchema,
|
|
1305
1643
|
});
|
|
1306
1644
|
|
|
1307
|
-
/**
|
|
1645
|
+
/**
|
|
1646
|
+
* A field of one entry that both the manifest and a live write changed since
|
|
1647
|
+
* the last apply. A field holding named parts (an agent's settings, its
|
|
1648
|
+
* greeting and suggestions by locale, a collection's documents by name) is
|
|
1649
|
+
* judged part by part, and `key` names the part.
|
|
1650
|
+
*/
|
|
1308
1651
|
const manifestConflictSchema = z.strictObject({
|
|
1309
1652
|
kind: manifestKindSchema,
|
|
1310
1653
|
name: z.string(),
|
|
1311
1654
|
field: z.string(),
|
|
1655
|
+
key: z.string().optional(),
|
|
1312
1656
|
});
|
|
1313
1657
|
|
|
1314
1658
|
/** Which value a conflict keeps: the one changed live in Cortex, or the manifest's. */
|
|
@@ -1318,9 +1662,10 @@ const conflictSideSchema = z.enum(["live", "manifest"]);
|
|
|
1318
1662
|
* What preview and sync take: the manifest, and how its conflicts resolve.
|
|
1319
1663
|
* A conflict takes its own entry in `resolutions`, else `prefer`; a sync
|
|
1320
1664
|
* left with one neither answers is refused as `unresolved_conflicts`,
|
|
1321
|
-
* naming them. A resolution
|
|
1665
|
+
* naming them. A resolution answers the conflict with its kind, name, field
|
|
1666
|
+
* and key; one for a field that no longer conflicts is ignored.
|
|
1322
1667
|
*/
|
|
1323
|
-
export const manifestApplyInputSchema =
|
|
1668
|
+
export const manifestApplyInputSchema = resolvedManifestSchema.extend({
|
|
1324
1669
|
prefer: conflictSideSchema.optional(),
|
|
1325
1670
|
resolutions: z.array(manifestConflictSchema.extend({ keep: conflictSideSchema })).default([]),
|
|
1326
1671
|
});
|
|
@@ -1,16 +1,68 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
1
3
|
import { pathToFileURL } from "node:url";
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { isRecord } from "../json/index";
|
|
6
|
+
import { DOCUMENT_LIMITS, documentName, manifestSchema } from "../manifest/index";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Imports a manifest file, TypeScript or JSON (Bun imports both), and checks
|
|
7
|
-
* its default export against the contract: `
|
|
8
|
-
* written, for a caller that lets Cortex fill its
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* its default export against the contract: `parsed` is the check, and
|
|
11
|
+
* `written` the export as written, for a caller that lets Cortex fill its
|
|
12
|
+
* own defaults, with each document `path` read, relative to the file, into
|
|
13
|
+
* the `data` Cortex takes (`resolvedManifestSchema`). Throws when the file
|
|
14
|
+
* does not import, exports nothing, or names a document file it cannot
|
|
15
|
+
* read, that is empty, or that is too large. Kept out of the contract's
|
|
16
|
+
* module, which the browser imports too.
|
|
11
17
|
*/
|
|
12
18
|
export async function loadManifestFile(path: string) {
|
|
13
19
|
const module = (await import(pathToFileURL(path).href)) as { default?: unknown };
|
|
14
20
|
if (module.default === undefined) throw new Error("The module has no default export.");
|
|
15
|
-
|
|
21
|
+
const parsed = manifestSchema.safeParse(module.default);
|
|
22
|
+
return {
|
|
23
|
+
written: parsed.success
|
|
24
|
+
? await readDocuments(module.default, dirname(path))
|
|
25
|
+
: module.default,
|
|
26
|
+
parsed,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `written` with every document `path` read from `directory` into `data`. */
|
|
31
|
+
async function readDocuments(written: unknown, directory: string) {
|
|
32
|
+
if (!isRecord(written) || !Array.isArray(written.collections)) return written;
|
|
33
|
+
const collections = await Promise.all(
|
|
34
|
+
written.collections.map(async (collection: unknown) => {
|
|
35
|
+
if (!isRecord(collection) || !Array.isArray(collection.documents)) return collection;
|
|
36
|
+
const documents = await Promise.all(
|
|
37
|
+
collection.documents.map((document: unknown) =>
|
|
38
|
+
isRecord(document) && typeof document.path === "string"
|
|
39
|
+
? readDocument(
|
|
40
|
+
String(collection.name),
|
|
41
|
+
document.path,
|
|
42
|
+
document.name,
|
|
43
|
+
directory,
|
|
44
|
+
)
|
|
45
|
+
: document,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return { ...collection, documents };
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
return { ...written, collections };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One document file as `{ name, data }`, named after the file unless `name` says otherwise. */
|
|
55
|
+
async function readDocument(collection: string, path: string, name: unknown, directory: string) {
|
|
56
|
+
const where = `Collection ${collection}: ${path}`;
|
|
57
|
+
const bytes = await readFile(resolve(directory, path)).catch((cause: unknown) => {
|
|
58
|
+
throw new Error(`${where} could not be read.`, { cause });
|
|
59
|
+
});
|
|
60
|
+
if (bytes.byteLength === 0) throw new Error(`${where} is empty.`);
|
|
61
|
+
if (bytes.byteLength > DOCUMENT_LIMITS.bytes) {
|
|
62
|
+
throw new Error(`${where} is larger than ${DOCUMENT_LIMITS.bytes} bytes.`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
name: documentName({ name: typeof name === "string" ? name : undefined, path }),
|
|
66
|
+
data: bytes.toString("base64"),
|
|
67
|
+
};
|
|
16
68
|
}
|
|
@@ -85,8 +85,31 @@ export type AttachmentSummary = {
|
|
|
85
85
|
description: string | null;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
88
|
+
/**
|
|
89
|
+
* How an anonymous agent's visitors' user ids begin: Cortex issues each
|
|
90
|
+
* visitor an id of its own, which their browser keeps. No issuer ever mints
|
|
91
|
+
* a subject shaped like this.
|
|
92
|
+
*/
|
|
93
|
+
export const VISITOR_USER_PREFIX = "visitor:";
|
|
94
|
+
|
|
95
|
+
/** The reCAPTCHA action a visitor's sign-in runs, which Cortex checks the token was made for. */
|
|
96
|
+
export const VISITOR_RECAPTCHA_ACTION = "cortex_visitor";
|
|
97
|
+
|
|
98
|
+
/** `POST /visitors`: a visitor's sign-in to an anonymous agent, with a reCAPTCHA token when the agent checks visitors. */
|
|
99
|
+
export type VisitorRequest = { recaptcha?: string };
|
|
100
|
+
|
|
101
|
+
/** A visitor's token, the bearer of every request after the sign-in. */
|
|
102
|
+
export type VisitorToken = { token: string };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Why a sign-in got no token: the agent checks visitors with reCAPTCHA and
|
|
106
|
+
* the request carried no token, or one that failed. `siteKey` is the key the
|
|
107
|
+
* widget runs reCAPTCHA with.
|
|
108
|
+
*/
|
|
109
|
+
export type VisitorRefusal = {
|
|
110
|
+
error: "recaptcha_required" | "recaptcha_failed";
|
|
111
|
+
siteKey: string;
|
|
112
|
+
};
|
|
90
113
|
|
|
91
114
|
export type ThreadSummary = {
|
|
92
115
|
id: string;
|
|
@@ -139,8 +162,6 @@ export type ClientToolBinding = {
|
|
|
139
162
|
toolId: string;
|
|
140
163
|
surface: ToolSurface;
|
|
141
164
|
embedOrigin: string;
|
|
142
|
-
/** Variables the initiate call's input reads. */
|
|
143
|
-
variables: string[];
|
|
144
165
|
};
|
|
145
166
|
|
|
146
167
|
/**
|
package/src/scaffold/files.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
|
|
|
29
29
|
* `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
|
|
30
30
|
* before the CLI publishes, so a scaffold pins what was current then.
|
|
31
31
|
*/
|
|
32
|
-
const CORTEX_VERSION = "0.
|
|
32
|
+
const CORTEX_VERSION = "0.17.0";
|
|
33
33
|
|
|
34
34
|
/** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
|
|
35
35
|
export const DOCKER_HOST = "host.docker.internal";
|
|
@@ -150,7 +150,7 @@ function packageJson({ name, datasets, setup }: Project) {
|
|
|
150
150
|
verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
|
|
151
151
|
},
|
|
152
152
|
dependencies: {
|
|
153
|
-
"@m6d/cortex": "~0.
|
|
153
|
+
"@m6d/cortex": "~0.5.0",
|
|
154
154
|
// drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
|
|
155
155
|
...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
|
|
156
156
|
hono: "^4.12.3",
|
|
@@ -198,7 +198,7 @@ if (!server) throw new Error("Apply from this project: its .env names the server
|
|
|
198
198
|
{
|
|
199
199
|
name: "store",
|
|
200
200
|
url: \`\${serverUrl}/datasets\`,
|
|
201
|
-
|
|
201
|
+
headers: [],
|
|
202
202
|
expose: ["orders"],
|
|
203
203
|
pinned: ["orders"],
|
|
204
204
|
audience: { server },
|
|
@@ -223,7 +223,7 @@ export default defineManifest({
|
|
|
223
223
|
name: "Helper",
|
|
224
224
|
server,
|
|
225
225
|
prompt: "You are a helpful assistant. Address the user as {{userName}}.",
|
|
226
|
-
// No identity provider yet:
|
|
226
|
+
// No identity provider yet: each browser is a visitor of its own.
|
|
227
227
|
auth: { kind: "anonymous" },
|
|
228
228
|
},
|
|
229
229
|
],${datasets ? datasetHosts : ""}
|