@poncho-ai/harness 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.14.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.14.2 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 175.01 KB
11
- ESM ⚡️ Build success in 76ms
10
+ ESM dist/index.js 177.01 KB
11
+ ESM ⚡️ Build success in 73ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 5663ms
14
- DTS dist/index.d.ts 21.38 KB
13
+ DTS ⚡️ Build success in 3459ms
14
+ DTS dist/index.d.ts 21.71 KB
@@ -0,0 +1,6 @@
1
+
2
+ > @poncho-ai/harness@0.11.2 lint /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
+ > eslint src/
4
+
5
+ sh: eslint: command not found
6
+  ELIFECYCLE  Command failed.
@@ -0,0 +1,137 @@
1
+
2
+ > @poncho-ai/harness@0.14.0 test /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
+ > vitest
4
+
5
+
6
+  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/harness
7
+
8
+ [event] step:completed {"type":"step:completed","step":1,"duration":1}
9
+ [event] step:started {"type":"step:started","step":2}
10
+ ✓ test/telemetry.test.ts  (3 tests) 11ms
11
+ ✓ test/schema-converter.test.ts  (27 tests) 21ms
12
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > discovers and calls tools over streamable HTTP
13
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
14
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
15
+
16
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > selects discovered tools by requested patterns
17
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
18
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
19
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
20
+
21
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
22
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":0,"filteredByPolicyCount":0,"filteredByIntentCount":0}
23
+
24
+ stderr | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
25
+ [poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
26
+
27
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > returns actionable errors for 403 permission failures
28
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
29
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
30
+
31
+ ✓ test/mcp.test.ts  (6 tests) 516ms
32
+ ✓ test/agent-parser.test.ts  (10 tests) 130ms
33
+ ✓ test/memory.test.ts  (4 tests) 192ms
34
+ ✓ test/state.test.ts  (5 tests) 294ms
35
+ ✓ test/model-factory.test.ts  (4 tests) 8ms
36
+ ✓ test/agent-identity.test.ts  (2 tests) 33ms
37
+ stdout | test/harness.test.ts > agent harness > registers default filesystem tools
38
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
39
+
40
+ stdout | test/harness.test.ts > agent harness > disables write_file by default in production environment
41
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
42
+
43
+ stdout | test/harness.test.ts > agent harness > allows disabling built-in tools via poncho.config.js
44
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
45
+
46
+ stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
47
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
48
+
49
+ stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
50
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
51
+
52
+ stdout | test/harness.test.ts > agent harness > does not auto-register exported tool objects from skill scripts
53
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
54
+
55
+ stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
56
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
57
+
58
+ stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
59
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
60
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:beta","requestedPatterns":[]}
61
+
62
+ stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
63
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
64
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:obsolete","requestedPatterns":[]}
65
+
66
+ stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
67
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
68
+
69
+ stdout | test/harness.test.ts > agent harness > does not refresh skills outside development mode
70
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
71
+
72
+ stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
73
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
74
+
75
+ stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
76
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:alpha","requestedPatterns":[]}
77
+
78
+ stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
79
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
80
+
81
+ stdout | test/harness.test.ts > agent harness > lists skill scripts through list_skill_scripts
82
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
83
+
84
+ stdout | test/harness.test.ts > agent harness > runs JavaScript/TypeScript skill scripts through run_skill_script
85
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
86
+
87
+ stdout | test/harness.test.ts > agent harness > runs AGENT-scope scripts from root scripts directory
88
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
89
+
90
+ stdout | test/harness.test.ts > agent harness > blocks path traversal in run_skill_script
91
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
92
+
93
+ stdout | test/harness.test.ts > agent harness > requires allowed-tools entries for non-standard script directories
94
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
95
+
96
+ stdout | test/harness.test.ts > agent harness > registers MCP tools dynamically for stacked active skills and supports deactivation
97
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
98
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
99
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
100
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-a","requestedPatterns":["remote/a"],"registeredCount":1,"activeSkills":["skill-a"]}
101
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":2,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
102
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-b","requestedPatterns":["remote/a","remote/b"],"registeredCount":2,"activeSkills":["skill-a","skill-b"]}
103
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
104
+ [poncho][mcp] {"event":"tools.refreshed","reason":"deactivate:skill-a","requestedPatterns":["remote/b"],"registeredCount":1,"activeSkills":["skill-b"]}
105
+
106
+ stdout | test/harness.test.ts > agent harness > supports flat tool access config format
107
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
108
+
109
+ stdout | test/harness.test.ts > agent harness > flat tool access takes priority over legacy defaults
110
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
111
+
112
+ stdout | test/harness.test.ts > agent harness > byEnvironment overrides flat tool access
113
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
114
+
115
+ stdout | test/harness.test.ts > agent harness > registerTools skips tools disabled via config
116
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
117
+
118
+ stdout | test/harness.test.ts > agent harness > approval access level registers the tool but marks it for approval
119
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
120
+
121
+ stdout | test/harness.test.ts > agent harness > tools without approval config do not require approval
122
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
123
+
124
+ stdout | test/harness.test.ts > agent harness > allows in-flight MCP calls to finish after skill deactivation
125
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
126
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
127
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
128
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-slow","requestedPatterns":["remote/slow"],"registeredCount":1,"activeSkills":["skill-slow"]}
129
+ [poncho][mcp] {"event":"tools.cleared","reason":"deactivate:skill-slow","requestedPatterns":[]}
130
+
131
+ ✓ test/harness.test.ts  (25 tests) 429ms
132
+
133
+  Test Files  9 passed (9)
134
+  Tests  86 passed (86)
135
+  Start at  10:02:54
136
+  Duration  4.19s (transform 2.16s, setup 0ms, collect 7.51s, tests 1.63s, environment 1ms, prepare 1.97s)
137
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`1f47bb4`](https://github.com/cesr/poncho-ai/commit/1f47bb49e5d48dc17644172012b057190b316469) Thanks [@cesr](https://github.com/cesr)! - Add conversation rename via double-click on the title in the web UI, standardize all credential config fields to the `*Env` naming pattern, and sync the init README template with the repo README.
8
+
9
+ - Updated dependencies [[`1f47bb4`](https://github.com/cesr/poncho-ai/commit/1f47bb49e5d48dc17644172012b057190b316469)]:
10
+ - @poncho-ai/sdk@1.0.3
11
+
3
12
  ## 0.14.1
4
13
 
5
14
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -111,8 +111,8 @@ type StateProviderName = "local" | "memory" | "redis" | "upstash" | "dynamodb";
111
111
  interface StateConfig {
112
112
  provider?: StateProviderName;
113
113
  ttl?: number;
114
- url?: string;
115
- token?: string;
114
+ urlEnv?: string;
115
+ tokenEnv?: string;
116
116
  table?: string;
117
117
  region?: string;
118
118
  }
@@ -154,8 +154,8 @@ interface MainMemory {
154
154
  interface MemoryConfig {
155
155
  enabled?: boolean;
156
156
  provider?: StateProviderName;
157
- url?: string;
158
- token?: string;
157
+ urlEnv?: string;
158
+ tokenEnv?: string;
159
159
  table?: string;
160
160
  region?: string;
161
161
  ttl?: number;
@@ -218,8 +218,8 @@ declare class LocalMcpBridge {
218
218
 
219
219
  interface StorageConfig {
220
220
  provider?: "local" | "memory" | "redis" | "upstash" | "dynamodb";
221
- url?: string;
222
- token?: string;
221
+ urlEnv?: string;
222
+ tokenEnv?: string;
223
223
  table?: string;
224
224
  region?: string;
225
225
  ttl?: number | {
@@ -274,6 +274,7 @@ interface PonchoConfig extends McpConfig {
274
274
  required?: boolean;
275
275
  type?: "bearer" | "header" | "custom";
276
276
  headerName?: string;
277
+ tokenEnv?: string;
277
278
  validate?: (token: string, req?: unknown) => Promise<boolean> | boolean;
278
279
  };
279
280
  state?: {
@@ -283,12 +284,20 @@ interface PonchoConfig extends McpConfig {
283
284
  };
284
285
  memory?: MemoryConfig;
285
286
  storage?: StorageConfig;
287
+ providers?: {
288
+ openai?: {
289
+ apiKeyEnv?: string;
290
+ };
291
+ anthropic?: {
292
+ apiKeyEnv?: string;
293
+ };
294
+ };
286
295
  telemetry?: {
287
296
  enabled?: boolean;
288
297
  otlp?: string;
289
298
  latitude?: {
290
- apiKey?: string;
291
- projectId?: string | number;
299
+ apiKeyEnv?: string;
300
+ projectIdEnv?: string;
292
301
  path?: string;
293
302
  documentPath?: string;
294
303
  };
@@ -363,12 +372,20 @@ type ModelProviderFactory = (modelName: string) => LanguageModel;
363
372
  * resolve via the base prefix. Longest match wins.
364
373
  */
365
374
  declare const getModelContextWindow: (modelName: string) => number;
375
+ interface ProviderConfig {
376
+ openai?: {
377
+ apiKeyEnv?: string;
378
+ };
379
+ anthropic?: {
380
+ apiKeyEnv?: string;
381
+ };
382
+ }
366
383
  /**
367
- * Creates a model provider factory for the specified AI provider
368
- * @param provider - The provider name ('openai' or 'anthropic')
369
- * @returns A function that takes a model name and returns a LanguageModel instance
384
+ * Creates a model provider factory for the specified AI provider.
385
+ * API keys are read from environment variables; override the env var
386
+ * name via the `providers` config in `poncho.config.js`.
370
387
  */
371
- declare const createModelProvider: (provider?: string) => ModelProviderFactory;
388
+ declare const createModelProvider: (provider?: string, config?: ProviderConfig) => ModelProviderFactory;
372
389
 
373
390
  interface ToolCall {
374
391
  id: string;
@@ -489,8 +506,8 @@ declare class AgentHarness {
489
506
  * Vercel AI SDK support.
490
507
  */
491
508
  interface LatitudeCaptureConfig {
492
- apiKey?: string;
493
- projectId?: string | number;
509
+ apiKeyEnv?: string;
510
+ projectIdEnv?: string;
494
511
  path?: string;
495
512
  defaultPath?: string;
496
513
  }
@@ -585,8 +602,8 @@ interface TelemetryConfig {
585
602
  enabled?: boolean;
586
603
  otlp?: string;
587
604
  latitude?: {
588
- apiKey?: string;
589
- projectId?: string | number;
605
+ apiKeyEnv?: string;
606
+ projectIdEnv?: string;
590
607
  path?: string;
591
608
  documentPath?: string;
592
609
  };
@@ -599,4 +616,4 @@ declare class TelemetryEmitter {
599
616
  private sendOtlp;
600
617
  }
601
618
 
602
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
619
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -330,8 +330,8 @@ var resolveStateConfig = (config) => {
330
330
  if (config?.storage) {
331
331
  return {
332
332
  provider: config.storage.provider,
333
- url: config.storage.url,
334
- token: config.storage.token,
333
+ urlEnv: config.storage.urlEnv,
334
+ tokenEnv: config.storage.tokenEnv,
335
335
  table: config.storage.table,
336
336
  region: config.storage.region,
337
337
  ttl: resolveTtl(config.storage.ttl, "conversations")
@@ -344,8 +344,8 @@ var resolveMemoryConfig = (config) => {
344
344
  return {
345
345
  enabled: config.storage.memory?.enabled ?? config.memory?.enabled,
346
346
  provider: config.storage.provider,
347
- url: config.storage.url,
348
- token: config.storage.token,
347
+ urlEnv: config.storage.urlEnv,
348
+ tokenEnv: config.storage.tokenEnv,
349
349
  table: config.storage.table,
350
350
  region: config.storage.region,
351
351
  ttl: resolveTtl(config.storage.ttl, "memory"),
@@ -1121,8 +1121,10 @@ var createMemoryStore = (agentId, config, options) => {
1121
1121
  return new InMemoryMemoryStore(ttl);
1122
1122
  }
1123
1123
  if (provider === "upstash") {
1124
- const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL ?? "";
1125
- const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? process.env.KV_REST_API_TOKEN ?? "";
1124
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
1125
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
1126
+ const url = process.env[urlEnv] ?? "";
1127
+ const token = process.env[tokenEnv] ?? "";
1126
1128
  if (url && token) {
1127
1129
  return new UpstashMemoryStore({
1128
1130
  baseUrl: url,
@@ -1134,7 +1136,8 @@ var createMemoryStore = (agentId, config, options) => {
1134
1136
  return new InMemoryMemoryStore(ttl);
1135
1137
  }
1136
1138
  if (provider === "redis") {
1137
- const url = config?.url ?? process.env.REDIS_URL ?? "";
1139
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
1140
+ const url = process.env[urlEnv] ?? "";
1138
1141
  if (url) {
1139
1142
  return new RedisMemoryStore({
1140
1143
  url,
@@ -1763,16 +1766,18 @@ var getModelContextWindow = (modelName) => {
1763
1766
  }
1764
1767
  return best ? MODEL_CONTEXT_WINDOWS[best] : DEFAULT_CONTEXT_WINDOW;
1765
1768
  };
1766
- var createModelProvider = (provider) => {
1769
+ var createModelProvider = (provider, config) => {
1767
1770
  const normalized = (provider ?? "anthropic").toLowerCase();
1768
1771
  if (normalized === "openai") {
1772
+ const apiKeyEnv2 = config?.openai?.apiKeyEnv ?? "OPENAI_API_KEY";
1769
1773
  const openai = createOpenAI({
1770
- apiKey: process.env.OPENAI_API_KEY
1774
+ apiKey: process.env[apiKeyEnv2]
1771
1775
  });
1772
1776
  return (modelName) => openai(modelName);
1773
1777
  }
1778
+ const apiKeyEnv = config?.anthropic?.apiKeyEnv ?? "ANTHROPIC_API_KEY";
1774
1779
  const anthropic = createAnthropic({
1775
- apiKey: process.env.ANTHROPIC_API_KEY
1780
+ apiKey: process.env[apiKeyEnv]
1776
1781
  });
1777
1782
  return (modelName) => anthropic(modelName);
1778
1783
  };
@@ -2812,18 +2817,51 @@ When configuring Latitude telemetry, use **exactly** these field names:
2812
2817
  telemetry: {
2813
2818
  enabled: true,
2814
2819
  latitude: {
2815
- apiKey: process.env.LATITUDE_API_KEY, // NOT "apiKeyEnv"
2816
- projectId: process.env.LATITUDE_PROJECT_ID, // string or number
2817
- path: "your/prompt-path", // optional, defaults to agent name
2820
+ apiKeyEnv: "LATITUDE_API_KEY", // env var name (default)
2821
+ projectIdEnv: "LATITUDE_PROJECT_ID", // env var name (default)
2822
+ path: "your/prompt-path", // optional, defaults to agent name
2818
2823
  },
2819
2824
  },
2820
2825
  \`\`\`
2821
2826
 
2822
- - The field is \`apiKey\` (not \`apiKeyEnv\`, \`api_key\`, or \`key\`).
2823
- - The field is \`projectId\` (not \`project_id\`, \`projectID\`, or \`project\`).
2827
+ - \`apiKeyEnv\` specifies the environment variable name for the Latitude API key (defaults to \`"LATITUDE_API_KEY"\`).
2828
+ - \`projectIdEnv\` specifies the environment variable name for the project ID (defaults to \`"LATITUDE_PROJECT_ID"\`).
2829
+ - With defaults, you only need \`telemetry: { latitude: {} }\` if the env vars are already named \`LATITUDE_API_KEY\` and \`LATITUDE_PROJECT_ID\`.
2824
2830
  - \`path\` must only contain letters, numbers, hyphens, underscores, dots, and slashes.
2825
2831
  - For a generic OTLP endpoint instead: \`telemetry: { otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }\`.
2826
- - Always read the env vars from \`process.env\` \u2014 do not hardcode secrets in \`poncho.config.js\`.
2832
+
2833
+ ## Credential Configuration Pattern
2834
+
2835
+ All credentials in \`poncho.config.js\` use the **env var name** pattern (\`*Env\` fields). Config specifies which environment variable to read \u2014 never the secret itself. Sensible defaults mean zero config when using conventional env var names.
2836
+
2837
+ \`\`\`javascript
2838
+ // poncho.config.js \u2014 credentials use *Env fields with defaults
2839
+ export default {
2840
+ // Model provider API keys (optional, defaults shown)
2841
+ providers: {
2842
+ anthropic: { apiKeyEnv: "ANTHROPIC_API_KEY" },
2843
+ openai: { apiKeyEnv: "OPENAI_API_KEY" },
2844
+ },
2845
+ auth: {
2846
+ required: true,
2847
+ tokenEnv: "PONCHO_AUTH_TOKEN", // default
2848
+ },
2849
+ storage: {
2850
+ provider: "upstash",
2851
+ urlEnv: "UPSTASH_REDIS_REST_URL", // default (falls back to KV_REST_API_URL)
2852
+ tokenEnv: "UPSTASH_REDIS_REST_TOKEN", // default (falls back to KV_REST_API_TOKEN)
2853
+ },
2854
+ telemetry: {
2855
+ latitude: {
2856
+ apiKeyEnv: "LATITUDE_API_KEY", // default
2857
+ projectIdEnv: "LATITUDE_PROJECT_ID", // default
2858
+ },
2859
+ },
2860
+ messaging: [{ platform: "slack" }], // reads SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET by default
2861
+ }
2862
+ \`\`\`
2863
+
2864
+ Since all fields have defaults, you only need to specify \`*Env\` when your env var name differs from the convention.
2827
2865
 
2828
2866
  ## When users ask about customization:
2829
2867
 
@@ -2838,6 +2876,7 @@ telemetry: {
2838
2876
  - To scope tools to a skill: keep server config in \`poncho.config.js\`, add desired \`allowed-tools\`/ \`approval-required\` patterns in that skill's \`SKILL.md\`, and remove global \`AGENT.md\` patterns if you do not want global availability.
2839
2877
  - Do not invent unsupported top-level config keys (for example \`model\` in \`poncho.config.js\`). Keep existing config structure unless README/spec explicitly says otherwise.
2840
2878
  - Keep \`poncho.config.js\` valid JavaScript and preserve existing imports/types/comments. If there is a JSDoc type import, do not rewrite it to a different package name.
2879
+ - Credentials always use \`*Env\` fields (env var names), never raw \`process.env.*\` values. For example, use \`apiKeyEnv: "MY_KEY"\` not \`apiKey: process.env.MY_KEY\`.
2841
2880
  - Preferred MCP config shape in \`poncho.config.js\`:
2842
2881
  \`mcp: [{ name: "linear", url: "https://mcp.linear.app/mcp", auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" } }]\`
2843
2882
  - If shell/CLI access exists, you can use \`poncho mcp add --url ... --name ... --auth-bearer-env ...\`, then \`poncho mcp tools list <server>\` and \`poncho mcp tools select <server>\`.
@@ -3157,7 +3196,7 @@ var AgentHarness = class {
3157
3196
  const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
3158
3197
  const memoryConfig = resolveMemoryConfig(config);
3159
3198
  if (!this.modelProviderInjected) {
3160
- this.modelProvider = createModelProvider(provider);
3199
+ this.modelProvider = createModelProvider(provider, config?.providers);
3161
3200
  }
3162
3201
  const bridge = new LocalMcpBridge(config);
3163
3202
  this.mcpBridge = bridge;
@@ -3184,10 +3223,12 @@ var AgentHarness = class {
3184
3223
  await bridge.discoverTools();
3185
3224
  await this.refreshMcpTools("initialize");
3186
3225
  const telemetryEnabled = config?.telemetry?.enabled !== false;
3187
- const latitudeApiKey = config?.telemetry?.latitude?.apiKey;
3188
- const rawProjectId = config?.telemetry?.latitude?.projectId;
3189
- const latitudeProjectId = typeof rawProjectId === "string" ? parseInt(rawProjectId, 10) : rawProjectId;
3190
3226
  const latitudeBlock = config?.telemetry?.latitude;
3227
+ const latApiKeyEnv = latitudeBlock?.apiKeyEnv ?? "LATITUDE_API_KEY";
3228
+ const latProjectIdEnv = latitudeBlock?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
3229
+ const latitudeApiKey = process.env[latApiKeyEnv];
3230
+ const rawProjectId = process.env[latProjectIdEnv];
3231
+ const latitudeProjectId = rawProjectId ? parseInt(rawProjectId, 10) : void 0;
3191
3232
  if (telemetryEnabled && latitudeApiKey && latitudeProjectId) {
3192
3233
  diag.setLogger(
3193
3234
  {
@@ -3209,14 +3250,10 @@ var AgentHarness = class {
3209
3250
  this.latitudeTelemetry = new LatitudeTelemetry(latitudeApiKey, { disableBatch: true });
3210
3251
  } else if (telemetryEnabled && latitudeBlock && (!latitudeApiKey || !latitudeProjectId)) {
3211
3252
  const missing = [];
3212
- if (!latitudeApiKey) missing.push("apiKey");
3213
- if (!latitudeProjectId) missing.push("projectId");
3214
- const unknownKeys = Object.keys(latitudeBlock).filter(
3215
- (k) => !["apiKey", "projectId", "path", "documentPath"].includes(k)
3216
- );
3217
- const hint = unknownKeys.length > 0 ? ` (found unknown key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.join(", ")} \u2013 did you mean "apiKey"?)` : "";
3253
+ if (!latitudeApiKey) missing.push(`${latApiKeyEnv} env var`);
3254
+ if (!latitudeProjectId) missing.push(`${latProjectIdEnv} env var`);
3218
3255
  console.warn(
3219
- `[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}${hint}. Traces will NOT be sent.`
3256
+ `[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`
3220
3257
  );
3221
3258
  }
3222
3259
  }
@@ -3242,8 +3279,8 @@ var AgentHarness = class {
3242
3279
  const config = this.loadedConfig;
3243
3280
  const telemetry = this.latitudeTelemetry;
3244
3281
  if (telemetry) {
3245
- const rawProjectId = config?.telemetry?.latitude?.projectId;
3246
- const projectId = typeof rawProjectId === "string" ? parseInt(rawProjectId, 10) : rawProjectId;
3282
+ const latProjectIdEnv2 = config?.telemetry?.latitude?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
3283
+ const projectId = parseInt(process.env[latProjectIdEnv2] ?? "", 10);
3247
3284
  const rawPath = config?.telemetry?.latitude?.path ?? this.parsedAgent?.frontmatter.name ?? "agent";
3248
3285
  const path = rawPath.replace(/[^\w\-./]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "") || "agent";
3249
3286
  const conversationUuid = input.conversationId ?? (typeof input.parameters?.__activeConversationId === "string" ? input.parameters.__activeConversationId : void 0);
@@ -3641,7 +3678,6 @@ ${textContent}` };
3641
3678
  const modelInstance = this.modelProvider(modelName);
3642
3679
  const cachedMessages = addPromptCacheBreakpoints(coreMessages, modelInstance);
3643
3680
  const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
3644
- const latitudeApiKey = this.loadedConfig?.telemetry?.latitude?.apiKey;
3645
3681
  const result = await streamText({
3646
3682
  model: modelInstance,
3647
3683
  system: integrityPrompt,
@@ -3651,7 +3687,7 @@ ${textContent}` };
3651
3687
  abortSignal: input.abortSignal,
3652
3688
  ...typeof maxTokens === "number" ? { maxTokens } : {},
3653
3689
  experimental_telemetry: {
3654
- isEnabled: telemetryEnabled && !!latitudeApiKey
3690
+ isEnabled: telemetryEnabled && !!this.latitudeTelemetry
3655
3691
  }
3656
3692
  });
3657
3693
  let fullText = "";
@@ -4114,9 +4150,11 @@ var LatitudeCapture = class {
4114
4150
  projectId;
4115
4151
  path;
4116
4152
  constructor(config) {
4117
- this.apiKey = config?.apiKey ?? process.env.LATITUDE_API_KEY;
4118
- const rawProjectId = config?.projectId ?? process.env.LATITUDE_PROJECT_ID;
4119
- const projectIdNumber = typeof rawProjectId === "number" ? rawProjectId : rawProjectId ? Number.parseInt(rawProjectId, 10) : Number.NaN;
4153
+ const apiKeyEnv = config?.apiKeyEnv ?? "LATITUDE_API_KEY";
4154
+ this.apiKey = process.env[apiKeyEnv];
4155
+ const projectIdEnv = config?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
4156
+ const rawProjectId = process.env[projectIdEnv];
4157
+ const projectIdNumber = rawProjectId ? Number.parseInt(rawProjectId, 10) : Number.NaN;
4120
4158
  this.projectId = Number.isFinite(projectIdNumber) ? projectIdNumber : void 0;
4121
4159
  const rawPath = config?.path ?? process.env.LATITUDE_PATH ?? process.env.LATITUDE_DOCUMENT_PATH ?? config?.defaultPath;
4122
4160
  this.path = rawPath;
@@ -5024,15 +5062,18 @@ var createStateStore = (config, options) => {
5024
5062
  return new InMemoryStateStore(ttl);
5025
5063
  }
5026
5064
  if (provider === "upstash") {
5027
- const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? "";
5028
- const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? "";
5065
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
5066
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
5067
+ const url = process.env[urlEnv] ?? "";
5068
+ const token = process.env[tokenEnv] ?? "";
5029
5069
  if (url && token) {
5030
5070
  return new UpstashStateStore(url, token, ttl);
5031
5071
  }
5032
5072
  return new InMemoryStateStore(ttl);
5033
5073
  }
5034
5074
  if (provider === "redis") {
5035
- const url = config?.url ?? process.env.REDIS_URL ?? "";
5075
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
5076
+ const url = process.env[urlEnv] ?? "";
5036
5077
  if (url) {
5037
5078
  return new RedisLikeStateStore(url, ttl);
5038
5079
  }
@@ -5058,15 +5099,18 @@ var createConversationStore = (config, options) => {
5058
5099
  return new InMemoryConversationStore(ttl);
5059
5100
  }
5060
5101
  if (provider === "upstash") {
5061
- const url = config?.url ?? (process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL || "");
5062
- const token = config?.token ?? (process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN || "");
5102
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
5103
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
5104
+ const url = process.env[urlEnv] ?? "";
5105
+ const token = process.env[tokenEnv] ?? "";
5063
5106
  if (url && token) {
5064
5107
  return new UpstashConversationStore(url, token, workingDir, ttl, options?.agentId);
5065
5108
  }
5066
5109
  return new InMemoryConversationStore(ttl);
5067
5110
  }
5068
5111
  if (provider === "redis") {
5069
- const url = config?.url ?? process.env.REDIS_URL ?? "";
5112
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
5113
+ const url = process.env[urlEnv] ?? "";
5070
5114
  if (url) {
5071
5115
  return new RedisLikeConversationStore(url, workingDir, ttl, options?.agentId);
5072
5116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "redis": "^5.10.0",
32
32
  "yaml": "^2.4.0",
33
33
  "zod": "^3.22.0",
34
- "@poncho-ai/sdk": "1.0.2"
34
+ "@poncho-ai/sdk": "1.0.3"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/mustache": "^4.2.6",
package/src/config.ts CHANGED
@@ -7,8 +7,8 @@ import type { StateConfig } from "./state.js";
7
7
 
8
8
  export interface StorageConfig {
9
9
  provider?: "local" | "memory" | "redis" | "upstash" | "dynamodb";
10
- url?: string;
11
- token?: string;
10
+ urlEnv?: string;
11
+ tokenEnv?: string;
12
12
  table?: string;
13
13
  region?: string;
14
14
  ttl?:
@@ -76,6 +76,7 @@ export interface PonchoConfig extends McpConfig {
76
76
  required?: boolean;
77
77
  type?: "bearer" | "header" | "custom";
78
78
  headerName?: string;
79
+ tokenEnv?: string;
79
80
  validate?: (token: string, req?: unknown) => Promise<boolean> | boolean;
80
81
  };
81
82
  state?: {
@@ -85,12 +86,16 @@ export interface PonchoConfig extends McpConfig {
85
86
  };
86
87
  memory?: MemoryConfig;
87
88
  storage?: StorageConfig;
89
+ providers?: {
90
+ openai?: { apiKeyEnv?: string };
91
+ anthropic?: { apiKeyEnv?: string };
92
+ };
88
93
  telemetry?: {
89
94
  enabled?: boolean;
90
95
  otlp?: string;
91
96
  latitude?: {
92
- apiKey?: string;
93
- projectId?: string | number;
97
+ apiKeyEnv?: string;
98
+ projectIdEnv?: string;
94
99
  path?: string;
95
100
  documentPath?: string;
96
101
  };
@@ -130,8 +135,8 @@ export const resolveStateConfig = (
130
135
  if (config?.storage) {
131
136
  return {
132
137
  provider: config.storage.provider,
133
- url: config.storage.url,
134
- token: config.storage.token,
138
+ urlEnv: config.storage.urlEnv,
139
+ tokenEnv: config.storage.tokenEnv,
135
140
  table: config.storage.table,
136
141
  region: config.storage.region,
137
142
  ttl: resolveTtl(config.storage.ttl, "conversations"),
@@ -147,8 +152,8 @@ export const resolveMemoryConfig = (
147
152
  return {
148
153
  enabled: config.storage.memory?.enabled ?? config.memory?.enabled,
149
154
  provider: config.storage.provider,
150
- url: config.storage.url,
151
- token: config.storage.token,
155
+ urlEnv: config.storage.urlEnv,
156
+ tokenEnv: config.storage.tokenEnv,
152
157
  table: config.storage.table,
153
158
  region: config.storage.region,
154
159
  ttl: resolveTtl(config.storage.ttl, "memory"),
package/src/harness.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  type MemoryStore,
23
23
  } from "./memory.js";
24
24
  import { LocalMcpBridge } from "./mcp.js";
25
- import { createModelProvider, getModelContextWindow, type ModelProviderFactory } from "./model-factory.js";
25
+ import { createModelProvider, getModelContextWindow, type ModelProviderFactory, type ProviderConfig } from "./model-factory.js";
26
26
  import { buildSkillContextWindow, loadSkillMetadata } from "./skill-context.js";
27
27
  import { streamText, type ModelMessage } from "ai";
28
28
  import { addPromptCacheBreakpoints } from "./prompt-cache.js";
@@ -367,18 +367,51 @@ When configuring Latitude telemetry, use **exactly** these field names:
367
367
  telemetry: {
368
368
  enabled: true,
369
369
  latitude: {
370
- apiKey: process.env.LATITUDE_API_KEY, // NOT "apiKeyEnv"
371
- projectId: process.env.LATITUDE_PROJECT_ID, // string or number
372
- path: "your/prompt-path", // optional, defaults to agent name
370
+ apiKeyEnv: "LATITUDE_API_KEY", // env var name (default)
371
+ projectIdEnv: "LATITUDE_PROJECT_ID", // env var name (default)
372
+ path: "your/prompt-path", // optional, defaults to agent name
373
373
  },
374
374
  },
375
375
  \`\`\`
376
376
 
377
- - The field is \`apiKey\` (not \`apiKeyEnv\`, \`api_key\`, or \`key\`).
378
- - The field is \`projectId\` (not \`project_id\`, \`projectID\`, or \`project\`).
377
+ - \`apiKeyEnv\` specifies the environment variable name for the Latitude API key (defaults to \`"LATITUDE_API_KEY"\`).
378
+ - \`projectIdEnv\` specifies the environment variable name for the project ID (defaults to \`"LATITUDE_PROJECT_ID"\`).
379
+ - With defaults, you only need \`telemetry: { latitude: {} }\` if the env vars are already named \`LATITUDE_API_KEY\` and \`LATITUDE_PROJECT_ID\`.
379
380
  - \`path\` must only contain letters, numbers, hyphens, underscores, dots, and slashes.
380
381
  - For a generic OTLP endpoint instead: \`telemetry: { otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }\`.
381
- - Always read the env vars from \`process.env\` — do not hardcode secrets in \`poncho.config.js\`.
382
+
383
+ ## Credential Configuration Pattern
384
+
385
+ All credentials in \`poncho.config.js\` use the **env var name** pattern (\`*Env\` fields). Config specifies which environment variable to read — never the secret itself. Sensible defaults mean zero config when using conventional env var names.
386
+
387
+ \`\`\`javascript
388
+ // poncho.config.js — credentials use *Env fields with defaults
389
+ export default {
390
+ // Model provider API keys (optional, defaults shown)
391
+ providers: {
392
+ anthropic: { apiKeyEnv: "ANTHROPIC_API_KEY" },
393
+ openai: { apiKeyEnv: "OPENAI_API_KEY" },
394
+ },
395
+ auth: {
396
+ required: true,
397
+ tokenEnv: "PONCHO_AUTH_TOKEN", // default
398
+ },
399
+ storage: {
400
+ provider: "upstash",
401
+ urlEnv: "UPSTASH_REDIS_REST_URL", // default (falls back to KV_REST_API_URL)
402
+ tokenEnv: "UPSTASH_REDIS_REST_TOKEN", // default (falls back to KV_REST_API_TOKEN)
403
+ },
404
+ telemetry: {
405
+ latitude: {
406
+ apiKeyEnv: "LATITUDE_API_KEY", // default
407
+ projectIdEnv: "LATITUDE_PROJECT_ID", // default
408
+ },
409
+ },
410
+ messaging: [{ platform: "slack" }], // reads SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET by default
411
+ }
412
+ \`\`\`
413
+
414
+ Since all fields have defaults, you only need to specify \`*Env\` when your env var name differs from the convention.
382
415
 
383
416
  ## When users ask about customization:
384
417
 
@@ -393,6 +426,7 @@ telemetry: {
393
426
  - To scope tools to a skill: keep server config in \`poncho.config.js\`, add desired \`allowed-tools\`/ \`approval-required\` patterns in that skill's \`SKILL.md\`, and remove global \`AGENT.md\` patterns if you do not want global availability.
394
427
  - Do not invent unsupported top-level config keys (for example \`model\` in \`poncho.config.js\`). Keep existing config structure unless README/spec explicitly says otherwise.
395
428
  - Keep \`poncho.config.js\` valid JavaScript and preserve existing imports/types/comments. If there is a JSDoc type import, do not rewrite it to a different package name.
429
+ - Credentials always use \`*Env\` fields (env var names), never raw \`process.env.*\` values. For example, use \`apiKeyEnv: "MY_KEY"\` not \`apiKey: process.env.MY_KEY\`.
396
430
  - Preferred MCP config shape in \`poncho.config.js\`:
397
431
  \`mcp: [{ name: "linear", url: "https://mcp.linear.app/mcp", auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" } }]\`
398
432
  - If shell/CLI access exists, you can use \`poncho mcp add --url ... --name ... --auth-bearer-env ...\`, then \`poncho mcp tools list <server>\` and \`poncho mcp tools select <server>\`.
@@ -758,10 +792,8 @@ export class AgentHarness {
758
792
  this.registerConfiguredBuiltInTools(config);
759
793
  const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
760
794
  const memoryConfig = resolveMemoryConfig(config);
761
- // Only create modelProvider if one wasn't injected (for production use)
762
- // Tests can inject a mock modelProvider via constructor options
763
795
  if (!this.modelProviderInjected) {
764
- this.modelProvider = createModelProvider(provider);
796
+ this.modelProvider = createModelProvider(provider, config?.providers);
765
797
  }
766
798
  const bridge = new LocalMcpBridge(config);
767
799
  this.mcpBridge = bridge;
@@ -793,14 +825,13 @@ export class AgentHarness {
793
825
  // Creating a new LatitudeTelemetry per run would break on the second call
794
826
  // because @opentelemetry/api silently ignores repeated global registrations.
795
827
  const telemetryEnabled = config?.telemetry?.enabled !== false;
796
- const latitudeApiKey = config?.telemetry?.latitude?.apiKey;
797
- const rawProjectId = config?.telemetry?.latitude?.projectId;
798
- const latitudeProjectId = typeof rawProjectId === 'string' ? parseInt(rawProjectId, 10) : rawProjectId;
799
828
  const latitudeBlock = config?.telemetry?.latitude;
829
+ const latApiKeyEnv = latitudeBlock?.apiKeyEnv ?? "LATITUDE_API_KEY";
830
+ const latProjectIdEnv = latitudeBlock?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
831
+ const latitudeApiKey = process.env[latApiKeyEnv];
832
+ const rawProjectId = process.env[latProjectIdEnv];
833
+ const latitudeProjectId = rawProjectId ? parseInt(rawProjectId, 10) : undefined;
800
834
  if (telemetryEnabled && latitudeApiKey && latitudeProjectId) {
801
- // Surface genuine OTLP export failures. Suppress "duplicate registration"
802
- // errors that fire when the dev server re-initializes the harness — the
803
- // first registration persists and telemetry keeps working.
804
835
  diag.setLogger(
805
836
  {
806
837
  error: (msg, ...args) => {
@@ -817,16 +848,10 @@ export class AgentHarness {
817
848
  this.latitudeTelemetry = new LatitudeTelemetry(latitudeApiKey, { disableBatch: true });
818
849
  } else if (telemetryEnabled && latitudeBlock && (!latitudeApiKey || !latitudeProjectId)) {
819
850
  const missing: string[] = [];
820
- if (!latitudeApiKey) missing.push("apiKey");
821
- if (!latitudeProjectId) missing.push("projectId");
822
- const unknownKeys = Object.keys(latitudeBlock).filter(
823
- (k) => !["apiKey", "projectId", "path", "documentPath"].includes(k),
824
- );
825
- const hint = unknownKeys.length > 0
826
- ? ` (found unknown key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.join(", ")} – did you mean "apiKey"?)`
827
- : "";
851
+ if (!latitudeApiKey) missing.push(`${latApiKeyEnv} env var`);
852
+ if (!latitudeProjectId) missing.push(`${latProjectIdEnv} env var`);
828
853
  console.warn(
829
- `[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}${hint}. Traces will NOT be sent.`,
854
+ `[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`,
830
855
  );
831
856
  }
832
857
  }
@@ -858,8 +883,8 @@ export class AgentHarness {
858
883
  const telemetry = this.latitudeTelemetry;
859
884
 
860
885
  if (telemetry) {
861
- const rawProjectId = config?.telemetry?.latitude?.projectId;
862
- const projectId = (typeof rawProjectId === 'string' ? parseInt(rawProjectId, 10) : rawProjectId) as number;
886
+ const latProjectIdEnv2 = config?.telemetry?.latitude?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
887
+ const projectId = parseInt(process.env[latProjectIdEnv2] ?? "", 10) as number;
863
888
  const rawPath = config?.telemetry?.latitude?.path ?? this.parsedAgent?.frontmatter.name ?? 'agent';
864
889
  // Sanitize path for Latitude's DOCUMENT_PATH_REGEXP: /^([\w-]+\/)*([\w-.])+$/
865
890
  const path = rawPath.replace(/[^\w\-./]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'agent';
@@ -1340,9 +1365,7 @@ ${boundedMainMemory.trim()}`
1340
1365
  const modelInstance = this.modelProvider(modelName);
1341
1366
  const cachedMessages = addPromptCacheBreakpoints(coreMessages, modelInstance);
1342
1367
 
1343
- // Stream response using Vercel AI SDK with telemetry enabled
1344
1368
  const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
1345
- const latitudeApiKey = this.loadedConfig?.telemetry?.latitude?.apiKey;
1346
1369
 
1347
1370
  const result = await streamText({
1348
1371
  model: modelInstance,
@@ -1353,7 +1376,7 @@ ${boundedMainMemory.trim()}`
1353
1376
  abortSignal: input.abortSignal,
1354
1377
  ...(typeof maxTokens === "number" ? { maxTokens } : {}),
1355
1378
  experimental_telemetry: {
1356
- isEnabled: telemetryEnabled && !!latitudeApiKey,
1379
+ isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
1357
1380
  },
1358
1381
  });
1359
1382
  // Stream text chunks — enforce overall run timeout per chunk.
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  export interface LatitudeCaptureConfig {
13
- apiKey?: string;
14
- projectId?: string | number;
13
+ apiKeyEnv?: string;
14
+ projectIdEnv?: string;
15
15
  path?: string;
16
16
  defaultPath?: string;
17
17
  }
@@ -26,15 +26,14 @@ export class LatitudeCapture {
26
26
  private readonly path?: string;
27
27
 
28
28
  constructor(config?: LatitudeCaptureConfig) {
29
- this.apiKey = config?.apiKey ?? process.env.LATITUDE_API_KEY;
29
+ const apiKeyEnv = config?.apiKeyEnv ?? "LATITUDE_API_KEY";
30
+ this.apiKey = process.env[apiKeyEnv];
30
31
 
31
- const rawProjectId = config?.projectId ?? process.env.LATITUDE_PROJECT_ID;
32
- const projectIdNumber =
33
- typeof rawProjectId === "number"
34
- ? rawProjectId
35
- : rawProjectId
36
- ? Number.parseInt(rawProjectId, 10)
37
- : Number.NaN;
32
+ const projectIdEnv = config?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
33
+ const rawProjectId = process.env[projectIdEnv];
34
+ const projectIdNumber = rawProjectId
35
+ ? Number.parseInt(rawProjectId, 10)
36
+ : Number.NaN;
38
37
  this.projectId = Number.isFinite(projectIdNumber) ? projectIdNumber : undefined;
39
38
 
40
39
  const rawPath =
package/src/memory.ts CHANGED
@@ -17,8 +17,8 @@ export interface MainMemory {
17
17
  export interface MemoryConfig {
18
18
  enabled?: boolean;
19
19
  provider?: StateProviderName;
20
- url?: string;
21
- token?: string;
20
+ urlEnv?: string;
21
+ tokenEnv?: string;
22
22
  table?: string;
23
23
  region?: string;
24
24
  ttl?: number;
@@ -495,16 +495,10 @@ export const createMemoryStore = (
495
495
  return new InMemoryMemoryStore(ttl);
496
496
  }
497
497
  if (provider === "upstash") {
498
- const url =
499
- config?.url ??
500
- process.env.UPSTASH_REDIS_REST_URL ??
501
- process.env.KV_REST_API_URL ??
502
- "";
503
- const token =
504
- config?.token ??
505
- process.env.UPSTASH_REDIS_REST_TOKEN ??
506
- process.env.KV_REST_API_TOKEN ??
507
- "";
498
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
499
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
500
+ const url = process.env[urlEnv] ?? "";
501
+ const token = process.env[tokenEnv] ?? "";
508
502
  if (url && token) {
509
503
  return new UpstashMemoryStore({
510
504
  baseUrl: url,
@@ -516,7 +510,8 @@ export const createMemoryStore = (
516
510
  return new InMemoryMemoryStore(ttl);
517
511
  }
518
512
  if (provider === "redis") {
519
- const url = config?.url ?? process.env.REDIS_URL ?? "";
513
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
514
+ const url = process.env[urlEnv] ?? "";
520
515
  if (url) {
521
516
  return new RedisMemoryStore({
522
517
  url,
@@ -49,23 +49,30 @@ export const getModelContextWindow = (modelName: string): number => {
49
49
  return best ? MODEL_CONTEXT_WINDOWS[best]! : DEFAULT_CONTEXT_WINDOW;
50
50
  };
51
51
 
52
+ export interface ProviderConfig {
53
+ openai?: { apiKeyEnv?: string };
54
+ anthropic?: { apiKeyEnv?: string };
55
+ }
56
+
52
57
  /**
53
- * Creates a model provider factory for the specified AI provider
54
- * @param provider - The provider name ('openai' or 'anthropic')
55
- * @returns A function that takes a model name and returns a LanguageModel instance
58
+ * Creates a model provider factory for the specified AI provider.
59
+ * API keys are read from environment variables; override the env var
60
+ * name via the `providers` config in `poncho.config.js`.
56
61
  */
57
- export const createModelProvider = (provider?: string): ModelProviderFactory => {
62
+ export const createModelProvider = (provider?: string, config?: ProviderConfig): ModelProviderFactory => {
58
63
  const normalized = (provider ?? "anthropic").toLowerCase();
59
64
 
60
65
  if (normalized === "openai") {
66
+ const apiKeyEnv = config?.openai?.apiKeyEnv ?? "OPENAI_API_KEY";
61
67
  const openai = createOpenAI({
62
- apiKey: process.env.OPENAI_API_KEY,
68
+ apiKey: process.env[apiKeyEnv],
63
69
  });
64
70
  return (modelName: string) => openai(modelName);
65
71
  }
66
72
 
73
+ const apiKeyEnv = config?.anthropic?.apiKeyEnv ?? "ANTHROPIC_API_KEY";
67
74
  const anthropic = createAnthropic({
68
- apiKey: process.env.ANTHROPIC_API_KEY,
75
+ apiKey: process.env[apiKeyEnv],
69
76
  });
70
77
  return (modelName: string) => anthropic(modelName);
71
78
  };
package/src/state.ts CHANGED
@@ -61,8 +61,8 @@ export type StateProviderName =
61
61
  export interface StateConfig {
62
62
  provider?: StateProviderName;
63
63
  ttl?: number;
64
- url?: string;
65
- token?: string;
64
+ urlEnv?: string;
65
+ tokenEnv?: string;
66
66
  table?: string;
67
67
  region?: string;
68
68
  }
@@ -1166,15 +1166,18 @@ export const createStateStore = (
1166
1166
  return new InMemoryStateStore(ttl);
1167
1167
  }
1168
1168
  if (provider === "upstash") {
1169
- const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? "";
1170
- const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? "";
1169
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
1170
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
1171
+ const url = process.env[urlEnv] ?? "";
1172
+ const token = process.env[tokenEnv] ?? "";
1171
1173
  if (url && token) {
1172
1174
  return new UpstashStateStore(url, token, ttl);
1173
1175
  }
1174
1176
  return new InMemoryStateStore(ttl);
1175
1177
  }
1176
1178
  if (provider === "redis") {
1177
- const url = config?.url ?? process.env.REDIS_URL ?? "";
1179
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
1180
+ const url = process.env[urlEnv] ?? "";
1178
1181
  if (url) {
1179
1182
  return new RedisLikeStateStore(url, ttl);
1180
1183
  }
@@ -1204,19 +1207,18 @@ export const createConversationStore = (
1204
1207
  return new InMemoryConversationStore(ttl);
1205
1208
  }
1206
1209
  if (provider === "upstash") {
1207
- const url =
1208
- config?.url ??
1209
- (process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL || "");
1210
- const token =
1211
- config?.token ??
1212
- (process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN || "");
1210
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
1211
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
1212
+ const url = process.env[urlEnv] ?? "";
1213
+ const token = process.env[tokenEnv] ?? "";
1213
1214
  if (url && token) {
1214
1215
  return new UpstashConversationStore(url, token, workingDir, ttl, options?.agentId);
1215
1216
  }
1216
1217
  return new InMemoryConversationStore(ttl);
1217
1218
  }
1218
1219
  if (provider === "redis") {
1219
- const url = config?.url ?? process.env.REDIS_URL ?? "";
1220
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
1221
+ const url = process.env[urlEnv] ?? "";
1220
1222
  if (url) {
1221
1223
  return new RedisLikeConversationStore(url, workingDir, ttl, options?.agentId);
1222
1224
  }
package/src/telemetry.ts CHANGED
@@ -4,8 +4,8 @@ export interface TelemetryConfig {
4
4
  enabled?: boolean;
5
5
  otlp?: string;
6
6
  latitude?: {
7
- apiKey?: string;
8
- projectId?: string | number;
7
+ apiKeyEnv?: string;
8
+ projectIdEnv?: string;
9
9
  path?: string;
10
10
  documentPath?: string;
11
11
  };
@@ -15,7 +15,7 @@ describe("telemetry emitter", () => {
15
15
 
16
16
  const emitter = new TelemetryEmitter({
17
17
  otlp: "https://otel.example.com/v1/logs",
18
- latitude: { apiKey: "lat_test" },
18
+ latitude: { apiKeyEnv: "LATITUDE_API_KEY" },
19
19
  });
20
20
 
21
21
  await expect(
@@ -32,8 +32,8 @@ describe("telemetry emitter", () => {
32
32
 
33
33
  const emitter = new TelemetryEmitter({
34
34
  latitude: {
35
- apiKey: "lat_test",
36
- projectId: "proj_123",
35
+ apiKeyEnv: "LATITUDE_API_KEY",
36
+ projectIdEnv: "LATITUDE_PROJECT_ID",
37
37
  documentPath: "agents/support-agent/AGENT.md",
38
38
  },
39
39
  });