DeepSeek Harness Plugin Config: Schema Validation, Secrets & HMR
This tutorial shows how to replace hardcoded values with a schema-driven configuration system for DeepSeek Harness plugins, covering TypeScript interfaces, Schemastery validation, cordis.yml setup, environment variable secrets, load-time validation, tool integration, common pitfalls, and HMR behavior.
1. Define Config Type and Schema
In Harness, configuration uses a TypeScript interface paired with a Schemastery schema. Edit scratch-plugin/src/wechat-publisher.ts:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'wechat-publisher'
export interface Config {
appId: string
appSecret: string
defaultAuthor: string
mode: 'browser' | 'api'
defaultDraft: boolean
timeoutMs: number
}
export const Config: Schema<Config> = Schema.object({
appId: Schema.string().required().description('公众号 AppID'),
appSecret: Schema.string().required().description('公众号 AppSecret'),
defaultAuthor: Schema.string().default('CodeToSuccess'),
mode: Schema.union(['browser', 'api'])
.default('browser')
.description('发布方式:browser=通过浏览器自动化操作公众号后台,api=通过已获得权限的官方接口发布'),
defaultDraft: Schema.boolean().default(true),
timeoutMs: Schema.number().default(30000),
})
export function apply(ctx: Context, config: Config) {
console.log('[wechat-publisher] mode:', config.mode)
console.log('[wechat-publisher] appId:', config.appId)
console.log('[wechat-publisher] author:', config.defaultAuthor)
}Two key points:
Config type provides TypeScript hints; Config Schema handles runtime validation.
Default values live only in the Schema (via .default(...)), not duplicated in business logic.
2. Pass Configuration in cordis.yml
Modify scratch-plugin/cordis.yml to add a config block:
- insert:
- id: wechat-publisher
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/wechat-publisher.ts'
config:
appId: 'wx1234567890abcdef'
appSecret: 'your-secret'
defaultAuthor: 'AI码到成功'
mode: 'browser'On restart the terminal prints the config values. Omitted fields ( defaultDraft, timeoutMs) are filled with schema defaults.
Secrets from Environment Variables
config:
appId: !!js process.env.WECHAT_APP_ID
appSecret: !!js process.env.WECHAT_APP_SECRET
defaultAuthor: 'AI码到成功'
mode: 'browser'This keeps appSecret out of the committed config file; secrets stay in deployment or local environment variables.
3. Configuration Errors Must Fail Loudly
Harness principle: invalid configuration should fail at plugin load, not halfway through execution.
Test by setting an invalid mode:
config:
appId: 'wx1234567890abcdef'
appSecret: 'your-secret'
mode: 'ftp' # ← not browser or apiStartup aborts with a validation error pointing to the offending field. Required fields ( appId, appSecret marked .required()) also cause immediate load failure — far friendlier than a runtime cannot read property of undefined.
Design Principle Checklist
No hardcoded tunables : Wrong: const TIMEOUT = 30000; Right: timeoutMs: Schema.number().default(30000) Config errors fail fast : Wrong: Crash at runtime; Right: Schema validation, fail at load
Single source of defaults : Wrong: Defaults in code + config; Right: Only in schema .default() Litmus test: Can you change the value in cordis.yml without touching code?
4. Wire Configuration into the Tool
The apply function receives config as its second argument; the tool closure accesses it directly:
export const inject = ['tools']
export function apply(ctx: Context, config: Config) {
ctx.tools.register(defineTool({
name: 'publish_article',
description: '将本地 Markdown 文件发布为微信公众号草稿。',
parameters: {
markdownPath: { type: 'string', required: true },
title: { type: 'string', required: true },
author: { type: 'string', required: false, description: '作者,默认取配置' },
},
output: {
schema: { type: 'object' },
render: (_args, value) => [{ type: 'text', text: value.message }],
},
async execute(args) {
const author = args.author || config.defaultAuthor
const mode = config.mode // read publish mode from config
console.log(`[wechat-publisher] publishing via ${mode}, author=${author}`)
return {
success: true,
message: `已通过 ${mode} 模式发布《${args.title}》(作者:${author})`
}
}
}))
}When the model omits author, the configured default author is used automatically.
5. Common Configuration Pitfalls
Don't export a plain object as Config. export const Config = { apiKey: '' } lacks the Standard Schema interface; Cordis will error at load. Must use Schema.object({...}).
Required fields use .required() ; optional fields get defaults. appId / appSecret are required — missing them blocks load. Fields with safe, explicit defaults (author, timeout) use .default() so the plugin runs unconfigured. Decision rule: if the plugin cannot start without the value, mark required.
Whether to set a default depends on business semantics, not merely "can it run".
Never hardcode or commit secrets. cordis.yml may hold appId, but appSecret should come from environment variables or a non-committed local config layer. The plugin only declares fields; it never stores secrets.
As configuration grows, group with nested Schema.object:
export interface Config {
credentials: {
appId: string
appSecret: string
}
publish: {
mode: 'browser' | 'api'
defaultDraft: boolean
defaultAuthor: string
}
timeoutMs: number
}
export const Config: Schema<Config> = Schema.object({
credentials: Schema.object({
appId: Schema.string().required().description('公众号 AppID'),
appSecret: Schema.string().required().description('公众号 AppSecret'),
}),
publish: Schema.object({
mode: Schema.union(['browser', 'api']).default('browser').description('发布方式'),
defaultDraft: Schema.boolean().default(true),
defaultAuthor: Schema.string().default('CodeToSuccess'),
}),
timeoutMs: Schema.number().default(30000),
})Corresponding cordis.yml structure:
config:
credentials:
appId: 'wx1234567890abcdef'
appSecret: 'your-secret'
publish:
mode: 'browser'
defaultDraft: true
defaultAuthor: 'AI码到成功'
timeoutMs: 30000Validation still works at each level.
6. HMR: When Does a Config Change Take Effect?
Cordis offers HMR (Hot Module Replacement), but HMR capability does not guarantee that every Harness launch mode will reload plugins on arbitrary config file changes.
Tested with:
pnpm dsh web --patch ./scratch-plugin/cordis.ymlChanging mode: 'browser' to mode: 'api' in the patched file did not re-run apply() or re-print the config logs. Therefore, no guarantee that config edits auto-apply in this mode.
What Is HMR Good For?
During plugin development, HMR enables:
Edit plugin
↓
Change detected
↓
Reload plugin
↓
New code / config activeThis avoids full Harness restarts on code changes in supported run modes .
For the dsh web --patch scenario used here, config change effectiveness must be verified empirically. If apply() doesn't re-run, the simplest reliable action is to restart Harness.
Key principle: never treat HMR as a prerequisite for business config activation. Configuration dictates runtime behavior; Schema validates legality; HMR is merely a developer-efficiency aid.
Summary
Config = TypeScript interface + same-named Schemastery schema; defaults in schema.
Users supply values in
cordis.yml config; missing fields fall back to defaults.
Schema validation runs at plugin load; invalid config fails immediately.
Design principles: no hardcoded tunables, loud config errors, single default location. apply(ctx, config) gives tools direct access to plugin configuration.
HMR exists but its applicability depends on the Harness run mode; don't rely on it for config propagation.
wechat-publisher progress: added appId, appSecret, mode, defaultAuthor. Next (final) episode: extract publish logic into a Service, broadcast results via events, and package as a distributable plugin.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
AI Code to Success
Focused on hardcore practical AI technologies (OpenClaw, ClaudeCode, LLMs, etc.) and HarmonyOS development. No hype—just real-world tips, pitfall chronicles, and productivity tools. Follow to transform workflows with code.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
