@poncho-ai/harness 0.40.0 → 0.41.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.40.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.41.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,9 +8,9 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 489.99 KB
11
+ ESM dist/index.js 493.42 KB
12
12
  ESM dist/isolate-VY35DGLM.js 49.43 KB
13
- ESM ⚡️ Build success in 173ms
13
+ ESM ⚡️ Build success in 238ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 7364ms
16
- DTS dist/index.d.ts 75.12 KB
15
+ DTS ⚡️ Build success in 7385ms
16
+ DTS dist/index.d.ts 77.00 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,108 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.41.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#110](https://github.com/cesr/poncho-ai/pull/110) [`7d57a88`](https://github.com/cesr/poncho-ai/commit/7d57a88e55a49ec04de3dbd415b2440bb727e31f) Thanks [@cesr](https://github.com/cesr)! - harness: allow programmatic agent + storage injection (no AGENT.md required)
8
+
9
+ `HarnessOptions` gains two optional fields that let callers construct a
10
+ `Harness` without an `AGENT.md` on disk and without the
11
+ `ensureAgentIdentity` filesystem dance:
12
+ - `agentDefinition?: string | ParsedAgent` — raw markdown or a pre-parsed
13
+ agent. When provided, `initialize()` skips the `AGENT.md` read.
14
+ - `storageEngine?: StorageEngine` — pre-constructed engine; required
15
+ whenever `agentDefinition` is provided. The engine's `agentId` (now a
16
+ public readonly field on the `StorageEngine` interface) becomes the
17
+ source of truth for partitioning, and is mirrored onto
18
+ `parsedAgent.frontmatter.id` so existing downstream readers continue
19
+ to resolve correctly.
20
+
21
+ When neither field is provided, behaviour is unchanged: the harness
22
+ reads `AGENT.md` from `workingDir`, calls `ensureAgentIdentity`, and
23
+ constructs the `StorageEngine` internally.
24
+
25
+ `refreshAgentIfChanged()` short-circuits when an agent definition was
26
+ injected — callers who update an agent re-instantiate the harness
27
+ rather than relying on disk file watching.
28
+
29
+ This is the first of a small set of changes that lets `@poncho-ai/harness`
30
+ be embedded as a library by consumer SaaS apps where each user has
31
+ their own per-tenant agent state in a database, no filesystem layout.
32
+
33
+ - [#111](https://github.com/cesr/poncho-ai/pull/111) [`ac18616`](https://github.com/cesr/poncho-ai/commit/ac18616b864189c91d0957c72c537933497505f4) Thanks [@cesr](https://github.com/cesr)! - harness: allow programmatic `PonchoConfig` injection
34
+
35
+ `HarnessOptions` gains an optional `config?: PonchoConfig` field. When
36
+ provided, `initialize()` skips `loadPonchoConfig` (which imports
37
+ `poncho.config.js` from `workingDir`) and uses the supplied object
38
+ directly. Downstream resolvers (`resolveMemoryConfig`,
39
+ `resolveStateConfig`, etc.) run as today, so any validation/normalization
40
+ they perform applies to injected configs identically.
41
+
42
+ Behaviour is unchanged when the field is absent: the disk loader runs
43
+ as before.
44
+
45
+ This is part of a small series of changes that enables
46
+ `@poncho-ai/harness` to be embedded as a library by a consumer SaaS
47
+ where each user's agent configuration comes from a database row, not a
48
+ `poncho.config.js` on disk.
49
+
50
+ - [#112](https://github.com/cesr/poncho-ai/pull/112) [`c22416b`](https://github.com/cesr/poncho-ai/commit/c22416b3d4c4557277aeabf53e70877be6436e85) Thanks [@cesr](https://github.com/cesr)! - harness: cache MCP clients per `(serverName, tenantId)` instead of rebuilding per call
51
+
52
+ When a tenant resolves a different bearer token than the host's
53
+ `process.env` default for an MCP server, the per-call handler used to
54
+ construct a brand-new `StreamableHttpMcpRpcClient` on every tool call.
55
+ For builders this rarely triggered. For consumer/SaaS deployments where
56
+ **every** call resolves a different per-user token, every tool call
57
+ forced a fresh `initialize` round-trip — no session reuse, high
58
+ latency, and a behaviour the recently-added 404 session-retry can't
59
+ help with because there was nothing to retry.
60
+
61
+ `LocalMcpBridge` now keeps a `Map<key, { client, token, lastUsed }>`
62
+ keyed by `(serverName, tenantId)`. Lookups reuse the cached client when
63
+ the token is unchanged and the entry is within the configured idle TTL
64
+ (default 15 minutes). On token rotation or TTL expiry the entry is
65
+ evicted lazily and rebuilt. `stopLocalServers()` closes all cached
66
+ tenant clients alongside the server-default ones.
67
+
68
+ The TTL is configurable via a constructor option (`tenantClientTtlMs`)
69
+ for tests and tuning.
70
+
71
+ ### Patch Changes
72
+
73
+ - [#109](https://github.com/cesr/poncho-ai/pull/109) [`4b5d974`](https://github.com/cesr/poncho-ai/commit/4b5d974345733ac9e68f36201dff7e7d8a8f0327) Thanks [@cesr](https://github.com/cesr)! - harness: re-initialize MCP session on 404 instead of staying wedged
74
+
75
+ Streamable-HTTP MCP clients with session state (e.g. Arcade's gateway
76
+ for Gmail / Google Calendar) issue an `Mcp-Session-Id` on initialize
77
+ and expire it after some idle window. The bridge cached `sessionId`
78
+ and `initialized` in process memory and never reset them, so once the
79
+ server returned 404 for a stale session every subsequent tool call
80
+ also 404'd until the host process restarted. Long-lived deployments
81
+ (e.g. Railway) hit this; serverless platforms masked it because each
82
+ invocation re-initialized.
83
+
84
+ The client now treats `404` with a stored `sessionId` as a session
85
+ expiry signal: it clears the session, re-runs `initialize`, and
86
+ retries the request once. A 404 from initialize itself (no session
87
+ yet) is still treated as a hard endpoint failure with no retry.
88
+
89
+ ## 0.40.1
90
+
91
+ ### Patch Changes
92
+
93
+ - [`8dec90d`](https://github.com/cesr/poncho-ai/commit/8dec90d4df246b0cc16adc9fae61a568db67cbfe) Thanks [@cesr](https://github.com/cesr)! - fix(harness): accept "UTC" (and "GMT") as valid cron timezones
94
+
95
+ `AGENT.md` cron jobs with `timezone: "UTC"` were rejected at parse time
96
+ with `Invalid timezone at AGENT.md frontmatter cron.<job>: "UTC"`. The
97
+ validator was matching against `Intl.supportedValuesOf("timeZone")`,
98
+ which returns canonical IANA names only (`"Etc/UTC"`) and excludes
99
+ common aliases like `"UTC"` and `"GMT"`, even though `Intl.DateTimeFormat`
100
+ accepts them everywhere. The error message ironically cited `"UTC"`
101
+ itself as a valid example.
102
+
103
+ Now delegates to `Intl.DateTimeFormat` directly, which accepts `"UTC"`,
104
+ `"GMT"`, every IANA name, and any platform alias the runtime knows about.
105
+
3
106
  ## 0.40.0
4
107
 
5
108
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -370,12 +370,26 @@ declare class LocalMcpBridge {
370
370
  private readonly unavailableServers;
371
371
  private readonly authFailedServers;
372
372
  private envResolver?;
373
+ /**
374
+ * Per-tenant MCP client cache. For consumer/SaaS deployments where every
375
+ * call resolves a different bearer token, building a fresh
376
+ * `StreamableHttpMcpRpcClient` per call would force a fresh `initialize`
377
+ * round-trip every time. We keep one client per `(serverName, tenantId)`
378
+ * with TTL-based idle eviction; on token rotation we evict the entry
379
+ * lazily and rebuild.
380
+ */
381
+ private readonly tenantClients;
382
+ /** Test/observability hook: bumped every time a new tenant client is constructed. */
383
+ tenantClientConstructions: number;
384
+ private readonly tenantClientTtlMs;
373
385
  /**
374
386
  * Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
375
387
  * Called by the harness after creating the secrets store.
376
388
  */
377
389
  setEnvResolver(resolver: (tenantId: string | undefined, envName: string) => Promise<string | undefined>): void;
378
- constructor(config: McpConfig | undefined);
390
+ constructor(config: McpConfig | undefined, options?: {
391
+ tenantClientTtlMs?: number;
392
+ });
379
393
  private getServerName;
380
394
  private log;
381
395
  /** Set of servers where discovery was deferred (no default token, has env resolver). */
@@ -389,6 +403,7 @@ declare class LocalMcpBridge {
389
403
  private tryDeferredDiscovery;
390
404
  startLocalServers(): Promise<void>;
391
405
  stopLocalServers(): Promise<void>;
406
+ private getOrCreateTenantClient;
392
407
  listServers(): RemoteMcpServerConfig[];
393
408
  listRemoteServers(): RemoteMcpServerConfig[];
394
409
  checkRemoteConnectivity(): Promise<Array<{
@@ -806,6 +821,8 @@ interface VfsDirEntry {
806
821
  type: "file" | "directory" | "symlink";
807
822
  }
808
823
  interface StorageEngine {
824
+ /** Partition key: every read/write is scoped to this agent id. */
825
+ readonly agentId: string;
809
826
  /** Run migrations and prepare the storage backend. */
810
827
  initialize(): Promise<void>;
811
828
  /** Gracefully release resources. */
@@ -1007,6 +1024,27 @@ interface HarnessOptions {
1007
1024
  toolDefinitions?: ToolDefinition[];
1008
1025
  modelProvider?: ModelProviderFactory;
1009
1026
  uploadStore?: UploadStore;
1027
+ /**
1028
+ * Inject the agent definition directly instead of reading AGENT.md from
1029
+ * `workingDir`. Pass raw markdown (string) or a pre-parsed `ParsedAgent`.
1030
+ * When provided, `storageEngine` is also required — the engine's
1031
+ * `agentId` becomes the source of truth for partitioning, and the
1032
+ * filesystem identity dance (`ensureAgentIdentity`) is skipped.
1033
+ */
1034
+ agentDefinition?: string | ParsedAgent;
1035
+ /**
1036
+ * Pre-constructed storage engine. When provided, the harness will not
1037
+ * create one internally. The engine's `agentId` is used wherever the
1038
+ * harness today reads `parsedAgent.frontmatter.id`.
1039
+ */
1040
+ storageEngine?: StorageEngine;
1041
+ /**
1042
+ * Inject a `PonchoConfig` object directly instead of importing
1043
+ * `poncho.config.js` from `workingDir`. When provided, the disk-based
1044
+ * loader is skipped. Downstream resolvers (`resolveMemoryConfig`,
1045
+ * `resolveStateConfig`, etc.) run as today regardless of source.
1046
+ */
1047
+ config?: PonchoConfig;
1010
1048
  }
1011
1049
  interface HarnessRunOutput {
1012
1050
  runId: string;
@@ -1037,6 +1075,7 @@ declare class AgentHarness {
1037
1075
  reminderStore?: ReminderStore;
1038
1076
  secretsStore?: SecretsStore;
1039
1077
  private loadedConfig?;
1078
+ private readonly injectedConfig?;
1040
1079
  private loadedSkills;
1041
1080
  private skillFingerprint;
1042
1081
  private lastSkillRefreshAt;
@@ -1051,6 +1090,8 @@ declare class AgentHarness {
1051
1090
  private _browserMod?;
1052
1091
  private parsedAgent?;
1053
1092
  private agentFileFingerprint;
1093
+ private injectedAgentDefinition?;
1094
+ private injectedStorageEngine;
1054
1095
  private mcpBridge?;
1055
1096
  private subagentManager?;
1056
1097
  private readonly archivedToolResultsByConversation;
@@ -1279,7 +1320,7 @@ declare class TelemetryEmitter {
1279
1320
  }
1280
1321
 
1281
1322
  declare class InMemoryEngine implements StorageEngine {
1282
- private readonly agentId;
1323
+ readonly agentId: string;
1283
1324
  private convs;
1284
1325
  private mem;
1285
1326
  private todoData;
@@ -1394,7 +1435,7 @@ declare class ConversationEgressMeter {
1394
1435
  }
1395
1436
  declare abstract class SqlStorageEngine implements StorageEngine {
1396
1437
  protected readonly dialect: Dialect;
1397
- protected readonly agentId: string;
1438
+ readonly agentId: string;
1398
1439
  protected abstract readonly executor: QueryExecutor;
1399
1440
  protected readonly egressMeter: ConversationEgressMeter;
1400
1441
  constructor(dialect: Dialect, agentId: string);
package/dist/index.js CHANGED
@@ -75,15 +75,10 @@ var validateCronExpression = (expr, path) => {
75
75
  );
76
76
  }
77
77
  };
78
- var KNOWN_TIMEZONES = (() => {
78
+ var validateTimezone = (tz, path) => {
79
79
  try {
80
- return new Set(Intl.supportedValuesOf("timeZone"));
80
+ new Intl.DateTimeFormat("en", { timeZone: tz });
81
81
  } catch {
82
- return null;
83
- }
84
- })();
85
- var validateTimezone = (tz, path) => {
86
- if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
87
82
  throw new Error(
88
83
  `Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`
89
84
  );
@@ -5916,6 +5911,11 @@ var McpHttpError = class extends Error {
5916
5911
  this.status = status;
5917
5912
  }
5918
5913
  };
5914
+ var McpSessionExpiredError = class extends Error {
5915
+ constructor() {
5916
+ super("MCP session expired");
5917
+ }
5918
+ };
5919
5919
  var StreamableHttpMcpRpcClient = class {
5920
5920
  endpoint;
5921
5921
  timeoutMs;
@@ -5967,6 +5967,9 @@ var StreamableHttpMcpRpcClient = class {
5967
5967
  if (response.status === 403) {
5968
5968
  throw new McpHttpError(403, "MCP server forbidden");
5969
5969
  }
5970
+ if (response.status === 404 && this.sessionId) {
5971
+ throw new McpSessionExpiredError();
5972
+ }
5970
5973
  if (!response.ok) {
5971
5974
  throw new Error(`MCP HTTP request failed with status ${response.status}`);
5972
5975
  }
@@ -6041,20 +6044,32 @@ var StreamableHttpMcpRpcClient = class {
6041
6044
  throw new Error(`MCP response missing JSON-RPC payload for id ${id}`);
6042
6045
  }
6043
6046
  async request(method, params) {
6044
- await this.ensureInitialized();
6045
- const id = this.idCounter++;
6046
- const payload = {
6047
- jsonrpc: "2.0",
6048
- id,
6049
- method,
6050
- params: params ?? {}
6051
- };
6052
- const payloads = await this.postMessage(payload);
6053
- const result = this.extractResult(payloads, id);
6054
- if (result.error) {
6055
- throw new Error(result.error.message ?? `MCP error on ${method}`);
6047
+ for (let attempt = 0; attempt < 2; attempt++) {
6048
+ try {
6049
+ await this.ensureInitialized();
6050
+ const id = this.idCounter++;
6051
+ const payload = {
6052
+ jsonrpc: "2.0",
6053
+ id,
6054
+ method,
6055
+ params: params ?? {}
6056
+ };
6057
+ const payloads = await this.postMessage(payload);
6058
+ const result = this.extractResult(payloads, id);
6059
+ if (result.error) {
6060
+ throw new Error(result.error.message ?? `MCP error on ${method}`);
6061
+ }
6062
+ return result.result;
6063
+ } catch (error) {
6064
+ if (error instanceof McpSessionExpiredError && attempt === 0) {
6065
+ this.sessionId = void 0;
6066
+ this.initialized = false;
6067
+ continue;
6068
+ }
6069
+ throw error;
6070
+ }
6056
6071
  }
6057
- return result.result;
6072
+ throw new Error(`MCP request to ${method} failed after session retry`);
6058
6073
  }
6059
6074
  async ensureInitialized() {
6060
6075
  if (this.initialized) {
@@ -6110,6 +6125,8 @@ var StreamableHttpMcpRpcClient = class {
6110
6125
  }
6111
6126
  }
6112
6127
  };
6128
+ var TENANT_CLIENT_TTL_MS = 15 * 60 * 1e3;
6129
+ var tenantClientKey = (serverName, tenantId) => `${serverName}\0${tenantId}`;
6113
6130
  var LocalMcpBridge = class {
6114
6131
  remoteServers;
6115
6132
  rpcClients = /* @__PURE__ */ new Map();
@@ -6117,6 +6134,18 @@ var LocalMcpBridge = class {
6117
6134
  unavailableServers = /* @__PURE__ */ new Map();
6118
6135
  authFailedServers = /* @__PURE__ */ new Set();
6119
6136
  envResolver;
6137
+ /**
6138
+ * Per-tenant MCP client cache. For consumer/SaaS deployments where every
6139
+ * call resolves a different bearer token, building a fresh
6140
+ * `StreamableHttpMcpRpcClient` per call would force a fresh `initialize`
6141
+ * round-trip every time. We keep one client per `(serverName, tenantId)`
6142
+ * with TTL-based idle eviction; on token rotation we evict the entry
6143
+ * lazily and rebuild.
6144
+ */
6145
+ tenantClients = /* @__PURE__ */ new Map();
6146
+ /** Test/observability hook: bumped every time a new tenant client is constructed. */
6147
+ tenantClientConstructions = 0;
6148
+ tenantClientTtlMs;
6120
6149
  /**
6121
6150
  * Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
6122
6151
  * Called by the harness after creating the secrets store.
@@ -6124,7 +6153,8 @@ var LocalMcpBridge = class {
6124
6153
  setEnvResolver(resolver) {
6125
6154
  this.envResolver = resolver;
6126
6155
  }
6127
- constructor(config) {
6156
+ constructor(config, options) {
6157
+ this.tenantClientTtlMs = options?.tenantClientTtlMs ?? TENANT_CLIENT_TTL_MS;
6128
6158
  this.remoteServers = (config?.mcp ?? []).filter(
6129
6159
  (entry) => typeof entry.url === "string"
6130
6160
  );
@@ -6295,6 +6325,35 @@ var LocalMcpBridge = class {
6295
6325
  await client.close();
6296
6326
  }
6297
6327
  this.rpcClients.clear();
6328
+ for (const [, entry] of this.tenantClients) {
6329
+ await entry.client.close();
6330
+ }
6331
+ this.tenantClients.clear();
6332
+ }
6333
+ getOrCreateTenantClient(serverName, tenantId, token, server) {
6334
+ const key = tenantClientKey(serverName, tenantId);
6335
+ const now2 = Date.now();
6336
+ const existing = this.tenantClients.get(key);
6337
+ if (existing) {
6338
+ const idle = now2 - existing.lastUsed > this.tenantClientTtlMs;
6339
+ const tokenChanged = existing.token !== token;
6340
+ if (idle || tokenChanged) {
6341
+ void existing.client.close();
6342
+ this.tenantClients.delete(key);
6343
+ } else {
6344
+ existing.lastUsed = now2;
6345
+ return existing.client;
6346
+ }
6347
+ }
6348
+ const client = new StreamableHttpMcpRpcClient(
6349
+ server.url,
6350
+ server.timeoutMs ?? 1e4,
6351
+ token,
6352
+ server.headers
6353
+ );
6354
+ this.tenantClients.set(key, { client, token, lastUsed: now2 });
6355
+ this.tenantClientConstructions += 1;
6356
+ return client;
6298
6357
  }
6299
6358
  listServers() {
6300
6359
  return [...this.remoteServers];
@@ -6416,15 +6475,15 @@ var LocalMcpBridge = class {
6416
6475
  try {
6417
6476
  const tokenEnv = server?.auth?.tokenEnv;
6418
6477
  let callClient = client;
6419
- if (tokenEnv && this.envResolver && context?.tenantId) {
6478
+ if (tokenEnv && this.envResolver && context?.tenantId && server) {
6420
6479
  const tenantToken = await this.envResolver(context.tenantId, tokenEnv);
6421
6480
  const defaultToken = process.env[tokenEnv];
6422
6481
  if (tenantToken && tenantToken !== defaultToken) {
6423
- callClient = new StreamableHttpMcpRpcClient(
6424
- server.url,
6425
- server.timeoutMs ?? 1e4,
6482
+ callClient = this.getOrCreateTenantClient(
6483
+ serverName,
6484
+ context.tenantId,
6426
6485
  tenantToken,
6427
- server.headers
6486
+ server
6428
6487
  );
6429
6488
  }
6430
6489
  }
@@ -8645,6 +8704,7 @@ var AgentHarness = class _AgentHarness {
8645
8704
  reminderStore;
8646
8705
  secretsStore;
8647
8706
  loadedConfig;
8707
+ injectedConfig;
8648
8708
  loadedSkills = [];
8649
8709
  skillFingerprint = "";
8650
8710
  lastSkillRefreshAt = 0;
@@ -8659,6 +8719,8 @@ var AgentHarness = class _AgentHarness {
8659
8719
  _browserMod;
8660
8720
  parsedAgent;
8661
8721
  agentFileFingerprint = "";
8722
+ injectedAgentDefinition;
8723
+ injectedStorageEngine = false;
8662
8724
  mcpBridge;
8663
8725
  subagentManager;
8664
8726
  archivedToolResultsByConversation = /* @__PURE__ */ new Map();
@@ -8826,6 +8888,17 @@ var AgentHarness = class _AgentHarness {
8826
8888
  this.modelProviderInjected = !!options.modelProvider;
8827
8889
  this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
8828
8890
  this.uploadStore = options.uploadStore;
8891
+ this.injectedConfig = options.config;
8892
+ if (options.agentDefinition !== void 0 && options.storageEngine === void 0) {
8893
+ throw new Error(
8894
+ "HarnessOptions.agentDefinition requires HarnessOptions.storageEngine \u2014 construct a StorageEngine with the desired agentId and pass both."
8895
+ );
8896
+ }
8897
+ this.injectedAgentDefinition = options.agentDefinition;
8898
+ if (options.storageEngine) {
8899
+ this.storageEngine = options.storageEngine;
8900
+ this.injectedStorageEngine = true;
8901
+ }
8829
8902
  if (options.toolDefinitions?.length) {
8830
8903
  this.dispatcher.registerMany(options.toolDefinitions);
8831
8904
  }
@@ -9242,6 +9315,9 @@ var AgentHarness = class _AgentHarness {
9242
9315
  if (this.environment !== "development") {
9243
9316
  return false;
9244
9317
  }
9318
+ if (this.injectedAgentDefinition !== void 0) {
9319
+ return false;
9320
+ }
9245
9321
  try {
9246
9322
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9247
9323
  const rawContent = await readFile8(agentFilePath, "utf8");
@@ -9309,15 +9385,23 @@ var AgentHarness = class _AgentHarness {
9309
9385
  }
9310
9386
  }
9311
9387
  async initialize() {
9312
- const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9313
- const agentRawContent = await readFile8(agentFilePath, "utf8");
9314
- this.parsedAgent = parseAgentMarkdown(agentRawContent);
9315
- this.agentFileFingerprint = agentRawContent;
9316
- const identity = await ensureAgentIdentity(this.workingDir);
9317
- if (!this.parsedAgent.frontmatter.id) {
9318
- this.parsedAgent.frontmatter.id = identity.id;
9388
+ if (this.injectedAgentDefinition !== void 0) {
9389
+ this.parsedAgent = typeof this.injectedAgentDefinition === "string" ? parseAgentMarkdown(this.injectedAgentDefinition) : this.injectedAgentDefinition;
9390
+ this.agentFileFingerprint = "";
9391
+ if (this.storageEngine) {
9392
+ this.parsedAgent.frontmatter.id = this.storageEngine.agentId;
9393
+ }
9394
+ } else {
9395
+ const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9396
+ const agentRawContent = await readFile8(agentFilePath, "utf8");
9397
+ this.parsedAgent = parseAgentMarkdown(agentRawContent);
9398
+ this.agentFileFingerprint = agentRawContent;
9399
+ const identity = await ensureAgentIdentity(this.workingDir);
9400
+ if (!this.parsedAgent.frontmatter.id) {
9401
+ this.parsedAgent.frontmatter.id = identity.id;
9402
+ }
9319
9403
  }
9320
- const config = await loadPonchoConfig(this.workingDir);
9404
+ const config = this.injectedConfig ?? await loadPonchoConfig(this.workingDir);
9321
9405
  this.loadedConfig = config;
9322
9406
  this.registerConfiguredBuiltInTools(config);
9323
9407
  const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
@@ -9333,15 +9417,21 @@ var AgentHarness = class _AgentHarness {
9333
9417
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
9334
9418
  this.registerSkillTools();
9335
9419
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
9336
- const storageProvider = config?.storage?.provider ?? "sqlite";
9337
- const engine = createStorageEngine({
9338
- provider: storageProvider,
9339
- workingDir: this.workingDir,
9340
- agentId,
9341
- urlEnv: config?.storage?.urlEnv
9342
- });
9343
- await engine.initialize();
9344
- this.storageEngine = engine;
9420
+ let engine;
9421
+ if (this.injectedStorageEngine && this.storageEngine) {
9422
+ engine = this.storageEngine;
9423
+ await engine.initialize();
9424
+ } else {
9425
+ const storageProvider = config?.storage?.provider ?? "sqlite";
9426
+ engine = createStorageEngine({
9427
+ provider: storageProvider,
9428
+ workingDir: this.workingDir,
9429
+ agentId,
9430
+ urlEnv: config?.storage?.urlEnv
9431
+ });
9432
+ await engine.initialize();
9433
+ this.storageEngine = engine;
9434
+ }
9345
9435
  const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024;
9346
9436
  const maxTotalStorage = config?.storage?.limits?.maxTotalStorage ?? 1024 * 1024 * 1024;
9347
9437
  const bashWorkingDir = this.environment === "production" ? null : this.workingDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -91,16 +91,14 @@ const validateCronExpression = (expr: string, path: string): void => {
91
91
  }
92
92
  };
93
93
 
94
- const KNOWN_TIMEZONES: Set<string> | null = (() => {
94
+ const validateTimezone = (tz: string, path: string): void => {
95
+ // Delegate to the runtime's Intl resolver rather than checking against
96
+ // `Intl.supportedValuesOf("timeZone")`. The latter returns only canonical
97
+ // IANA names (e.g. "Etc/UTC") and rejects common aliases like "UTC" and
98
+ // "GMT", even though `Intl.DateTimeFormat` accepts them everywhere.
95
99
  try {
96
- return new Set(Intl.supportedValuesOf("timeZone"));
100
+ new Intl.DateTimeFormat("en", { timeZone: tz });
97
101
  } catch {
98
- return null;
99
- }
100
- })();
101
-
102
- const validateTimezone = (tz: string, path: string): void => {
103
- if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
104
102
  throw new Error(
105
103
  `Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`,
106
104
  );