feat(#476): unify callback auth — headers replace body/query - #3
Open
mindfn wants to merge 9 commits into
Open
Conversation
…body/query to HTTP headers Replaces per-handler invocationId/callbackToken validation with a single Fastify preHandler hook (registerCallbackAuthHook) that reads X-Invocation-Id and X-Callback-Token headers, verifies via InvocationRegistry, and decorates request.callbackAuth. Migrates 30+ callback handlers, schedule routes, and MCP server callbackPost/callbackGet to the new protocol. Deletes callbackAuthSchema (no longer needed). Updates 26 test files (~250 changes) to send auth as headers instead of body/query params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hook level Addresses gpt52 review P1: the preHandler silently ignored invalid callback headers, causing schedule routes to fall through to panel path instead of returning 401 (breaking zts212653#474 fail-closed semantics). Now: headers present + invalid → immediate 401 from preHandler. No headers → no-op (panel request). Removes dead INVALID_CALLBACK_CREDENTIALS code from schedule.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… plugin Codex review P1: callbackAuthRoutes was a sibling Fastify plugin without its own registerCallbackAuthHook, so request-permission and permission-status always returned 401 due to Fastify hook encapsulation. Fix: callbackAuthRoutes now accepts registry and registers its own hook. registerCallbackAuthHook is made idempotent (hasRequestDecorator guard). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blocker 1: preHandler now falls back to body/query credentials when headers are absent, with a deprecation log so existing MCP clients continue to work during the migration window. Blocker 2: outbox replay extracts invocationId/callbackToken from legacy entry bodies (pre-zts212653#476 format) into headers so the new preHandler accepts them. Includes regression tests for both paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ex P2) Partial legacy credentials (e.g. only invocationId in body without callbackToken) were silently treated as panel traffic. Now consistent with partial-header behavior: fail-closed → 401. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
May 9, 2026
#2 Startup alerts: ServiceStatusPanel now shows toast notifications when service start fails (error state or timeout after 120s), and when the start/install API returns a non-ok response. #8 Health check type gap: services without healthEndpoint now get a TCP port probe fallback — if the port is listening, status reports 'running' instead of permanently 'unknown'. #3 Install robustness: all 4 install scripts now upgrade pip before installing packages, and venv creation failures are caught with explicit error messages instead of silently continuing. [宪宪/Opus-46🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
May 10, 2026
#2 Startup alerts: ServiceStatusPanel now shows toast notifications when service start fails (error state or timeout after 120s), and when the start/install API returns a non-ok response. #8 Health check type gap: services without healthEndpoint now get a TCP port probe fallback — if the port is listening, status reports 'running' instead of permanently 'unknown'. #3 Install robustness: all 4 install scripts now upgrade pip before installing packages, and venv creation failures are caught with explicit error messages instead of silently continuing. [宪宪/Opus-46🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
May 13, 2026
#2 Startup alerts: ServiceStatusPanel now shows toast notifications when service start fails (error state or timeout after 120s), and when the start/install API returns a non-ok response. #8 Health check type gap: services without healthEndpoint now get a TCP port probe fallback — if the port is listening, status reports 'running' instead of permanently 'unknown'. #3 Install robustness: all 4 install scripts now upgrade pip before installing packages, and venv creation failures are caught with explicit error messages instead of silently continuing. [宪宪/Opus-46🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 12, 2026
…plification Bug #1: resolveMainRepoPath() now delegates to resolveStartupProjectRoot() instead of using `git worktree list`, which returned the git main worktree instead of the startup directory. Tests updated with mainProjectRoot isolation. Bug #2: SkillsDriftBanner effect now uses AbortController to cancel in-flight drift-check requests on unmount/re-render, preventing double API calls caused by React 18 StrictMode. Bug #3: Both SkillsDriftBanner and AllProjectsSyncBanner rewritten from modal popup to flat list format per 铲屎官 spec: "类型 - skillName 描述". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 16, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 16, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 16, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 17, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 17, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jun 17, 2026
铲屎官外网实测 #3 反馈: - 安装插件按钮添加 hover tooltip "点击上传插件安装包,支持上传 .tar.gz" - 扩展插件区新增插件开发文档链接(指向 im-connector-plugin-guide.md) - 文档链接带简要说明:"接口说明 · 配置参数 · 打包部署" 插件开发指南已存在(593行),覆盖完整接口、YAML manifest、 打包格式、安装流程、参考实现。feat doc 新增 AC-B10 追踪。 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 10, 2026
Eval cat for eval:harness-ledger had no access to GuardRejectionEventLog data before this change — unlike other eval domains whose data lives in git-accessible files, harness-ledger data is in Redis ZSET only. Without pre-injection, the cat made blind verdicts with no event context. KD-17 ruling (Fable): trigger produces normalized snapshot → eval cat sees real data in invocation → generator reads same stored snapshot by evalRunId. Single data source, zero drift between decision and artifact. Changes: - New: harness-ledger-snapshot-provider.ts — produces normalized guard-rejection-event snapshot, persists to run-snapshots/<evalRunId>.json - Rewritten: generator adapter reads stored snapshot by evalRunId instead of taking GuardRejectionEventLog directly (fail-closed on missing) - Modified: trigger-now.ts + eval-domain-daily.ts — pre-step snapshot production before eval cat invocation (manual=fail-closed, scheduled=fail-open) - Modified: eval-cat-invocation.ts — precomputedEvidence passthrough - Modified: types.ts — evalRunId on PromptSegmentsSourceSelector - Tests: 14/14 pass including KD-17 fail-closed + resolver round-trips [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 10, 2026
* feat(F257): wire eval:harness-ledger generator to publish_verdict pipeline Connect the harness-ledger eval domain end-to-end: generator adapter reads GuardRejectionEventLog.queryWindow() events, produces verdict markdown + bundle artifacts (snapshot/attribution/provenance) following the QC adapter pattern. Domain gated on Redis (guardRejectionLog). Four changes: 1. NEW harness-ledger-generator-adapter.ts — VerdictGenerator factory taking GuardRejectionEventLog as provider; discriminator check, window validation, event aggregation by kind/guard, Eval Hub compatible verdict markdown with YAML frontmatter 2. index.ts verdictGenerators wiring — conditional on guardRejectionLog 3. index.ts wiredPublishDomains — mirrors verdictGenerators condition 4. eval-harness-ledger.yaml enabled: true — producers and generator both wired, domain ready for weekly cron and manual trigger Tests: 10 new (harness-ledger-generator-adapter), 53 total F257 green. Build: tsc + biome clean. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): P1 Redis fail-closed + bundle schema compliance (codex review) Three fixes from codex (terra) review of PR #24: 1. Redis fail-closed: added queryWindowStrict() to GuardRejectionEventLog; adapter now uses strict path so Redis outage → 500 (not false zero-event verdict). Business-facing queryWindow remains fail-open. 2. Bundle schema compliance: attribution.findings now conforms to attributionFindingSchema (id, frictionSignal, attribution with evidence anchored to snapshot components, proposedAction). Evidence anchors validated by assertAttributionAnchors cross-ref against snapshot metrics. Changed window.durationDays → durationHours per bundleSnapshotSchema. 3. Domain disabled: reverted enabled: true → false until eval cat has data access (pre-injection snapshot or query tool) to avoid evidence-free weekly verdicts. Tests: +3 (ThrowingRedis regression, zero-events resolver round-trip, mixed-events resolver round-trip). All 13 pass. Resolver validates full schema + cross-reference chain end-to-end. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 snapshot-first pattern for eval cat data access (P1 #3) Eval cat for eval:harness-ledger had no access to GuardRejectionEventLog data before this change — unlike other eval domains whose data lives in git-accessible files, harness-ledger data is in Redis ZSET only. Without pre-injection, the cat made blind verdicts with no event context. KD-17 ruling (Fable): trigger produces normalized snapshot → eval cat sees real data in invocation → generator reads same stored snapshot by evalRunId. Single data source, zero drift between decision and artifact. Changes: - New: harness-ledger-snapshot-provider.ts — produces normalized guard-rejection-event snapshot, persists to run-snapshots/<evalRunId>.json - Rewritten: generator adapter reads stored snapshot by evalRunId instead of taking GuardRejectionEventLog directly (fail-closed on missing) - Modified: trigger-now.ts + eval-domain-daily.ts — pre-step snapshot production before eval cat invocation (manual=fail-closed, scheduled=fail-open) - Modified: eval-cat-invocation.ts — precomputedEvidence passthrough - Modified: types.ts — evalRunId on PromptSegmentsSourceSelector - Tests: 14/14 pass including KD-17 fail-closed + resolver round-trips [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 error path — no blind eval cat invocation on snapshot failure Terra's P1 (codex re-review df21b07): snapshot failure/absence still invoked the eval cat without evidence. KD-17 invariant requires "no snapshot → no invocation" — blind verdicts plus generator missing-run-id 500 is worse than skipping one eval cycle. Fixes: - eval-domain-daily.ts: snapshot error or provider absent → deliver SKIPPED message to domain thread + return (no cat invoke). "Fail-open" means no retry-storm, NOT invoking cat blind - trigger-now.ts: provider absent → 503 harness_ledger_snapshot_unavailable; snapshot throws → 503 harness_ledger_snapshot_failed. Cat never invoked - New helper: buildHarnessLedgerSnapshotSkippedMessage (same pattern as buildPublishPrereqSkippedMessage — stable header for log scrubbers) Tests (6 new, 51 total): - scheduled: provider absent → SKIPPED message, no invoke - scheduled: queryWindowStrict throw → SKIPPED message, no invoke - scheduled: success → evidence in content, cat invoked - manual: provider absent → 503 - manual: snapshot throw → 503 - manual: success → evidence in content, 200 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(F257): re-enable eval:harness-ledger domain (KD-17 data access resolved) KD-17 snapshot-first pattern gives eval cat data access to guard rejection events — the blocker that kept the domain disabled is resolved. Re-enable to allow weekly cron admission and baseline accumulation. Changes: - eval-harness-ledger.yaml: remove `enabled: false`, update comment to document KD-17 snapshot-first resolution - Weekly gate test: verify eval:harness-ledger appears in admission alongside capability-wakeup and sop 51/51 tests green including real registry admission coverage. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): P1-A MCP evalRunId schema + P1-B path/window validation, guardId removal Round-3 codex review fixes: P1-A: evalRunId was missing from MCP Zod schema — tool would strip it before reaching API. Added required evalRunId with regex validation (hlr-<ts>-<hex8>) to promptSegmentsSourceRefsShape + TypeScript union. P1-B: Three-layer defense-in-depth for evalRunId path safety: 1. MCP Zod: regex /^hlr-\d+-[a-f0-9]{8}$/ 2. API validation: format check + required (was optional) 3. Generator adapter: format re-check before filesystem access Window mismatch check: selector window must === stored snapshot window (KD-17 single-source invariant — prevents cat claiming different window than what snapshot actually covers). guardId removed from v1: was a pass-through label, not a real filter. Removed from types, validation, generator adapter bundle, and invocation instructions per codex option-1. Tests: replaced guardId-filter test with window-mismatch rejection + evalRunId format/traversal defense tests. All evalRunIds updated to format-valid (hlr-<ts>-<hex8>). Shared stable window constants prevent test drift from Date.now() timing. 52/52 green. Why: codex terra identified that evalRunId was silently dropped at the MCP layer (Zod strips unknown fields), and the generator had no defense against path traversal via malicious evalRunId. These fixes close the schema gap and add the path-safety invariant at all three validation layers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 last-hop — exact sourceRefs JSON in snapshot injection Terra round-4 P1: eval cat received window as ISO text but generator enforces exact epoch-ms match. Cat would need to ISO→epoch convert, risking 1ms drift → stable harness_ledger_adapter_window_mismatch. Fix: snapshot provider now emits a fenced JSON block with the exact sourceRefs object (kind, windowStartMs, windowEndMs, evalRunId) — all values the eval cat needs to copy verbatim. Instructions updated to say "copy the exact sourceRefs JSON" instead of just "copy evalRunId". Both paths (scheduled + manual trigger) inject the same summary with the complete sourceRefs block. Tests: strengthened scheduled + manual success assertions — verify delivered content includes fenced sourceRefs JSON with all three required fields (windowStartMs as number, windowEndMs as number, evalRunId matching safe format regex). 52/52 green. Why: KD-17's single-source invariant requires zero drift between trigger, eval cat, and generator. The window mismatch check added in a8ef88c would have made every real publish_verdict call 500 because the cat had no way to provide exact epoch-ms values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 10, 2026
…ire→host mapping, closed unions
P1-1: Exact lookup port with capability-specific AnchorLookupCapable interface.
isAnchorLookupCapable() type guard links capabilities().anchorLookup boolean
to callable getByAnchor(). Coordinator handles timeout/unhealthy/contract-error
fallback with same semantics as search; null = legal miss, no fallback.
P1-2: Canonical parser validates input — domain allowlist, non-empty nativeId,
guarded decodeOpaque with try/catch. Grammar opaqueNativeId uses `+` (non-empty).
6 negative conformance vectors added (no separator, unknown domain, empty nativeId,
empty domain, malformed percent-encoding, empty string).
P1-3: Wire→Host degrade meta adapter explicitly mapped. WireDegradeReason
(5 values) → host degradeReason via adapter (not meta passthrough). Notes that
parent ADR §5 `meta: response.meta` must become explicit adapter call.
effectiveMode aligned as WireSearchMode closed union. servedBy not passed to host.
P2-1: Domain vocabulary unified — RetrievalProvider.domain uses CanonicalDomain
('doc'|'conv'|'global'), not 'conversation'.
P2-2: All Wire string fields tightened to closed unions:
WireError.code → WireErrorCode (6 values incl invalid_request, unsupported_version)
WireResponseMeta.servedBy → 'primary' | 'fallback'
WireResponseMeta.effectiveMode → WireSearchMode
WireCapabilitiesResponse.version → literal 'v0'
WireProvenance.type → 'scanner' | 'conversation' | 'derived'
RequestOrigin.source → 'clowder' | 'echomem'
[宪宪/claude-opus-4-6🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 11, 2026
* feat(F257): wire eval:harness-ledger generator to publish_verdict pipeline Connect the harness-ledger eval domain end-to-end: generator adapter reads GuardRejectionEventLog.queryWindow() events, produces verdict markdown + bundle artifacts (snapshot/attribution/provenance) following the QC adapter pattern. Domain gated on Redis (guardRejectionLog). Four changes: 1. NEW harness-ledger-generator-adapter.ts — VerdictGenerator factory taking GuardRejectionEventLog as provider; discriminator check, window validation, event aggregation by kind/guard, Eval Hub compatible verdict markdown with YAML frontmatter 2. index.ts verdictGenerators wiring — conditional on guardRejectionLog 3. index.ts wiredPublishDomains — mirrors verdictGenerators condition 4. eval-harness-ledger.yaml enabled: true — producers and generator both wired, domain ready for weekly cron and manual trigger Tests: 10 new (harness-ledger-generator-adapter), 53 total F257 green. Build: tsc + biome clean. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): P1 Redis fail-closed + bundle schema compliance (codex review) Three fixes from codex (terra) review of PR #24: 1. Redis fail-closed: added queryWindowStrict() to GuardRejectionEventLog; adapter now uses strict path so Redis outage → 500 (not false zero-event verdict). Business-facing queryWindow remains fail-open. 2. Bundle schema compliance: attribution.findings now conforms to attributionFindingSchema (id, frictionSignal, attribution with evidence anchored to snapshot components, proposedAction). Evidence anchors validated by assertAttributionAnchors cross-ref against snapshot metrics. Changed window.durationDays → durationHours per bundleSnapshotSchema. 3. Domain disabled: reverted enabled: true → false until eval cat has data access (pre-injection snapshot or query tool) to avoid evidence-free weekly verdicts. Tests: +3 (ThrowingRedis regression, zero-events resolver round-trip, mixed-events resolver round-trip). All 13 pass. Resolver validates full schema + cross-reference chain end-to-end. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 snapshot-first pattern for eval cat data access (P1 #3) Eval cat for eval:harness-ledger had no access to GuardRejectionEventLog data before this change — unlike other eval domains whose data lives in git-accessible files, harness-ledger data is in Redis ZSET only. Without pre-injection, the cat made blind verdicts with no event context. KD-17 ruling (Fable): trigger produces normalized snapshot → eval cat sees real data in invocation → generator reads same stored snapshot by evalRunId. Single data source, zero drift between decision and artifact. Changes: - New: harness-ledger-snapshot-provider.ts — produces normalized guard-rejection-event snapshot, persists to run-snapshots/<evalRunId>.json - Rewritten: generator adapter reads stored snapshot by evalRunId instead of taking GuardRejectionEventLog directly (fail-closed on missing) - Modified: trigger-now.ts + eval-domain-daily.ts — pre-step snapshot production before eval cat invocation (manual=fail-closed, scheduled=fail-open) - Modified: eval-cat-invocation.ts — precomputedEvidence passthrough - Modified: types.ts — evalRunId on PromptSegmentsSourceSelector - Tests: 14/14 pass including KD-17 fail-closed + resolver round-trips [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 error path — no blind eval cat invocation on snapshot failure Terra's P1 (codex re-review df21b07): snapshot failure/absence still invoked the eval cat without evidence. KD-17 invariant requires "no snapshot → no invocation" — blind verdicts plus generator missing-run-id 500 is worse than skipping one eval cycle. Fixes: - eval-domain-daily.ts: snapshot error or provider absent → deliver SKIPPED message to domain thread + return (no cat invoke). "Fail-open" means no retry-storm, NOT invoking cat blind - trigger-now.ts: provider absent → 503 harness_ledger_snapshot_unavailable; snapshot throws → 503 harness_ledger_snapshot_failed. Cat never invoked - New helper: buildHarnessLedgerSnapshotSkippedMessage (same pattern as buildPublishPrereqSkippedMessage — stable header for log scrubbers) Tests (6 new, 51 total): - scheduled: provider absent → SKIPPED message, no invoke - scheduled: queryWindowStrict throw → SKIPPED message, no invoke - scheduled: success → evidence in content, cat invoked - manual: provider absent → 503 - manual: snapshot throw → 503 - manual: success → evidence in content, 200 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(F257): re-enable eval:harness-ledger domain (KD-17 data access resolved) KD-17 snapshot-first pattern gives eval cat data access to guard rejection events — the blocker that kept the domain disabled is resolved. Re-enable to allow weekly cron admission and baseline accumulation. Changes: - eval-harness-ledger.yaml: remove `enabled: false`, update comment to document KD-17 snapshot-first resolution - Weekly gate test: verify eval:harness-ledger appears in admission alongside capability-wakeup and sop 51/51 tests green including real registry admission coverage. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): P1-A MCP evalRunId schema + P1-B path/window validation, guardId removal Round-3 codex review fixes: P1-A: evalRunId was missing from MCP Zod schema — tool would strip it before reaching API. Added required evalRunId with regex validation (hlr-<ts>-<hex8>) to promptSegmentsSourceRefsShape + TypeScript union. P1-B: Three-layer defense-in-depth for evalRunId path safety: 1. MCP Zod: regex /^hlr-\d+-[a-f0-9]{8}$/ 2. API validation: format check + required (was optional) 3. Generator adapter: format re-check before filesystem access Window mismatch check: selector window must === stored snapshot window (KD-17 single-source invariant — prevents cat claiming different window than what snapshot actually covers). guardId removed from v1: was a pass-through label, not a real filter. Removed from types, validation, generator adapter bundle, and invocation instructions per codex option-1. Tests: replaced guardId-filter test with window-mismatch rejection + evalRunId format/traversal defense tests. All evalRunIds updated to format-valid (hlr-<ts>-<hex8>). Shared stable window constants prevent test drift from Date.now() timing. 52/52 green. Why: codex terra identified that evalRunId was silently dropped at the MCP layer (Zod strips unknown fields), and the generator had no defense against path traversal via malicious evalRunId. These fixes close the schema gap and add the path-safety invariant at all three validation layers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(F257): KD-17 last-hop — exact sourceRefs JSON in snapshot injection Terra round-4 P1: eval cat received window as ISO text but generator enforces exact epoch-ms match. Cat would need to ISO→epoch convert, risking 1ms drift → stable harness_ledger_adapter_window_mismatch. Fix: snapshot provider now emits a fenced JSON block with the exact sourceRefs object (kind, windowStartMs, windowEndMs, evalRunId) — all values the eval cat needs to copy verbatim. Instructions updated to say "copy the exact sourceRefs JSON" instead of just "copy evalRunId". Both paths (scheduled + manual trigger) inject the same summary with the complete sourceRefs block. Tests: strengthened scheduled + manual success assertions — verify delivered content includes fenced sourceRefs JSON with all three required fields (windowStartMs as number, windowEndMs as number, evalRunId matching safe format regex). 52/52 green. Why: KD-17's single-source invariant requires zero drift between trigger, eval cat, and generator. The window mismatch check added in a8ef88c would have made every real publish_verdict call 500 because the cat had no way to provide exact epoch-ms values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 13, 2026
check_network modifies PIP_INDEX_URL/HF_ENDPOINT/NO_PROXY -- running it before the arch gate at step 3.5 means incompatible interpreters trigger network operations that should never run. Moved from step 2.5 to step 3.7 (after gate, before venv/deps). Test fixes: - getInstallArchGate: sed range stops at #3.7 (excludes check_network), adds CAT_CAFE_HOME/VENV_NAME so step 3.5 probe won't hit host python3. - getInstallPipelineDecision: tracks check_network invocation via stub. - 3 new tests verify ordering: blocked path skips check_network, passed paths call it before dep selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 13, 2026
…ns, rebuildable index Fable's key inputs: - §5 Sunset capability gap: 188 orphan passages prove ConversationProvider lacks lifecycle management. Added as explicit capability gap with fix plan. - §9 Rejection records: nuanced per-item instead of blanket cut. #1/#2/#3 kept (valid regardless of SaaS/client), #5 reversed (hook+rebuild is legitimate under rebuildable-index principle), #8 reframed. - "Rebuildable index" principle: truth source is always docs+messages, memory service is compiled index. Dramatically simplifies ingestion from message-queue reliability to cache-rebuild semantics. - Retrieve dual-mode made explicit (doc=grep original, conv=return passages) - Media extension point noted (co-creator: "文本 图之类的") - Routing matrix demoted to appendix as test baseline - Validation criteria added per phase 313 → 386 lines. Still well under the 1190-line SaaS version. [布偶猫/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 13, 2026
…emorySearchResult, remember() semantics Fable review findings on 49c1fa0: - EvidenceStatus (business state: done/active/archived) vs LifecycleState (retrieval lifecycle: active/dormant/superseded/invalidated) are orthogonal dimensions — add distinction note, fix §1 table mixing, restore status as MemoryQuery filter parameter - Add MemorySearchResult type with meta.degraded/degradeReason/effectiveMode — the "空结果 + degraded: true" promise now has a type to live in - Add remember() behavioral semantics (same key+revision → no-op) — contracts existing IndexBuilder sourceHash idempotent skip, Phase 1 zero-behavior-change - Fix §5.1 "passage 级联删除" → "父子 lifecycle 一致性" (self-contradiction with Iron Law #5) - Fix getDb escape count 16→12 (Fable traced to her own round-1 grep error) - Fix capability #1 coverage: "单馆内搜索" → "含跨 namespace federation" - Add conversation incremental ingestion note to capability #3 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 14, 2026
…tection (zts212653#1083) * fix(services): unify Qwen3-ASR into whisper-stt service + fix Rosetta arch detection Why: Qwen3-ASR was modeled as a separate lifecycle service sharing the same port (9876), env vars (WHISPER_URL), and API endpoint as Whisper STT. This caused the Console to show two competing ASR services for the same slot. Separately, Rosetta x64 Node on Apple Silicon misdetected the hardware as Intel, recommending wrong models and causing install deadlocks. Changes: - zts212653#863: Merge qwen3-asr into whisper-stt as backend model variants. Qwen3-ASR-1.7B-8bit is now the default recommendation on macOS arm64, with Whisper models retained as alternatives. Remove qwen3-asr from service-manifest.ts env var maps and SERVICE_MANIFESTS array. - zts212653#1061: Use `sysctl -n hw.optional.arm64` on macOS instead of process.arch to detect true hardware architecture. This correctly identifies Apple Silicon even when Node.js runs under Rosetta 2. Also fix detectGpu() to use resolveArch() instead of process.arch. Closes zts212653#863, Closes zts212653#1061 [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(services): dispatch install/server scripts by model backend Why: Codex P1 review — after merging Qwen3-ASR into the whisper-stt recommendation list, selecting a Qwen3-ASR model would trigger whisper-install.sh (which installs mlx-whisper) and whisper-server.sh (which launches whisper-api.py). Neither can load Qwen3-ASR models, so the default macOS arm64 install would fail on first use. Fix: Both scripts now inspect WHISPER_MODEL for "Qwen3-ASR" substring: - whisper-install.sh: switches PIP_DEPS_ARM64 to mlx-audio when Qwen3 - whisper-server.sh: dispatches to qwen3-asr-api.py when Qwen3 The venv name stays whisper-venv for both backends — reinstall is needed when switching between Qwen3-ASR and Whisper models. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(lint): replace non-ASCII arrows in shell script comments CI check:scripts-ascii-only requires pure ASCII in scripts/services/*.sh to avoid bash set -u multi-byte unbound and PowerShell 5.1 UTF-8 bugs. Replace Unicode arrows (U+2192) with ASCII '->' in header comments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(services): add regression tests for whisper-stt model dispatch (zts212653#863) Reviewer requested regression coverage proving the dispatch logic works: - Qwen3-ASR models -> mlx-audio install + qwen3-asr-api.py server - Whisper models -> mlx-whisper install + whisper-api.py server Adds 11 tests in whisper-dispatch.test.mjs: - Static guard: actual script files contain dispatch patterns - Behavioral: bash snippet tests verify correct backend selection for each model variant (Qwen3-ASR 8bit/4bit, whisper turbo/small, empty fallback, faster-whisper short names) Registered as check:whisper-dispatch in the CI check pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(services): address cross-cat review P1/P2 findings (zts212653#863 zts212653#1061) P1-1: install-template.sh used uname -m for platform detection, which reports x86_64 under Rosetta 2. Now uses sysctl -n hw.optional.arm64 on macOS, matching the TypeScript environment-detector fix. Without this, the API would recommend Qwen3-ASR on Apple Silicon but install-template would select the faster-whisper (x86) dependency branch. P1-2: service lifecycle only recognized whisper-api.py as an owned whisper-stt process. After zts212653#863, whisper-server.sh can also launch qwen3-asr-api.py, which was treated as foreign — breaking stop/restart. Added additionalRuntimeScripts field to ServiceLifecycleManifest and registered qwen3-asr-api.py for whisper-stt. P2-1: manifest isDefault was on Qwen3-ASR-1.7B-8bit (MLX-only), making deriveLegacyServiceConfig pick an unrunnable model on non-macOS. Moved isDefault to whisper-large-v3-turbo; the YAML matrix still recommends Qwen3-ASR on macOS arm64. P2-2: added lifecycle ownership test verifying both whisper-api.py and qwen3-asr-api.py are recognized as owned whisper-stt processes. Review: @codex (缅因猫/砚砚) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): update service name in lifecycle failure tests (zts212653#863) The whisper-stt manifest name changed from 'Whisper STT' to 'Speech Recognition' in the ASR unification, but the failure-handling tests still expected the old name in response messages. Update all three occurrences: already-running, stop-failed, stopped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(services): add Rosetta regression tests for resolveArch (zts212653#1061) Review P2: CI would not catch a regression back to process.arch for Rosetta detection because native arm64 runners report the correct arch either way. Fix by: 1. Export resolveArch with optional DI params (platform, arch, sysctl probe) so tests can simulate Rosetta without global mocking. 2. Add 5 focused tests covering: Rosetta scenario (darwin+x64+sysctl=1 → arm64), native macOS, real Intel Mac (sysctl null/0 → x64), and Linux (process.arch used, sysctl not called). The key regression guard: if someone reverts resolveArch to use process.arch on macOS, the "Rosetta scenario" test will fail because injected arch='x64' would be returned instead of arm64. Review: @codex (缅因猫/砚砚) R2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove dead qwen3-asr-server.sh after ASR unification (zts212653#863) qwen3-asr-server.sh was the standalone Qwen3-ASR launcher. Now that whisper-server.sh dispatches to qwen3-asr-api.py based on model name, this script is unreachable dead code — no manifest, no reference. qwen3-asr-api.py is kept: it's the active Python API that whisper-server.sh launches for Qwen3-ASR models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Merge qwen3-asr-api.py into whisper-api.py for true single-service ASR (zts212653#863) whisper-api.py now handles all three backends in one file: - mlx-audio (Qwen3-ASR models, Apple Silicon) - mlx-whisper (Whisper models, Apple Silicon) - faster-whisper (Whisper models, CPU/CUDA fallback) Backend is selected by model name at startup — Qwen3-ASR models route to mlx-audio, everything else tries mlx-whisper then faster-whisper. This eliminates: - qwen3-asr-api.py (merged into whisper-api.py) - Shell dispatch in whisper-server.sh (always launches whisper-api.py) - additionalRuntimeScripts in service manifest (single script = no extra process ownership tracking needed) Install-time pip dispatch in whisper-install.sh is kept — the correct Python package (mlx-audio vs mlx-whisper) must be installed before the unified API can load the right backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(asr): address R3 review P2s — async lock, temp cleanup, docs (zts212653#863) P2-1: Replace threading.Lock with asyncio.Lock + asyncio.to_thread to avoid blocking the uvicorn event loop during ML inference. P2-2: Wrap _convert_to_wav() in try/except BaseException to clean up the temp wav file when ffmpeg fails or times out. P2-3: Update SETUP.md and SETUP.zh-CN.md to reference the unified whisper-server.sh + WHISPER_MODEL instead of deleted qwen3-asr-server.sh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(services): migrate legacy qwen3-asr config/env on ASR unification (zts212653#863) The qwen3-asr service was registered in upstream/main with its own manifest, env vars (QWEN3_ASR_ENABLED, QWEN3_ASR_MODEL), and services.json config key. Removing it without migration would orphan existing users' installed/enabled state after upgrade. Three migration layers: 1. Config: services.json 'qwen3-asr' key auto-migrates to 'whisper-stt' on first load (service-config.ts LEGACY_SERVICE_ALIASES) 2. Env bridge: deriveLegacyServiceConfig for whisper-stt falls back to QWEN3_ASR_ENABLED / CAT_CAFE_SERVICE_QWEN3_ASR_ENABLED / QWEN3_ASR_MODEL (service-manifest.ts LEGACY_ENV_FALLBACKS) 3. Process ownership: additionalRuntimeScripts includes legacy qwen3-asr-api.py so lifecycle management can detect/stop old sidecars Tests: 7 new (config migration + env bridge), 70 lifecycle + 8 dispatch all pass with 0 regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: explicit ASR disable overrides legacy Qwen fallback + preserve Qwen default model Two bugs found by codex-connector R7 review: 1. ASR_ENABLED=0 + stale QWEN3_ASR_ENABLED=1 would re-enable whisper-stt because the fallback guard checked `legacyEnabled !== true` (enters on false). Changed to `legacyEnabled == null` so explicit disables are final. 2. QWEN3_ASR_ENABLED=1 without QWEN3_ASR_MODEL fell through to the manifest default (whisper-large-v3-turbo), silently migrating Qwen users to Whisper. Added defaultModel to LEGACY_ENV_FALLBACKS so activation from Qwen env vars defaults to Qwen3-ASR-1.7B-8bit. Tests: 3 new (disable override ×2, Qwen default ×1), 88 total pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate MLX install on Python interpreter arch, not just hardware (zts212653#1061) Maintainer verdict: the install-template.sh Rosetta fix was incomplete. sysctl detects arm64 hardware but the resolved Python interpreter might be x86_64 (Rosetta). Installing arm64-only MLX wheels into an x86_64 venv still fails. Fix: is_darwin_arm64 now requires BOTH hw_arch=arm64 AND RESOLVED_PYTHON_ARCH=arm64|aarch64. When hardware is arm64 but Python is x86_64, falls back to non-MLX deps with a remediation message. Tests: 1 static guard + 6 behavioral regression covering all hw×python_arch combinations (arm64+arm64, arm64+x86_64, arm64+unknown, x86_64+x86_64, Linux). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate darwin/arm64 MLX recommendations on pythonArch (zts212653#1061) The recommendation matrix suggested Qwen3-ASR/MLX models for all darwin/arm64 users regardless of Python interpreter architecture. On Apple Silicon with x86_64 Rosetta Python, these MLX models can't be installed — the installer correctly falls back to faster-whisper deps, but the model is still MLX-only, causing a runtime mismatch. Split darwin/arm64 whisper-stt into two entries: - pythonArch: native → MLX models (Qwen3-ASR, mlx-whisper) - pythonArch: [x86-emulated, missing] → faster-whisper models The pythonArch matching infrastructure already existed (used by win32/arm64 entries). Tests: 2 new matrix tests confirming x86-emulated and missing Python get faster-whisper, not MLX. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: missing Python stays on MLX path + strip installed on migration Two P1 bugs from R9 review: 1. pythonArch=missing on Apple Silicon was routed to faster-whisper short names, but the installer bootstraps arm64 Python → MLX path → snapshot_download("large-v3-turbo") fails (not a HF repo ID). Fix: missing now matches the MLX entry since bootstrap provides arm64 Python. 2. Config migration preserved installed:true from old qwen3-asr, but whisper-stt uses a different venv (whisper-venv vs asr-venv). Auto-start would fail because the venv doesn't exist. Fix: strip installed flag during migration, forcing reinstall. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: bootstrap sysctl, migration model backfill, setup.sh unify (zts212653#863 zts212653#1061) Address @sol complete review (3 P1 + 3 P2): P1-1: python-resolve.sh _pbs_target_triple() now uses sysctl on Darwin instead of uname -m, so Rosetta terminals bootstrap arm64 Python (not x86_64) on Apple Silicon. Matches environment-detector approach. P1-2: migrateServiceConfig backfills Qwen default model when old config has no selectedModel. Old service allowed implicit defaults; new whisper-server.sh requires WHISPER_MODEL or exits 1. P1-3: setup.sh --install-missing delegates ASR install to the unified whisper-install.sh instead of hardcoding asr-venv + mlx-audio. P2-1: Migration sets installed: false explicitly (not stripped/undefined). Stripping made reconciler infer installed=true from enabled=true, skipping the venv rebuild that the different path requires. P2-2: resolveArch() uses process.arch='arm64' as one-direction fallback when sysctl transiently fails on native Apple Silicon. P2-3: Added static guards: python-resolve.sh sysctl usage, setup.sh whisper-install.sh delegation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: setup model consistency, shell fallback, production tests (zts212653#863 zts212653#1061) Sol re-review R2 fixes (3 remaining findings): P1: setup.sh install/startup model mismatch — ASR_DEFAULT_MODEL is now platform-detected (sysctl on Darwin), written to .env WHISPER_MODEL, and passed to the installer. Eliminates hardcoded Qwen model that fails on non-MLX platforms. P2-2: shell bootstrap sysctl fallback — _pbs_target_triple() falls back to uname -m=arm64 when sysctl fails (one-direction: x64 process can't report arm64). Mirrors TypeScript resolveArch. P2-3: production code tests — whisper-dispatch.test.mjs now tests real production code: - getBootstrapTriple() sources python-resolve.sh and calls the real _pbs_target_triple() with mocked uname/sysctl (6 cases) - getInstallDispatch() extracts dispatch logic from whisper-install.sh via sed, tests primary package in full deps string (5 cases) - getPlatformDetection() extracts detection block from install-template.sh via sed, evals with mocked commands (6 cases) - Static guards verify script content invariants (6 cases) Total: 23 shell tests + 47 API tests = 70/70 pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: setup model selection checks both hardware AND Python arch (zts212653#1061) Sol R3 finding: setup.sh only checked sysctl (hardware) when selecting the ASR default model. On Apple Silicon + Rosetta x86_64 Python, this selected Qwen (MLX model) but the installer correctly detected no MLX support and used faster-whisper — deterministic startup failure. Now mirrors install-template.sh dual-signal check: - Hardware arm64 (sysctl) + arm64 Python → Qwen3-ASR (MLX) - Hardware arm64 + x86_64 Python (Rosetta) → large-v3-turbo - Intel/Linux → large-v3-turbo Uses python3's platform.machine() directly rather than calling resolve_python_312 (which may download Python — too heavy for the interactive feature-selection stage). Added 4 behavioral tests that extract and eval the production model selection block from setup.sh with mocked uname/sysctl/python3, plus a static guard verifying the Python arch check exists. Shell tests: 28/28 pass | API tests: 47/47 pass | Total: 75/75 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: setup uses canonical Python resolver for model selection (zts212653#1061) Sol R4 finding: setup.sh used python3 (PATH) for arch detection but installer uses resolve_python_312 (python3.13→python3.12→python3→...). In multi-Python environments these can be different interpreters with different architectures, causing model/loader mismatch. Now sources python-resolve.sh and calls _try_system_pythons (same probe order as installer) to determine RESOLVED_PYTHON_ARCH. Fallback chain: 1. _try_system_pythons → resolver's canonical Python 2. _try_uv / _try_pyenv / _try_brew → other resolver sources 3. No Python 3.12+ found → assume arm64 (installer will bootstrap arm64 Python via sysctl-based _pbs_target_triple on Apple Silicon) Model selection moved from feature-selection section (too early, before user confirms) to after all interactive prompts but before .env generation. Tests: - 5 setup model selection tests (was 4): added "no Python 3.12+" - Cross-validation test: asserts setup model backend matches installer is_darwin_arm64 for all hw/pyArch combinations - Static guard: verifies setup sources python-resolve.sh, uses _try_system_pythons, reads RESOLVED_PYTHON_ARCH Shell: 29/29 | API: 47/47 | Total: 76/76 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: complete resolver probe chain with project-local/legacy cache (zts212653#1061) Sol R5 finding: setup.sh skipped _try_project_python and _try_legacy_project_python in the resolver probe chain. When all system/uv/pyenv/brew probes failed, setup assumed installer would bootstrap arm64 Python. But installer first checks project-local cache (~/.cat-cafe/python/) which may contain x86_64 Python from a prior Rosetta installation — causing Qwen model + faster-whisper loader mismatch. Probe chain now mirrors resolve_python_312 exactly (minus download): 1. _try_system_pythons 2. _try_uv / _try_pyenv / _try_brew 3. _try_project_python / _try_legacy_project_python ← NEW 4. No Python found → assume bootstrap arm64 (sysctl-based) Added tests: - Apple Silicon + cached project x86_64 Python → large-v3-turbo - Apple Silicon + cached project arm64 Python → Qwen - Apple Silicon + legacy cached x86_64 Python → large-v3-turbo - Static guards for _try_project_python / _try_legacy_project_python Shell: 32/32 | API: 47/47 | Total: 79/79 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reconcile stale venv architecture on install (zts212653#1061) A prior Rosetta installation may leave an x86_64 venv (whisper-venv) that the resolver now replaces with arm64 Python. The unified Qwen→whisper-venv path (zts212653#863) makes this a new failure mode: reusing such a venv installs wrong-arch wheels (e.g. MLX into x86_64 runtime). install-template.sh now probes the existing venv Python's platform.machine() and compares against RESOLVED_PYTHON_ARCH. On mismatch it removes and recreates the venv. Safe fallback: unknown arch on either side keeps the existing venv. Tests: +7 (1 static guard + 6 behavioral) covering both directions (arm64→x86_64, x86_64→arm64), matching, and unknown-arch edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: server startup path must also verify venv arch (zts212653#1061) Sol R7 identified the critical gap: the normal startup path (env bridge -> installed:true -> startup reconciler -> whisper-server.sh) bypasses the installer entirely. The venv reconciliation added in 1e1e808 is unreachable via this path — a stale Rosetta x86_64 venv activates directly and fails at MLX import. whisper-server.sh now sources python-resolve.sh and runs the canonical resolver probe chain before activating the venv. If the venv Python arch differs from the resolver's current RESOLVED_PYTHON_ARCH, it removes the stale venv and falls through to the existing auto-install block (which calls the installer, which rebuilds correctly). Also fixes P2: replaces non-ASCII chars (-> and !=) in install-template.sh to pass the scripts ASCII-only guard. State transition coverage (setup/install/reconfigure/startup/direct): - install path: install-template.sh reconciliation (1e1e808) - startup path: whisper-server.sh compat check (this commit) - Both use the same truth source: python-resolve.sh probe chain Tests: +6 (1 static guard + 5 behavioral for server path), total 45. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: server startup validates backend deps after model switch (zts212653#863) Env bridge sets installed:true without calling installer. After a model switch (e.g. Whisper → Qwen3-ASR), the existing venv passes the arch check but lacks the new model's pip deps (mlx-audio vs mlx-whisper). Add a backend dependency import probe between the arch check and auto-install block in whisper-server.sh. The probe mirrors whisper-api.py's startup chain: Qwen → mlx_audio (no fallback); non-Qwen → mlx_whisper || faster_whisper (fallback). Missing deps trigger venv rebuild via the existing auto-install path. Also fix getServerVenvCheck sed range in tests — after inserting the backend dep block between the arch check and auto-install block, the old range captured both blocks (MODEL unset + set -u = abort). Tests: 52/52 pass (8 suites), including 6 new backend dep tests + 1 new static guard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: gate all darwin/arm64 MLX recommendations on pythonArch (zts212653#1061) resolveArch() now reports arm64 under Rosetta, but only whisper-stt had pythonArch entries. The other three services (mlx-tts, embedding-model, llm-postprocess) matched the unguarded { os: darwin, arch: arm64 } and recommended MLX models that install-template.sh would refuse to install with an x86 Python interpreter. Add pythonArch: [native, missing] to all darwin/arm64 MLX entries and insert x86-emulated fallback entries mirroring each service's x64 path: - mlx-tts: edge-tts - embedding-model: jina/e5 (sentence-transformers) - llm-postprocess: Qwen transformers Tests: 28/28 recommendation-matrix + 52/52 whisper-dispatch pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add customModelHint to Rosetta fallback matrix entries (zts212653#1061) Three new x86-emulated darwin/arm64 entries lacked customModelHint, making them the only matrix entries with installable models but no custom-model format guidance. Users could enter MLX model IDs in the custom input without seeing the incompatibility warning. Copy the corresponding x64 hint for each service, with MLX incompatibility explicitly called out in the unsupported field: - mlx-tts: edge-tts voice hints - embedding-model: sentence-transformers hints - llm-postprocess: transformers CausalLM hints Tests: assert customModelHint exists and unsupported mentions MLX. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: TTS Rosetta hint uses dispatch-valid install tokens, not voice names tts-server.sh only recognizes exact 'edge-tts' as the edge provider token (line 42). Voice names like zh-CN-XiaoxiaoNeural are synthesis request parameters, not install-model tokens — they fall to the default qwen3-clone provider which needs MLX deps unavailable on Rosetta. Fix: change TTS Rosetta hint example from voice names to dispatch-valid tokens (edge-tts, zh_CN-huayan-medium). Explicitly warn that Neural voice names are request params, not install tokens. Test: assert hint example contains no 'Neural' voice names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: model-aware venv arch decision prevents destroying MLX venv (zts212653#1061) Maintainer P1: whisper-server.sh unconditionally deleted arm64 venv when resolver found x86_64 Python, even if the model (Qwen3-ASR) requires arm64. The arm64 venv has its own Python symlink and works independently of PATH Python — deleting it and rebuilding with x86_64 installs faster-whisper deps instead of mlx-audio, breaking the model. Changes: - Venv arch check is now model-aware: MLX models (Qwen3-ASR) keep arm64 venv when it has working MLX deps, regardless of PATH Python - Model-arch gate: MLX model + x86_64 resolver + no venv → fail fast with explicit remediation instead of silently installing wrong deps - 7 new regression tests covering all 3 maintainer-requested scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: model-arch gate in install pipeline rejects MLX on non-arm64 (zts212653#1061) Maintainer P1: direct install path (POST /api/services/:id/install) bypassed whisper-server.sh gate. whisper-install.sh now declares REQUIRED_PYTHON_ARCH="arm64" for mlx-community/* models, and install-template.sh rejects incompatible interpreters before touching venv/network/deps. Gate logic: - arm64 Python resolved → pass (MLX compatible) - x86_64 Python resolved → block with remediation guidance - no Python + Apple Silicon hardware → pass (bootstrap arm64) - no Python + Intel/Linux → block (bootstrap would get x86_64) 6 regression tests covering all 4 maintainer-requested scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace non-ASCII chars in shell scripts (CI lint) Shell scripts must be pure ASCII (PR zts212653#674 / F167 Case E6 policy). Replace Unicode arrows and em-dashes in comments with ASCII equivalents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: align MLX model-arch invariant across install pipeline (zts212653#1061) Address @sol R13 local review — 2×P1 + 2×P2: P1-1: whisper-install.sh classifier now matches *Qwen3-ASR* in addition to mlx-community/* for REQUIRED_PYTHON_ARCH declaration, consistent with the server and deps classifier. P1-2: install-template.sh step 3.5 no longer fail-opens on unknown Python arch. After check_python3, unknown = probe failure (not "no Python"), so fail-closed prevents x86 interpreter loading MLX wheels. P2-3: install-template.sh step 0.5 rejects non-arm64 hardware before check_python3/check_network, preventing useless Python downloads on Intel/Linux when an MLX model is requested. P2-4: install-template.sh step 5 venv arch reconciliation now preserves an arm64 venv that matches REQUIRED_PYTHON_ARCH, even when the PATH resolver reports x86_64 (Rosetta). Consistent with whisper-server.sh model-aware preservation logic. Failure-mode audit: all four share "incomplete MLX model-arch invariant in the install path". Server path was correct; install path lagged. Tests: 72 pass / 0 fail (7 new: 3 classifier + 4 venv preservation). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: step 3.5 checks existing venv before rejecting (R14 reconfigure) Sol R14: step 3.5 gate exits before step 5 venv preservation and step 6 dep selection, making reconfigure-with-healthy-arm64-venv impossible. Fix: step 3.5 now probes the existing venv. If it matches REQUIRED_PYTHON_ARCH, override is_darwin_arm64 + RESOLVED_PYTHON_ARCH so downstream dep selection and venv reconciliation use the correct arch signals. This makes the step 5 P2-4 code unreachable (arch signals already match), so it's reverted to the original simpler form. End-to-end pipeline test verifies gate -> venv -> dep selection as one unit, not isolated blocks. Key scenario: arm64 venv + x86 resolver + MLX model -> passed + mlx-deps (was: blocked). Push back on P2-2 (check_network ordering): check_network is a read-only connectivity probe with no file/venv/dep changes. Moving it after a whisper-specific gate would couple the shared template to one service. Cosmetically imperfect but functionally harmless. Tests: 72 pass / 0 fail. install-template.sh = 350 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: move check_network after arch gate, isolate test venv probes (R15) check_network modifies PIP_INDEX_URL/HF_ENDPOINT/NO_PROXY -- running it before the arch gate at step 3.5 means incompatible interpreters trigger network operations that should never run. Moved from step 2.5 to step 3.7 (after gate, before venv/deps). Test fixes: - getInstallArchGate: sed range stops at #3.7 (excludes check_network), adds CAT_CAFE_HOME/VENV_NAME so step 3.5 probe won't hit host python3. - getInstallPipelineDecision: tracks check_network invocation via stub. - 3 new tests verify ordering: blocked path skips check_network, passed paths call it before dep selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make blocked-path network test falsifiable (R16) The check_network stub previously set a variable that was only echoed after _run returned. Since gate's exit 1 terminates bash, the echo never ran -- the test always reported networkCalled=false regardless of actual call order (unfalsifiable). Fix: stub now echoes "NETWORK_RAN" marker at call time. The marker appears in e.stdout before exit 1 kills the process, so the test correctly detects when check_network runs before the gate. Counterfactual verified: swapping check_network before gate produces marker in caught stdout -> test fails as expected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: legacy additionalRuntimeScript must not satisfy already-running path (zts212653#863) After migration from qwen3-asr to whisper-stt, a legacy qwen3-asr-api.py process on the same port would be classified as "owned" by isServiceProcessCommand, pass the shallow /health check, and cause startService to return "already-running" without launching the current whisper-server.sh. Root cause: isServiceProcessCommand matches both primary scripts AND additionalRuntimeScripts (legacy), but the readiness check did not distinguish between them. Fix: split runtime script resolution into primary (auto-derived from start script name) vs full (primary + additionalRuntimeScripts). Extract shared command-matching logic into matchCommandAgainstScripts. Export new isPrimaryServiceProcess that excludes legacy scripts. In startService, verify at least one owned PID is a primary process before declaring "already-running" — legacy-only listeners fall through to terminate and restart with the current start script. Tests: - isPrimaryServiceProcess distinguishes primary vs legacy scripts - E2E regression: legacy listener + healthy /health -> terminated + current start script launched - 72 lifecycle tests + 75 whisper dispatch tests pass [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 14, 2026
… call count The zts212653#1145 backstop test was a false green: updateCallCount===1 fired on the userMessageId backfill (call #1), not the catch-block's failed write (call #3). The throw never reached the intended code path, and get() was hardcoded to return 'running' regardless of actual state. Fix: trigger throw on `status==='failed' && !expectedStatus` (catch-block signature) instead of call count. Track record state so get() returns the realistic status after each successful update. Verify the full chain: catch-block throw → record stays running → CAS backstop → terminal failed. Addresses codex review P2 on 37ad884. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 14, 2026
… call count The zts212653#1145 backstop test was a false green: updateCallCount===1 fired on the userMessageId backfill (call #1), not the catch-block's failed write (call #3). The throw never reached the intended code path, and get() was hardcoded to return 'running' regardless of actual state. Fix: trigger throw on `status==='failed' && !expectedStatus` (catch-block signature) instead of call count. Track record state so get() returns the realistic status after each successful update. Verify the full chain: catch-block throw → record stays running → CAS backstop → terminal failed. Addresses codex review P2 on 37ad884. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 14, 2026
zts212653#1146) * fix(invocation): classify interrupted sessions for self-heal + add QueueProcessor terminal write backstop (zts212653#1145) Two reliability fixes for invocation availability: 1. classifyResumeFailure() now matches "session interrupted" / "already interrupted" / "session terminated" as 'missing_session', triggering session self-heal (drop stale sessionId + retry fresh). Previously, our stall auto-kill (SIGTERM after 7 min idle-silent) caused the CLI daemon to mark sessions "interrupted", but the classifier returned null → no self-heal → cascading resume failures on every subsequent invocation until manual intervention or session TTL expiry. 2. QueueProcessor.executeEntry() finally block now calls ensureTerminalStatus() with RouteChainCompletionTracker, matching messages.ts F194 Z3 parity. If the catch block fails to update the record to 'failed' (e.g. Redis blip), the CAS-guarded backstop ensures the invocation reaches a terminal status instead of staying 'running' and becoming a zombie after 600s grace. The previously silent `catch { /* ignore */ }` now logs a warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(invocation): raise stall auto-kill from 7 to 15 min (zts212653#1145) 7 minutes was too aggressive for LLM inference — when CLI waits for an API response with large context, it appears idle-silent (no CPU growth, no NDJSON output) even though a legitimate network request is in flight. This caused false stall kills, which interrupted active invocations. 15 min gives ample room for long inference while still catching truly stuck CLIs well before CLI_TIMEOUT_MS (30 min). F194 zombie detection won't false-fire during this window because InvocationTracker slot is held by the live generator. Root cause of: codex sessions getting "interrupted" mid-work. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: update stall threshold assertion to match 15-min change (zts212653#1145) Existing test hardcoded 7*60_000 (420000ms). Updated to match the new 15*60_000 (900000ms) threshold from the prior commit. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(invocation): align stall auto-kill to CLI_TIMEOUT_MS (30 min) (zts212653#1145) The ProcessLivenessProbe cannot distinguish "CLI waiting for LLM API response" from "CLI truly stuck" — both present as idle-silent (no CPU growth, no NDJSON output). Any threshold shorter than CLI_TIMEOUT_MS causes false kills during normal inference. Evidence: thread_mrj033qnuamv9ghe (sol) and thread_mriwjlo1955spve4 (codex) both show repeated "CLI 响应超时 (420s)" kills during active tool-call → API-wait cycles. Codex review invocation was killed after only 16s of work + 7m of waiting for terra API response. Align to 30 min so CLI_TIMEOUT_MS is the single binding idle constraint. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(invocation): address sol review P1/P2/P3 — dynamic stall config, tracker leak, negative tests (zts212653#1145) P1: Replace hardcoded 30-min constant with buildStallAutoKillConfig() that tracks resolved CLI_TIMEOUT_MS. When CLI_TIMEOUT_MS=0 (disabled), stallAutoKill is turned off entirely. Custom values (e.g. 60 min) are respected instead of being capped at 30 min. P2: Move chainTracker.release() out of the .get() guard in QueueProcessor.ts so it always runs — prevents Map leak when InvocationRecordStoreLike lacks .get(). P3: Add negative regex tests (keyboard interrupt, process interrupted, interrupted system call) to verify the interrupt classifier doesn't match non-session interrupt messages. Tests: 118/118 pass (3 new assertions for buildStallAutoKillConfig + 3 negative regex assertions). [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(QueueProcessor): add ensureTerminalStatus backstop regression test (zts212653#1145) Addresses codex review P2: the QueueProcessor terminal backstop had no test coverage. New test verifies that when the catch-block update(failed) throws (e.g. Redis blip), ensureTerminalStatus fires a CAS write with expectedStatus=running to ensure the record reaches terminal status. Also exercises the .get() path and confirms the backstop reads the record before attempting the CAS update. [宪宪/claude-opus-4-6🐾] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(test): backstop test false-green — trigger on patch semantics not call count The zts212653#1145 backstop test was a false green: updateCallCount===1 fired on the userMessageId backfill (call #1), not the catch-block's failed write (call #3). The throw never reached the intended code path, and get() was hardcoded to return 'running' regardless of actual state. Fix: trigger throw on `status==='failed' && !expectedStatus` (catch-block signature) instead of call count. Track record state so get() returns the realistic status after each successful update. Verify the full chain: catch-block throw → record stays running → CAS backstop → terminal failed. Addresses codex review P2 on 37ad884. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This was referenced Jul 20, 2026
mindfn
added a commit
that referenced
this pull request
Jul 21, 2026
…erve-only) (#47) * feat(F257): #4 签名 lint O2→O1 — 检测层(observe-only)+ 账本闭环显式 deferred Why: 消息末行 [昵称/模型🐾] 签名此前仅靠 prompt 文本约定(O2 观测层),零 结构覆盖 → dev-7a882ba0(Fable 漏签靠 operator 人工发现)。本切片交付 #4 的 **检测层**(O1 结构强制):agent 消息末行是否以被识别的猫签名结尾。 两阶段(owner vision-guardian 校准,AC 完成 ≠ feature 完成): - ① 检测层(本切片):复用 isCatSignatureLine(F167,不重造正则)→ post-seam 记 message.extra.signatureLint(message 级可观测、denominator-bearing、 observe-only 非阻断,绝不 reject 已持久化用户可见消息)。 - ② 账本闭环(deferred 到切片 2 / #3 后):检测到 miss 自动 emit deviation 归因 obj-identity-integrity 进 harness ledger,自动化 dev-7a882ba0 的手动 report_harness_signal。deferred 原因:ledger 读 DeviationEventLog/ GuardRejectionEventLog/eval verdict 不扫 message.extra;正确 deviation 需 registered objective(否则重蹈 #3 修的 free-string 考古)+ segment/condition 归因基建(condition_hit 需 condition registry+typed fact,均属 #2/#3 数据根)。 extra-only ≠ #4 完成——已在 spec #4 + 代码 SCOPE 注释显式在案。 设计自决:observe-only 非阻断(阻断逼近铁律5/硬排除,风险高);#4 检测 ⊥ #3 (不依赖 registry);signatureLintExtra helper 提取使 post-message 巨型 handler biome 复杂度对基线 0 delta。 证据:19 单测(纯函数+helper)+ 2 wiring e2e(extra.signatureLint signed true/false)+ 79 blast-radius 回归全绿;tsc/biome clean;新文件 65 行、无 any。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #4 检测层完整性 — sol R3 REQUEST CHANGES 全部修复(P1×3 + P2×1) R3 review 四项全部核实成立(读码验证,非反射接受),本 commit 逐项修复 + red→green: P1-3 严格契约 matcher(correctness):lintCatSignature 之前复用 isCatSignatureLine (routing 的 permissive STRIP matcher,容忍 [Spark🐾] 无模型 / [砚砚/GPT-5.5] 无爪), 把契约违规签名误判为 compliant。改用严格 `[nickname/model🐾]` 正则(slash+model+paw 必需),断言 governance-l0「签名必须含模型型号」。strip=permissive(routing) 与 lint=strict(compliance) 分离。两个原正例 flip 成反例 + 保留严格正例。 P1-1 覆盖(coverage):签名 lint 之前只接 callback post-message 两支,漏掉 serial/ parallel 以 origin:'stream' 持久化的普通 agent 最终文本 → 从不 post_message 的猫不入 分母,签名率系统性偏差。route-serial.ts:2805 + route-parallel.ts:1248 各接 signatureLintExtra(storedContent)。加 serial+parallel 集成测试(signed/unsigned 均入分母)。 P1-2 持久化(durability):serializeExtra 写入但 safeParseExtra 白名单重建时丢弃 signatureLint → Redis 回读后字段消失(in-memory 测试测不到)。parser 加 signatureLint strict 支持(typeof signed === 'boolean')+ serialize→parse round-trip 测试(true/false/ malformed drop/其他字段不受影响)。 P2-1 可达(reachability):GET /api/messages 白名单、两处 callback socket broadcast、 web ChatMessage.extra、server AgentMessage.extra 均漏 signatureLint → Console 读不到。 逐一透传(broadcast 转发 storedMsg.extra.signatureLint,单一真相源)+ cold-hydration (GET)+ live-broadcast 契约测试。无 UI 渲染(#6 消费)。 证据:F257 slice-1 全套 82 tests + blast-radius 回归 60 tests 全绿;tsc/web-tsc clean; biome 新文件 clean,route-serial/parallel/callbacks 复杂度对基线 0 delta;safeParseExtra 48→50、messages GET 70→73(+2/+3,逐字段 established pattern,函数本已远超限,无新增 erroring function)。两阶段账本闭环仍 deferred(sol 已 accept)。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #4 R4 — P1 provider-qualified model + P2 §16e 全链 reachability sol R4 REQUEST CHANGES 两项核实成立(读码 + runtime 真相源),逐项修 + §16e 全量 sweep: P1 严格 matcher 拒真实模型标识(correctness):STRICT_SIGNATURE_LINE_RE 假设单斜杠,但 opencode 运行时 defaultModel="codex-for-me/gpt-5.4"(provider-qualified,含斜杠)→ [金渐层/codex-for-me/gpt-5.4🐾] 被误判 unsigned、按 cohort 污染分母。改:首斜杠分隔 nickname/model,model 允许 provider-qualified 斜杠,两段 trim 后非空必需(拒 [ /m🐾] / [n/ 🐾])。flip multi-slash 为正例 + roster provider-qualified 正例 + 空段负例。 P2 §16e 全链 reachability(补锅匠 → failure-mode sweep):R3 只补 server 侧 + web ChatMessage 类型,漏 web 投影链全体 + 2 处 dup-recovery broadcast。做全量 sweep(api 20 点 + web 14 点)逐点核 fixed/N-A,修复 14 点: - API: A13/A14 dup-recovery broadcast forward(+ A11-A14 抽 pickSignatureLint helper, callbacks 复杂度对基线 0 delta) - Web 类型: W3 AgentMsg / W4 BackgroundAgentMessage / W5 history-response - Web runtime: W6 cold-hydration(guard+emit) / W7 mergeMessageExtra(sweep 发现的 uncited 点) / W8-W14 useAgentMessages callback side-patch×7(pickSignatureLint 统一 forward) - sweep 发现 sol 未列的 3 个 drop 点(W7/W13/W14)——证明逐点补 sol 列表会再 bounce Divergence: 6+ 异构 allowlist(不同 key 子集),不可统一 projector;pickSignatureLint 1-line forwarder per-package DRY(sweep 建议)。 证据:api F257+回归 105 全绿;web reachability vitest 8 + web tsc clean;biome clean、 callbacks 复杂度 0-delta;无 any。A18/A19(F246 dispatch origination,fresh extra 无 drop)+ A16/A17(full passthrough)文档化 N-A。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 21, 2026
…rity + fail-closed) (#48) * feat(F257): #3 objective registry — 只读发现(取代 report_harness_signal objectiveId 考古) 修复清单 #3:report_harness_signal 的 objectiveId 是自由 string,零 list/schema/注入清单 → 三次上报三次考古。本 PR 建 objective registry 只读发现层: - registry YAML(docs/harness-feedback/objectives/registry.yaml,KD-3 registry-as-data)= 定义层(id/statement/segments)。**只登 canonized 目标**——redesign §2 名义 8 objectives 但只 canonize 了 2 个 id(obj-routing-delivery + obj-identity-integrity);不 invent 另 7 个 id(会伪造 V2 依赖的契约),registry 随 canonize 增长。 - loader(objective-registry.ts):parseObjectiveRegistry 纯函数(malformed 行丢弃不抛、 malformed YAML → 空)+ loadObjectiveRegistry fail-safe。 - GET /api/callbacks/objectives(镜像 rich-block-rules 只读路由)。 - cat_cafe_list_objectives MCP tool(镜像 rich-block-rules-tool;A_READ_LOCAL + readonly allowlist 注册)。 - report_harness_signal objectiveId description 同步指向 list_objectives。 边界:只做定义层 + 发现,不做 V2(evaluator/metrics/condition registry)。objectiveId 仍 自由(discovery-only,软校验留 V2)。 证据:9 tests(parseObjectiveRegistry 4 + loadObjectiveRegistry 2 + 路由 e2e + shipped registry 验证)全绿;api + mcp-server build clean;biome clean。切片 2a,独立 #47。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R1 — single authority + fail-closed + contract tests + map delta Addresses sol's slice-2a R1 REQUEST CHANGES (all four, no pushback). ## P1-1 — remove `segments` (second writable authority) The registry authored `id/statement/segments`, duplicating the unit→objective membership that frozen redesign §4.8 keeps in the versioned UnitEvaluationManifest. Dropped `segments` from YAML + loader type + API output + MCP tool output; the registry is now the canonical `id + statement` definition layer only. Attachments become a V2 derived read-model from UnitEvaluationManifest — one authority. ## P1-2 — fail-closed loader (no silent empty), strict validation, lower complexity parseObjectiveRegistry/loadObjectiveRegistry now return a discriminated ObjectiveRegistryResult ({ok:true,registry} | {ok:false,error}). Unreadable file / malformed YAML / non-mapping root / non-positive-integer version / non-array objectives / any invalid row / duplicate id → explicit failure, never {version:1,objectives:[]}. Validates positive-integer version, trimmed non-empty id matching ^obj-[a-z0-9]+(?:-[a-z0-9]+)*$, trimmed non-empty statement, unique ids (sol repros now rejected: version -2.5, whitespace id/statement, dup ids). Route returns 503 (uncached) on failure; MCP tool surfaces it via its existing !ok error path. Validation extracted into helpers → below the Biome complexity-17 warning. Route gains an optional injectable registry path (test seam). ## P2-1 — MCP contract tests restored to true green Added cat_cafe_list_objectives to EXPECTED_TOOLS / EXPECTED_COLLAB_TOOLS / EXPECTED_READONLY_TOOLS, and cat_cafe_report_harness_signal to TOOLS/COLLAB (the PR #42 leftover that made the suite baseline-red 385/387) — not masking the new mismatch behind the old failure name. Added focused handler tests (success / honest empty / 503 HTTP failure / network error). Full mcp-server suite 391/391. ## P2-2 — architecture map delta harness-eval cell: added objective-registry.ts / callback-docs-routes.ts / list-objectives-tool.ts code anchors + registry.yaml doc anchor + scan hints + why this is the definition layer of the SAME control plane, not a parallel registry. ## Evidence - api: f257-objective-registry 18/0, callback-docs-route 4/0 (200 + 503 fail-closed). - mcp-server full suite 391/391 (was 385/387). - biome: changed files clean (useLiteralKeys + parser complexity + format resolved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R2 — path-free 503 + strict key-set (reject unknown, not strip) Addresses sol's 2a R2 REQUEST CHANGES (both small, no scope change). R1's four findings were confirmed CLOSED. ## P2-1 — unauthenticated 503 leaked internal path / fs errno callback-docs-routes.ts /api/callbacks/objectives is unauthenticated but returned the loader's raw error, which contains the registry path + Node fs errno (e.g. "...registry unreadable at /private/secret/install/objectives.yaml: ENOENT..."), forwarded onward via the MCP tool's response.text(). Now: log the full internal reason server-side (request.log.error) and return a stable, path-free 503 body `{ error: 'Objective registry unavailable' }`. Test asserts the body contains neither the passed path nor ENOENT/errno, and is uncached; the MCP tool still recognizes 503 as an errorResult. ## P2-2 — "strict + single authority" schema was still fail-open on forbidden fields The loader read only id/statement and silently dropped everything else — so a stray `segments` (or a typo / future unversioned field) could reappear in the YAML, mislead a human into treating it as membership authority, while the runtime ignored it. Worse, the R1 test asserted that strip-behavior as "valid". Now registryVersion=1 fail-closes on the key set: root allows only registryVersion/objectives; each entry allows only id/statement. A `segments` key REJECTS (message points to the UnitEvaluationManifest §4.8 authority), not strips; any unknown key rejects. Future fields must bump the schema. Removed the fail-open "ignores segments" test; added red→green rejections for segments, unknown entry key, and unknown root key. ## Evidence - api: f257-objective-registry 21/0 (now includes segments/unknown-key rejections + descriptive-message assertion), callback-docs-route 4/0 (path-free 503 asserted). - shipped registry.yaml still validates (id/statement only). biome changed files clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R3 — pin registryVersion to exactly 1 (fail closed on unsupported) Addresses sol's 2a R3 sole finding (R2's two remain CLOSED). ## P2-1 — unsupported schema versions were accepted under v1 semantics The key-set contract advertised "registryVersion=1 allows only … / future fields must bump schema", but parseObjectiveRegistry accepted EVERY positive integer and applied the v1 parser (repro: v1/v2/v999 all ok:true). That advertises an unimplemented version back to discovery clients as supported. Now the version gate requires exactly SUPPORTED_REGISTRY_ VERSION (=1); anything else fails closed with a clear message. A future v2 must ship an explicit versioned parser + bump the constant, not ride the positive-integer gate. Tests: changed the version-2 "valid" fixture to 1; added v2 + v999 rejection cases (the existing -2.5 / 0 / 1.5 / missing cases still reject as ≠1). ## Evidence - f257-objective-registry 23/0. biome changed files clean; build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
registerCallbackAuthHookpreHandler middleware that readsX-Invocation-Id/X-Callback-Tokenheaders, verifies viaInvocationRegistry.verify(), and decoratesrequest.callbackAuthrequireCallbackAuth(request, reply)callbackPost/callbackGetto send credentials as HTTP headers instead of in body/querycallbackAuthSchema(Zod schema for body validation — no longer needed)Test plan
callback-auth-prehandler.test.js— unit tests for valid/invalid/missing/partial headersheaders: { 'x-invocation-id': ..., 'x-callback-token': ... }registerCallbackAuthHooksince they bypass the parent pluginINVALID_CALLBACK_CREDENTIALScode)pnpm checkpasses (Biome)pnpm lintpasses (TypeScript)Closes zts212653#476
Supersedes #2 (closed due to stale base)
🤖 Generated with Claude Code