@agentworkforce/harness-kit

中文文档
0.19.0发布于 2026/05/09TypeScriptES Module构建来源已验证近 30 天下载 1.1万

Composable primitives for spawning a persona's harness (claude, codex, opencode) with its MCP servers, env vars, and permissions wired up correctly.

原始使用文档
文档目录11 节
  1. Install
  2. What's in the box
  3. buildInteractiveSpec(input) — translate a persona to an int…
  4. useRunnablePersona(intent) — run a persona non-interactively
  5. Claude harness guarantees
  6. Codex / opencode
  7. Env reference resolution
  8. Two resolution policies
  9. Persona input rendering
  10. API surface
  11. Status

@agentworkforce/harness-kit

Composable primitives for spawning a persona's harness (claude, codex, opencode) with its MCP servers, env vars, and permissions wired up correctly.

This is the layer that @agentworkforce/cli sits on top of. If you're building your own orchestrator on top of @agentworkforce/workload-router and want the same behaviors the CLI provides — env-ref resolution, MCP isolation, permission flag translation — depend on this package rather than reimplementing them.

The router (@agentworkforce/workload-router) models what a persona is. This kit models how to launch it on a given harness. Both are harness-agnostic on their own; the per-harness knowledge lives here.

Install

pnpm add @agentworkforce/harness-kit @agentworkforce/workload-router

What's in the box

buildInteractiveSpec(input) — translate a persona to an interactive argv

Takes the fields off a PersonaSelection (harness, model, systemPrompt, harnessSettings, mcpServers, permissions) and returns {bin, args, initialPrompt, warnings}. Pure — no I/O, no stderr writes. Warnings are returned so your caller routes them wherever makes sense.

import { resolvePersona } from '@agentworkforce/workload-router';
import {
  buildInteractiveSpec,
  resolveMcpServersLenient,
  resolveStringMapLenient,
  formatDropWarnings
} from '@agentworkforce/harness-kit';
import { spawn } from 'node:child_process';

const selection = resolvePersona('persona-authoring');

// Resolve env + MCP refs against the current process environment. Missing
// refs don't throw — they come back on `.dropped` so you can warn the user.
const envResolution = resolveStringMapLenient(selection.env, process.env, 'env');
const mcpResolution = resolveMcpServersLenient(selection.mcpServers, process.env);

const warnings = formatDropWarnings(
  envResolution.dropped,
  mcpResolution.dropped,
  mcpResolution.droppedServers
);
for (const w of warnings) console.warn(w);

// Build the exec spec.
const spec = buildInteractiveSpec({
  harness: selection.runtime.harness,
  personaId: selection.personaId,
  model: selection.runtime.model,
  systemPrompt: selection.runtime.systemPrompt,
  harnessSettings: selection.runtime.harnessSettings,
  mcpServers: mcpResolution.servers,
  permissions: selection.permissions
});
for (const w of spec.warnings) console.warn(w);

// Spawn the harness.
const args = spec.initialPrompt ? [...spec.args, spec.initialPrompt] : [...spec.args];
spawn(spec.bin, args, {
  stdio: 'inherit',
  env: { ...process.env, ...(envResolution.value ?? {}) }
});

useRunnablePersona(intent) — run a persona non-interactively

For orchestrators that need a programmatic sendMessage() surface, the kit also exposes a thin runner around the same router + harness translation path. It resolves the persona, launches the selected harness in non-interactive mode, captures stdout/stderr, reports progress chunks, supports cancellation and timeouts, and returns a stable execution result.

useRunnablePersona follows the router's internal built-in resolver. For optional pack/local personas, resolve a PersonaSelection through your source cascade and call useRunnableSelection(selection).

import { useRunnablePersona } from '@agentworkforce/harness-kit';

