@north-light/api 0.3.566 → 0.3.568

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.
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.35";
2
+ export declare const CONTRACT_LEVEL = "1.36";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -12,7 +12,7 @@
12
12
  //
13
13
  // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
14
14
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
15
- export const CONTRACT_LEVEL = '1.35';
15
+ export const CONTRACT_LEVEL = '1.36';
16
16
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
17
17
  export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
18
18
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -6,6 +6,7 @@ import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, MOUSE_MODE_DEFAULTS,
6
6
  import { BINDING_CATALOG, BINDING_IDS, isAttachPaneBinding } from './keybindings/catalog.js';
7
7
  import { notFound } from './errors.js';
8
8
  import { emitEvent } from './events/emit.js';
9
+ import { LEDGER_ENV } from './ledger/rates.js';
9
10
  import { atomicWriteJson, readJsonIfExists, ensureDir } from './fs-utils.js';
10
11
  import { scopeRoot, requireScopeRoot, findProjectScopeRoots, userScopeRoot } from './scope.js';
11
12
  import { listInstalledPluginsInRoot } from './installed-plugins.js';
@@ -805,7 +806,13 @@ function mergeApi(raw, defaults) {
805
806
  const tcp = raw.tcp;
806
807
  return { tcp: typeof tcp === 'string' ? tcp : defaults.tcp };
807
808
  }
809
+ let ledgerKeyWarned = false;
808
810
  function mergeConfig(partial) {
811
+ // The ledger's rates and switches moved from a stored `ledger` key to the deployment's `CRTR_LEDGER_*` variables; the stored key is no longer read.
812
+ if (!ledgerKeyWarned && partial['ledger'] !== undefined) {
813
+ ledgerKeyWarned = true;
814
+ emitEvent({ level: 'warn', event: 'config.ledger.ignored', fields: { variables: Object.values(LEDGER_ENV) } });
815
+ }
809
816
  const defaults = defaultScopeConfig();
810
817
  const schema_version = defaults.schema_version;
811
818
  const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
@@ -866,17 +873,6 @@ function mergeConfig(partial) {
866
873
  if (paths?.user_files !== undefined && (typeof paths.user_files !== 'string' || !paths.user_files.startsWith('/'))) {
867
874
  throw new Error('paths.user_files must be an absolute path');
868
875
  }
869
- const ledger = partial.ledger;
870
- if (ledger?.take_pct !== undefined && (typeof ledger.take_pct !== 'number' || !Number.isFinite(ledger.take_pct) || ledger.take_pct < 0 || ledger.take_pct > 100)) {
871
- throw new Error('ledger.take_pct must be a number from 0 to 100');
872
- }
873
- for (const key of ['sandbox_usd_per_gb_hour', 'sandbox_standby_s', 'storage_usd_per_gb_month', 'sandbox_disk_usd_per_gb_month', 'infra_free_usd_per_month']) {
874
- const value = ledger?.[key];
875
- if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value) || value < 0))
876
- throw new Error(`ledger.${key} must be a number of at least 0`);
877
- }
878
- if (ledger?.r2_move !== undefined && typeof ledger.r2_move !== 'boolean')
879
- throw new Error('ledger.r2_move must be true or false');
880
876
  const store = partial.store;
881
877
  const runtime = partial.runtime;
882
878
  if (runtime?.person_uids !== undefined && (!Array.isArray(runtime.person_uids)
@@ -886,7 +882,7 @@ function mergeConfig(partial) {
886
882
  const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
887
883
  const bin = normalizeBin(partial.bin);
888
884
  const humanActions = normalizeHumanActions(partial.humanActions);
889
- return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, document_port, consolidate_replies, keybindings, tmux_passthrough, modelLadders, providerOptions, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, api, ...(paths ? { paths } : {}), ...(ledger ? { ledger } : {}), ...(store ? { store } : {}), ...(runtime ? { runtime } : {}), spawnEnv };
885
+ return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, document_port, consolidate_replies, keybindings, tmux_passthrough, modelLadders, providerOptions, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, api, ...(paths ? { paths } : {}), ...(store ? { store } : {}), ...(runtime ? { runtime } : {}), spawnEnv };
890
886
  }
