@poncho-ai/harness 0.33.0 → 0.34.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 +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +64 -16
- package/package.json +1 -1
- package/src/harness.ts +67 -17
- package/src/telemetry.ts +24 -4
- package/test/harness.test.ts +354 -0
- package/test/telemetry.test.ts +39 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.34.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,8 +8,8 @@
|
|
|
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[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m336.31 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 164ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m33.
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 7760ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m33.99 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`3f096f2`](https://github.com/cesr/poncho-ai/commit/3f096f28b9ab797b52f1b725778976929156cce9) Thanks [@cesr](https://github.com/cesr)! - fix: scope MCP tools to skills via server-level claiming
|
|
8
|
+
|
|
9
|
+
MCP tools from configured servers are now globally available by default. When a skill claims any tool from a server via `allowed-tools`, the entire server becomes skill-managed — its tools are only available when the claiming skill is active (or declared in AGENT.md `allowed-tools`).
|
|
10
|
+
|
|
11
|
+
## 0.33.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`d8fe87c`](https://github.com/cesr/poncho-ai/commit/d8fe87c68d42878829422750f98e3c70a425e3e3) Thanks [@cesr](https://github.com/cesr)! - fix: OTLP trace exporter reliability and error visibility
|
|
16
|
+
- Use provider instance directly instead of global `trace.getTracer()` to avoid silent failure when another library registers a tracer provider first
|
|
17
|
+
- Append `/v1/traces` to base OTLP endpoints so users can pass either the base URL or the full signal-specific URL
|
|
18
|
+
- Surface HTTP status code and response body on export failures
|
|
19
|
+
- Enable OTel diagnostic logger at WARN level for internal SDK errors
|
|
20
|
+
|
|
3
21
|
## 0.33.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -759,6 +759,14 @@ declare class AgentHarness {
|
|
|
759
759
|
private getAgentScriptIntent;
|
|
760
760
|
private getAgentMcpApprovalPatterns;
|
|
761
761
|
private getAgentScriptApprovalPatterns;
|
|
762
|
+
/**
|
|
763
|
+
* Return the set of MCP server names that have at least one tool claimed by
|
|
764
|
+
* any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
|
|
765
|
+
* server, the entire server is considered "skill-managed" — none of its tools
|
|
766
|
+
* are auto-exposed globally; only explicitly declared tools become available
|
|
767
|
+
* (via agent-level allowed-tools or active skill allowed-tools).
|
|
768
|
+
*/
|
|
769
|
+
private getSkillManagedMcpServers;
|
|
762
770
|
private getRequestedMcpPatterns;
|
|
763
771
|
private getRequestedScriptPatterns;
|
|
764
772
|
private getRequestedMcpApprovalPatterns;
|
package/dist/index.js
CHANGED
|
@@ -882,6 +882,7 @@ Connect your Poncho agent to messaging platforms so it responds to @mentions.
|
|
|
882
882
|
1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app "From scratch"
|
|
883
883
|
2. Under **OAuth & Permissions**, add these Bot Token Scopes:
|
|
884
884
|
- \`app_mentions:read\`
|
|
885
|
+
- \`channels:history\` (needed to fetch thread context when mentioned in a reply)
|
|
885
886
|
- \`chat:write\`
|
|
886
887
|
- \`reactions:write\`
|
|
887
888
|
3. Under **Event Subscriptions**, enable events:
|
|
@@ -5343,7 +5344,7 @@ var createSubagentTools = (manager) => [
|
|
|
5343
5344
|
];
|
|
5344
5345
|
|
|
5345
5346
|
// src/harness.ts
|
|
5346
|
-
import { trace, context as otelContext, SpanStatusCode, SpanKind } from "@opentelemetry/api";
|
|
5347
|
+
import { trace, context as otelContext, SpanStatusCode, SpanKind, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
5347
5348
|
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
5348
5349
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
5349
5350
|
|
|
@@ -5357,10 +5358,15 @@ function sanitizeEventForLog(event) {
|
|
|
5357
5358
|
return value;
|
|
5358
5359
|
});
|
|
5359
5360
|
}
|
|
5361
|
+
var TRACES_PATH = "/v1/traces";
|
|
5362
|
+
function ensureTracesPath(url) {
|
|
5363
|
+
if (url.endsWith(TRACES_PATH)) return url;
|
|
5364
|
+
return url.replace(/\/+$/, "") + TRACES_PATH;
|
|
5365
|
+
}
|
|
5360
5366
|
function normalizeOtlp(opt) {
|
|
5361
5367
|
if (!opt) return void 0;
|
|
5362
|
-
if (typeof opt === "string") return opt ? { url: opt } : void 0;
|
|
5363
|
-
return opt.url ? opt : void 0;
|
|
5368
|
+
if (typeof opt === "string") return opt ? { url: ensureTracesPath(opt) } : void 0;
|
|
5369
|
+
return opt.url ? { ...opt, url: ensureTracesPath(opt.url) } : void 0;
|
|
5364
5370
|
}
|
|
5365
5371
|
var TelemetryEmitter = class {
|
|
5366
5372
|
config;
|
|
@@ -5414,7 +5420,10 @@ var TelemetryEmitter = class {
|
|
|
5414
5420
|
]
|
|
5415
5421
|
})
|
|
5416
5422
|
});
|
|
5417
|
-
} catch {
|
|
5423
|
+
} catch (err) {
|
|
5424
|
+
console.warn(
|
|
5425
|
+
`[poncho][telemetry] OTLP log delivery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5426
|
+
);
|
|
5418
5427
|
}
|
|
5419
5428
|
}
|
|
5420
5429
|
};
|
|
@@ -5507,6 +5516,16 @@ var ToolDispatcher = class {
|
|
|
5507
5516
|
};
|
|
5508
5517
|
|
|
5509
5518
|
// src/harness.ts
|
|
5519
|
+
function formatOtlpError(err) {
|
|
5520
|
+
if (!(err instanceof Error)) return String(err);
|
|
5521
|
+
const parts = [];
|
|
5522
|
+
const code = err.code;
|
|
5523
|
+
if (code != null) parts.push(`HTTP ${code}`);
|
|
5524
|
+
if (err.message) parts.push(err.message);
|
|
5525
|
+
const data = err.data;
|
|
5526
|
+
if (data) parts.push(data);
|
|
5527
|
+
return parts.join(" \u2014 ") || "unknown error";
|
|
5528
|
+
}
|
|
5510
5529
|
var now = () => Date.now();
|
|
5511
5530
|
var FIRST_CHUNK_TIMEOUT_MS = 9e4;
|
|
5512
5531
|
var MAX_TRANSIENT_STEP_RETRIES = 1;
|
|
@@ -6283,6 +6302,25 @@ var AgentHarness = class _AgentHarness {
|
|
|
6283
6302
|
getAgentScriptApprovalPatterns() {
|
|
6284
6303
|
return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
|
|
6285
6304
|
}
|
|
6305
|
+
/**
|
|
6306
|
+
* Return the set of MCP server names that have at least one tool claimed by
|
|
6307
|
+
* any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
|
|
6308
|
+
* server, the entire server is considered "skill-managed" — none of its tools
|
|
6309
|
+
* are auto-exposed globally; only explicitly declared tools become available
|
|
6310
|
+
* (via agent-level allowed-tools or active skill allowed-tools).
|
|
6311
|
+
*/
|
|
6312
|
+
getSkillManagedMcpServers() {
|
|
6313
|
+
const servers = /* @__PURE__ */ new Set();
|
|
6314
|
+
for (const skill of this.loadedSkills) {
|
|
6315
|
+
for (const pattern of skill.allowedTools.mcp) {
|
|
6316
|
+
const slash = pattern.indexOf("/");
|
|
6317
|
+
if (slash > 0) {
|
|
6318
|
+
servers.add(pattern.slice(0, slash));
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
return servers;
|
|
6323
|
+
}
|
|
6286
6324
|
getRequestedMcpPatterns() {
|
|
6287
6325
|
const patterns = new Set(this.getAgentMcpIntent());
|
|
6288
6326
|
for (const skillName of this.activeSkillNames) {
|
|
@@ -6294,6 +6332,17 @@ var AgentHarness = class _AgentHarness {
|
|
|
6294
6332
|
patterns.add(pattern);
|
|
6295
6333
|
}
|
|
6296
6334
|
}
|
|
6335
|
+
if (this.mcpBridge) {
|
|
6336
|
+
const managedServers = this.getSkillManagedMcpServers();
|
|
6337
|
+
const discoveredTools = this.mcpBridge.listDiscoveredTools();
|
|
6338
|
+
for (const toolName of discoveredTools) {
|
|
6339
|
+
const slash = toolName.indexOf("/");
|
|
6340
|
+
const serverName = slash > 0 ? toolName.slice(0, slash) : toolName;
|
|
6341
|
+
if (!managedServers.has(serverName)) {
|
|
6342
|
+
patterns.add(toolName);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6297
6346
|
return [...patterns];
|
|
6298
6347
|
}
|
|
6299
6348
|
getRequestedScriptPatterns() {
|
|
@@ -6603,6 +6652,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
6603
6652
|
const telemetryEnabled = config?.telemetry?.enabled !== false;
|
|
6604
6653
|
const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : void 0;
|
|
6605
6654
|
if (otlpConfig) {
|
|
6655
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
|
|
6606
6656
|
const exporter = new OTLPTraceExporter({
|
|
6607
6657
|
url: otlpConfig.url,
|
|
6608
6658
|
headers: otlpConfig.headers
|
|
@@ -6615,7 +6665,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
6615
6665
|
provider2.register();
|
|
6616
6666
|
this.otlpTracerProvider = provider2;
|
|
6617
6667
|
this.hasOtlpExporter = true;
|
|
6618
|
-
console.info(`[poncho][telemetry] OTLP exporter active \u2192 ${otlpConfig.url}`);
|
|
6668
|
+
console.info(`[poncho][telemetry] OTLP trace exporter active \u2192 ${otlpConfig.url}`);
|
|
6619
6669
|
}
|
|
6620
6670
|
}
|
|
6621
6671
|
async buildBrowserStoragePersistence(config, sessionId) {
|
|
@@ -6761,17 +6811,13 @@ var AgentHarness = class _AgentHarness {
|
|
|
6761
6811
|
await this.mcpBridge?.stopLocalServers();
|
|
6762
6812
|
if (this.otlpSpanProcessor) {
|
|
6763
6813
|
await this.otlpSpanProcessor.shutdown().catch((err) => {
|
|
6764
|
-
console.warn(
|
|
6765
|
-
`[poncho][telemetry] OTLP span processor shutdown error: ${err instanceof Error ? err.message : String(err)}`
|
|
6766
|
-
);
|
|
6814
|
+
console.warn(`[poncho][telemetry] OTLP span processor shutdown error: ${formatOtlpError(err)}`);
|
|
6767
6815
|
});
|
|
6768
6816
|
this.otlpSpanProcessor = void 0;
|
|
6769
6817
|
}
|
|
6770
6818
|
if (this.otlpTracerProvider) {
|
|
6771
6819
|
await this.otlpTracerProvider.shutdown().catch((err) => {
|
|
6772
|
-
console.warn(
|
|
6773
|
-
`[poncho][telemetry] OTLP tracer provider shutdown error: ${err instanceof Error ? err.message : String(err)}`
|
|
6774
|
-
);
|
|
6820
|
+
console.warn(`[poncho][telemetry] OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
|
|
6775
6821
|
});
|
|
6776
6822
|
this.otlpTracerProvider = void 0;
|
|
6777
6823
|
}
|
|
@@ -6785,8 +6831,8 @@ var AgentHarness = class _AgentHarness {
|
|
|
6785
6831
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
6786
6832
|
*/
|
|
6787
6833
|
async *runWithTelemetry(input) {
|
|
6788
|
-
if (this.hasOtlpExporter) {
|
|
6789
|
-
const tracer =
|
|
6834
|
+
if (this.hasOtlpExporter && this.otlpTracerProvider) {
|
|
6835
|
+
const tracer = this.otlpTracerProvider.getTracer("gen_ai");
|
|
6790
6836
|
const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
|
|
6791
6837
|
const rootSpan = tracer.startSpan(`invoke_agent ${agentName}`, {
|
|
6792
6838
|
kind: SpanKind.INTERNAL,
|
|
@@ -6815,7 +6861,9 @@ var AgentHarness = class _AgentHarness {
|
|
|
6815
6861
|
rootSpan.end();
|
|
6816
6862
|
try {
|
|
6817
6863
|
await this.otlpSpanProcessor?.forceFlush();
|
|
6818
|
-
} catch {
|
|
6864
|
+
} catch (err) {
|
|
6865
|
+
const detail = formatOtlpError(err);
|
|
6866
|
+
console.warn(`[poncho][telemetry] OTLP span flush failed: ${detail}`);
|
|
6819
6867
|
}
|
|
6820
6868
|
}
|
|
6821
6869
|
} else {
|
|
@@ -7684,8 +7732,8 @@ ${textContent}` };
|
|
|
7684
7732
|
return;
|
|
7685
7733
|
}
|
|
7686
7734
|
const toolSpans = /* @__PURE__ */ new Map();
|
|
7687
|
-
if (this.hasOtlpExporter) {
|
|
7688
|
-
const tracer =
|
|
7735
|
+
if (this.hasOtlpExporter && this.otlpTracerProvider) {
|
|
7736
|
+
const tracer = this.otlpTracerProvider.getTracer("gen_ai");
|
|
7689
7737
|
for (const call of approvedCalls) {
|
|
7690
7738
|
const toolDef = this.dispatcher.get(call.name);
|
|
7691
7739
|
toolSpans.set(call.id, tracer.startSpan(`execute_tool ${call.name}`, {
|
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -37,10 +37,22 @@ import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
|
|
|
37
37
|
import { createSearchTools } from "./search-tools.js";
|
|
38
38
|
import { createSubagentTools } from "./subagent-tools.js";
|
|
39
39
|
import type { SubagentManager } from "./subagent-manager.js";
|
|
40
|
-
import { trace, context as otelContext, SpanStatusCode, SpanKind } from "@opentelemetry/api";
|
|
40
|
+
import { trace, context as otelContext, SpanStatusCode, SpanKind, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
41
41
|
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
42
42
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
43
43
|
import { normalizeOtlp } from "./telemetry.js";
|
|
44
|
+
|
|
45
|
+
/** Extract useful details from OTLPExporterError (has .code + .data) or plain Error. */
|
|
46
|
+
function formatOtlpError(err: unknown): string {
|
|
47
|
+
if (!(err instanceof Error)) return String(err);
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
const code = (err as { code?: number }).code;
|
|
50
|
+
if (code != null) parts.push(`HTTP ${code}`);
|
|
51
|
+
if (err.message) parts.push(err.message);
|
|
52
|
+
const data = (err as { data?: string }).data;
|
|
53
|
+
if (data) parts.push(data);
|
|
54
|
+
return parts.join(" — ") || "unknown error";
|
|
55
|
+
}
|
|
44
56
|
import {
|
|
45
57
|
isSiblingScriptsPattern,
|
|
46
58
|
matchesRelativeScriptPattern,
|
|
@@ -983,8 +995,30 @@ export class AgentHarness {
|
|
|
983
995
|
return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
|
|
984
996
|
}
|
|
985
997
|
|
|
998
|
+
/**
|
|
999
|
+
* Return the set of MCP server names that have at least one tool claimed by
|
|
1000
|
+
* any loaded skill's `allowedTools.mcp`. When ANY skill claims tools from a
|
|
1001
|
+
* server, the entire server is considered "skill-managed" — none of its tools
|
|
1002
|
+
* are auto-exposed globally; only explicitly declared tools become available
|
|
1003
|
+
* (via agent-level allowed-tools or active skill allowed-tools).
|
|
1004
|
+
*/
|
|
1005
|
+
private getSkillManagedMcpServers(): Set<string> {
|
|
1006
|
+
const servers = new Set<string>();
|
|
1007
|
+
for (const skill of this.loadedSkills) {
|
|
1008
|
+
for (const pattern of skill.allowedTools.mcp) {
|
|
1009
|
+
const slash = pattern.indexOf("/");
|
|
1010
|
+
if (slash > 0) {
|
|
1011
|
+
servers.add(pattern.slice(0, slash));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return servers;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
986
1018
|
private getRequestedMcpPatterns(): string[] {
|
|
987
1019
|
const patterns = new Set<string>(this.getAgentMcpIntent());
|
|
1020
|
+
|
|
1021
|
+
// Add patterns from active skills.
|
|
988
1022
|
for (const skillName of this.activeSkillNames) {
|
|
989
1023
|
const skill = this.loadedSkills.find((entry) => entry.name === skillName);
|
|
990
1024
|
if (!skill) {
|
|
@@ -994,6 +1028,26 @@ export class AgentHarness {
|
|
|
994
1028
|
patterns.add(pattern);
|
|
995
1029
|
}
|
|
996
1030
|
}
|
|
1031
|
+
|
|
1032
|
+
// MCP servers whose tools are NOT claimed by any skill are "unmanaged" —
|
|
1033
|
+
// all their discovered tools are globally available so that configuring a
|
|
1034
|
+
// server in poncho.config.js makes its tools accessible by default.
|
|
1035
|
+
//
|
|
1036
|
+
// Once ANY skill claims tools from a server (even a single tool), that
|
|
1037
|
+
// server becomes "skill-managed" and ALL of its tools require explicit
|
|
1038
|
+
// declaration (agent-level or active-skill) to be available.
|
|
1039
|
+
if (this.mcpBridge) {
|
|
1040
|
+
const managedServers = this.getSkillManagedMcpServers();
|
|
1041
|
+
const discoveredTools = this.mcpBridge.listDiscoveredTools();
|
|
1042
|
+
for (const toolName of discoveredTools) {
|
|
1043
|
+
const slash = toolName.indexOf("/");
|
|
1044
|
+
const serverName = slash > 0 ? toolName.slice(0, slash) : toolName;
|
|
1045
|
+
if (!managedServers.has(serverName)) {
|
|
1046
|
+
patterns.add(toolName);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
997
1051
|
return [...patterns];
|
|
998
1052
|
}
|
|
999
1053
|
|
|
@@ -1346,6 +1400,7 @@ export class AgentHarness {
|
|
|
1346
1400
|
const telemetryEnabled = config?.telemetry?.enabled !== false;
|
|
1347
1401
|
const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : undefined;
|
|
1348
1402
|
if (otlpConfig) {
|
|
1403
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
|
|
1349
1404
|
const exporter = new OTLPTraceExporter({
|
|
1350
1405
|
url: otlpConfig.url,
|
|
1351
1406
|
headers: otlpConfig.headers,
|
|
@@ -1358,7 +1413,7 @@ export class AgentHarness {
|
|
|
1358
1413
|
provider.register();
|
|
1359
1414
|
this.otlpTracerProvider = provider;
|
|
1360
1415
|
this.hasOtlpExporter = true;
|
|
1361
|
-
console.info(`[poncho][telemetry] OTLP exporter active → ${otlpConfig.url}`);
|
|
1416
|
+
console.info(`[poncho][telemetry] OTLP trace exporter active → ${otlpConfig.url}`);
|
|
1362
1417
|
}
|
|
1363
1418
|
}
|
|
1364
1419
|
|
|
@@ -1523,21 +1578,13 @@ export class AgentHarness {
|
|
|
1523
1578
|
await this.mcpBridge?.stopLocalServers();
|
|
1524
1579
|
if (this.otlpSpanProcessor) {
|
|
1525
1580
|
await this.otlpSpanProcessor.shutdown().catch((err) => {
|
|
1526
|
-
console.warn(
|
|
1527
|
-
`[poncho][telemetry] OTLP span processor shutdown error: ${
|
|
1528
|
-
err instanceof Error ? err.message : String(err)
|
|
1529
|
-
}`,
|
|
1530
|
-
);
|
|
1581
|
+
console.warn(`[poncho][telemetry] OTLP span processor shutdown error: ${formatOtlpError(err)}`);
|
|
1531
1582
|
});
|
|
1532
1583
|
this.otlpSpanProcessor = undefined;
|
|
1533
1584
|
}
|
|
1534
1585
|
if (this.otlpTracerProvider) {
|
|
1535
1586
|
await this.otlpTracerProvider.shutdown().catch((err) => {
|
|
1536
|
-
console.warn(
|
|
1537
|
-
`[poncho][telemetry] OTLP tracer provider shutdown error: ${
|
|
1538
|
-
err instanceof Error ? err.message : String(err)
|
|
1539
|
-
}`,
|
|
1540
|
-
);
|
|
1587
|
+
console.warn(`[poncho][telemetry] OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
|
|
1541
1588
|
});
|
|
1542
1589
|
this.otlpTracerProvider = undefined;
|
|
1543
1590
|
}
|
|
@@ -1553,8 +1600,8 @@ export class AgentHarness {
|
|
|
1553
1600
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
1554
1601
|
*/
|
|
1555
1602
|
async *runWithTelemetry(input: RunInput): AsyncGenerator<AgentEvent> {
|
|
1556
|
-
if (this.hasOtlpExporter) {
|
|
1557
|
-
const tracer =
|
|
1603
|
+
if (this.hasOtlpExporter && this.otlpTracerProvider) {
|
|
1604
|
+
const tracer = this.otlpTracerProvider.getTracer("gen_ai");
|
|
1558
1605
|
const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
|
|
1559
1606
|
|
|
1560
1607
|
const rootSpan = tracer.startSpan(`invoke_agent ${agentName}`, {
|
|
@@ -1586,7 +1633,10 @@ export class AgentHarness {
|
|
|
1586
1633
|
rootSpan.end();
|
|
1587
1634
|
try {
|
|
1588
1635
|
await this.otlpSpanProcessor?.forceFlush();
|
|
1589
|
-
} catch
|
|
1636
|
+
} catch (err: unknown) {
|
|
1637
|
+
const detail = formatOtlpError(err);
|
|
1638
|
+
console.warn(`[poncho][telemetry] OTLP span flush failed: ${detail}`);
|
|
1639
|
+
}
|
|
1590
1640
|
}
|
|
1591
1641
|
} else {
|
|
1592
1642
|
yield* this.run(input);
|
|
@@ -2642,8 +2692,8 @@ ${boundedMainMemory.trim()}`
|
|
|
2642
2692
|
// OTel GenAI execute_tool spans for tool call visibility in traces
|
|
2643
2693
|
type OtelSpan = ReturnType<ReturnType<typeof trace.getTracer>["startSpan"]>;
|
|
2644
2694
|
const toolSpans = new Map<string, OtelSpan>();
|
|
2645
|
-
if (this.hasOtlpExporter) {
|
|
2646
|
-
const tracer =
|
|
2695
|
+
if (this.hasOtlpExporter && this.otlpTracerProvider) {
|
|
2696
|
+
const tracer = this.otlpTracerProvider.getTracer("gen_ai");
|
|
2647
2697
|
for (const call of approvedCalls) {
|
|
2648
2698
|
const toolDef = this.dispatcher.get(call.name);
|
|
2649
2699
|
toolSpans.set(call.id, tracer.startSpan(`execute_tool ${call.name}`, {
|
package/src/telemetry.ts
CHANGED
|
@@ -18,10 +18,26 @@ export interface OtlpConfig {
|
|
|
18
18
|
|
|
19
19
|
export type OtlpOption = string | OtlpConfig;
|
|
20
20
|
|
|
21
|
+
const TRACES_PATH = "/v1/traces";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensures the OTLP URL points to the trace-ingest endpoint.
|
|
25
|
+
*
|
|
26
|
+
* Users typically configure the *base* OTLP endpoint (e.g.
|
|
27
|
+
* `https://gateway.example.com/api/v1/otlp`) but `OTLPTraceExporter` uses
|
|
28
|
+
* the `url` constructor option as-is — it only appends `/v1/traces`
|
|
29
|
+
* automatically when reading from the `OTEL_EXPORTER_OTLP_ENDPOINT` env var.
|
|
30
|
+
* Without this fixup traces are POSTed to the wrong path and silently lost.
|
|
31
|
+
*/
|
|
32
|
+
function ensureTracesPath(url: string): string {
|
|
33
|
+
if (url.endsWith(TRACES_PATH)) return url;
|
|
34
|
+
return url.replace(/\/+$/, "") + TRACES_PATH;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
export function normalizeOtlp(opt: OtlpOption | undefined): OtlpConfig | undefined {
|
|
22
38
|
if (!opt) return undefined;
|
|
23
|
-
if (typeof opt === "string") return opt ? { url: opt } : undefined;
|
|
24
|
-
return opt.url ? opt : undefined;
|
|
39
|
+
if (typeof opt === "string") return opt ? { url: ensureTracesPath(opt) } : undefined;
|
|
40
|
+
return opt.url ? { ...opt, url: ensureTracesPath(opt.url) } : undefined;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface TelemetryConfig {
|
|
@@ -87,8 +103,12 @@ export class TelemetryEmitter {
|
|
|
87
103
|
],
|
|
88
104
|
}),
|
|
89
105
|
});
|
|
90
|
-
} catch {
|
|
91
|
-
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn(
|
|
108
|
+
`[poncho][telemetry] OTLP log delivery failed: ${
|
|
109
|
+
err instanceof Error ? err.message : String(err)
|
|
110
|
+
}`,
|
|
111
|
+
);
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
|
package/test/harness.test.ts
CHANGED
|
@@ -1037,6 +1037,360 @@ allowed-tools:
|
|
|
1037
1037
|
await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
|
|
1038
1038
|
});
|
|
1039
1039
|
|
|
1040
|
+
it("unclaimed MCP tools are globally available without allowed-tools declaration", async () => {
|
|
1041
|
+
process.env.LINEAR_TOKEN = "token-123";
|
|
1042
|
+
const mcpServer = createServer(async (req, res) => {
|
|
1043
|
+
if (req.method === "DELETE") {
|
|
1044
|
+
res.statusCode = 200;
|
|
1045
|
+
res.end();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const chunks: Buffer[] = [];
|
|
1049
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
1050
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1051
|
+
const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
|
|
1052
|
+
if (payload.method === "initialize") {
|
|
1053
|
+
res.setHeader("Content-Type", "application/json");
|
|
1054
|
+
res.setHeader("Mcp-Session-Id", "sess");
|
|
1055
|
+
res.end(
|
|
1056
|
+
JSON.stringify({
|
|
1057
|
+
jsonrpc: "2.0",
|
|
1058
|
+
id: payload.id,
|
|
1059
|
+
result: {
|
|
1060
|
+
protocolVersion: "2025-03-26",
|
|
1061
|
+
capabilities: { tools: { listChanged: true } },
|
|
1062
|
+
serverInfo: { name: "remote", version: "1.0.0" },
|
|
1063
|
+
},
|
|
1064
|
+
}),
|
|
1065
|
+
);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (payload.method === "notifications/initialized") {
|
|
1069
|
+
res.statusCode = 202;
|
|
1070
|
+
res.end();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (payload.method === "tools/list") {
|
|
1074
|
+
res.setHeader("Content-Type", "application/json");
|
|
1075
|
+
res.end(
|
|
1076
|
+
JSON.stringify({
|
|
1077
|
+
jsonrpc: "2.0",
|
|
1078
|
+
id: payload.id,
|
|
1079
|
+
result: {
|
|
1080
|
+
tools: [
|
|
1081
|
+
{ name: "list_issues", inputSchema: { type: "object", properties: {} } },
|
|
1082
|
+
{ name: "save_issue", inputSchema: { type: "object", properties: {} } },
|
|
1083
|
+
],
|
|
1084
|
+
},
|
|
1085
|
+
}),
|
|
1086
|
+
);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
res.statusCode = 404;
|
|
1090
|
+
res.end();
|
|
1091
|
+
});
|
|
1092
|
+
await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
|
|
1093
|
+
const address = mcpServer.address();
|
|
1094
|
+
if (!address || typeof address === "string") throw new Error("Unexpected address");
|
|
1095
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-unclaimed-mcp-"));
|
|
1096
|
+
// AGENT.md with no allowed-tools and no skills — MCP tools should be globally available
|
|
1097
|
+
await writeFile(
|
|
1098
|
+
join(dir, "AGENT.md"),
|
|
1099
|
+
`---
|
|
1100
|
+
name: unclaimed-mcp-agent
|
|
1101
|
+
model:
|
|
1102
|
+
provider: anthropic
|
|
1103
|
+
name: claude-opus-4-5
|
|
1104
|
+
---
|
|
1105
|
+
|
|
1106
|
+
# Agent with unclaimed MCP tools
|
|
1107
|
+
`,
|
|
1108
|
+
"utf8",
|
|
1109
|
+
);
|
|
1110
|
+
await writeFile(
|
|
1111
|
+
join(dir, "poncho.config.js"),
|
|
1112
|
+
`export default {
|
|
1113
|
+
mcp: [
|
|
1114
|
+
{
|
|
1115
|
+
name: "linear",
|
|
1116
|
+
url: "http://127.0.0.1:${address.port}/mcp",
|
|
1117
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
1118
|
+
}
|
|
1119
|
+
]
|
|
1120
|
+
};
|
|
1121
|
+
`,
|
|
1122
|
+
"utf8",
|
|
1123
|
+
);
|
|
1124
|
+
const harness = new AgentHarness({ workingDir: dir });
|
|
1125
|
+
await harness.initialize();
|
|
1126
|
+
const toolNames = () => harness.listTools().map((t) => t.name);
|
|
1127
|
+
// Unclaimed tools should be globally available
|
|
1128
|
+
expect(toolNames()).toContain("linear/list_issues");
|
|
1129
|
+
expect(toolNames()).toContain("linear/save_issue");
|
|
1130
|
+
await harness.shutdown();
|
|
1131
|
+
await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
it("claiming any tool from a server scopes the entire server", async () => {
|
|
1135
|
+
process.env.LINEAR_TOKEN = "token-123";
|
|
1136
|
+
const mcpServer = createServer(async (req, res) => {
|
|
1137
|
+
if (req.method === "DELETE") {
|
|
1138
|
+
res.statusCode = 200;
|
|
1139
|
+
res.end();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const chunks: Buffer[] = [];
|
|
1143
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
1144
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1145
|
+
const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
|
|
1146
|
+
if (payload.method === "initialize") {
|
|
1147
|
+
res.setHeader("Content-Type", "application/json");
|
|
1148
|
+
res.setHeader("Mcp-Session-Id", "sess");
|
|
1149
|
+
res.end(
|
|
1150
|
+
JSON.stringify({
|
|
1151
|
+
jsonrpc: "2.0",
|
|
1152
|
+
id: payload.id,
|
|
1153
|
+
result: {
|
|
1154
|
+
protocolVersion: "2025-03-26",
|
|
1155
|
+
capabilities: { tools: { listChanged: true } },
|
|
1156
|
+
serverInfo: { name: "remote", version: "1.0.0" },
|
|
1157
|
+
},
|
|
1158
|
+
}),
|
|
1159
|
+
);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (payload.method === "notifications/initialized") {
|
|
1163
|
+
res.statusCode = 202;
|
|
1164
|
+
res.end();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (payload.method === "tools/list") {
|
|
1168
|
+
res.setHeader("Content-Type", "application/json");
|
|
1169
|
+
res.end(
|
|
1170
|
+
JSON.stringify({
|
|
1171
|
+
jsonrpc: "2.0",
|
|
1172
|
+
id: payload.id,
|
|
1173
|
+
result: {
|
|
1174
|
+
tools: [
|
|
1175
|
+
{ name: "list_issues", inputSchema: { type: "object", properties: {} } },
|
|
1176
|
+
{ name: "save_issue", inputSchema: { type: "object", properties: {} } },
|
|
1177
|
+
{ name: "other_tool", inputSchema: { type: "object", properties: {} } },
|
|
1178
|
+
],
|
|
1179
|
+
},
|
|
1180
|
+
}),
|
|
1181
|
+
);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (payload.method === "tools/call") {
|
|
1185
|
+
res.setHeader("Content-Type", "application/json");
|
|
1186
|
+
res.end(
|
|
1187
|
+
JSON.stringify({
|
|
1188
|
+
jsonrpc: "2.0",
|
|
1189
|
+
id: payload.id,
|
|
1190
|
+
result: { result: { ok: true } },
|
|
1191
|
+
}),
|
|
1192
|
+
);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
res.statusCode = 404;
|
|
1196
|
+
res.end();
|
|
1197
|
+
});
|
|
1198
|
+
await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
|
|
1199
|
+
const address = mcpServer.address();
|
|
1200
|
+
if (!address || typeof address === "string") throw new Error("Unexpected address");
|
|
1201
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-server-scoped-"));
|
|
1202
|
+
await writeFile(
|
|
1203
|
+
join(dir, "AGENT.md"),
|
|
1204
|
+
`---
|
|
1205
|
+
name: server-scoped-agent
|
|
1206
|
+
model:
|
|
1207
|
+
provider: anthropic
|
|
1208
|
+
name: claude-opus-4-5
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
# Server Scoped Agent
|
|
1212
|
+
`,
|
|
1213
|
+
"utf8",
|
|
1214
|
+
);
|
|
1215
|
+
await writeFile(
|
|
1216
|
+
join(dir, "poncho.config.js"),
|
|
1217
|
+
`export default {
|
|
1218
|
+
mcp: [
|
|
1219
|
+
{
|
|
1220
|
+
name: "remote",
|
|
1221
|
+
url: "http://127.0.0.1:${address.port}/mcp",
|
|
1222
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
};
|
|
1226
|
+
`,
|
|
1227
|
+
"utf8",
|
|
1228
|
+
);
|
|
1229
|
+
// Skill claims only 2 of the 3 tools from "remote" server
|
|
1230
|
+
await mkdir(join(dir, "skills", "linear"), { recursive: true });
|
|
1231
|
+
await writeFile(
|
|
1232
|
+
join(dir, "skills", "linear", "SKILL.md"),
|
|
1233
|
+
`---
|
|
1234
|
+
name: linear
|
|
1235
|
+
description: Linear issue tracking
|
|
1236
|
+
allowed-tools:
|
|
1237
|
+
- mcp:remote/list_issues
|
|
1238
|
+
- mcp:remote/save_issue
|
|
1239
|
+
---
|
|
1240
|
+
# Linear Skill
|
|
1241
|
+
`,
|
|
1242
|
+
"utf8",
|
|
1243
|
+
);
|
|
1244
|
+
const harness = new AgentHarness({ workingDir: dir });
|
|
1245
|
+
await harness.initialize();
|
|
1246
|
+
const toolNames = () => harness.listTools().map((t) => t.name);
|
|
1247
|
+
|
|
1248
|
+
// Before activation: entire server is skill-managed, so ALL tools are hidden
|
|
1249
|
+
// (even other_tool which the skill didn't explicitly claim)
|
|
1250
|
+
expect(toolNames()).not.toContain("remote/list_issues");
|
|
1251
|
+
expect(toolNames()).not.toContain("remote/save_issue");
|
|
1252
|
+
expect(toolNames()).not.toContain("remote/other_tool");
|
|
1253
|
+
|
|
1254
|
+
// Activate the linear skill — only its declared tools appear
|
|
1255
|
+
const activate = harness.listTools().find((t) => t.name === "activate_skill")!;
|
|
1256
|
+
const deactivate = harness.listTools().find((t) => t.name === "deactivate_skill")!;
|
|
1257
|
+
await activate.handler({ name: "linear" }, {} as any);
|
|
1258
|
+
|
|
1259
|
+
expect(toolNames()).toContain("remote/list_issues");
|
|
1260
|
+
expect(toolNames()).toContain("remote/save_issue");
|
|
1261
|
+
// other_tool is NOT claimed by any active skill, and the server is managed
|
|
1262
|
+
expect(toolNames()).not.toContain("remote/other_tool");
|
|
1263
|
+
|
|
1264
|
+
// Deactivate — all tools from the managed server hidden again
|
|
1265
|
+
await deactivate.handler({ name: "linear" }, {} as any);
|
|
1266
|
+
expect(toolNames()).not.toContain("remote/list_issues");
|
|
1267
|
+
expect(toolNames()).not.toContain("remote/save_issue");
|
|
1268
|
+
expect(toolNames()).not.toContain("remote/other_tool");
|
|
1269
|
+
|
|
1270
|
+
await harness.shutdown();
|
|
1271
|
+
await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("wildcard skill claim scopes all tools from that server", async () => {
|
|
1275
|
+
process.env.LINEAR_TOKEN = "token-123";
|
|
1276
|
+
const mcpServer = createServer(async (req, res) => {
|
|
1277
|
+
if (req.method === "DELETE") {
|
|
1278
|
+
res.statusCode = 200;
|
|
1279
|
+
res.end();
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const chunks: Buffer[] = [];
|
|
1283
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
1284
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1285
|
+
const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
|
|
1286
|
+
if (payload.method === "initialize") {
|
|
1287
|
+
res.setHeader("Content-Type", "application/json");
|
|
1288
|
+
res.setHeader("Mcp-Session-Id", "sess");
|
|
1289
|
+
res.end(
|
|
1290
|
+
JSON.stringify({
|
|
1291
|
+
jsonrpc: "2.0",
|
|
1292
|
+
id: payload.id,
|
|
1293
|
+
result: {
|
|
1294
|
+
protocolVersion: "2025-03-26",
|
|
1295
|
+
capabilities: { tools: { listChanged: true } },
|
|
1296
|
+
serverInfo: { name: "linear", version: "1.0.0" },
|
|
1297
|
+
},
|
|
1298
|
+
}),
|
|
1299
|
+
);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (payload.method === "notifications/initialized") {
|
|
1303
|
+
res.statusCode = 202;
|
|
1304
|
+
res.end();
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (payload.method === "tools/list") {
|
|
1308
|
+
res.setHeader("Content-Type", "application/json");
|
|
1309
|
+
res.end(
|
|
1310
|
+
JSON.stringify({
|
|
1311
|
+
jsonrpc: "2.0",
|
|
1312
|
+
id: payload.id,
|
|
1313
|
+
result: {
|
|
1314
|
+
tools: [
|
|
1315
|
+
{ name: "list_issues", inputSchema: { type: "object", properties: {} } },
|
|
1316
|
+
{ name: "save_issue", inputSchema: { type: "object", properties: {} } },
|
|
1317
|
+
{ name: "save_comment", inputSchema: { type: "object", properties: {} } },
|
|
1318
|
+
],
|
|
1319
|
+
},
|
|
1320
|
+
}),
|
|
1321
|
+
);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
res.statusCode = 404;
|
|
1325
|
+
res.end();
|
|
1326
|
+
});
|
|
1327
|
+
await new Promise<void>((resolveOpen) => mcpServer.listen(0, () => resolveOpen()));
|
|
1328
|
+
const address = mcpServer.address();
|
|
1329
|
+
if (!address || typeof address === "string") throw new Error("Unexpected address");
|
|
1330
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-wildcard-claim-"));
|
|
1331
|
+
await writeFile(
|
|
1332
|
+
join(dir, "AGENT.md"),
|
|
1333
|
+
`---
|
|
1334
|
+
name: wildcard-agent
|
|
1335
|
+
model:
|
|
1336
|
+
provider: anthropic
|
|
1337
|
+
name: claude-opus-4-5
|
|
1338
|
+
---
|
|
1339
|
+
|
|
1340
|
+
# Wildcard Agent
|
|
1341
|
+
`,
|
|
1342
|
+
"utf8",
|
|
1343
|
+
);
|
|
1344
|
+
await writeFile(
|
|
1345
|
+
join(dir, "poncho.config.js"),
|
|
1346
|
+
`export default {
|
|
1347
|
+
mcp: [
|
|
1348
|
+
{
|
|
1349
|
+
name: "linear",
|
|
1350
|
+
url: "http://127.0.0.1:${address.port}/mcp",
|
|
1351
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
1352
|
+
}
|
|
1353
|
+
]
|
|
1354
|
+
};
|
|
1355
|
+
`,
|
|
1356
|
+
"utf8",
|
|
1357
|
+
);
|
|
1358
|
+
// Skill claims all linear tools with wildcard
|
|
1359
|
+
await mkdir(join(dir, "skills", "linear"), { recursive: true });
|
|
1360
|
+
await writeFile(
|
|
1361
|
+
join(dir, "skills", "linear", "SKILL.md"),
|
|
1362
|
+
`---
|
|
1363
|
+
name: linear
|
|
1364
|
+
description: Linear integration
|
|
1365
|
+
allowed-tools:
|
|
1366
|
+
- mcp:linear/*
|
|
1367
|
+
---
|
|
1368
|
+
# Linear Skill
|
|
1369
|
+
`,
|
|
1370
|
+
"utf8",
|
|
1371
|
+
);
|
|
1372
|
+
const harness = new AgentHarness({ workingDir: dir });
|
|
1373
|
+
await harness.initialize();
|
|
1374
|
+
const toolNames = () => harness.listTools().map((t) => t.name);
|
|
1375
|
+
|
|
1376
|
+
// None of the linear tools should be available (all claimed by wildcard)
|
|
1377
|
+
expect(toolNames()).not.toContain("linear/list_issues");
|
|
1378
|
+
expect(toolNames()).not.toContain("linear/save_issue");
|
|
1379
|
+
expect(toolNames()).not.toContain("linear/save_comment");
|
|
1380
|
+
|
|
1381
|
+
// Activate the skill
|
|
1382
|
+
const activate = harness.listTools().find((t) => t.name === "activate_skill")!;
|
|
1383
|
+
await activate.handler({ name: "linear" }, {} as any);
|
|
1384
|
+
|
|
1385
|
+
// All linear tools now available
|
|
1386
|
+
expect(toolNames()).toContain("linear/list_issues");
|
|
1387
|
+
expect(toolNames()).toContain("linear/save_issue");
|
|
1388
|
+
expect(toolNames()).toContain("linear/save_comment");
|
|
1389
|
+
|
|
1390
|
+
await harness.shutdown();
|
|
1391
|
+
await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1040
1394
|
it("supports flat tool access config format", async () => {
|
|
1041
1395
|
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-flat-tool-access-"));
|
|
1042
1396
|
await writeFile(
|
package/test/telemetry.test.ts
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { TelemetryEmitter } from "../src/telemetry.js";
|
|
2
|
+
import { TelemetryEmitter, normalizeOtlp } from "../src/telemetry.js";
|
|
3
|
+
|
|
4
|
+
describe("normalizeOtlp", () => {
|
|
5
|
+
it("appends /v1/traces to a base URL string", () => {
|
|
6
|
+
expect(normalizeOtlp("https://gateway.example.com/api/v1/otlp")).toEqual({
|
|
7
|
+
url: "https://gateway.example.com/api/v1/otlp/v1/traces",
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("does not double-append when URL already ends with /v1/traces", () => {
|
|
12
|
+
expect(normalizeOtlp("https://api.honeycomb.io/v1/traces")).toEqual({
|
|
13
|
+
url: "https://api.honeycomb.io/v1/traces",
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips trailing slashes before appending", () => {
|
|
18
|
+
expect(normalizeOtlp("https://gateway.example.com/")).toEqual({
|
|
19
|
+
url: "https://gateway.example.com/v1/traces",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("appends /v1/traces to object config", () => {
|
|
24
|
+
expect(
|
|
25
|
+
normalizeOtlp({
|
|
26
|
+
url: "https://gateway.example.com/otlp",
|
|
27
|
+
headers: { Authorization: "Bearer tok" },
|
|
28
|
+
}),
|
|
29
|
+
).toEqual({
|
|
30
|
+
url: "https://gateway.example.com/otlp/v1/traces",
|
|
31
|
+
headers: { Authorization: "Bearer tok" },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns undefined for falsy input", () => {
|
|
36
|
+
expect(normalizeOtlp(undefined)).toBeUndefined();
|
|
37
|
+
expect(normalizeOtlp("")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
3
40
|
|
|
4
41
|
describe("telemetry emitter", () => {
|
|
5
42
|
it("delegates to custom handler when configured", async () => {
|
|
@@ -14,7 +51,7 @@ describe("telemetry emitter", () => {
|
|
|
14
51
|
global.fetch = vi.fn().mockRejectedValue(new Error("network down"));
|
|
15
52
|
|
|
16
53
|
const emitter = new TelemetryEmitter({
|
|
17
|
-
otlp: "https://otel.example.com/v1/
|
|
54
|
+
otlp: "https://otel.example.com/v1/traces",
|
|
18
55
|
});
|
|
19
56
|
|
|
20
57
|
await expect(
|