const persona = useRunnablePersona('persona-authoring');
const run = persona.sendMessage('Draft a persona for workflow artifact writing.', {
  workingDirectory: process.cwd(),
  name: 'persona-author',
  timeoutSeconds: persona.selection.runtime.harnessSettings.timeoutSeconds,
  inputs: {
    TARGET_DIR: '.agentworkforce/workforce/personas',
    CREATE_MODE: 'local',
    TASK_DESCRIPTION: 'Write a workflow artifact as structured JSON.'
  },
  onProgress: (chunk) => process.stderr.write(chunk.text)
});

const result = await run;
if (result.status !== 'completed') {
  throw new Error(result.stderr || `persona run failed: ${result.status}`);
}
console.log(result.output);

If the selected persona declares inputs, sendMessage(..., { inputs }) resolves those values before spawn, substitutes $NAME / ${NAME} in the system prompt, and injects the resolved values into the child process env. Resolution uses explicit inputs, then process.env[spec.env ?? NAME], then default, and throws when a required input is still unset.

The runner maps harnesses to their non-interactive command shapes: claude --print, codex exec, and opencode run. It writes generated config files such as opencode.json only for the duration of the child process and restores or removes them afterward. Skill installation is opt-in with installSkills: true; callers that need stronger filesystem isolation should keep using a mount/sandbox layer around the runner.

Claude harness guarantees

When harness === 'claude', buildInteractiveSpec always emits both:

  • --mcp-config '{"mcpServers": …}' — even if empty
  • --strict-mcp-config — forces Claude Code to ignore user/project MCP sources

This means a persona session only sees MCP servers the persona itself declares. Your ~/.claude.json and any project .claude/ MCP config are invisible inside the session. That's the whole point of persona isolation; if you want a personal MCP in the session, declare it on the persona.

Codex / opencode

Current state: these harnesses don't expose runtime MCP injection or permission controls on their CLIs. buildInteractiveSpec carries the system prompt as the initial positional [PROMPT] argument (since neither has a --system-prompt flag) and returns a warning string if the persona declares mcpServers or permissions. The caller decides whether to print, fail, or continue.

Env reference resolution

The kit supports two forms of env references inside persona JSON:

FormSemantics
"$VAR"Whole-string reference. The entire value is replaced.
"Bearer ${VAR}"Braced; each ${VAR} is interpolated in place.

Unbraced $VAR mid-string is kept as a literal — this prevents a stray $ in a JSON value from accidentally being treated as a reference, and it keeps missing-var errors pointed at a specific field name.

Two resolution policies

Pick the one that matches your error-handling preference:

FunctionMissing ref →Use when
makeEnvRefResolver(env) / resolveStringMap(map, env, prefix)throws MissingEnvRefErrorYou want fail-fast — e.g. CI scripts where a missing secret is a configuration bug.
makeLenientResolver(env) / resolveStringMapLenient(map, env, prefix)returns {ok:false, field, ref} (or drops the entry on the Lenient map helper)You want graceful fallback — e.g. letting an MCP server authenticate via OAuth if the Bearer token isn't set.

The CLI uses the lenient path; it drops missing env entries and unset MCP headers with a warning, and only aborts if a structural field (url, command, any arg) can't be resolved.

Persona input rendering

Persona inputs are distinct from env references. Inputs are prompt-visible runtime values declared on a persona, such as TARGET_DIR, PACKAGE_NAME, or CREATE_MODE. Use them for non-secret launch context, not API keys.

import { renderPersonaInputs, resolvePersonaInputs } from '@agentworkforce/harness-kit';

const { values } = resolvePersonaInputs(
  {
    TARGET_DIR: { env: 'MY_TARGET_DIR', default: './out' },
    CREATE_MODE: { default: 'local' }
  },
  { TARGET_DIR: '/tmp/personas' },
  process.env
);

const systemPrompt = renderPersonaInputs(
  'Write to $TARGET_DIR using ${CREATE_MODE} mode.',
  values
);

resolvePersonaInputs fails hard for missing required inputs. That is intentional: unlike secret env refs, an input is usually structural context the persona needs to follow its contract.

API surface