891
887
  /** Raw (un-defaulted) partial config for one scope, or null if the scope has
892
888
  * no root or no config.json. Used by `readMergedLaunchConfig` to layer
@@ -0,0 +1,24 @@
1
+ export declare const LEDGER_ENV: {
2
+ readonly sandboxUsdPerGbHour: "CRTR_LEDGER_SANDBOX_USD_PER_GB_HOUR";
3
+ readonly sandboxStandbyS: "CRTR_LEDGER_SANDBOX_STANDBY_S";
4
+ readonly storageUsdPerGbMonth: "CRTR_LEDGER_STORAGE_USD_PER_GB_MONTH";
5
+ readonly sandboxDiskUsdPerGbMonth: "CRTR_LEDGER_SANDBOX_DISK_USD_PER_GB_MONTH";
6
+ readonly infraFreeUsdPerMonth: "CRTR_LEDGER_INFRA_FREE_USD_PER_MONTH";
7
+ readonly takePct: "CRTR_LEDGER_TAKE_PCT";
8
+ readonly r2Move: "CRTR_LEDGER_R2_MOVE";
9
+ };
10
+ export declare const DEFAULT_STANDBY_S = 30;
11
+ export declare const DEFAULT_INFRA_FREE_USD = 50;
12
+ export declare const sandboxUsdPerGbHour: () => number | null;
13
+ export declare const sandboxStandbyS: () => number;
14
+ export declare const storageUsdPerGbMonth: () => number | null;
15
+ export declare const sandboxDiskUsdPerGbMonth: () => number | null;
16
+ export declare const infraFreeUsdPerMonth: () => number;
17
+ /** Northlight's take on each priced model or tool charge, in percent; 0 (the default) writes no take rows. */
18
+ export declare const takePercent: () => number;
19
+ /** The `ledger-r2` lane moves closed months into the blob store only while this is true (off by default until 5b is accepted). */
20
+ export declare const r2MoveEnabled: () => boolean;
21
+ /** Throws on the first malformed ledger variable. Called once at daemon boot. */
22
+ export declare function assertLedgerEnv(): void;
23
+ /** Say once per process that a lane writes nothing because its rate is unset. */
24
+ export declare function warnRateUnset(lane: string, ...names: string[]): void;
@@ -0,0 +1,64 @@
1
+ // The ledger's platform prices and switches (design-fleet D56), read from the deployment's environment on every role (a Mac, the api
2
+ // role, the reconcile role) the way the other deployment settings are (`CRTR_R2_BUCKET`, `BLAXEL_REGION`). They are Northlight's
3
+ // list prices, not one person's setting. A rate that is unset is `null`: its lane writes nothing and says so once. A set but
4
+ // malformed value throws, and `assertLedgerEnv()` runs at daemon boot so the daemon refuses to start on it.
5
+ import { emitEvent } from '../events/emit.js';
6
+ export const LEDGER_ENV = {
7
+ sandboxUsdPerGbHour: 'CRTR_LEDGER_SANDBOX_USD_PER_GB_HOUR',
8
+ sandboxStandbyS: 'CRTR_LEDGER_SANDBOX_STANDBY_S',
9
+ storageUsdPerGbMonth: 'CRTR_LEDGER_STORAGE_USD_PER_GB_MONTH',
10
+ sandboxDiskUsdPerGbMonth: 'CRTR_LEDGER_SANDBOX_DISK_USD_PER_GB_MONTH',
11
+ infraFreeUsdPerMonth: 'CRTR_LEDGER_INFRA_FREE_USD_PER_MONTH',
12
+ takePct: 'CRTR_LEDGER_TAKE_PCT',
13
+ r2Move: 'CRTR_LEDGER_R2_MOVE',
14
+ };
15
+ export const DEFAULT_STANDBY_S = 30;
16
+ export const DEFAULT_INFRA_FREE_USD = 50;
17
+ function number(name, max = Infinity) {
18
+ const raw = process.env[name];
19
+ if (raw === undefined || raw === '')
20
+ return null;
21
+ // Only a plain decimal: `Number()` would take whitespace as 0 and hex, binary, exponent or `Infinity` spellings as numbers.
22
+ const value = /^\d+(\.\d+)?$/.test(raw) ? Number(raw) : NaN;
23
+ if (!Number.isFinite(value) || value > max)
24
+ throw new Error(`${name} must be a number from 0${max === Infinity ? ' up' : ` to ${max}`}, got ${JSON.stringify(raw)}`);
25
+ return value;
26
+ }
27
+ function flag(name) {
28
+ const raw = process.env[name];
29
+ if (raw === undefined || raw === '')
30
+ return false;
31
+ if (raw === 'true' || raw === '1')
32
+ return true;
33
+ if (raw === 'false' || raw === '0')
34
+ return false;
35
+ throw new Error(`${name} must be true, false, 1 or 0, got ${JSON.stringify(raw)}`);
36
+ }
37
+ export const sandboxUsdPerGbHour = () => number(LEDGER_ENV.sandboxUsdPerGbHour);
38
+ export const sandboxStandbyS = () => number(LEDGER_ENV.sandboxStandbyS) ?? DEFAULT_STANDBY_S;
39
+ export const storageUsdPerGbMonth = () => number(LEDGER_ENV.storageUsdPerGbMonth);
40
+ export const sandboxDiskUsdPerGbMonth = () => number(LEDGER_ENV.sandboxDiskUsdPerGbMonth);
41
+ export const infraFreeUsdPerMonth = () => number(LEDGER_ENV.infraFreeUsdPerMonth) ?? DEFAULT_INFRA_FREE_USD;
42
+ /** Northlight's take on each priced model or tool charge, in percent; 0 (the default) writes no take rows. */
43
+ export const takePercent = () => number(LEDGER_ENV.takePct, 100) ?? 0;
44
+ /** The `ledger-r2` lane moves closed months into the blob store only while this is true (off by default until 5b is accepted). */
45
+ export const r2MoveEnabled = () => flag(LEDGER_ENV.r2Move);
46
+ /** Throws on the first malformed ledger variable. Called once at daemon boot. */
47
+ export function assertLedgerEnv() {
48
+ sandboxUsdPerGbHour();
49
+ sandboxStandbyS();
50
+ storageUsdPerGbMonth();
51
+ sandboxDiskUsdPerGbMonth();
52
+ infraFreeUsdPerMonth();
53
+ takePercent();
54
+ r2MoveEnabled();
55
+ }
56
+ const warned = new Set();
57
+ /** Say once per process that a lane writes nothing because its rate is unset. */
58
+ export function warnRateUnset(lane, ...names) {
59
+ const key = `${lane}:${names.join(',')}`;
60
+ if (warned.has(key))
61
+ return;
62
+ warned.add(key);
63
+ emitEvent({ level: 'warn', event: 'ledger.rate_unset', fields: { lane, unset: names } });
64
+ }
@@ -377,10 +377,6 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
377
377
  surface: "specialized";
