From 3a34d416d43de5ef5434dfd0c009c038d6254842 Mon Sep 17 00:00:00 2001 From: ajspig Date: Thu, 3 Sep 2026 12:32:44 -0400 Subject: [PATCH] feat(harness-plugin-core): simplify telemetry headers and HOME-first config path - Identity is three headers: X-Honcho-Host `name/version (platform)`, X-Honcho-Plugin `name/version`, X-Honcho-Agent-Model. X-Honcho-Runtime is dropped. TelemetryIdentity gains `plugin` and `platform`. - configPath() resolves env.HOME (then USERPROFILE) before os.homedir(), since Bun's homedir() ignores in-process HOME changes and plugin tests were hitting the real ~/.honcho/config.json. - Extensionless internal imports so consumers no longer need allowImportingTsExtensions to type-check against the source exports. Co-Authored-By: Claude Fable 5.1 --- harness-plugin-core/README.md | 8 ++- harness-plugin-core/src/config.ts | 11 +++- harness-plugin-core/src/index.ts | 11 ++-- harness-plugin-core/src/telemetry.ts | 48 +++++++++----- harness-plugin-core/tests/config.test.ts | 12 +++- harness-plugin-core/tests/telemetry.test.ts | 70 ++++++++++----------- harness-plugin-core/tsconfig.json | 1 - 7 files changed, 99 insertions(+), 62 deletions(-) diff --git a/harness-plugin-core/README.md b/harness-plugin-core/README.md index 0b4525de..60cfefc5 100644 --- a/harness-plugin-core/README.md +++ b/harness-plugin-core/README.md @@ -43,11 +43,12 @@ Pass `telemetryHeaders()` as the SDK's `defaultHeaders`. Arbitrary headers are a | Header | Meaning | Example | |---|---|---| -| `X-Honcho-Host` | Agent host name, or `name/version` | `harness/1.3.13` | -| `X-Honcho-Plugin` | Honcho plugin version | `0.1.3` | -| `X-Honcho-Runtime` | This package's version (always sent) | `0.1.0` | +| `X-Honcho-Host` | Host harness, `name/version (platform)` | `harness/2.1.3 (darwin)` | +| `X-Honcho-Plugin` | Honcho integration, `name/version` | `harness-honcho/0.2.11` | | `X-Honcho-Agent-Model` | The agent's completion model, not a Honcho model | `claude-sonnet-4-5` | +Omit `hostVersion` when the harness does not expose it; `platform` defaults to `process.platform`. + ```ts import { Honcho } from '@honcho-ai/sdk' import { loadConfig, setTelemetryHeaders, telemetryHeaders } from '@honcho-ai/harness-plugin-core' @@ -61,6 +62,7 @@ const honcho = new Honcho({ defaultHeaders: telemetryHeaders({ host: 'harness', hostVersion: '1.3.13', + plugin: 'harness-honcho', pluginVersion: '0.1.3', model: 'claude-sonnet-4-5', }), diff --git a/harness-plugin-core/src/config.ts b/harness-plugin-core/src/config.ts index 126cdb78..2bc9b88f 100644 --- a/harness-plugin-core/src/config.ts +++ b/harness-plugin-core/src/config.ts @@ -216,8 +216,17 @@ export function resolveConfig( } } +/** + * `HONCHO_CONFIG_PATH` → `$HOME/.honcho/config.json` (`USERPROFILE` on Windows). + * + * `env.HOME` is consulted before `os.homedir()` because Bun's `homedir()` ignores + * in-process changes to `process.env.HOME`, so tests that redirect HOME would + * otherwise read and write the real config file. + */ export function configPath(env: NodeJS.Dict = process.env): string { - return env.HONCHO_CONFIG_PATH || join(homedir(), '.honcho', 'config.json') + if (env.HONCHO_CONFIG_PATH) return env.HONCHO_CONFIG_PATH + const home = env.HOME || env.USERPROFILE || homedir() + return join(home, '.honcho', 'config.json') } export function loadConfig(opts: { diff --git a/harness-plugin-core/src/index.ts b/harness-plugin-core/src/index.ts index ab47a2b3..4f234eee 100644 --- a/harness-plugin-core/src/index.ts +++ b/harness-plugin-core/src/index.ts @@ -7,7 +7,7 @@ export { resolveConfig, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, -} from './config.ts' +} from './config' export type { AuthConfig, @@ -15,15 +15,16 @@ export type { HostBlock, ResolvedConfig, RootConfig, -} from './config.ts' +} from './config' export { + hostHeaderValue, + pluginHeaderValue, telemetryHeaders, setTelemetryHeaders, HEADER_AGENT_MODEL, HEADER_HOST, HEADER_PLUGIN, - HEADER_RUNTIME, -} from './telemetry.ts' +} from './telemetry' -export type { TelemetryIdentity } from './telemetry.ts' +export type { TelemetryIdentity } from './telemetry' diff --git a/harness-plugin-core/src/telemetry.ts b/harness-plugin-core/src/telemetry.ts index eb8b2dc3..1eef504d 100644 --- a/harness-plugin-core/src/telemetry.ts +++ b/harness-plugin-core/src/telemetry.ts @@ -1,12 +1,14 @@ -import { version } from './index.ts' - /** Optional identity a host plugin knows at Honcho-client construction time. */ export interface TelemetryIdentity { - /** Host app name, e.g. `cursor`, `opencode`. */ + /** Host harness name, e.g. `harness`. */ host?: string - /** Host app version, e.g. `2026.8.1`. */ + /** Host harness version, e.g. `2.1.3`. Omit when the harness does not expose it. */ hostVersion?: string - /** Honcho plugin version, e.g. `0.1.2`. */ + /** OS platform. Defaults to `process.platform`. */ + platform?: string + /** Integration (plugin) name, e.g. `harness-honcho`. */ + plugin?: string + /** Integration version, e.g. `0.2.11`. */ pluginVersion?: string /** Agent completion model, e.g. `claude-sonnet-4-5`. Not a Honcho deriver/dialectic model. */ model?: string @@ -14,7 +16,6 @@ export interface TelemetryIdentity { export const HEADER_HOST = 'X-Honcho-Host' export const HEADER_PLUGIN = 'X-Honcho-Plugin' -export const HEADER_RUNTIME = 'X-Honcho-Runtime' export const HEADER_AGENT_MODEL = 'X-Honcho-Agent-Model' function sanitize(value: unknown): string | undefined { @@ -23,24 +24,39 @@ function sanitize(value: unknown): string | undefined { return s || undefined } -function hostValue(id: TelemetryIdentity): string | undefined { - const name = sanitize(id.host) - const ver = sanitize(id.hostVersion) - if (name && ver) return `${name}/${ver}` - return name || ver +/** A `name/version` product token. Characters that would break parsing become `-`. */ +function token(name: unknown, ver: unknown): string | undefined { + const clean = (v: unknown) => sanitize(v)?.replace(/[\s()/;]+/g, '-') + const n = clean(name) + const v = clean(ver) + if (n && v) return `${n}/${v}` + return n || v +} + +/** `X-Honcho-Host` value: `harness/2.1.3 (darwin)`. Undefined when the host is unknown. */ +export function hostHeaderValue(id: TelemetryIdentity = {}): string | undefined { + const host = token(id.host, id.hostVersion) + if (!host) return undefined + const platform = token(id.platform ?? process.platform, undefined) + return platform ? `${host} (${platform})` : host +} + +/** `X-Honcho-Plugin` value: `harness-honcho/0.2.11`. Undefined when the plugin is unknown. */ +export function pluginHeaderValue(id: TelemetryIdentity = {}): string | undefined { + return token(id.plugin, id.pluginVersion) } /** - * Headers to pass as the SDK's `defaultHeaders`. Missing fields are omitted. - * `X-Honcho-Runtime` is always this package's version. + * Headers to pass as the SDK's `defaultHeaders`. Fields are omitted when unknown, so a + * partial identity (e.g. just `model`) only touches the headers it names. */ export function telemetryHeaders( id: TelemetryIdentity = {}, extra?: Record ): Record { - const headers: Record = { [HEADER_RUNTIME]: version } - const host = hostValue(id) - const plugin = sanitize(id.pluginVersion) + const headers: Record = {} + const host = hostHeaderValue(id) + const plugin = pluginHeaderValue(id) const model = sanitize(id.model) if (host) headers[HEADER_HOST] = host if (plugin) headers[HEADER_PLUGIN] = plugin diff --git a/harness-plugin-core/tests/config.test.ts b/harness-plugin-core/tests/config.test.ts index 2c1b6a3e..e091f6df 100644 --- a/harness-plugin-core/tests/config.test.ts +++ b/harness-plugin-core/tests/config.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'bun:test' -import { normalizeBaseUrl, resolveConfig } from '../src/index.ts' +import { homedir } from 'node:os' +import { join } from 'node:path' +import { configPath, normalizeBaseUrl, resolveConfig } from '../src/index' const emptyEnv = {} @@ -69,3 +71,11 @@ describe('resolveConfig', () => { expect(cfg.workspace).toBe('my-host') }) }) + +describe('configPath', () => { + test('HONCHO_CONFIG_PATH, then $HOME, then os.homedir()', () => { + expect(configPath({ HONCHO_CONFIG_PATH: '/x/cfg.json', HOME: '/h' })).toBe('/x/cfg.json') + expect(configPath({ HOME: '/scratch' })).toBe('/scratch/.honcho/config.json') + expect(configPath({})).toBe(join(homedir(), '.honcho', 'config.json')) + }) +}) diff --git a/harness-plugin-core/tests/telemetry.test.ts b/harness-plugin-core/tests/telemetry.test.ts index 170c9e7a..fa67e489 100644 --- a/harness-plugin-core/tests/telemetry.test.ts +++ b/harness-plugin-core/tests/telemetry.test.ts @@ -3,54 +3,54 @@ import { HEADER_AGENT_MODEL, HEADER_HOST, HEADER_PLUGIN, - HEADER_RUNTIME, setTelemetryHeaders, telemetryHeaders, - version, -} from '../src/index.ts' +} from '../src/index' describe('telemetryHeaders', () => { - test('empty identity still sends the runtime version', () => { - expect(telemetryHeaders()).toEqual({ [HEADER_RUNTIME]: version }) - }) - - test('maps identity to headers', () => { - expect( - telemetryHeaders({ - host: 'opencode', - hostVersion: '1.3.13', - pluginVersion: '0.1.3', - model: 'claude-sonnet-4-5', - }) - ).toEqual({ - [HEADER_RUNTIME]: version, - [HEADER_HOST]: 'opencode/1.3.13', - [HEADER_PLUGIN]: '0.1.3', + test('maps identity to the three headers', () => { + const headers = telemetryHeaders({ + host: 'harness', + hostVersion: '2.1.3', + platform: 'darwin', + plugin: 'harness-honcho', + pluginVersion: '0.2.11', + model: 'claude-sonnet-4-5', + }) + expect(headers).toEqual({ + [HEADER_HOST]: 'harness/2.1.3 (darwin)', + [HEADER_PLUGIN]: 'harness-honcho/0.2.11', [HEADER_AGENT_MODEL]: 'claude-sonnet-4-5', }) }) - test('merges extra headers last, skipping blanks', () => { - const headers = telemetryHeaders({ host: 'codex', pluginVersion: '0.1.1' }, { - 'X-Custom': 'yes', + test('omits unknown fields and defaults platform', () => { + expect(telemetryHeaders()).toEqual({}) + expect(telemetryHeaders({ host: 'harness' })).toEqual({ + [HEADER_HOST]: `harness (${process.platform})`, + }) + }) + + test('strips separators that would break parsing', () => { + expect(telemetryHeaders({ host: 'a b;(c)/d', hostVersion: '1\r\n2', platform: 'darwin' })).toEqual({ + [HEADER_HOST]: 'a-b-c-d/1-2 (darwin)', + }) + }) + + test('extra headers win, blanks are dropped', () => { + const headers = telemetryHeaders({ plugin: 'harness-honcho' }, { [HEADER_PLUGIN]: 'override', 'X-Empty': ' ', }) - expect(headers[HEADER_HOST]).toBe('codex') - expect(headers[HEADER_PLUGIN]).toBe('override') - expect(headers['X-Custom']).toBe('yes') - expect(headers).not.toHaveProperty('X-Empty') + expect(headers).toEqual({ [HEADER_PLUGIN]: 'override' }) }) }) -describe('setTelemetryHeaders', () => { - test('mutates an existing header map in place', () => { - const headers = telemetryHeaders({ host: 'cursor', pluginVersion: '0.1.2' }) - const returned = setTelemetryHeaders(headers, { model: 'claude-opus-4' }) - expect(returned).toBe(headers) - expect(headers[HEADER_HOST]).toBe('cursor') - expect(headers[HEADER_PLUGIN]).toBe('0.1.2') - expect(headers[HEADER_RUNTIME]).toBe(version) - expect(headers[HEADER_AGENT_MODEL]).toBe('claude-opus-4') +test('setTelemetryHeaders updates only the named fields in place', () => { + const headers = telemetryHeaders({ plugin: 'harness-honcho', pluginVersion: '0.1.2' }) + expect(setTelemetryHeaders(headers, { model: 'claude-opus-4' })).toBe(headers) + expect(headers).toEqual({ + [HEADER_PLUGIN]: 'harness-honcho/0.1.2', + [HEADER_AGENT_MODEL]: 'claude-opus-4', }) }) diff --git a/harness-plugin-core/tsconfig.json b/harness-plugin-core/tsconfig.json index 96d10fea..be81d664 100644 --- a/harness-plugin-core/tsconfig.json +++ b/harness-plugin-core/tsconfig.json @@ -3,7 +3,6 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "noEmit": true, "strict": true, "skipLibCheck": true,