@vizuh/sabi 0.1.3 → 0.1.4

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/mod/sabi.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // @vizuh/sabi 0.1.3 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
1
+ // @vizuh/sabi 0.1.4 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
2
2
  // Source and docs: https://github.com/vizuh/sabi
3
3
 
4
4
  // packages/core/src/state.ts
@@ -331,8 +331,8 @@ function servesInputModalities(declared, required) {
331
331
  return required.every((modality) => declared.includes(modality));
332
332
  }
333
333
  function firstServingTier(tiers, required, declared) {
334
- for (const [name, tier] of Object.entries(tiers)) {
335
- if (servesInputModalities(declared(tier), required)) return name;
334
+ for (const name of Object.keys(tiers).sort()) {
335
+ if (servesInputModalities(declared(tiers[name]), required)) return name;
336
336
  }
337
337
  return void 0;
338
338
  }
@@ -441,6 +441,9 @@ function validateModelMetadata(value, label) {
441
441
  if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) {
442
442
  fail(`capabilities.${field}`, "must be an array of nonempty strings");
443
443
  }
444
+ if ((field === "inputModalities" || field === "outputModalities") && items.length === 0) {
445
+ fail(`capabilities.${field}`, "must not be empty");
446
+ }
444
447
  const strings = items;
445
448
  if (new Set(strings).size !== strings.length) fail(`capabilities.${field}`, "must not contain duplicates");
446
449
  const allowed = field.endsWith("Modalities") ? MODALITIES : field === "structuredOutput" ? ["json_object", "json_schema"] : void 0;
@@ -530,11 +533,30 @@ function validateConfig(value, source = "<inline>") {
530
533
  throw new Error(`Sabi config ${source}: alias '${alias}' targets unknown tier '${target}'`);
531
534
  }
532
535
  }
536
+ const knownRules = new Set(POLICY_ORDER);
533
537
  for (const [condition, tier] of Object.entries(policy)) {
538
+ if (!knownRules.has(condition)) {
539
+ throw new Error(`Sabi config ${source}: policy rule '${condition}' is not a known rule (${POLICY_ORDER.join(", ")})`);
540
+ }
534
541
  if (typeof tier !== "string" || tier !== "off" && !Object.hasOwn(models, tier)) {
535
542
  throw new Error(`Sabi config ${source}: policy rule '${condition}' targets unknown tier '${tier}'`);
536
543
  }
537
544
  }
545
+ if (Object.values(aliases).includes("auto")) {
546
+ const fallbackTier = typeof policy.unclassified === "string" && policy.unclassified !== "off" ? policy.unclassified : "cheap";
547
+ if (!Object.hasOwn(models, fallbackTier)) {
548
+ throw new Error(`Sabi config ${source}: policy.unclassified must resolve to a declared tier (got '${String(policy.unclassified)}')`);
549
+ }
550
+ }
551
+ const transportFallback = config.transportFallback;
552
+ if (transportFallback !== void 0) {
553
+ if (!isObject(transportFallback)) {
554
+ throw new Error(`Sabi config ${source}: transportFallback must be an object`);
555
+ }
556
+ if (transportFallback.enabled !== void 0 && typeof transportFallback.enabled !== "boolean") {
557
+ throw new Error(`Sabi config ${source}: transportFallback.enabled must be a boolean`);
558
+ }
559
+ }
538
560
  const judge = config.judge;