// Persona inputs
export class MissingPersonaInputError extends Error { input: string; env: string }
export function resolvePersonaInputs(inputs, provided, processEnv): PersonaInputResolution
export function renderPersonaInputs(systemPrompt, values): string
export interface PersonaInputResolution { values: Record<string,string> }
export type PersonaInputValues = Record<string, string | number | boolean | null | undefined>

// Env refs
export class MissingEnvRefError extends Error { ref: string; referencedBy: string }
export function makeEnvRefResolver(env): (value, field) => string
export function makeLenientResolver(env): (value, field) => LenientResult
export function resolveStringMap(map, env, prefix): Record<string,string> | undefined
export function resolveStringMapLenient(map, env, prefix): { value, dropped: DroppedRef[] }
export type LenientResult = { ok: true; value: string } | { ok: false; field: string; ref: string }
export interface DroppedRef { field: string; ref: string }

// MCP
export function resolveMcpServersLenient(servers, env): McpResolution
export function formatDropWarnings(envDrops, mcpDrops, mcpServerDrops): string[]
export interface McpResolution {
  servers: Record<string, McpServerSpec> | undefined;
  dropped: DroppedRef[];
  droppedServers: DroppedMcpServer[];
}
export interface DroppedMcpServer { name: string; refs: string[] }

// Harness
export function buildInteractiveSpec(input: BuildInteractiveSpecInput): InteractiveSpec
export interface BuildInteractiveSpecInput {
  harness: Harness;
  personaId: string;
  model: string;
  systemPrompt: string;
  harnessSettings?: HarnessSettings;
  mcpServers?: Record<string, McpServerSpec>;
  permissions?: PersonaPermissions;
}
export interface InteractiveSpec {
  bin: string;
  args: readonly string[];
  initialPrompt: string | null;
  warnings: string[];
}

// Runnable personas
export function useRunnablePersona(intent, options?): RunnablePersonaContext
export function useRunnableSelection(selection, options?): RunnablePersonaContext
export interface RunnablePersonaContext {
  selection: PersonaSelection;
  install: PersonaInstallContext;
  sendMessage(task, options?): PersonaExecution;
}
export interface PersonaExecutionResult {
  status: 'completed' | 'failed' | 'cancelled' | 'timeout';
  output: string;
  stderr: string;
  exitCode: number | null;
  durationMs: number;
}

Status

Small, stable surface focused on the three things a harness spawner needs: resolve env refs, resolve MCP config, and build argv. The default exports are still pure when you use buildInteractiveSpec directly. The useRunnablePersona convenience is intentionally the small side-effecting layer for consumers that want the same harness knowledge plus a captured non-interactive child process.

版本与发布记录

@agentworkforce/harness-kit 共发布 24 个版本,首次发布于 2026/04/18;过去 12 个月发布了 24 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.19.0latest2026/05/096 个月前
0.18.02026/05/096 个月前
0.17.12026/05/096 个月前
0.17.02026/05/096 个月前
0.16.02026/05/086 个月前
0.15.12026/05/086 个月前

关于 @agentworkforce/harness-kit 的常见问题

@agentworkforce/harness-kit 是什么?

Composable primitives for spawning a persona's harness (claude, codex, opencode) with its MCP servers, env vars, and permissions wired up correctly.

如何安装 @agentworkforce/harness-kit?

在项目目录运行 npm install @agentworkforce/harness-kit,或使用 pnpm add / yarn add / bun add。

@agentworkforce/harness-kit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@agentworkforce/harness-kit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@agentworkforce/harness-kit 可以商用吗?

@agentworkforce/harness-kit 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@agentworkforce/harness-kit 的下载量有多少?

根据 npm 官方下载统计,@agentworkforce/harness-kit 在 2026/10/05 – 2026/11/03 这 30 天内被下载 10,635 次,日均约 355 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@agentworkforce/harness-kit 还在维护吗?

最新版本 0.19.0 发布于 2026/05/09(6 个月前);至今共发布 24 个版本,其中过去 12 个月发布了 24 个;首次发布于 2026/04/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。