@query-farm/vgi 0.43.0 → 0.44.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.
@@ -6,6 +6,7 @@ import type { OverloadContext } from "../../functions/registry.js";
6
6
  import { batchToScalarDict, deserializeBatch, serializeBatch, batchFromColumns } from "../../util/arrow/index.js";
7
7
  import { toUint8Array } from "../../util/bytes.js";
8
8
  import { normalizeSchemaPath } from "../../schema-path.js";
9
+ import { conformRecord } from "../../codec/conform.js";
9
10
 
10
11
  // The Python vgi-rpc framework wraps ALL non-void unary results in a single
11
12
  // "result" column. For ArrowSerializableDataclass types, the result is serialized
@@ -39,7 +40,8 @@ export function normalizeRequestSchemaPaths(params: Record<string, any>): void {
39
40
 
40
41
  /**
41
42
  * Wrap a dict of values into a single "result" Binary column.
42
- * Builds a 1-row batch from the values using the given schema,
43
+ * Builds a 1-row batch from the values using the given schema (conformed to
44
+ * the protocol's container rules, see `conformRecord`),
43
45
  * serializes it to IPC bytes, and returns { result: bytes }.
44
46
  */
45
47
  export function wrapResult(
@@ -47,8 +49,12 @@ export function wrapResult(
47
49
  innerSchema: VgiSchema | import("../../arrow/index.js").VgiSchema,
48
50
  ): { result: Uint8Array } {
49
51
  const a = innerSchema as VgiSchema;
52
+ // Python's container rules (a non-nullable list/map is never null, list
53
+ // items never are): `conformRecord` writes `[]` where a handler left a list
54
+ // out, and an unset list item as its zero value.
55
+ const row = conformRecord(a, values);
50
56
  const batch = batchFromColumns(
51
- Object.fromEntries(a.fields.map(f => [f.name, [values[f.name] ?? null]])),
57
+ Object.fromEntries(a.fields.map(f => [f.name, [row[f.name] ?? null]])),
52
58
  a,
53
59
  );
54
60
  return { result: serializeBatch(batch) };
@@ -72,7 +72,9 @@ export function serializeBindRequest(req: BindRequest): VgiBatch {
72
72
  return buildSingleRowBatch(BIND_REQUEST_SCHEMA, {
73
73
  function_name: req.function_name,
74
74
  arguments: serializeArguments(req.arguments),
75
- function_type: req.function_type,
75
+ // The wire carries the Python enum member NAME ("TABLE"), not the TS
76
+ // enum's value ("table"); each FunctionType value upper-cases to its name.
77
+ function_type: req.function_type == null ? req.function_type : String(req.function_type).toUpperCase(),
76
78
  input_schema: req.input_schema ? serializeSchema(req.input_schema) : null,
77
79
  settings: req.settings ? serializeBatch(req.settings) : null,
78
80
  secrets: req.secrets ? serializeBatch(req.secrets) : null,
@@ -8,6 +8,7 @@ import {
8
8
  batchFromColumns,
9
9
  } from "../../arrow/index.js";
10
10
  import { toUint8Array as toUint8ArrayBase } from "../../util/bytes.js";
11
+ import { conformRecord } from "../../codec/conform.js";
11
12
 
12
13
  /**
13
14
  * Coerce a wire value (string / Uint8Array / Buffer / etc.) to a Uint8Array.
@@ -25,7 +26,8 @@ export function toUint8Array(val: any): Uint8Array {
25
26
 
26
27
  /**
27
28
  * Build a single-row RecordBatch from a {fieldName: value} dict matching the
28
- * given schema. Auto-coerces JS numbers to BigInt for Int64 fields.
29
+ * given schema. Auto-coerces JS numbers to BigInt for Int64 fields, and
30
+ * applies the protocol's container rules (`conformRecord`).
29
31
  */
30
32
  export function buildSingleRowBatch(
31
33
  schema: VgiSchema,
@@ -35,8 +37,9 @@ export function buildSingleRowBatch(
35
37
  // declares one — vectorFromArray/columnFromArray rejects plain Number for
36
38
  // 64-bit ints under both backends.
37
39
  const cols: Record<string, any[]> = {};
40
+ const row = conformRecord(schema, values);
38
41
  for (const f of schema.fields) {
39
- let val = values[f.name];
42
+ let val = row[f.name];
40
43
  if (isInt(f.type) && (f.type as any).bitWidth === 64 && typeof val === "number") {
41
44
  val = BigInt(val);
42
45
  }
package/src/rpc-server.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  // 2. The worker's `hostedProtocols()`, in the order returned -- on EVERY
14
14
  // transport.
15
15
  // 3. `vgi_rpc.Reflection.v1` -- on every transport.
16
- // 4. `vgi_rpc.Identity.v1` -- HTTP only, and only when the worker supplies
16
+ // 4. `vgi_rpc.Identity.v2` -- HTTP only, and only when the worker supplies
17
17
  // `resolveToken` and/or `mintGrant` (or grant keys are configured).
18
18
  // 5. `vgi.attach_tickets.v1` -- HTTP only, and only when the signing key was
19
19
  // configured explicitly (never a generated one) *and* the worker can
@@ -44,7 +44,7 @@ export type ServerTransport = "pipe" | "unix" | "tcp" | "iroh" | "stream" | "htt
44
44
  const TRANSPORTS: ReadonlySet<string> = new Set<ServerTransport>(["pipe", "unix", "tcp", "iroh", "stream", "http"]);
45
45
 
46
46
  /** Transports that authenticate their callers, and so may host
47
- * `vgi_rpc.Identity.v1`. Identity answers questions about *other* callers'
47
+ * `vgi_rpc.Identity.v2`. Identity answers questions about *other* callers'
48
48
  * credentials; its allowlist is a list of principals, which a transport with
49
49
  * no caller identity cannot check. */
50
50
  const IDENTITY_TRANSPORTS: ReadonlySet<string> = new Set<ServerTransport>(["http"]);
@@ -78,14 +78,14 @@ export interface HostingOptions {
78
78
  *
79
79
  * Each needs a distinct wire name, which may not be `vgi.v2` and may not
80
80
  * use the reserved `vgi_rpc.` prefix: reflection is hosted automatically and
81
- * `vgi_rpc.Identity.v1` is enabled through {@link resolveToken} /
81
+ * `vgi_rpc.Identity.v2` is enabled through {@link resolveToken} /
82
82
  * {@link mintGrant}. Violations are a startup error naming this hook.
83
83
  */
84
84
  hostedProtocols?: () => Iterable<Protocol>;
85
85
 
86
86
  /**
87
87
  * Resolve an opaque bearer credential to the identity it authenticates as.
88
- * Supplying it hosts `vgi_rpc.Identity.v1`'s `introspect_token` -- on HTTP,
88
+ * Supplying it hosts `vgi_rpc.Identity.v2`'s `introspect_token` -- on HTTP,
89
89
  * the transport that authenticates callers. Absent, the method is absent
90
90
  * (not hosted-and-refusing), which keeps a dependency upgrade from growing a
91
91
  * credential-to-identity oracle on every existing worker.
@@ -232,7 +232,7 @@ function validatedHostedProtocols(hosting: HostingOptions, primary: Protocol): P
232
232
  throw new Error(
233
233
  `${owner} entry ${index} is named '${name}', which claims the reserved '${RESERVED_PREFIX}' prefix. ` +
234
234
  "Framework protocols are not supplied through this hook: reflection is hosted automatically, and " +
235
- "vgi_rpc.Identity.v1 is enabled by supplying resolveToken and/or mintGrant.",
235
+ "vgi_rpc.Identity.v2 is enabled by supplying resolveToken and/or mintGrant.",
236
236
  );
237
237
  }
238
238
  if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) {
@@ -259,7 +259,7 @@ function validatedHostedProtocols(hosting: HostingOptions, primary: Protocol): P
259
259
  return out;
260
260
  }
261
261
 
262
- /** Build the `vgi_rpc.Identity.v1` implementation, or `undefined`.
262
+ /** Build the `vgi_rpc.Identity.v2` implementation, or `undefined`.
263
263
  *
264
264
  * `undefined` unless the worker supplies at least one hook -- and that is the
265
265
  * point: the server then does not host the protocol at all, rather than
@@ -329,7 +329,7 @@ function resolveIntrospectPrincipals(
329
329
  }
330
330
  if (principals.length === 0) {
331
331
  throw new Error(
332
- "This worker supplies resolveToken, which hosts the vgi_rpc.Identity.v1 protocol, but no " +
332
+ "This worker supplies resolveToken, which hosts the vgi_rpc.Identity.v2 protocol, but no " +
333
333
  `introspector allowlist was configured. Set ${INTROSPECT_PRINCIPALS_ENV} (comma-separated) or pass ` +
334
334
  "introspectPrincipals.\n\n" +
335
335
  "There is no permissive default on purpose: introspection is a separate capability from " +