539
561
  if (judge !== void 0) {
540
562
  if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
@@ -556,6 +578,14 @@ function validateConfig(value, source = "<inline>") {
556
578
  if (!Array.isArray(judge.callOn) || judge.callOn.some((rule) => typeof rule !== "string")) {
557
579
  throw new Error(`Sabi config ${source}: judge.callOn must be an array of policy rule names`);
558
580
  }
581
+ for (const rule of judge.callOn) {
582
+ if (!knownRules.has(rule)) {
583
+ throw new Error(`Sabi config ${source}: judge.callOn rule '${rule}' is not a known rule (${POLICY_ORDER.join(", ")})`);
584
+ }
585
+ }
586
+ }
587
+ if (judge.includeSnippets !== void 0 && typeof judge.includeSnippets !== "boolean") {
588
+ throw new Error(`Sabi config ${source}: judge.includeSnippets must be a boolean`);
559
589
  }
560
590
  for (const [name, value2] of Object.entries(judge.thresholds ?? {})) {
561
591
  if (typeof value2 !== "number" || value2 < 0 || value2 > 1) {
@@ -632,25 +662,126 @@ function validateConfig(value, source = "<inline>") {
632
662
  if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
633
663
  throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
634
664
  }
665
+ if (tier.inputModalities !== void 0) {
666
+ if (!Array.isArray(tier.inputModalities) || tier.inputModalities.length === 0 || tier.inputModalities.some((modality) => typeof modality !== "string" || !MODALITIES.includes(modality))) {
667
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must be a nonempty array of: ${MODALITIES.join(", ")}`);
668
+ }
669
+ if (new Set(tier.inputModalities).size !== tier.inputModalities.length) {
670
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must not contain duplicates`);
671
+ }
672
+ }
673
+ if (tier.contextWindow !== void 0 && (typeof tier.contextWindow !== "number" || !Number.isSafeInteger(tier.contextWindow) || tier.contextWindow < 1)) {
674
+ throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.contextWindow must be a safe integer >= 1`);
675
+ }
635
676
  }
636
677
  }
637
678
  return { ...config, upstreams, models, aliases, policy };
638
679
  }
639
680
 
640
681
  // packages/core/src/log.ts
641
- import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
642
- import { createHash, randomUUID } from "node:crypto";
682
+ import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
683
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
684
+ import os2 from "node:os";
643
685
  import path2 from "node:path";
644
686
  function defaultLogPath() {
645
687
  return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
646
688
  }
689
+ function appendPrivateLine(logFile, line) {
690
+ const dir = path2.dirname(logFile);
691
+ mkdirSync(dir, { recursive: true, mode: 448 });
692
+ appendFileSync(logFile, line, { mode: 384 });
693
+ if (process.platform !== "win32") {
694
+ chmodSync(dir, 448);
695
+ chmodSync(logFile, 384);
696
+ }
697
+ }
698
+ function identitySaltPath(env = process.env) {
699
+ const override = env.SABI_ID_SALT_FILE?.trim();
700
+ if (override) return override;
701
+ const base = env.XDG_CONFIG_HOME?.trim() || path2.join(os2.homedir(), ".config");
702
+ return path2.join(base, "sabi", ".identity-salt");
703
+ }
704
+ var cachedSalt;
705
+ var cachedSaltSource;
706
+ var ephemeralSaltWarned = false;
707
+ function loadOrCreateSalt(source) {
708
+ try {
709
+ if (existsSync2(source)) {
710
+ const stored = readFileSync2(source, "utf8").trim();
711
+ if (stored.length >= 16) return stored;
712
+ }
713
+ const fresh = randomBytes(32).toString("hex");
714
+ mkdirSync(path2.dirname(source), { recursive: true });
715
+ writeFileSync(source, `${fresh}
716
+ `, { mode: 384 });
717
+ try {
718
+ chmodSync(source, 384);
719
+ } catch {
720
+ }
721
+ return fresh;
722
+ } catch {
723
+ return void 0;
724
+ }
725
+ }
726
+ function getIdentitySalt(env = process.env) {
727
+ const override = env.SABI_ID_SALT?.trim();
728
+ if (override) return override;
729
+ const source = identitySaltPath(env);
730
+ if (cachedSalt !== void 0 && cachedSaltSource === source) return cachedSalt;
731
+ const salt = loadOrCreateSalt(source);
732
+ if (salt === void 0) {
733
+ if (!ephemeralSaltWarned) {
734
+ ephemeralSaltWarned = true;
735
+ console.warn(`Sabi: identity salt at ${source} is unreadable \u2014 using an ephemeral per-process salt`);
736
+ }
737
+ cachedSalt = randomBytes(32).toString("hex");
738
+ cachedSaltSource = source;
739
+ return cachedSalt;
740
+ }
741
+ cachedSalt = salt;
742
+ cachedSaltSource = source;
743
+ return salt;
744
+ }
745
+ var logWriteFailures = 0;
746
+ var warnedLogFiles = /* @__PURE__ */ new Set();
747
+ function logWriteFailurePath(logFile) {
748
+ return `${logFile}.write-failures.json`;
749
+ }
750
+ function recordLogWriteFailure(logFile, error) {
751
+ logWriteFailures += 1;
752
+ if (!warnedLogFiles.has(logFile)) {
753
+ warnedLogFiles.add(logFile);
754
+ console.warn(`Sabi: could not append to the decision log at ${logFile}: ${error?.message ?? error} (telemetry degraded, will retry next round)`);
755
+ }
756
+ try {
757
+ const sidecar = logWriteFailurePath(logFile);
758
+ let failures = 0;
759
+ try {
760
+ const prior = JSON.parse(readFileSync2(sidecar, "utf8"));
761
+ if (typeof prior.failures === "number" && Number.isFinite(prior.failures)) failures = Math.floor(prior.failures);
762
+ } catch {
763
+ }
764
+ const state = {
765
+ failures: failures + 1,
766
+ lastTs: (/* @__PURE__ */ new Date()).toISOString(),
767
+ lastError: String(error?.message ?? error).slice(0, 120)
768
+ };
769
+ mkdirSync(path2.dirname(sidecar), { recursive: true });
770
+ writeFileSync(sidecar, `${JSON.stringify(state)}
771
+ `);
772
+ } catch {
773
+ }
774
+ }
647
775
  function appendDecision(record, logFile = defaultLogPath()) {
648
- mkdirSync(path2.dirname(logFile), { recursive: true });
649
- appendFileSync(logFile, `${JSON.stringify(record)}
776
+ try {
777
+ appendPrivateLine(logFile, `${JSON.stringify(record)}
650
778
  `);
779
+ } catch (error) {
780
+ recordLogWriteFailure(logFile, error);
781
+ }
651
782
  }
652
783
  function hashIdentity(kind, ...parts) {
653
- return createHash("sha256").update(JSON.stringify([kind, ...parts])).digest("hex");
784
+ return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
654
785
  }
655
786
  function sessionIdFor(session, client = "unknown") {
656
787
  return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizuh/sabi",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Adaptive inference scheduling for Command Code: one bundled mod that routes each continuing round by model, effort and trajectory state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/sabi.config.json CHANGED
@@ -68,6 +68,9 @@
68
68
  "exploration": "cheap",
69
69
  "unclassified": "cheap"
70
70
  },
71
+ "transportFallback": {
72
+ "enabled": false
73
+ },
71
74
  "telemetry": {
72
75
  "allowlistOnly": true,
73
76
  "captureSnippets": false,