refactor: expose extension sdk boundary seams

This commit is contained in:
Peter Steinberger
2026-04-27 21:58:04 +01:00
parent 3e497f5e2b
commit 662de55e07
20 changed files with 154 additions and 25 deletions

View File

@@ -1,2 +1,2 @@
eab8d67d3601d697253d2cc265ea3b18c719a0fa8ca6e983bf1edcd38b9382e7 plugin-sdk-api-baseline.json
8c7e737467cb84d3a1c80c677cb94b26fe4d5739d154245c48d744aef80f53f8 plugin-sdk-api-baseline.jsonl
5b59db88172a184e0cbac3301c51304686b6184c952ae5ed9f09b001dd65e3f3 plugin-sdk-api-baseline.json
a195e76ddd1e09cb4023b247a684cd4e93c2662971adb4378ec126a78c537582 plugin-sdk-api-baseline.jsonl

View File

@@ -257,7 +257,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/webhook-path` | Webhook path normalization helpers |
| `plugin-sdk/web-media` | Shared remote/local media loading helpers |
| `plugin-sdk/zod` | Re-exported `zod` for plugin SDK consumers |
| `plugin-sdk/testing` | Public extension test helpers including `installCommonResolveTargetErrorCases`, `shouldAckReaction`, `writeSkill`, `createTestRegistry`, and live generation env loading |
| `plugin-sdk/testing` | Public extension test helpers including plugin registry/runtime mocks, schema/media/live-test helpers, `installCommonResolveTargetErrorCases`, `writeSkill`, `createTestRegistry`, and live generation env loading |
</Accordion>
<Accordion title="Memory subpaths">

View File

@@ -1,4 +1,4 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
export type CodexContextProjection = {
developerInstructionAddition?: string;

View File

@@ -1,4 +1,3 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { AssistantMessage, Usage } from "@mariozechner/pi-ai";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import {
@@ -13,6 +12,7 @@ import {
runAgentHarnessAfterCompactionHook,
runAgentHarnessBeforeCompactionHook,
TOOL_PROGRESS_OUTPUT_MAX_CHARS,
type AgentMessage,
type EmbeddedRunAttemptParams,
type EmbeddedRunAttemptResult,
type MessagingToolSend,

View File

@@ -1,5 +1,4 @@
import fs from "node:fs/promises";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import {
assembleHarnessContextEngine,
@@ -29,6 +28,7 @@ import {
registerNativeHookRelay,
setActiveEmbeddedRun,
supportsModelTools,
type AgentMessage,
type EmbeddedRunAttemptParams,
type EmbeddedRunAttemptResult,
type NativeHookRelayEvent,

View File

@@ -1,11 +1,11 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { SessionManager } from "@mariozechner/pi-coding-agent";
import {
acquireSessionWriteLock,
emitSessionTranscriptUpdate,
runAgentHarnessBeforeMessageWriteHook,
type AgentMessage,
} from "openclaw/plugin-sdk/agent-harness-runtime";
export async function mirrorCodexAppServerTranscript(params: {

View File

@@ -2,6 +2,8 @@ import fs from "node:fs";
import path from "node:path";
import { collectFilesSync, relativeToCwd } from "./check-file-utils.js";
type Offender = { file: string; hint: string; line?: number; specifier?: string };
const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
{
pattern: /["']openclaw\/plugin-sdk["']/,
@@ -33,13 +35,10 @@ const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
},
];
const FORBIDDEN_TEST_SUPPORT_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
{
pattern:
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["'](?:\.\.\/){2,}src\/(?:agents|channels|config|infra|plugins|routing|security|test-helpers|test-utils)\/[^"']+["']/,
hint: "Use openclaw/plugin-sdk/testing or a focused plugin-sdk test/runtime subpath instead of core internals.",
},
];
const STATIC_RELATIVE_MODULE_PATTERN = /\b(?:import|export)\b[\s\S]*?\bfrom\s*["']([^"']+)["']/g;
const RELATIVE_CORE_HINT =
"Use openclaw/plugin-sdk/testing or a focused plugin-sdk test/runtime subpath instead of core internals.";
function isExtensionTestFile(filePath: string): boolean {
return /\.test\.[cm]?[jt]sx?$/u.test(filePath) || /\.e2e\.test\.[cm]?[jt]sx?$/u.test(filePath);
@@ -56,23 +55,57 @@ function collectExtensionTestFiles(rootDir: string): string[] {
});
}
function lineNumberForOffset(content: string, offset: number): number {
let line = 1;
for (let index = 0; index < offset; index += 1) {
if (content.charCodeAt(index) === 10) {
line += 1;
}
}
return line;
}
function resolvesToRepoSrc(filePath: string, specifier: string): boolean {
if (!specifier.startsWith(".")) {
return false;
}
const resolved = path.resolve(path.dirname(filePath), specifier);
const repoRelative = path.relative(process.cwd(), resolved).replaceAll(path.sep, "/");
return repoRelative === "src" || repoRelative.startsWith("src/");
}
function collectRelativeCoreImportOffenders(filePath: string, content: string): Offender[] {
const offenders: Offender[] = [];
for (const match of content.matchAll(STATIC_RELATIVE_MODULE_PATTERN)) {
const specifier = match[1];
if (!specifier || !resolvesToRepoSrc(filePath, specifier)) {
continue;
}
offenders.push({
file: filePath,
hint: RELATIVE_CORE_HINT,
line: lineNumberForOffset(content, match.index ?? 0),
specifier,
});
}
return offenders;
}
function main() {
const extensionsDir = path.join(process.cwd(), "extensions");
const files = collectExtensionTestFiles(extensionsDir);
const offenders: Array<{ file: string; hint: string }> = [];
const offenders: Offender[] = [];
for (const file of files) {
const content = fs.readFileSync(file, "utf8");
const rules = isExtensionTestSupportFile(file)
? [...FORBIDDEN_PATTERNS, ...FORBIDDEN_TEST_SUPPORT_PATTERNS]
: FORBIDDEN_PATTERNS;
for (const rule of rules) {
for (const rule of FORBIDDEN_PATTERNS) {
if (!rule.pattern.test(content)) {
continue;
}
offenders.push({ file, hint: rule.hint });
break;
}
offenders.push(...collectRelativeCoreImportOffenders(file, content));
}
if (offenders.length > 0) {
@@ -80,7 +113,11 @@ function main() {
"Extension test files must stay on extension test bridges or public plugin-sdk surfaces.",
);
for (const offender of offenders.toSorted((a, b) => a.file.localeCompare(b.file))) {
console.error(`- ${relativeToCwd(offender.file)}: ${offender.hint}`);
const location = offender.line
? `${relativeToCwd(offender.file)}:${offender.line}`
: relativeToCwd(offender.file);
const specifier = offender.specifier ? ` (${offender.specifier})` : "";
console.error(`- ${location}${specifier}: ${offender.hint}`);
}
process.exit(1);
}

View File

@@ -9,6 +9,7 @@ import { truncateUtf16Safe } from "../utils.js";
export const TOOL_PROGRESS_OUTPUT_MAX_CHARS = 8_000;
export type { AgentMessage } from "@mariozechner/pi-agent-core";
export type {
AgentHarness,
AgentHarnessAttemptParams,
@@ -24,11 +25,12 @@ export type {
EmbeddedRunAttemptParams,
EmbeddedRunAttemptResult,
} from "../agents/pi-embedded-runner/run/types.js";
export type { ContextEngine as HarnessContextEngine } from "../context-engine/types.js";
export type { CompactEmbeddedPiSessionParams } from "../agents/pi-embedded-runner/compact.js";
export type { EmbeddedPiCompactResult } from "../agents/pi-embedded-runner/types.js";
export type { AnyAgentTool } from "../agents/tools/common.js";
export type { MessagingToolSend } from "../agents/pi-embedded-messaging.types.js";
export type { AgentApprovalEventData } from "../infra/agent-events.js";
export type { AgentApprovalEventData, AgentEventPayload } from "../infra/agent-events.js";
export type { ExecApprovalDecision } from "../infra/exec-approvals.js";
export type { NormalizedUsage } from "../agents/usage.js";
export type {
@@ -57,8 +59,10 @@ export type {
export { VERSION as OPENCLAW_VERSION } from "../version.js";
export { formatErrorMessage } from "../infra/errors.js";
export { formatApprovalDisplayPath } from "../infra/approval-display-paths.js";
export { emitAgentEvent } from "../infra/agent-events.js";
export { emitAgentEvent, onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js";
export { buildAgentRuntimePlan } from "../agents/runtime-plan/build.js";
export { classifyEmbeddedPiRunResultForModelFallback } from "../agents/pi-embedded-runner/result-fallback-classifier.js";
export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";
export { resolveUserPath } from "../utils.js";
export { callGatewayTool } from "../agents/tools/gateway.js";
@@ -123,6 +127,7 @@ export {
} from "../agents/harness/lifecycle-hook-helpers.js";
export {
buildNativeHookRelayCommand,
__testing as nativeHookRelayTesting,
registerNativeHookRelay,
} from "../agents/harness/native-hook-relay.js";

View File

@@ -1,6 +1,7 @@
// Public agent/model/runtime helpers for plugins that integrate with core agent flows.
export * from "../agents/agent-scope.js";
export { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
export * from "../agents/current-time.js";
export * from "../agents/date-time.js";
export * from "../agents/defaults.js";

View File

@@ -8,6 +8,7 @@ export {
resolvePluginConfigObject,
} from "./plugin-config-runtime.js";
export {
clearConfigCache,
clearRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshot,
getRuntimeConfigSnapshot,

View File

@@ -1,2 +1,4 @@
/** Root OpenClaw configuration Zod schema — the full `openclaw.json` shape. */
export { OpenClawSchema } from "../config/zod-schema.js";
export { validateJsonSchemaValue } from "../plugins/schema-validator.js";
export type { JsonSchemaObject } from "../shared/json-schema.types.js";

View File

@@ -7,8 +7,11 @@ export type {
} from "../infra/diagnostic-events.js";
export {
emitDiagnosticEvent,
emitTrustedDiagnosticEvent,
isDiagnosticsEnabled,
onInternalDiagnosticEvent,
onDiagnosticEvent,
resetDiagnosticEventsForTest,
} from "../infra/diagnostic-events.js";
export type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
export {

View File

@@ -3,3 +3,7 @@
export * from "../hooks/fire-and-forget.js";
export * from "../hooks/internal-hooks.js";
export * from "../hooks/message-hook-mappers.js";
export {
initializeGlobalHookRunner,
resetGlobalHookRunner,
} from "../plugins/hook-runner-global.js";

View File

@@ -1 +1,5 @@
export * from "../memory-host-sdk/engine-embeddings.js";
export {
clearMemoryEmbeddingProviders,
registerMemoryEmbeddingProvider,
} from "../plugins/memory-embedding-providers.js";

View File

@@ -5,4 +5,9 @@ export type {
MemoryCorpusSupplement,
MemoryCorpusSupplementRegistration,
} from "../plugins/memory-state.js";
export { listMemoryCorpusSupplements } from "../plugins/memory-state.js";
export {
clearMemoryPluginState,
listMemoryCorpusSupplements,
registerMemoryCapability,
registerMemoryCorpusSupplement,
} from "../plugins/memory-state.js";

View File

@@ -4,4 +4,5 @@ export {
getProviderEnvVars,
listKnownProviderAuthEnvVarNames,
omitEnvKeysCaseInsensitive,
resolveProviderAuthEnvVarCandidates,
} from "../secrets/provider-env-vars.js";

View File

@@ -1,4 +1,5 @@
export { buildGuardedModelFetch } from "../agents/provider-transport-fetch.js";
export { buildOpenAICompletionsParams } from "../agents/openai-transport-stream.js";
export { stripSystemPromptCacheBoundary } from "../agents/system-prompt-cache-boundary.js";
export { transformTransportMessages } from "../agents/transport-message-transform.js";
export {

View File

@@ -6,5 +6,9 @@ export {
resolveTextChunkLimit,
} from "../auto-reply/chunk.js";
export type { ChunkMode } from "../auto-reply/chunk.js";
export { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
export {
isSilentReplyPayloadText,
isSilentReplyText,
SILENT_REPLY_TOKEN,
} from "../auto-reply/tokens.js";
export type { ReplyPayload } from "./reply-payload.js";

View File

@@ -1,2 +1,4 @@
export { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
export { getChannelsCommandSecretTargetIds } from "../cli/command-secret-targets.js";
export { resolveSecretRefValues } from "../secrets/resolve.js";
export { applyResolvedAssignments, createResolverContext } from "../secrets/runtime-shared.js";

View File

@@ -30,6 +30,10 @@ export {
resetPluginRuntimeStateForTest,
setActivePluginRegistry,
} from "../plugins/runtime.js";
export {
listImportedBundledPluginFacadeIds,
resetFacadeRuntimeStateForTest,
} from "./facade-runtime.js";
export { capturePluginRegistration } from "../plugins/captured-registration.js";
export { resolveProviderPluginChoice } from "../plugins/provider-auth-choice.runtime.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
@@ -40,9 +44,28 @@ export {
createRequestCaptureJsonFetch,
installPinnedHostnameTestHooks,
} from "../media-understanding/audio.test-helpers.ts";
export { isLiveTestEnabled } from "../agents/live-test-helpers.js";
export {
createSingleUserPromptMessage,
extractNonEmptyAssistantText,
isLiveProfileKeyModeEnabled,
isLiveTestEnabled,
} from "../agents/live-test-helpers.js";
export { createSandboxTestContext } from "../agents/sandbox/test-fixtures.js";
export { writeSkill } from "../agents/skills.e2e-test-helpers.js";
export {
castAgentMessage,
makeAgentAssistantMessage,
makeAgentUserMessage,
} from "../agents/test-helpers/agent-message-fixtures.js";
export { collectProviderApiKeys } from "../agents/live-auth-keys.js";
export { isModelNotFoundErrorMessage } from "../agents/live-model-errors.js";
export {
isAuthErrorMessage,
isBillingErrorMessage,
isOverloadedErrorMessage,
isServerErrorMessage,
isTimeoutErrorMessage,
} from "../agents/pi-embedded-helpers/failover-matches.js";
export { maybeLoadShellEnvForGenerationProviders } from "../test-utils/generation-live-test-helpers.js";
export { __testing } from "../acp/control-plane/manager.js";
export { __testing as acpManagerTesting } from "../acp/control-plane/manager.js";
@@ -50,6 +73,36 @@ export { runAcpRuntimeAdapterContract } from "../acp/runtime/adapter-contract.te
export { handleAcpCommand } from "../auto-reply/reply/commands-acp.js";
export { buildCommandTestParams } from "../auto-reply/reply/commands-spawn.test-harness.js";
export { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
export { isTruthyEnvValue } from "../infra/env.js";
export { getShellEnvAppliedKeys } from "../infra/shell-env.js";
export { encodePngRgba, fillPixel } from "../media/png-encode.js";
export {
parseLiveCsvFilter as parseCsvFilter,
parseProviderModelMap,
redactLiveApiKey,
} from "../media-generation/live-test-helpers.js";
export {
DEFAULT_LIVE_MUSIC_MODELS,
resolveConfiguredLiveMusicModels,
resolveLiveMusicAuthStore,
} from "../music-generation/live-test-helpers.js";
export {
canRunBufferBackedImageToVideoLiveLane,
canRunBufferBackedVideoToVideoLiveLane,
DEFAULT_LIVE_VIDEO_MODELS,
resolveConfiguredLiveVideoModels,
resolveLiveVideoAuthStore,
resolveLiveVideoResolution,
} from "../video-generation/live-test-helpers.js";
export { normalizeVideoGenerationDuration } from "../video-generation/duration-support.js";
export { parseVideoGenerationModelRef } from "../video-generation/model-ref.js";
export type {
GeneratedVideoAsset,
VideoGenerationMode,
VideoGenerationModeCapabilities,
VideoGenerationProvider,
VideoGenerationRequest,
} from "../video-generation/types.js";
export { jsonResponse, requestBodyText, requestUrl } from "../test-helpers/http.js";
export { mockPinnedHostnameResolution } from "../test-helpers/ssrf.js";
export { createTestRegistry } from "../test-utils/channel-plugins.js";
@@ -62,3 +115,9 @@ export { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-t
export { captureEnv, withEnv, withEnvAsync } from "../test-utils/env.js";
export { withFetchPreconnect, type FetchMock } from "../test-utils/fetch-mock.js";
export { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
export { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
export { buildPluginApi } from "../plugins/api-builder.js";
export {
createCapturedPluginRegistration,
type CapturedPluginRegistration,
} from "../plugins/captured-registration.js";