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 <noreply@anthropic.com>
This commit is contained in:
parent
2ad56a4d71
commit
3a34d416d4
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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<string> = 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: {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = { [HEADER_RUNTIME]: version }
|
||||
const host = hostValue(id)
|
||||
const plugin = sanitize(id.pluginVersion)
|
||||
const headers: Record<string, string> = {}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue