@gendigital/sage-openclaw 0.9.0 → 0.10.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.
@@ -2961,7 +2961,7 @@ var init_index_min = __esm({
2961
2961
  });
2962
2962
 
2963
2963
  // ../core/src/model-download-worker.ts
2964
- var import_promises5 = require("node:fs/promises");
2964
+ var import_promises7 = require("node:fs/promises");
2965
2965
 
2966
2966
  // ../core/src/config.ts
2967
2967
  var import_node_path = require("node:path");
@@ -7082,6 +7082,14 @@ var LoggingConfigSchema = external_exports.object({
7082
7082
  max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
7083
7083
  max_files: external_exports.number().int().min(0).default(3)
7084
7084
  });
7085
+ var OperationalLogLevelSchema = external_exports.enum(["debug", "info", "warn", "error"]);
7086
+ var OperationalLoggingConfigSchema = external_exports.object({
7087
+ enabled: external_exports.boolean().default(true),
7088
+ level: OperationalLogLevelSchema.default("info"),
7089
+ path: external_exports.string().default("~/.sage/operational.jsonl"),
7090
+ max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
7091
+ max_files: external_exports.number().int().min(0).default(3)
7092
+ });
7085
7093
  var FileCheckConfigSchema = external_exports.object({
7086
7094
  endpoint: external_exports.string().optional(),
7087
7095
  timeout_seconds: external_exports.number().default(5),
@@ -7131,6 +7139,7 @@ var ConfigSchema = external_exports.object({
7131
7139
  allowlist: AllowlistConfigSchema.default({}),
7132
7140
  exceptions: ExceptionsConfigSchema.default({}),
7133
7141
  logging: LoggingConfigSchema.default({}),
7142
+ operational_logging: OperationalLoggingConfigSchema.default({}),
7134
7143
  sensitivity: SensitivitySchema.default("balanced"),
7135
7144
  disabled_threats: external_exports.array(external_exports.string()).default([]),
7136
7145
  brand_key: external_exports.string().min(1).max(32).regex(/^[a-z0-9_-]+$/u).optional(),
@@ -7146,6 +7155,28 @@ var HookTypeSchema = external_exports.enum([
7146
7155
  ]);
7147
7156
 
7148
7157
  // ../core/src/config.ts
7158
+ var SAGE_DIR = "~/.sage";
7159
+ function resolvedSageDir() {
7160
+ return resolvePath(SAGE_DIR);
7161
+ }
7162
+ function defaultConfigPath() {
7163
+ return (0, import_node_path.join)(resolvedSageDir(), "config.json");
7164
+ }
7165
+ function defaultCachePath() {
7166
+ return (0, import_node_path.join)(resolvedSageDir(), "cache.json");
7167
+ }
7168
+ function defaultAllowlistPath() {
7169
+ return (0, import_node_path.join)(resolvedSageDir(), "allowlist.json");
7170
+ }
7171
+ function defaultExceptionsPath() {
7172
+ return (0, import_node_path.join)(resolvedSageDir(), "exceptions.json");
7173
+ }
7174
+ function defaultAuditPath() {
7175
+ return (0, import_node_path.join)(resolvedSageDir(), "audit.jsonl");
7176
+ }
7177
+ function defaultOperationalLogPath() {
7178
+ return (0, import_node_path.join)(resolvedSageDir(), "operational.jsonl");
7179
+ }
7149
7180
  function resolvePath(pathStr) {
7150
7181
  if (pathStr.startsWith("~/") || pathStr === "~") {
7151
7182
  const home = getHomeDir();
@@ -7153,6 +7184,117 @@ function resolvePath(pathStr) {
7153
7184
  }
7154
7185
  return pathStr;
7155
7186
  }
7187
+ function isWithinDirectory(baseDir, targetPath) {
7188
+ const rel = (0, import_node_path.relative)(baseDir, targetPath);
7189
+ if (rel === "") return true;
7190
+ if ((0, import_node_path.isAbsolute)(rel)) return false;
7191
+ return rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`);
7192
+ }
7193
+ function normalizeStateFilePath(configuredPath, fallbackPath, field, logger) {
7194
+ const sageDir = resolvedSageDir();
7195
+ const trimmed = configuredPath.trim();
7196
+ if (trimmed === "") {
7197
+ logger.warn(`Config ${field}.path is empty; using default`, {
7198
+ configuredPath,
7199
+ defaultPath: fallbackPath
7200
+ });
7201
+ return fallbackPath;
7202
+ }
7203
+ const expanded = resolvePath(trimmed);
7204
+ const resolved = (0, import_node_path.isAbsolute)(expanded) ? (0, import_node_path.resolve)(expanded) : (0, import_node_path.resolve)(sageDir, expanded);
7205
+ if (isWithinDirectory(sageDir, resolved)) {
7206
+ if (resolved === sageDir) {
7207
+ logger.warn(`Config ${field}.path must point to a file; using default`, {
7208
+ configuredPath,
7209
+ defaultPath: fallbackPath
7210
+ });
7211
+ return fallbackPath;
7212
+ }
7213
+ return resolved;
7214
+ }
7215
+ logger.warn(`Config ${field}.path escapes ${sageDir}; using default`, {
7216
+ configuredPath,
7217
+ defaultPath: fallbackPath
7218
+ });
7219
+ return fallbackPath;
7220
+ }
7221
+ var BRAND_KEY_RE = /^[a-z0-9_-]+$/u;
7222
+ function sanitizeBrandKey(data, logger) {
7223
+ const brandKey = data.brand_key;
7224
+ if (brandKey === void 0) return data;
7225
+ if (typeof brandKey === "string" && brandKey.length >= 1 && brandKey.length <= 32 && BRAND_KEY_RE.test(brandKey)) {
7226
+ return data;
7227
+ }
7228
+ logger.warn(`Invalid brand_key in config \u2014 ignoring`, { brand_key: brandKey });
7229
+ const { brand_key: _2, ...rest } = data;
7230
+ return rest;
7231
+ }
7232
+ function sanitizeConfigPaths(config, logger) {
7233
+ const cachePath = defaultCachePath();
7234
+ const allowlistPath = defaultAllowlistPath();
7235
+ const exceptionsPath = defaultExceptionsPath();
7236
+ const auditPath = defaultAuditPath();
7237
+ const operationalLogPath = defaultOperationalLogPath();
7238
+ return {
7239
+ ...config,
7240
+ cache: {
7241
+ ...config.cache,
7242
+ path: normalizeStateFilePath(config.cache.path, cachePath, "cache", logger)
7243
+ },
7244
+ allowlist: {
7245
+ ...config.allowlist,
7246
+ path: normalizeStateFilePath(config.allowlist.path, allowlistPath, "allowlist", logger)
7247
+ },
7248
+ exceptions: {
7249
+ ...config.exceptions,
7250
+ path: normalizeStateFilePath(config.exceptions.path, exceptionsPath, "exceptions", logger)
7251
+ },
7252
+ logging: {
7253
+ ...config.logging,
7254
+ path: normalizeStateFilePath(config.logging.path, auditPath, "logging", logger)
7255
+ },
7256
+ operational_logging: {
7257
+ ...config.operational_logging,
7258
+ path: normalizeStateFilePath(
7259
+ config.operational_logging.path,
7260
+ operationalLogPath,
7261
+ "operational_logging",
7262
+ logger
7263
+ )
7264
+ }
7265
+ };
7266
+ }
7267
+ function defaultConfig(logger) {
7268
+ return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
7269
+ }
7270
+ function parseConfig(raw, path, logger) {
7271
+ let data;
7272
+ try {
7273
+ data = JSON.parse(raw);
7274
+ } catch (e) {
7275
+ logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
7276
+ return defaultConfig(logger);
7277
+ }
7278
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
7279
+ logger.warn(`Config file ${path} does not contain a JSON object`);
7280
+ return defaultConfig(logger);
7281
+ }
7282
+ const sanitized = sanitizeBrandKey(data, logger);
7283
+ try {
7284
+ return sanitizeConfigPaths(ConfigSchema.parse(sanitized), logger);
7285
+ } catch (e) {
7286
+ logger.warn(`Config validation failed, using defaults`, { error: String(e) });
7287
+ return defaultConfig(logger);
7288
+ }
7289
+ }
7290
+ async function loadConfig(configPath, logger = nullLogger) {
7291
+ const path = configPath ? resolvePath(configPath) : defaultConfigPath();
7292
+ try {
7293
+ return parseConfig(await getFileContent(path), path, logger);
7294
+ } catch {
7295
+ return defaultConfig(logger);
7296
+ }
7297
+ }
7156
7298
 
7157
7299
  // ../core/src/installation-id.ts
7158
7300
  var import_node_crypto = require("node:crypto");
@@ -7440,7 +7582,7 @@ var import_node_path14 = require("node:path");
7440
7582
  var import_node_url = require("node:url");
7441
7583
  var import_meta = {};
7442
7584
  function resolveVersion() {
7443
- if (true) return "0.9.0";
7585
+ if (true) return "0.10.0";
7444
7586
  try {
7445
7587
  const pkgPath = (0, import_node_path14.join)((0, import_node_path14.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
7446
7588
  const pkg = JSON.parse(getFileContentSync(pkgPath));
@@ -7545,7 +7687,19 @@ async function ensureModelsAvailable(args) {
7545
7687
  const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
7546
7688
  if (required.length === 0) return [];
7547
7689
  const missing = missingRequiredModels(schema, sageDir);
7548
- if (missing.length === 0) return [...required];
7690
+ logger.debug("Model availability check started", {
7691
+ schema,
7692
+ requiredModels: required,
7693
+ missingModels: missing
7694
+ });
7695
+ if (missing.length === 0) {
7696
+ logger.debug("Model availability check completed", {
7697
+ schema,
7698
+ result: "already_available",
7699
+ installedModels: required
7700
+ });
7701
+ return [...required];
7702
+ }
7549
7703
  const manifest = await fetchModelManifest({
7550
7704
  iid: args.iid,
7551
7705
  schema,
@@ -7555,8 +7709,15 @@ async function ensureModelsAvailable(args) {
7555
7709
  logger
7556
7710
  });
7557
7711
  if (!manifest) {
7558
- logger.debug("model manifest unavailable; skipping background download");
7559
- return required.filter((name) => !missing.includes(name));
7712
+ const installed2 = required.filter((name) => !missing.includes(name));
7713
+ logger.debug("Model availability check completed", {
7714
+ schema,
7715
+ result: "skipped",
7716
+ skippedReason: "manifest_unavailable",
7717
+ installedModels: installed2,
7718
+ missingModels: missing
7719
+ });
7720
+ return installed2;
7560
7721
  }
7561
7722
  const installed = new Set(required.filter((name) => !missing.includes(name)));
7562
7723
  for (const name of missing) {
@@ -7578,7 +7739,212 @@ async function ensureModelsAvailable(args) {
7578
7739
  logger.warn(`download of model '${name}' threw: ${err}`);
7579
7740
  }
7580
7741
  }
7581
- return [...installed];
7742
+ const installedModels = [...installed];
7743
+ logger.debug("Model availability check completed", {
7744
+ schema,
7745
+ result: "checked",
7746
+ installedModels,
7747
+ missingModels: required.filter((name) => !installed.has(name))
7748
+ });
7749
+ return installedModels;
7750
+ }
7751
+
7752
+ // ../core/src/jsonl-log-writer.ts
7753
+ var import_promises5 = require("node:fs/promises");
7754
+ var import_node_path15 = require("node:path");
7755
+ var import_promises6 = require("node:timers/promises");
7756
+ var writeQueues = /* @__PURE__ */ new Map();
7757
+ var ROTATE_LOCK_TIMEOUT_MS = 250;
7758
+ var ROTATE_LOCK_STALE_MS = 3e4;
7759
+ var ROTATE_LOCK_POLL_MS = 50;
7760
+ async function shouldRotate(filePath, maxBytes, maxFiles) {
7761
+ if (maxBytes <= 0 || maxFiles <= 0) return false;
7762
+ try {
7763
+ const s3 = await (0, import_promises5.stat)(filePath);
7764
+ return s3.size >= maxBytes;
7765
+ } catch {
7766
+ return false;
7767
+ }
7768
+ }
7769
+ async function removeStaleRotateLock(lockPath) {
7770
+ try {
7771
+ const s3 = await (0, import_promises5.stat)(lockPath);
7772
+ if (Date.now() - s3.mtimeMs < ROTATE_LOCK_STALE_MS) return;
7773
+ await (0, import_promises5.rmdir)(lockPath);
7774
+ } catch {
7775
+ }
7776
+ }
7777
+ async function acquireRotateLock(filePath) {
7778
+ const lockPath = `${filePath}.lock`;
7779
+ const deadline = Date.now() + ROTATE_LOCK_TIMEOUT_MS;
7780
+ while (true) {
7781
+ try {
7782
+ await (0, import_promises5.mkdir)(lockPath);
7783
+ return async () => {
7784
+ try {
7785
+ await (0, import_promises5.rmdir)(lockPath);
7786
+ } catch {
7787
+ }
7788
+ };
7789
+ } catch {
7790
+ await removeStaleRotateLock(lockPath);
7791
+ const remainingMs = deadline - Date.now();
7792
+ if (remainingMs <= 0) return void 0;
7793
+ await (0, import_promises6.setTimeout)(Math.min(ROTATE_LOCK_POLL_MS, remainingMs));
7794
+ }
7795
+ }
7796
+ }
7797
+ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
7798
+ if (!await shouldRotate(filePath, maxBytes, maxFiles)) return;
7799
+ const releaseLock = await acquireRotateLock(filePath);
7800
+ if (!releaseLock) return;
7801
+ try {
7802
+ if (!await shouldRotate(filePath, maxBytes, maxFiles)) return;
7803
+ try {
7804
+ await (0, import_promises5.unlink)(`${filePath}.${maxFiles}`);
7805
+ } catch {
7806
+ }
7807
+ for (let i = maxFiles - 1; i >= 1; i--) {
7808
+ try {
7809
+ await (0, import_promises5.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
7810
+ } catch {
7811
+ }
7812
+ }
7813
+ try {
7814
+ await (0, import_promises5.rename)(filePath, `${filePath}.1`);
7815
+ } catch {
7816
+ }
7817
+ } finally {
7818
+ await releaseLock();
7819
+ }
7820
+ }
7821
+ async function appendJsonlEntryNow(path, config, entry) {
7822
+ await (0, import_promises5.mkdir)((0, import_node_path15.dirname)(path), { recursive: true });
7823
+ await rotateIfNeeded(path, config.max_bytes, config.max_files);
7824
+ await (0, import_promises5.appendFile)(path, `${JSON.stringify(entry)}
7825
+ `);
7826
+ }
7827
+ async function appendJsonlEntry(config, entry) {
7828
+ const path = resolvePath(config.path);
7829
+ const previousWrite = writeQueues.get(path) ?? Promise.resolve();
7830
+ const nextWrite = previousWrite.catch(() => {
7831
+ }).then(() => appendJsonlEntryNow(path, config, entry));
7832
+ writeQueues.set(path, nextWrite);
7833
+ try {
7834
+ await nextWrite;
7835
+ } finally {
7836
+ if (writeQueues.get(path) === nextWrite) writeQueues.delete(path);
7837
+ }
7838
+ }
7839
+
7840
+ // ../core/src/operational-log.ts
7841
+ var LEVEL_PRIORITY = {
7842
+ debug: 10,
7843
+ info: 20,
7844
+ warn: 30,
7845
+ error: 40
7846
+ };
7847
+ var FLUSH_TIMEOUT_MS = 250;
7848
+ function shouldLog(config, level) {
7849
+ return config.enabled && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[config.level];
7850
+ }
7851
+ function normalizeData(data) {
7852
+ if (!data) return void 0;
7853
+ const seen = /* @__PURE__ */ new WeakSet();
7854
+ try {
7855
+ return JSON.parse(
7856
+ JSON.stringify(data, (_key, value) => {
7857
+ if (typeof value === "bigint") return value.toString();
7858
+ if (value instanceof Error) {
7859
+ return { name: value.name, message: value.message, stack: value.stack };
7860
+ }
7861
+ if (value && typeof value === "object") {
7862
+ if (seen.has(value)) return "[Circular]";
7863
+ seen.add(value);
7864
+ }
7865
+ return value;
7866
+ })
7867
+ );
7868
+ } catch {
7869
+ return { serialization_error: "Failed to serialize log data" };
7870
+ }
7871
+ }
7872
+ async function writeOperationalLogEntry(config, entry) {
7873
+ if (!shouldLog(config, entry.level)) return;
7874
+ try {
7875
+ await appendJsonlEntry(config, entry);
7876
+ } catch {
7877
+ }
7878
+ }
7879
+ function createOperationalLogger(config, runtime) {
7880
+ const pendingWrites = /* @__PURE__ */ new Set();
7881
+ function log(component, level, message, data) {
7882
+ if (!shouldLog(config, level)) return;
7883
+ const entry = {
7884
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7885
+ level,
7886
+ runtime,
7887
+ component,
7888
+ message,
7889
+ ...data ? { data: normalizeData(data) } : {}
7890
+ };
7891
+ const write = writeOperationalLogEntry(config, entry);
7892
+ pendingWrites.add(write);
7893
+ void write.finally(() => pendingWrites.delete(write));
7894
+ }
7895
+ async function flush() {
7896
+ const deadline = Date.now() + FLUSH_TIMEOUT_MS;
7897
+ while (pendingWrites.size > 0) {
7898
+ const remainingMs = deadline - Date.now();
7899
+ if (remainingMs <= 0) return;
7900
+ let timeout;
7901
+ try {
7902
+ const result = await Promise.race([
7903
+ Promise.allSettled([...pendingWrites]).then(() => "drained"),
7904
+ new Promise((resolve4) => {
7905
+ timeout = setTimeout(() => resolve4("timeout"), remainingMs);
7906
+ timeout.unref?.();
7907
+ })
7908
+ ]);
7909
+ if (result === "timeout") return;
7910
+ } finally {
7911
+ if (timeout) clearTimeout(timeout);
7912
+ }
7913
+ }
7914
+ }
7915
+ function forComponent(component) {
7916
+ return {
7917
+ debug(msg, data) {
7918
+ log(component, "debug", msg, data);
7919
+ },
7920
+ info(msg, data) {
7921
+ log(component, "info", msg, data);
7922
+ },
7923
+ warn(msg, data) {
7924
+ log(component, "warn", msg, data);
7925
+ },
7926
+ error(msg, data) {
7927
+ log(component, "error", msg, data);
7928
+ },
7929
+ flush
7930
+ };
7931
+ }
7932
+ return {
7933
+ debug(component, msg, data) {
7934
+ log(component, "debug", msg, data);
7935
+ },
7936
+ info(component, msg, data) {
7937
+ log(component, "info", msg, data);
7938
+ },
7939
+ warn(component, msg, data) {
7940
+ log(component, "warn", msg, data);
7941
+ },
7942
+ error(component, msg, data) {
7943
+ log(component, "error", msg, data);
7944
+ },
7945
+ forComponent,
7946
+ flush
7947
+ };
7582
7948
  }
7583
7949
 
7584
7950
  // ../core/src/model-download-worker.ts
@@ -7591,22 +7957,63 @@ async function readWorkerArgs() {
7591
7957
  const schema = env.SAGE_MODEL_SCHEMA || MODEL_SCHEMA_VERSION;
7592
7958
  return { sageDir, agentRuntime, agentRuntimeVersion, versionApp, schema };
7593
7959
  }
7960
+ async function createWorkerLogger(agentRuntime) {
7961
+ try {
7962
+ const config = await loadConfig();
7963
+ return createOperationalLogger(
7964
+ config.operational_logging,
7965
+ agentRuntime
7966
+ ).forComponent("model-download-worker");
7967
+ } catch {
7968
+ return nullLogger;
7969
+ }
7970
+ }
7594
7971
  async function workerMain() {
7595
7972
  const args = await readWorkerArgs();
7596
7973
  if (!args) return;
7597
- await (0, import_promises5.mkdir)(getModelStorageRoot(args.sageDir), { recursive: true }).catch(() => {
7598
- });
7599
- const iid = await getInstallationId(args.sageDir).catch(() => void 0);
7600
- if (!iid) return;
7601
- await ensureModelsAvailable({
7602
- sageDir: args.sageDir,
7603
- iid,
7604
- agentRuntime: args.agentRuntime,
7605
- agentRuntimeVersion: args.agentRuntimeVersion,
7606
- versionApp: args.versionApp,
7607
- schema: args.schema,
7608
- logger: nullLogger
7609
- });
7974
+ const logger = await createWorkerLogger(args.agentRuntime);
7975
+ try {
7976
+ logger.debug("Model download worker started", {
7977
+ schema: args.schema,
7978
+ agentRuntime: args.agentRuntime,
7979
+ agentRuntimeVersion: args.agentRuntimeVersion,
7980
+ versionApp: args.versionApp
7981
+ });
7982
+ await (0, import_promises7.mkdir)(getModelStorageRoot(args.sageDir), { recursive: true }).catch(() => {
7983
+ });
7984
+ const iid = await getInstallationId(args.sageDir).catch(() => void 0);
7985
+ if (!iid) {
7986
+ logger.debug("Model download worker completed", {
7987
+ result: "skipped",
7988
+ skippedReason: "missing_installation_id"
7989
+ });
7990
+ await logger.flush?.();
7991
+ return;
7992
+ }
7993
+ const installedModels = await ensureModelsAvailable({
7994
+ sageDir: args.sageDir,
7995
+ iid,
7996
+ agentRuntime: args.agentRuntime,
7997
+ agentRuntimeVersion: args.agentRuntimeVersion,
7998
+ versionApp: args.versionApp,
7999
+ schema: args.schema,
8000
+ logger
8001
+ });
8002
+ logger.debug("Model download worker completed", {
8003
+ result: "completed",
8004
+ installedModels
8005
+ });
8006
+ await logger.flush?.();
8007
+ } catch (error) {
8008
+ logger.error("Model download worker failed open", {
8009
+ error: String(error),
8010
+ schema: args.schema,
8011
+ agentRuntime: args.agentRuntime,
8012
+ agentRuntimeVersion: args.agentRuntimeVersion,
8013
+ versionApp: args.versionApp
8014
+ });
8015
+ await logger.flush?.();
8016
+ }
7610
8017
  }
7611
8018
  var isWorkerEntry = (() => {
7612
8019
  try {