@rulvar/core 1.169.0 → 1.170.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.
package/dist/index.d.ts CHANGED
@@ -10839,6 +10839,28 @@ interface McpConfig {
10839
10839
  listMs?: number;
10840
10840
  callMs?: number;
10841
10841
  };
10842
+ /**
10843
+ * streamable-http only (RV1516): headers injected into EVERY wire
10844
+ * request through a wrapped fetch. The hook form is awaited before
10845
+ * each send, so it IS the refresh point: rotate a token in the hook
10846
+ * and the next request carries it, with no reconnect and no
10847
+ * library-invented 401 retry (transport failures surface exactly as
10848
+ * before; the engine's RetryPolicy owns retries).
10849
+ */
10850
+ http?: {
10851
+ headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
10852
+ };
10853
+ /**
10854
+ * What a listChanged notification means for THIS source (RV1516).
10855
+ * 'rekey' is the documented default: the session cache invalidates
10856
+ * and subsequently spawned agents import the changed list under a new
10857
+ * toolsetHash. 'refuse' fails closed instead: the notification
10858
+ * poisons the source, every later tools() call refuses typed, and
10859
+ * only close() (a deliberate host reset) clears it. In-flight spawn
10860
+ * snapshots are untouched either way. Composes with the toolset
10861
+ * attestation: refuse at the source vs refuse at the spawn.
10862
+ */
10863
+ drift?: "rekey" | "refuse";
10842
10864
  }
10843
10865
  /**
10844
10866
  * The ToolSource returned by {@link mcp}: the frozen ToolSource seam
package/dist/index.js CHANGED
@@ -3894,6 +3894,9 @@ function validateBounds(cfg) {
3894
3894
  const value = cfg.timeouts?.[key];
3895
3895
  if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) throw new ConfigError(`mcp: 'timeouts.${key}' must be a positive finite number of milliseconds, got ${String(value)}`);
3896
3896
  }
3897
+ if (cfg.drift !== void 0 && cfg.drift !== "rekey" && cfg.drift !== "refuse") throw new ConfigError(`mcp: 'drift' must be 'rekey' or 'refuse', got '${String(cfg.drift)}'`);
3898
+ const headers = cfg.http?.headers;
3899
+ if (headers !== void 0 && typeof headers !== "function" && typeof headers !== "object") throw new ConfigError("mcp: 'http.headers' must be a record of header values or a (possibly async) function returning one");
3897
3900
  }
3898
3901
  function validateConfig(cfg) {
3899
3902
  validateBounds(cfg);
@@ -3905,6 +3908,7 @@ function validateConfig(cfg) {
3905
3908
  if (cfg.command === void 0) throw new ConfigError("mcp: the stdio transport requires 'command'");
3906
3909
  forbid("url");
3907
3910
  forbid("server");
3911
+ forbid("http");
3908
3912
  return;
3909
3913
  case "streamable-http":
3910
3914
  if (cfg.url === void 0) throw new ConfigError("mcp: the streamable-http transport requires 'url'");
@@ -3917,6 +3921,7 @@ function validateConfig(cfg) {
3917
3921
  forbid("command");
3918
3922
  forbid("args");
3919
3923
  forbid("url");
3924
+ forbid("http");
3920
3925
  return;
3921
3926
  default: throw new ConfigError(`mcp: unknown transport '${String(cfg.transport)}'`);
3922
3927
  }
@@ -3937,6 +3942,23 @@ function mapContent(result) {
3937
3942
  text: block.text ?? ""
3938
3943
  } : block);
3939
3944
  }
3945
+ /**
3946
+ * Wraps fetch so EVERY wire request of the streamable-http transport
3947
+ * consults the declared headers before send (RV1516): the hook form is
3948
+ * the per-request refresh point for rotating tokens, so no reconnect
3949
+ * and no library-invented 401 retry exists or is needed.
3950
+ */
3951
+ function perRequestHeaders(headersOption) {
3952
+ return async (url, init) => {
3953
+ const extra = typeof headersOption === "function" ? await headersOption() : headersOption;
3954
+ const headers = new Headers(init?.headers);
3955
+ for (const [name, value] of Object.entries(extra ?? {})) headers.set(name, value);
3956
+ return fetch(url, {
3957
+ ...init,
3958
+ headers
3959
+ });
3960
+ };
3961
+ }
3940
3962
  function errorText(result) {
3941
3963
  const text = (result.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
3942
3964
  return text === "" ? "MCP tool reported an error" : text;
@@ -3959,6 +3981,7 @@ function mcp(cfg) {
3959
3981
  let cache;
3960
3982
  let generation = 0;
3961
3983
  let inFlight;
3984
+ let poisoned = false;
3962
3985
  const connect = async () => {
3963
3986
  const client = new Client({
3964
3987
  name: "rulvar",
@@ -3972,7 +3995,8 @@ function mcp(cfg) {
3972
3995
  });
3973
3996
  await client.connect(transport);
3974
3997
  } else if (cfg.transport === "streamable-http") {
3975
- const transport = new StreamableHTTPClientTransport(new URL(cfg.url ?? ""));
3998
+ const declaredHeaders = cfg.http?.headers;
3999
+ const transport = new StreamableHTTPClientTransport(new URL(cfg.url ?? ""), declaredHeaders === void 0 ? void 0 : { fetch: perRequestHeaders(declaredHeaders) });
3976
4000
  await client.connect(transport);
3977
4001
  } else {
3978
4002
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
@@ -4003,6 +4027,7 @@ function mcp(cfg) {
4003
4027
  client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
4004
4028
  generation += 1;
4005
4029
  cache = void 0;
4030
+ if (cfg.drift === "refuse") poisoned = true;
4006
4031
  });
4007
4032
  return client;
4008
4033
  };
@@ -4058,6 +4083,7 @@ function mcp(cfg) {
4058
4083
  return {
4059
4084
  id: sourceIdOf(cfg),
4060
4085
  tools: async () => {
4086
+ if (poisoned) throw new ConfigError(`mcp: the tool list of '${sourceIdOf(cfg)}' changed after import (listChanged) and drift policy 'refuse' holds the source closed; close() and re-create the source (and re-record any toolset attestation) to import the changed list deliberately`);
4061
4087
  if (cache !== void 0) return cache;
4062
4088
  if (inFlight !== void 0) return inFlight;
4063
4089
  const fetch = (async () => {
@@ -4084,6 +4110,7 @@ function mcp(cfg) {
4084
4110
  const pending = clientPromise;
4085
4111
  clientPromise = void 0;
4086
4112
  cache = void 0;
4113
+ poisoned = false;
4087
4114
  if (pending === void 0) return;
4088
4115
  let client;
4089
4116
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.169.0",
3
+ "version": "1.170.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",