Expose audited hardcoded tunables as plugin config

The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.

- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
  readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
  read-render already documented that the consumer applies the caps, so
  they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
  schemastery default). Also fixes the stale GREP_LIMIT references in
  search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
  RunInternals.graceMs test seam is gone: graceMs is now a required
  SpawnSpec field filled from config, so tests exercise the real
  config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
  fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
  rollback-journal modes serve filesystems where WAL's shared-memory
  files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
  hook/result stderr summary. The duplicated summarize() helpers merge
  into hook-protocol's summarizeStderr(stderr, maxChars), beside the
  HookResultRecord field it feeds, with the bound parameterized the
  same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
  English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
  fires far too late). Also corrects the BasicCompactService class doc,
  which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
  FsIoInternals.streamMinSize seam — the read-routing bound lives in
  the consumer (tool-fs), where it is now config. This is item 1 of
  the proposed prune-write-only-fs-surface RFC, annotated accordingly.

Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
This commit is contained in:
Tianyi Cui
2026-07-04 17:37:23 +08:00
parent bee8132a7f
commit 774d460889
45 changed files with 592 additions and 171 deletions
+6 -7
View File
@@ -27,6 +27,7 @@ import {
matchesMatcher,
mergeHookOutputs,
runHook,
summarizeStderr,
type HookOutput,
type MatcherGroup,
type MergedHookOutcome,
@@ -49,12 +50,15 @@ export interface Config {
model?: string
/** Default per-hook timeout in ms when a hook sets none (Codex default: 600000). */
defaultTimeoutMs?: number
/** Character cap for the `hook/result` event's persisted stderr summary. */
stderrSummaryMaxChars?: number
}
export const Config: z<Config> = z.object({
configPath: z.string().required(),
model: z.string().default(''),
defaultTimeoutMs: z.number().default(600_000),
stderrSummaryMaxChars: z.number().default(500),
})
let handlerCounter = 0
@@ -64,12 +68,6 @@ function nextHandlerId(point: string): string {
const PLUGIN_SOURCE: MessageSource = { kind: 'plugin', plugin: 'hooks-codex' }
function summarize(stderr: string): string | undefined {
const t = stderr.trim()
if (t.length === 0) return undefined
return t.length > 500 ? t.slice(0, 500) + '…' : t
}
export function apply(ctx: Context, config: Config): void {
let parsed: CodexHookConfig = {}
try {
@@ -85,6 +83,7 @@ export function apply(ctx: Context, config: Config): void {
}
const defaultTimeoutMs = config.defaultTimeoutMs ?? 600_000
const stderrSummaryMaxChars = config.stderrSummaryMaxChars ?? 500
const model = config.model ?? ''
async function runPoint(
@@ -140,7 +139,7 @@ export function apply(ctx: Context, config: Config): void {
ctx.logger.warn(`hooks-codex: ${point} hook emitted a systemMessage, which is not yet surfaced (ignored)`)
}
if (session && opts.turn !== undefined) {
const stderrSummary = summarize(output.stderr)
const stderrSummary = summarizeStderr(output.stderr, stderrSummaryMaxChars)
appendHookResult(session, {
turn: opts.turn, point, handlerId,
decision: output.decision ?? (output.continue === false ? 'stop' : 'pass'),