@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.
Files changed (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /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.string().regex(DATASET_IDENTIFIER_PATTERN).max(64);
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 the server, so a host can trust the shape and only
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,3 @@
1
+ export * from "./datasets";
2
+ export * from "./dataset-sql";
3
+ export * from "./dataset-sql-guide";
@@ -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 it is stored or sent
29
- * on, in cc and the server alike: U+0000 is dropped from keys and values
30
- * because Postgres refuses it in text and jsonb on both cc's execution log
31
- * and the server's transcript, then the credentials the request presented
32
- * are redacted. Dropping first, on the patterns as well, means a credential
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 the server does.
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 inside an IPv4-mapped IPv6 one, which arrives dotted from a
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
- function isBlockedV6(address: string) {
49
- const normalized = address.toLowerCase();
50
- if (normalized.startsWith("::ffff:")) {
51
- const mapped = mappedV4(normalized.slice("::ffff:".length));
52
- return mapped === null || isBlockedV4(mapped);
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 the server): `[]` admits nothing.
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