@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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +103 -0
- package/dist/index.d.ts +44 -3
- package/dist/index.js +133 -43
- package/package.json +1 -1
- package/src/agent-parser.ts +6 -8
- package/src/harness.ts +81 -18
- package/src/mcp.ts +102 -23
- package/src/storage/engine.ts +2 -0
- package/src/storage/memory-engine.ts +1 -1
- package/src/storage/sql-dialect.ts +1 -1
- package/test/agent-parser.test.ts +18 -0
- package/test/harness-config-injection.test.ts +63 -0
- package/test/harness-injection.test.ts +93 -0
- package/test/mcp-tenant-cache.test.ts +311 -0
- package/test/mcp.test.ts +174 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
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
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m493.42 KB[39m
|
|
12
12
|
[32mESM[39m [1mdist/isolate-VY35DGLM.js [22m[32m49.43 KB[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 238ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 7385ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m77.00 KB[39m
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78
|
+
var validateTimezone = (tz, path) => {
|
|
79
79
|
try {
|
|
80
|
-
|
|
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
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
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
|
-
|
|
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 =
|
|
6424
|
-
|
|
6425
|
-
|
|
6482
|
+
callClient = this.getOrCreateTenantClient(
|
|
6483
|
+
serverName,
|
|
6484
|
+
context.tenantId,
|
|
6426
6485
|
tenantToken,
|
|
6427
|
-
server
|
|
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
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
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
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
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
package/src/agent-parser.ts
CHANGED
|
@@ -91,16 +91,14 @@ const validateCronExpression = (expr: string, path: string): void => {
|
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
const
|
|
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
|
-
|
|
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
|
);
|