378
378
  owner: string;
379
379
  };
380
- ledger: {
381
- surface: "specialized";
382
- owner: string;
383
- };
384
380
  store: {
385
381
  surface: "specialized";
386
382
  owner: string;
@@ -469,7 +469,6 @@ export const CONFIG_SETTINGS_COVERAGE = {
469
469
  remoteCanvas: { surface: 'specialized', owner: 'canvas config commands' },
470
470
  api: { surface: 'specialized', owner: 'sys connect' },
471
471
  paths: { surface: 'specialized', owner: 'runtime layout' },
472
- ledger: { surface: 'specialized', owner: 'money ledger (take_pct, sandbox rates)' },
473
472
  store: { surface: 'specialized', owner: 'daemon conversation store' },
474
473
  runtime: { surface: 'specialized', owner: 'app-facing listener' },
475
474
  spawnEnv: { surface: 'specialized', owner: 'spawn environment allow-list' },
package/dist/types.d.ts CHANGED
@@ -402,22 +402,6 @@ export interface ScopeConfig {
402
402
  paths?: {
403
403
  user_files?: string;
404
404
  };
405
- /** The money ledger. `take_pct` (0-100, default 0) is Northlight's take on each priced model or tool charge; at 0 no take rows are written. */
406
- ledger?: {
407
- take_pct?: number;
408
- /** USD per GB-hour of Blaxel sandbox awake time; the `ledger-sandbox` lane writes no rows while it is unset. */
409
- sandbox_usd_per_gb_hour?: number;
410
- /** Seconds a sandbox stays awake after its broker goes idle or exits (default 30). */
411
- sandbox_standby_s?: number;
412
- /** USD per GB-month of R2 storage; the `ledger-storage` lane writes no `r2` rows while it is unset. */
413
- storage_usd_per_gb_month?: number;
414
- /** USD per GB-month of Blaxel sandbox disk; the `ledger-storage` lane writes no `blaxel` rows while it is unset. */
415
- sandbox_disk_usd_per_gb_month?: number;
416
- /** USD of sandbox and storage a billing account uses free each UTC month (default 50); `billed_usd` on those rows is the part above it. */
417
- infra_free_usd_per_month?: number;
418
- /** The `ledger-r2` lane moves closed months into the blob store only while this is true (off until 5b is accepted). */
419
- r2_move?: boolean;
420
- };
421
405
  /** Conversation store configuration for the daemon. */
422
406
  store?: {
423
407
  root?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.566",
3
+ "version": "0.3.568",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",