runline 0.27.0 → 0.27.2

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.
@@ -46,6 +46,13 @@ export interface EngineHooks {
46
46
  * ignores resourceLimits, so the watchdog is the only memory backstop.
47
47
  */
48
48
  export declare function shouldArmRssWatchdog(versions?: Partial<Record<string, string>>): boolean;
49
+ /**
50
+ * Ceiling on an armed timeout: setTimeout stores its delay in an int32,
51
+ * so past ~24.8 days it overflows and fires at 0ms. Oversized values are
52
+ * rejected with an error that teaches the caller to pass 0 — the explicit
53
+ * "no timeout" — instead of overflowing into an instant kill.
54
+ */
55
+ export declare const MAX_TIMEOUT_MS: number;
49
56
  /**
50
57
  * Executes agent code in a node:worker_threads worker.
51
58
  *
@@ -48,6 +48,13 @@ function workerShape(pluginNames, helpData, memoryLimitMb) {
48
48
  export function shouldArmRssWatchdog(versions = process.versions) {
49
49
  return Boolean(versions.bun);
50
50
  }
51
+ /**
52
+ * Ceiling on an armed timeout: setTimeout stores its delay in an int32,
53
+ * so past ~24.8 days it overflows and fires at 0ms. Oversized values are
54
+ * rejected with an error that teaches the caller to pass 0 — the explicit
55
+ * "no timeout" — instead of overflowing into an instant kill.
56
+ */
57
+ export const MAX_TIMEOUT_MS = 2 ** 31 - 1;
51
58
  // Extra slack on top of the configured memory limit for the RSS watchdog,
52
59
  // since whole-process RSS includes the host's own working set.
53
60
  const RSS_WATCHDOG_SLACK_BYTES = 128 * 1024 * 1024;
@@ -149,6 +156,16 @@ export class ExecutionEngine {
149
156
  const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
150
157
  const memoryLimitBytes = options?.memoryLimitBytes ?? this.config.memoryLimitBytes;
151
158
  const logs = [];
159
+ if (!Number.isFinite(timeoutMs) ||
160
+ timeoutMs < 0 ||
161
+ timeoutMs > MAX_TIMEOUT_MS) {
162
+ return {
163
+ result: null,
164
+ error: `Invalid timeoutMs: ${timeoutMs}. ` +
165
+ `Pass 0 (no timeout) or 1..${MAX_TIMEOUT_MS} ms.`,
166
+ logs,
167
+ };
168
+ }
152
169
  const plugins = this.registry.listPlugins();
153
170
  const pluginNames = plugins.map((p) => p.name);
154
171
  const helpData = buildHelpData(plugins);
@@ -190,21 +207,28 @@ export class ExecutionEngine {
190
207
  settled = true;
191
208
  clearTimeout(timeoutTimer);
192
209
  clearInterval(rssTimer);
210
+ worker.unref?.();
193
211
  worker.off("message", onMessage);
194
212
  worker.off("error", onError);
195
213
  worker.off("exit", onExit);
196
214
  this.release(entry, pooled, retire);
197
215
  resolve(r);
198
216
  };
217
+ // The acquire-time unref must not let the host exit mid-run: hold
218
+ // the loop for the run's duration, whether or not a timer is armed.
219
+ worker.ref?.();
199
220
  // A spinning body blocks the worker's event loop, so it can never be
200
221
  // asked to stop — terminate is the only remedy and the worker is gone.
201
- const timeoutTimer = setTimeout(() => {
202
- finish({
203
- result: null,
204
- error: `Execution timed out after ${timeoutMs}ms`,
205
- logs,
206
- }, true);
207
- }, timeoutMs);
222
+ // 0 means no timeout: the caller explicitly opted out of the backstop.
223
+ const timeoutTimer = timeoutMs === 0
224
+ ? undefined
225
+ : setTimeout(() => {
226
+ finish({
227
+ result: null,
228
+ error: `Execution timed out after ${timeoutMs}ms`,
229
+ logs,
230
+ }, true);
231
+ }, timeoutMs);
208
232
  let rssTimer;
209
233
  if (shouldArmRssWatchdog()) {
210
234
  const baselineRss = process.memoryUsage().rss;
@@ -435,6 +459,12 @@ function formatError(cause) {
435
459
  // code gets a cwd-relative `require`, so a bare specifier would resolve
436
460
  // against the caller's cwd instead of runline's own node_modules.
437
461
  const ferrosearchPath = createRequire(import.meta.url).resolve("@shift-labs/ferrosearch");
462
+ // typebox ships ESM only; require() of an .mjs works everywhere runline
463
+ // runs (bun, and node since 22.12's require(esm)). Resolved host-side
464
+ // for the same reason as ferrosearch: eval'd worker code gets a
465
+ // cwd-relative require, so a bare specifier would resolve against the
466
+ // caller's cwd instead of runline's own node_modules.
467
+ const typeboxValuePath = createRequire(import.meta.url).resolve("typebox/value");
438
468
  function buildHelpData(plugins) {
439
469
  const data = {};
440
470
  for (const p of plugins) {
@@ -443,6 +473,7 @@ function buildHelpData(plugins) {
443
473
  access: a.access,
444
474
  description: a.description,
445
475
  inputs: helpInputs(a.inputSchema),
476
+ ...(isTypedInputSchema(a.inputSchema) ? { schema: a.inputSchema } : {}),
446
477
  }));
447
478
  }
448
479
  return data;
@@ -570,6 +601,9 @@ const __toJson = (v) => v === undefined ? undefined : JSON.parse(JSON.stringify(
570
601
  // also means an index built by agent code is invisible to that limit.
571
602
  const { FerroSearch } = require(${JSON.stringify(ferrosearchPath)});
572
603
 
604
+ // The host's own validator, for actions.check on typed schemas.
605
+ const { Check: __schemaCheck, Errors: __schemaErrors } = require(${JSON.stringify(typeboxValuePath)});
606
+
573
607
  const __help = ${JSON.stringify(helpData)};
574
608
 
575
609
  const __makeProxy = (path = []) => new Proxy(() => undefined, {
@@ -629,6 +663,82 @@ const __search = (() => {
629
663
  return index;
630
664
  })();
631
665
 
666
+ // instancePath "/members/0/externalId" → ["members", "0", "externalId"],
667
+ // with JSON-pointer escapes (~1 for /, ~0 for ~) undone per part.
668
+ const __pointerParts = (instancePath) =>
669
+ String(instancePath || '').split('/').filter((p) => p.length)
670
+ .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
671
+
672
+ // ["members", "0", "externalId"] → "members[0].externalId"
673
+ const __fieldPath = (instancePath, prop) => {
674
+ const parts = __pointerParts(instancePath);
675
+ if (prop !== undefined) parts.push(String(prop));
676
+ let out = '';
677
+ for (const part of parts) {
678
+ if (/^\\d+$/.test(part)) out += '[' + part + ']';
679
+ else out += out ? '.' + part : part;
680
+ }
681
+ return out;
682
+ };
683
+
684
+ const __valueAt = (root, instancePath) => {
685
+ let v = root;
686
+ for (const part of __pointerParts(instancePath)) {
687
+ if (v == null) return undefined;
688
+ v = v[part];
689
+ }
690
+ return v;
691
+ };
692
+
693
+ const __valueType = (v) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v;
694
+
695
+ // Deep validation for typed schemas (SHFT-1187): the same Check/Errors
696
+ // the host runs before execute, on the same schema, so the preflight
697
+ // verdict and the execution verdict cannot diverge. The familiar
698
+ // missing/unknown/typeErrors buckets are derived from the validator's
699
+ // structured errors — keyword and params, never message parsing — and
700
+ // the errors array carries each raw "<path> <message>" line exactly as
701
+ // a rejected execution would report it. Grouping type errors by instance
702
+ // path keeps a failed union one typeError instead of one per branch.
703
+ const __checkTyped = (entry, provided, signature) => {
704
+ if (__schemaCheck(entry.schema, provided)) {
705
+ return { ok: true, missing: [], unknown: [], typeErrors: [], errors: [], signature };
706
+ }
707
+ const missing = [];
708
+ const unknown = [];
709
+ const errors = [];
710
+ const byPath = new Map();
711
+ for (const err of __schemaErrors(entry.schema, provided)) {
712
+ errors.push((err.instancePath || '/') + ' ' + err.message);
713
+ const params = err.params || {};
714
+ if (err.keyword === 'required') {
715
+ for (const prop of params.requiredProperties || []) missing.push(__fieldPath(err.instancePath, prop));
716
+ } else if (err.keyword === 'additionalProperties') {
717
+ for (const prop of params.additionalProperties || []) unknown.push(__fieldPath(err.instancePath, prop));
718
+ } else {
719
+ const group = byPath.get(err.instancePath) || [];
720
+ group.push(err);
721
+ byPath.set(err.instancePath, group);
722
+ }
723
+ }
724
+ const typeErrors = [];
725
+ for (const [instancePath, group] of byPath) {
726
+ const field = __fieldPath(instancePath) || '(input)';
727
+ const value = __valueAt(provided, instancePath);
728
+ const consts = group.filter((e) => e.keyword === 'const' && 'allowedValue' in (e.params || {}));
729
+ const typeErr = group.find((e) => e.keyword === 'type' && (e.params || {}).type);
730
+ if (consts.length) {
731
+ // A union of literals: the branches ARE the allowed values.
732
+ typeErrors.push({ field, expected: consts.map((e) => String(e.params.allowedValue)).join(' | '), actual: __fmt(value) });
733
+ } else if (typeErr) {
734
+ typeErrors.push({ field, expected: String(typeErr.params.type), actual: __valueType(value) });
735
+ } else {
736
+ typeErrors.push({ field, expected: group[0].message, actual: __fmt(value) });
737
+ }
738
+ }
739
+ return { ok: false, missing, unknown, typeErrors, errors, signature };
740
+ };
741
+
632
742
  const __actionsApi = {
633
743
  list(plugin) {
634
744
  const paths = Object.keys(__index);
@@ -667,6 +777,13 @@ const __actionsApi = {
667
777
  const near = __actionsApi.find(path, 3).map((n) => n.path);
668
778
  return { ok: false, error: 'Unknown action: ' + path, suggestions: near };
669
779
  }
780
+ const signature = __formatSignature(hit.plugin, hit.entry);
781
+ if (hit.entry.schema) {
782
+ // Mirror the host's normalizeActionInput: a check with no args is
783
+ // a call with an empty argument set; anything else — null, a
784
+ // scalar — is validated as given, exactly as execute would.
785
+ return __checkTyped(hit.entry, args === undefined ? {} : args, signature);
786
+ }
670
787
  const inputs = hit.entry.inputs || {};
671
788
  const provided = args && typeof args === 'object' ? args : {};
672
789
  const missing = [];
@@ -695,7 +812,8 @@ const __actionsApi = {
695
812
  missing,
696
813
  unknown,
697
814
  typeErrors,
698
- signature: __formatSignature(hit.plugin, hit.entry),
815
+ errors: [],
816
+ signature,
699
817
  };
700
818
  },
701
819
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "description": "Code mode for agents \u2014 turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",