@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
|
@@ -11,10 +11,15 @@ import { isRecord } from "../json";
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export const DATASET_IDENTIFIER_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
14
|
+
/** The longest dataset or host name. */
|
|
15
|
+
export const DATASET_IDENTIFIER_MAX = 64;
|
|
14
16
|
/** `column` on the dataset queried, or `relation.relation.column` along declared relations. */
|
|
15
17
|
export const DATASET_COLUMN_REF_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*$/;
|
|
16
18
|
|
|
17
|
-
export const datasetIdentifierSchema = z
|
|
19
|
+
export const datasetIdentifierSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(DATASET_IDENTIFIER_PATTERN)
|
|
22
|
+
.max(DATASET_IDENTIFIER_MAX);
|
|
18
23
|
export const datasetColumnRefSchema = z.string().regex(DATASET_COLUMN_REF_PATTERN).max(256);
|
|
19
24
|
|
|
20
25
|
export const DATASET_COLUMN_TYPES = ["string", "number", "boolean", "date"] as const;
|
|
@@ -355,46 +360,6 @@ export const datasetResultSchema = z.object({
|
|
|
355
360
|
});
|
|
356
361
|
export type DatasetResult = z.infer<typeof datasetResultSchema>;
|
|
357
362
|
|
|
358
|
-
export const DATASET_ERROR_KINDS = [
|
|
359
|
-
"invalid_query",
|
|
360
|
-
"unsupported",
|
|
361
|
-
"forbidden",
|
|
362
|
-
"timeout",
|
|
363
|
-
"internal",
|
|
364
|
-
] as const;
|
|
365
|
-
export type DatasetErrorKind = (typeof DATASET_ERROR_KINDS)[number];
|
|
366
|
-
|
|
367
|
-
/** The HTTP status each error kind travels under, both from a host and from the server's own refusals. */
|
|
368
|
-
export const DATASET_ERROR_STATUS = {
|
|
369
|
-
invalid_query: 400,
|
|
370
|
-
unsupported: 400,
|
|
371
|
-
forbidden: 403,
|
|
372
|
-
timeout: 504,
|
|
373
|
-
internal: 500,
|
|
374
|
-
} as const satisfies Record<DatasetErrorKind, number>;
|
|
375
|
-
|
|
376
|
-
export const datasetErrorSchema = z.object({
|
|
377
|
-
error: z.object({
|
|
378
|
-
kind: z.enum(DATASET_ERROR_KINDS),
|
|
379
|
-
detail: z.string().max(512),
|
|
380
|
-
}),
|
|
381
|
-
});
|
|
382
|
-
export type DatasetError = z.infer<typeof datasetErrorSchema>;
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* A dataset host as an agent's published configuration carries it: the
|
|
386
|
-
* datasets the agent exposes, already the published manifest's definitions,
|
|
387
|
-
* and which of them ride in every turn. The Control Center runs the queries
|
|
388
|
-
* for a host it registered, so the server needs no URL or credential here.
|
|
389
|
-
*/
|
|
390
|
-
export const datasetHostDeclarationSchema = z.object({
|
|
391
|
-
name: datasetIdentifierSchema,
|
|
392
|
-
manifestVersion: datasetManifestSchema.shape.version,
|
|
393
|
-
datasets: z.array(datasetDefinitionSchema).max(500),
|
|
394
|
-
pinned: z.array(datasetIdentifierSchema).max(50).default([]),
|
|
395
|
-
});
|
|
396
|
-
export type DatasetHostDeclaration = z.infer<typeof datasetHostDeclarationSchema>;
|
|
397
|
-
|
|
398
363
|
/** The definitions a query may reach, by dataset name. */
|
|
399
364
|
export type DatasetCatalog = ReadonlyMap<string, DatasetDefinition>;
|
|
400
365
|
|
|
@@ -659,7 +624,7 @@ export function boundDatasetResult(
|
|
|
659
624
|
/**
|
|
660
625
|
* Every way a query can be wrong given the catalog, as messages the model
|
|
661
626
|
* can act on; an empty list means the host will understand it. Checked
|
|
662
|
-
* before anything leaves
|
|
627
|
+
* before anything leaves Cortex, so a host can trust the shape and only
|
|
663
628
|
* has to translate.
|
|
664
629
|
*/
|
|
665
630
|
export function validateDatasetQuery(catalog: DatasetCatalog, query: DatasetQuery) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The egress policy as far as no socket is needed: which addresses are
|
|
3
|
+
* private, which hosts the deployment admits, which URLs a secret may travel
|
|
4
|
+
* to, and the redaction of the credentials an answer echoes back. Free of
|
|
5
|
+
* node modules, so a browser bundle judges a URL the same way Cortex does.
|
|
6
|
+
* Cortex's `guardedFetch` enforces the rest at the socket.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
canCarrySecret,
|
|
11
|
+
egressAllowedHosts,
|
|
12
|
+
isEgressAllowedHost,
|
|
13
|
+
isPrivateAddress,
|
|
14
|
+
normalizeHost,
|
|
15
|
+
} from "./url";
|
|
16
|
+
export { credentialForms, redactedUrl, scrubAnswer, scrubCredentialStrings } from "./scrub";
|
|
@@ -25,11 +25,11 @@ export function credentialForms(value: string) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* The one boundary every upstream body crosses before
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* The one boundary every upstream body crosses before Cortex stores it or
|
|
29
|
+
* sends it on: U+0000 is dropped from keys and values because Postgres
|
|
30
|
+
* refuses it in text and jsonb, in the tool execution log and the thread's
|
|
31
|
+
* transcript alike, then the credentials the request presented are
|
|
32
|
+
* redacted. Dropping first, on the patterns as well, means a credential
|
|
33
33
|
* an upstream spelled with U+0000 inside it still matches; redacting first
|
|
34
34
|
* would let the strip reassemble it.
|
|
35
35
|
*/
|
|
@@ -59,3 +59,32 @@ export function scrubCredentialStrings(value: unknown, credentials: string[]): u
|
|
|
59
59
|
]),
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What a party answered (a manifest, a tool it lists, the values it fills),
|
|
65
|
+
* fit to store or show a model: the credentials it was sent redacted from
|
|
66
|
+
* every string in it, or null when one of its `names` carries one, since a
|
|
67
|
+
* redacted identifier would name something else. Functions in it, such as a
|
|
68
|
+
* tool's executor, stay as they are.
|
|
69
|
+
*/
|
|
70
|
+
export function scrubAnswer<T>(answer: T, credentials: string[], names: readonly string[] = []) {
|
|
71
|
+
const carries = names.some((name) => {
|
|
72
|
+
const plain = name.replaceAll("\u0000", "");
|
|
73
|
+
return scrubCredentialStrings(plain, credentials) !== plain;
|
|
74
|
+
});
|
|
75
|
+
return carries ? null : (scrubCredentialStrings(answer, credentials) as T);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A URL as it may be logged or shown: its origin and path, without the
|
|
80
|
+
* userinfo, query or fragment a secret can ride in. Text that does not
|
|
81
|
+
* parse as a URL keeps what comes before its query or fragment.
|
|
82
|
+
*/
|
|
83
|
+
export function redactedUrl(raw: string) {
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(raw);
|
|
86
|
+
return `${url.origin}${url.pathname}`;
|
|
87
|
+
} catch {
|
|
88
|
+
return raw.split(/[?#]/)[0] ?? raw;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The egress policy as far as a URL alone can tell: which addresses are
|
|
3
3
|
* private, which hosts the deployment admits, and where a secret may be
|
|
4
4
|
* sent. Pure and free of node modules, so a browser bundle judges a URL the
|
|
5
|
-
* same way
|
|
5
|
+
* same way Cortex does.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
type Range = { first: number; last: number };
|
|
@@ -33,8 +33,8 @@ function isBlockedV4(address: string) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* The IPv4 address
|
|
37
|
-
* DNS answer (`::ffff:8.8.8.8`) and as two hex groups from a parsed URL
|
|
36
|
+
* The IPv4 address in the last 32 bits of an IPv6 one, which arrives dotted
|
|
37
|
+
* from a DNS answer (`::ffff:8.8.8.8`) and as two hex groups from a parsed URL
|
|
38
38
|
* (`::ffff:808:808`). `null` when the tail is neither.
|
|
39
39
|
*/
|
|
40
40
|
function mappedV4(mapped: string) {
|
|
@@ -45,15 +45,28 @@ function mappedV4(mapped: string) {
|
|
|
45
45
|
return [high >> 8, high & 255, low >> 8, low & 255].join(".");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The IPv4 address an IPv6 one reaches, for the prefixes where only the
|
|
50
|
+
* address may follow: IPv4-mapped (`::ffff:0:0/96`) and NAT64's well-known
|
|
51
|
+
* `64:ff9b::/96`. `undefined` for any other IPv6 address.
|
|
52
|
+
*/
|
|
53
|
+
function embeddedV4(address: string) {
|
|
54
|
+
for (const prefix of ["::ffff:", "64:ff9b::"]) {
|
|
55
|
+
if (address.startsWith(prefix)) return mappedV4(address.slice(prefix.length));
|
|
53
56
|
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBlockedV6(address: string) {
|
|
61
|
+
const normalized = normalizeHost(address);
|
|
62
|
+
const embedded = embeddedV4(normalized);
|
|
63
|
+
if (embedded !== undefined) return embedded === null || isBlockedV4(embedded);
|
|
54
64
|
return (
|
|
55
65
|
normalized === "::" ||
|
|
56
66
|
normalized === "::1" ||
|
|
67
|
+
// NAT64's local-use /48: the operator may embed the IPv4 address at
|
|
68
|
+
// any RFC 6052 prefix length, so no position can be read reliably.
|
|
69
|
+
normalized.startsWith("64:ff9b:1:") ||
|
|
57
70
|
normalized.startsWith("fc") ||
|
|
58
71
|
normalized.startsWith("fd") ||
|
|
59
72
|
/^fe[89ab]/.test(normalized)
|
|
@@ -115,7 +128,7 @@ export function isEgressAllowedHost(hostname: string, allowed = egressAllowedHos
|
|
|
115
128
|
* http to loopback, or over plain http to a host the policy admits. Pass
|
|
116
129
|
* `allowed` explicitly where the policy is known from elsewhere (the browser
|
|
117
130
|
* form reads it from the session) or must stay strict regardless of it (a
|
|
118
|
-
* URL the browser or the CLI dials, not
|
|
131
|
+
* URL the browser or the CLI dials, not Cortex): `[]` admits nothing.
|
|
119
132
|
*/
|
|
120
133
|
export function canCarrySecret(url: string, allowed = egressAllowedHosts()) {
|
|
121
134
|
try {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The one error envelope a backend answers Cortex with, in the dataset
|
|
5
|
+
* protocol and the server protocol alike: `{ error: { kind, detail } }`
|
|
6
|
+
* under the kind's HTTP status, or a closer one HTTP defines (401 for a
|
|
7
|
+
* wrong server key, 404 for an unknown tool, 413 for a body too large). The
|
|
8
|
+
* detail reaches the model.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const BACKEND_ERROR_KINDS = [
|
|
12
|
+
"invalid_request",
|
|
13
|
+
"unsupported",
|
|
14
|
+
"forbidden",
|
|
15
|
+
"timeout",
|
|
16
|
+
"internal",
|
|
17
|
+
] as const;
|
|
18
|
+
export type BackendErrorKind = (typeof BACKEND_ERROR_KINDS)[number];
|
|
19
|
+
|
|
20
|
+
/** The HTTP status each kind travels under. */
|
|
21
|
+
export const BACKEND_ERROR_STATUS = {
|
|
22
|
+
invalid_request: 400,
|
|
23
|
+
unsupported: 400,
|
|
24
|
+
forbidden: 403,
|
|
25
|
+
timeout: 504,
|
|
26
|
+
internal: 500,
|
|
27
|
+
} as const satisfies Record<BackendErrorKind, number>;
|
|
28
|
+
|
|
29
|
+
/** The longest detail Cortex accepts; a backend cuts a longer one down to it. */
|
|
30
|
+
export const BACKEND_ERROR_DETAIL_MAX = 512;
|
|
31
|
+
|
|
32
|
+
export const backendErrorSchema = z.object({
|
|
33
|
+
error: z.object({
|
|
34
|
+
kind: z.enum(BACKEND_ERROR_KINDS),
|
|
35
|
+
detail: z.string().max(BACKEND_ERROR_DETAIL_MAX),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
export type BackendError = z.infer<typeof backendErrorSchema>;
|
|
@@ -7,6 +7,11 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
7
7
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** `value` when it is a record, else undefined. */
|
|
11
|
+
export function asRecord(value: unknown) {
|
|
12
|
+
return isRecord(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* One reference token of a JSON Pointer (RFC 6901): `~` is written `~0` and `/` is written
|
|
12
17
|
* `~1`. Escaping replaces `~` first and unescaping `~1` first, so a literal `~1` survives the
|