import { randomUUID } from "node:crypto"; import { pluginOperationIssueOriginKind } from "@paperclipai/shared"; import type { PaperclipPluginManifestV1, PluginCapability, PluginEventType, PluginIssueOriginKind, PluginManagedAgentResolution, PluginManagedRoutineResolution, PluginManagedSkillResolution, CompanySkill, Company, Project, Routine, RoutineRun, Issue, IssueComment, IssueThreadInteraction, CreateIssueThreadInteraction, IssueDocument, Agent, Goal, } from "@paperclipai/shared"; import type { EventFilter, PluginContext, PluginEntityRecord, PluginEntityUpsert, PluginJobContext, PluginLauncherRegistration, PluginEvent, ScopeKey, ToolResult, ToolRunContext, PluginWorkspace, AgentSession, AgentSessionEvent, PluginLocalFolderEntry, PluginLocalFolderStatus, } from "./types.js"; import type { PluginEnvironmentValidateConfigParams, PluginEnvironmentValidationResult, PluginEnvironmentProbeParams, PluginEnvironmentProbeResult, PluginEnvironmentLease, PluginEnvironmentAcquireLeaseParams, PluginEnvironmentResumeLeaseParams, PluginEnvironmentReleaseLeaseParams, PluginEnvironmentDestroyLeaseParams, PluginEnvironmentRealizeWorkspaceParams, PluginEnvironmentRealizeWorkspaceResult, PluginEnvironmentExecuteParams, PluginEnvironmentExecuteResult, } from "./protocol.js"; export interface TestHarnessOptions { /** Plugin manifest used to seed capability checks and metadata. */ manifest: PaperclipPluginManifestV1; /** Optional capability override. Defaults to `manifest.capabilities`. */ capabilities?: PluginCapability[]; /** Initial config returned by `ctx.config.get()`. */ config?: Record; } export interface TestHarnessLogEntry { level: "info" | "warn" | "error" | "debug"; message: string; meta?: Record; } export interface TestHarness { /** Fully-typed in-memory plugin context passed to `plugin.setup(ctx)`. */ ctx: PluginContext; /** Seed host entities for `ctx.companies/projects/issues/agents/goals` reads. */ seed(input: { companies?: Company[]; projects?: Project[]; issues?: Issue[]; issueComments?: IssueComment[]; agents?: Agent[]; goals?: Goal[]; }): void; setConfig(config: Record): void; /** Dispatch a host or plugin event to registered handlers. */ emit(eventType: PluginEventType | `plugin.${string}`, payload: unknown, base?: Partial): Promise; /** Execute a previously-registered scheduled job handler. */ runJob(jobKey: string, partial?: Partial): Promise; /** Invoke a `ctx.data.register(...)` handler by key. */ getData(key: string, params?: Record): Promise; /** Invoke a `ctx.actions.register(...)` handler by key. */ performAction(key: string, params?: Record): Promise; /** Execute a registered tool handler via `ctx.tools.execute(...)`. */ executeTool(name: string, params: unknown, runCtx?: Partial): Promise; /** Read raw in-memory state for assertions. */ getState(input: ScopeKey): unknown; /** Simulate a streaming event arriving for an active session. */ simulateSessionEvent(sessionId: string, event: Omit): void; logs: TestHarnessLogEntry[]; activity: Array<{ message: string; entityType?: string; entityId?: string; metadata?: Record }>; metrics: Array<{ name: string; value: number; tags?: Record }>; telemetry: Array<{ eventName: string; dimensions?: Record }>; dbQueries: Array<{ sql: string; params?: unknown[] }>; dbExecutes: Array<{ sql: string; params?: unknown[] }>; } // --------------------------------------------------------------------------- // Environment test harness types // --------------------------------------------------------------------------- /** Recorded environment lifecycle event for assertion helpers. */ export interface EnvironmentEventRecord { type: | "validateConfig" | "probe" | "acquireLease" | "resumeLease" | "releaseLease" | "destroyLease" | "realizeWorkspace" | "execute"; driverKey: string; environmentId: string; timestamp: string; params: Record; result?: unknown; error?: string; } /** Options for creating an environment-aware test harness. */ export interface EnvironmentTestHarnessOptions extends TestHarnessOptions { /** Environment driver hooks provided by the plugin under test. */ environmentDriver: { driverKey: string; onValidateConfig?: (params: PluginEnvironmentValidateConfigParams) => Promise; onProbe?: (params: PluginEnvironmentProbeParams) => Promise; onAcquireLease?: (params: PluginEnvironmentAcquireLeaseParams) => Promise; onResumeLease?: (params: PluginEnvironmentResumeLeaseParams) => Promise; onReleaseLease?: (params: PluginEnvironmentReleaseLeaseParams) => Promise; onDestroyLease?: (params: PluginEnvironmentDestroyLeaseParams) => Promise; onRealizeWorkspace?: (params: PluginEnvironmentRealizeWorkspaceParams) => Promise; onExecute?: (params: PluginEnvironmentExecuteParams) => Promise; }; } /** Extended test harness with environment driver simulation. */ export interface EnvironmentTestHarness extends TestHarness { /** Recorded environment lifecycle events for assertion. */ environmentEvents: EnvironmentEventRecord[]; /** Invoke the environment driver's validateConfig hook. */ validateConfig(params: PluginEnvironmentValidateConfigParams): Promise; /** Invoke the environment driver's probe hook. */ probe(params: PluginEnvironmentProbeParams): Promise; /** Invoke the environment driver's acquireLease hook. */ acquireLease(params: PluginEnvironmentAcquireLeaseParams): Promise; /** Invoke the environment driver's resumeLease hook. */ resumeLease(params: PluginEnvironmentResumeLeaseParams): Promise; /** Invoke the environment driver's releaseLease hook. */ releaseLease(params: PluginEnvironmentReleaseLeaseParams): Promise; /** Invoke the environment driver's destroyLease hook. */ destroyLease(params: PluginEnvironmentDestroyLeaseParams): Promise; /** Invoke the environment driver's realizeWorkspace hook. */ realizeWorkspace(params: PluginEnvironmentRealizeWorkspaceParams): Promise; /** Invoke the environment driver's execute hook. */ execute(params: PluginEnvironmentExecuteParams): Promise; } // --------------------------------------------------------------------------- // Environment event assertion helpers // --------------------------------------------------------------------------- /** Filter environment events by type. */ export function filterEnvironmentEvents( events: EnvironmentEventRecord[], type: EnvironmentEventRecord["type"], ): EnvironmentEventRecord[] { return events.filter((e) => e.type === type); } /** Assert that environment events occurred in the expected order. */ export function assertEnvironmentEventOrder( events: EnvironmentEventRecord[], expectedOrder: EnvironmentEventRecord["type"][], ): void { const actual = events.map((e) => e.type); const matched: EnvironmentEventRecord["type"][] = []; let cursor = 0; for (const eventType of actual) { if (cursor < expectedOrder.length && eventType === expectedOrder[cursor]) { matched.push(eventType); cursor++; } } if (matched.length !== expectedOrder.length) { throw new Error( `Environment event order mismatch.\nExpected: ${JSON.stringify(expectedOrder)}\nActual: ${JSON.stringify(actual)}`, ); } } /** Assert that a full lease lifecycle (acquire → release) occurred for an environment. */ export function assertLeaseLifecycle( events: EnvironmentEventRecord[], environmentId: string, ): { acquire: EnvironmentEventRecord; release: EnvironmentEventRecord } { const acquire = events.find((e) => e.type === "acquireLease" && e.environmentId === environmentId); const release = events.find((e) => (e.type === "releaseLease" || e.type === "destroyLease") && e.environmentId === environmentId); if (!acquire) throw new Error(`No acquireLease event found for environment ${environmentId}`); if (!release) throw new Error(`No releaseLease/destroyLease event found for environment ${environmentId}`); if (acquire.timestamp > release.timestamp) { throw new Error(`acquireLease occurred after release for environment ${environmentId}`); } return { acquire, release }; } /** Assert that workspace realization occurred between lease acquire and release. */ export function assertWorkspaceRealizationLifecycle( events: EnvironmentEventRecord[], environmentId: string, ): EnvironmentEventRecord { const lifecycle = assertLeaseLifecycle(events, environmentId); const realize = events.find( (e) => e.type === "realizeWorkspace" && e.environmentId === environmentId, ); if (!realize) throw new Error(`No realizeWorkspace event found for environment ${environmentId}`); if (realize.timestamp < lifecycle.acquire.timestamp) { throw new Error(`realizeWorkspace occurred before acquireLease for environment ${environmentId}`); } if (realize.timestamp > lifecycle.release.timestamp) { throw new Error(`realizeWorkspace occurred after release for environment ${environmentId}`); } return realize; } /** Assert that an execute call occurred within the lease lifecycle. */ export function assertExecutionLifecycle( events: EnvironmentEventRecord[], environmentId: string, ): EnvironmentEventRecord[] { const lifecycle = assertLeaseLifecycle(events, environmentId); const execEvents = events.filter( (e) => e.type === "execute" && e.environmentId === environmentId, ); if (execEvents.length === 0) { throw new Error(`No execute events found for environment ${environmentId}`); } for (const exec of execEvents) { if (exec.timestamp < lifecycle.acquire.timestamp || exec.timestamp > lifecycle.release.timestamp) { throw new Error(`Execute event occurred outside lease lifecycle for environment ${environmentId}`); } } return execEvents; } /** Assert that an event recorded an error. */ export function assertEnvironmentError( events: EnvironmentEventRecord[], type: EnvironmentEventRecord["type"], environmentId?: string, ): EnvironmentEventRecord { const match = events.find( (e) => e.type === type && e.error != null && (!environmentId || e.environmentId === environmentId), ); if (!match) { throw new Error(`No error event of type '${type}'${environmentId ? ` for environment ${environmentId}` : ""}`); } return match; } // --------------------------------------------------------------------------- // Fake environment plugin driver // --------------------------------------------------------------------------- /** Options for creating a fake environment driver for contract testing. */ export interface FakeEnvironmentDriverOptions { driverKey?: string; /** Simulated acquire delay in ms. */ acquireDelayMs?: number; /** If true, probe will return `ok: false`. */ probeFailure?: boolean; /** If true, acquireLease will throw. */ acquireFailure?: string; /** If true, execute will return a non-zero exit code. */ executeFailure?: boolean; /** Custom metadata returned on lease acquire. */ leaseMetadata?: Record; } /** * Create a fake environment driver suitable for contract testing. * * This returns a driver hooks object compatible with `EnvironmentTestHarnessOptions.environmentDriver`. * It simulates the full environment lifecycle with configurable failure injection. */ export function createFakeEnvironmentDriver(options: FakeEnvironmentDriverOptions = {}): EnvironmentTestHarnessOptions["environmentDriver"] { const driverKey = options.driverKey ?? "fake"; const leases = new Map }>(); let leaseCounter = 0; return { driverKey, async onValidateConfig(params) { if (!params.config || typeof params.config !== "object") { return { ok: false, errors: ["Config must be an object"] }; } return { ok: true, normalizedConfig: params.config }; }, async onProbe(_params) { if (options.probeFailure) { return { ok: false, summary: "Simulated probe failure", diagnostics: [{ severity: "error", message: "Probe failed" }] }; } return { ok: true, summary: "Fake environment is healthy" }; }, async onAcquireLease(params) { if (options.acquireFailure) { throw new Error(options.acquireFailure); } if (options.acquireDelayMs) { await new Promise((resolve) => setTimeout(resolve, options.acquireDelayMs)); } const providerLeaseId = `fake-lease-${++leaseCounter}`; const metadata = { ...options.leaseMetadata, acquiredAt: new Date().toISOString(), runId: params.runId }; leases.set(providerLeaseId, { providerLeaseId, metadata }); return { providerLeaseId, metadata }; }, async onResumeLease(params) { const existing = leases.get(params.providerLeaseId); if (!existing) { throw new Error(`Lease ${params.providerLeaseId} not found — cannot resume`); } return { providerLeaseId: existing.providerLeaseId, metadata: { ...existing.metadata, resumed: true } }; }, async onReleaseLease(params) { if (params.providerLeaseId) { leases.delete(params.providerLeaseId); } }, async onDestroyLease(params) { if (params.providerLeaseId) { leases.delete(params.providerLeaseId); } }, async onRealizeWorkspace(params) { return { cwd: params.workspace.localPath ?? params.workspace.remotePath ?? "/tmp/fake-workspace", metadata: { realized: true }, }; }, async onExecute(params) { if (options.executeFailure) { return { exitCode: 1, timedOut: false, stdout: "", stderr: "Simulated execution failure" }; } return { exitCode: 0, timedOut: false, stdout: `Executed: ${params.command} ${(params.args ?? []).join(" ")}`.trim(), stderr: "", }; }, }; } type EventRegistration = { name: PluginEventType | `plugin.${string}`; filter?: EventFilter; fn: (event: PluginEvent) => Promise; }; function normalizeScope(input: ScopeKey): Required> & Pick { return { scopeKind: input.scopeKind, scopeId: input.scopeId, namespace: input.namespace ?? "default", stateKey: input.stateKey, }; } function stateMapKey(input: ScopeKey): string { const normalized = normalizeScope(input); return `${normalized.scopeKind}|${normalized.scopeId ?? ""}|${normalized.namespace}|${normalized.stateKey}`; } function allowsEvent(filter: EventFilter | undefined, event: PluginEvent): boolean { if (!filter) return true; if (filter.companyId && filter.companyId !== String((event.payload as Record | undefined)?.companyId ?? "")) return false; if (filter.projectId && filter.projectId !== String((event.payload as Record | undefined)?.projectId ?? "")) return false; if (filter.agentId && filter.agentId !== String((event.payload as Record | undefined)?.agentId ?? "")) return false; return true; } function requireCapability(manifest: PaperclipPluginManifestV1, allowed: Set, capability: PluginCapability) { if (allowed.has(capability)) return; throw new Error(`Plugin '${manifest.id}' is missing required capability '${capability}' in test harness`); } function requireCompanyId(companyId?: string): string { if (!companyId) throw new Error("companyId is required for this operation"); return companyId; } function isInCompany( record: T | null | undefined, companyId: string, ): record is T { return Boolean(record && record.companyId === companyId); } /** * Create an in-memory host harness for plugin worker tests. * * The harness enforces declared capabilities and simulates host APIs, so tests * can validate plugin behavior without spinning up the Paperclip server runtime. */ export function createTestHarness(options: TestHarnessOptions): TestHarness { const manifest = options.manifest; const capabilitySet = new Set(options.capabilities ?? manifest.capabilities); let currentConfig = { ...(options.config ?? {}) }; const logs: TestHarnessLogEntry[] = []; const activity: TestHarness["activity"] = []; const metrics: TestHarness["metrics"] = []; const telemetry: TestHarness["telemetry"] = []; const dbQueries: TestHarness["dbQueries"] = []; const dbExecutes: TestHarness["dbExecutes"] = []; const state = new Map(); const entities = new Map(); const entityExternalIndex = new Map(); const companies = new Map(); const projects = new Map(); const routines = new Map(); const routineRuns = new Map(); const issues = new Map(); const blockedByIssueIds = new Map(); const issueComments = new Map(); const issueInteractions = new Map(); const issueDocuments = new Map(); const agents = new Map(); const goals = new Map(); const projectWorkspaces = new Map(); const localFolderStatuses = new Map(); const localFolderFiles = new Map(); const sessions = new Map(); const sessionEventCallbacks = new Map void>(); const events: EventRegistration[] = []; const jobs = new Map Promise>(); const launchers = new Map(); const dataHandlers = new Map) => Promise>(); const actionHandlers = new Map) => Promise>(); const toolHandlers = new Map Promise>(); function localFolderKey(companyId: string, folderKey: string): string { return `${companyId}:${folderKey}`; } function localFolderFileKey(companyId: string, folderKey: string, relativePath: string): string { return `${localFolderKey(companyId, folderKey)}:${relativePath}`; } function normalizeLocalFolderRelativePath(relativePath: string): string { const parts: string[] = []; for (const segment of relativePath.split(/[\\/]+/)) { if (!segment || segment === ".") continue; if (segment === "..") throw new Error("Local folder path traversal is not allowed"); parts.push(segment); } return parts.join("/"); } function notConfiguredLocalFolderStatus(folderKey: string): PluginLocalFolderStatus { return { folderKey, configured: false, path: null, realPath: null, access: "readWrite", readable: false, writable: false, requiredDirectories: [], requiredFiles: [], missingDirectories: [], missingFiles: [], healthy: false, problems: [{ code: "not_configured", message: "No local folder path is configured." }], checkedAt: new Date().toISOString(), }; } function issueRelationSummary(issueId: string) { const issue = issues.get(issueId); if (!issue) throw new Error(`Issue not found: ${issueId}`); const summarize = (candidateId: string) => { const related = issues.get(candidateId); if (!related || related.companyId !== issue.companyId) return null; return { id: related.id, identifier: related.identifier, title: related.title, status: related.status, priority: related.priority, assigneeAgentId: related.assigneeAgentId, assigneeUserId: related.assigneeUserId, }; }; const blockedBy = (blockedByIssueIds.get(issueId) ?? []) .map(summarize) .filter((value): value is NonNullable => value !== null); const blocks = [...blockedByIssueIds.entries()] .filter(([, blockers]) => blockers.includes(issueId)) .map(([blockedIssueId]) => summarize(blockedIssueId)) .filter((value): value is NonNullable => value !== null); return { blockedBy, blocks }; } const defaultPluginOriginKind: PluginIssueOriginKind = `plugin:${manifest.id}`; function managedAgentDeclaration(agentKey: string) { const declaration = manifest.agents?.find((agent) => agent.agentKey === agentKey); if (!declaration) throw new Error(`Managed agent declaration not found: ${agentKey}`); return declaration; } function isManagedAgent(agent: Agent, agentKey: string) { const marker = agent.metadata?.paperclipManagedResource; return Boolean( marker && typeof marker === "object" && !Array.isArray(marker) && (marker as Record).pluginKey === manifest.id && (marker as Record).resourceKind === "agent" && (marker as Record).resourceKey === agentKey, ); } function managedAgentMetadata(agentKey: string, existing?: Record | null) { return { ...(existing ?? {}), paperclipManagedResource: { pluginKey: manifest.id, resourceKind: "agent", resourceKey: agentKey, }, }; } function managedResolution( agentKey: string, companyId: string, agent: Agent | null, status: PluginManagedAgentResolution["status"], ): PluginManagedAgentResolution { return { pluginKey: manifest.id, resourceKind: "agent", resourceKey: agentKey, companyId, agentId: agent?.id ?? null, agent, status, approvalId: null, }; } function normalizePluginOriginKind(originKind: unknown = defaultPluginOriginKind): PluginIssueOriginKind { if (originKind == null || originKind === "") return defaultPluginOriginKind; if (typeof originKind !== "string") throw new Error("Plugin issue originKind must be a string"); if (originKind === defaultPluginOriginKind || originKind.startsWith(`${defaultPluginOriginKind}:`)) { return originKind as PluginIssueOriginKind; } throw new Error(`Plugin may only use originKind values under ${defaultPluginOriginKind}`); } const ctx: PluginContext = { manifest, config: { async get() { return { ...currentConfig }; }, }, localFolders: { declarations() { return manifest.localFolders ?? []; }, async configure(input) { requireCapability(manifest, capabilitySet, "local.folders"); const status = { folderKey: input.folderKey, configured: true, path: input.path, realPath: input.path, access: input.access ?? "readWrite", readable: true, writable: input.access === "read" ? false : true, requiredDirectories: input.requiredDirectories ?? [], requiredFiles: input.requiredFiles ?? [], missingDirectories: [], missingFiles: [], healthy: true, problems: [], checkedAt: new Date().toISOString(), } satisfies PluginLocalFolderStatus; localFolderStatuses.set(localFolderKey(input.companyId, input.folderKey), status); return status; }, async status(companyId, folderKey) { requireCapability(manifest, capabilitySet, "local.folders"); return localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? notConfiguredLocalFolderStatus(folderKey); }, async list(companyId, folderKey, options) { requireCapability(manifest, capabilitySet, "local.folders"); const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)); if (!status?.configured) throw new Error("Local folder is not configured"); const prefix = normalizeLocalFolderRelativePath(options?.relativePath ?? ""); const prefixWithSlash = prefix ? `${prefix}/` : ""; const entries = new Map(); for (const [key, contents] of localFolderFiles) { const filePrefix = `${localFolderKey(companyId, folderKey)}:`; if (!key.startsWith(filePrefix)) continue; const filePath = key.slice(filePrefix.length); if (prefix && filePath !== prefix && !filePath.startsWith(prefixWithSlash)) continue; const remainder = prefix ? filePath.slice(prefixWithSlash.length) : filePath; const [name] = remainder.split("/"); if (!name) continue; const entryPath = prefix ? `${prefix}/${name}` : name; const isNested = remainder.includes("/"); if (!options?.recursive && isNested) { entries.set(entryPath, { path: entryPath, name, kind: "directory", size: null, modifiedAt: null, }); continue; } entries.set(filePath, { path: filePath, name: filePath.split("/").pop() ?? filePath, kind: "file", size: Buffer.byteLength(contents, "utf8"), modifiedAt: null, }); } const maxEntries = options?.maxEntries && options.maxEntries > 0 ? options.maxEntries : entries.size; const allEntries = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path)); return { folderKey, relativePath: options?.relativePath ?? null, entries: allEntries.slice(0, maxEntries), truncated: allEntries.length > maxEntries, }; }, async readText(companyId, folderKey, relativePath) { requireCapability(manifest, capabilitySet, "local.folders"); const normalizedPath = normalizeLocalFolderRelativePath(relativePath); const contents = localFolderFiles.get(localFolderFileKey(companyId, folderKey, normalizedPath)); if (contents === undefined) throw new Error(`Local folder file not found: ${relativePath}`); return contents; }, async writeTextAtomic(companyId, folderKey, relativePath, contents) { requireCapability(manifest, capabilitySet, "local.folders"); const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? { folderKey, configured: true, path: `memory://${manifest.id}/${companyId}/${folderKey}`, realPath: `memory://${manifest.id}/${companyId}/${folderKey}`, access: "readWrite", readable: true, writable: true, requiredDirectories: [], requiredFiles: [], missingDirectories: [], missingFiles: [], healthy: true, problems: [], checkedAt: new Date().toISOString(), } satisfies PluginLocalFolderStatus; if (status.access !== "readWrite" || !status.writable) { throw new Error("Local folder is not configured for writes"); } localFolderStatuses.set(localFolderKey(companyId, folderKey), status); localFolderFiles.set(localFolderFileKey(companyId, folderKey, normalizeLocalFolderRelativePath(relativePath)), contents); return status; }, async deleteFile(companyId, folderKey, relativePath) { requireCapability(manifest, capabilitySet, "local.folders"); const status = localFolderStatuses.get(localFolderKey(companyId, folderKey)) ?? notConfiguredLocalFolderStatus(folderKey); if (status.configured && (status.access !== "readWrite" || !status.writable)) { throw new Error("Local folder is not configured for writes"); } localFolderFiles.delete(localFolderFileKey(companyId, folderKey, normalizeLocalFolderRelativePath(relativePath))); return status; }, }, events: { on(name: PluginEventType | `plugin.${string}`, filterOrFn: EventFilter | ((event: PluginEvent) => Promise), maybeFn?: (event: PluginEvent) => Promise): () => void { requireCapability(manifest, capabilitySet, "events.subscribe"); let registration: EventRegistration; if (typeof filterOrFn === "function") { registration = { name, fn: filterOrFn }; } else { if (!maybeFn) throw new Error("event handler is required"); registration = { name, filter: filterOrFn, fn: maybeFn }; } events.push(registration); return () => { const idx = events.indexOf(registration); if (idx !== -1) events.splice(idx, 1); }; }, async emit(name, companyId, payload) { requireCapability(manifest, capabilitySet, "events.emit"); await harness.emit(`plugin.${manifest.id}.${name}`, payload, { companyId }); }, }, jobs: { register(key, fn) { requireCapability(manifest, capabilitySet, "jobs.schedule"); jobs.set(key, fn); }, }, launchers: { register(launcher) { launchers.set(launcher.id, launcher); }, }, db: { namespace: manifest.database ? `test_${manifest.id.replace(/[^a-z0-9_]+/g, "_")}` : "", async query(sql, params) { requireCapability(manifest, capabilitySet, "database.namespace.read"); dbQueries.push({ sql, params }); return []; }, async execute(sql, params) { requireCapability(manifest, capabilitySet, "database.namespace.write"); dbExecutes.push({ sql, params }); return { rowCount: 0 }; }, }, http: { async fetch(url, init) { requireCapability(manifest, capabilitySet, "http.outbound"); return fetch(url, init); }, }, secrets: { async resolve(secretRef) { requireCapability(manifest, capabilitySet, "secrets.read-ref"); return `resolved:${secretRef}`; }, }, activity: { async log(entry) { requireCapability(manifest, capabilitySet, "activity.log.write"); activity.push(entry); }, }, state: { async get(input) { requireCapability(manifest, capabilitySet, "plugin.state.read"); return state.has(stateMapKey(input)) ? state.get(stateMapKey(input)) : null; }, async set(input, value) { requireCapability(manifest, capabilitySet, "plugin.state.write"); state.set(stateMapKey(input), value); }, async delete(input) { requireCapability(manifest, capabilitySet, "plugin.state.write"); state.delete(stateMapKey(input)); }, }, entities: { async upsert(input: PluginEntityUpsert) { const externalKey = input.externalId ? `${input.entityType}|${input.scopeKind}|${input.scopeId ?? ""}|${input.externalId}` : null; const existingId = externalKey ? entityExternalIndex.get(externalKey) : undefined; const existing = existingId ? entities.get(existingId) : undefined; const now = new Date().toISOString(); const previousExternalKey = existing?.externalId ? `${existing.entityType}|${existing.scopeKind}|${existing.scopeId ?? ""}|${existing.externalId}` : null; const record: PluginEntityRecord = existing ? { ...existing, entityType: input.entityType, scopeKind: input.scopeKind, scopeId: input.scopeId ?? null, externalId: input.externalId ?? null, title: input.title ?? null, status: input.status ?? null, data: input.data, updatedAt: now, } : { id: randomUUID(), entityType: input.entityType, scopeKind: input.scopeKind, scopeId: input.scopeId ?? null, externalId: input.externalId ?? null, title: input.title ?? null, status: input.status ?? null, data: input.data, createdAt: now, updatedAt: now, }; entities.set(record.id, record); if (previousExternalKey && previousExternalKey !== externalKey) { entityExternalIndex.delete(previousExternalKey); } if (externalKey) entityExternalIndex.set(externalKey, record.id); return record; }, async list(query) { let out = [...entities.values()]; if (query.entityType) out = out.filter((r) => r.entityType === query.entityType); if (query.scopeKind) out = out.filter((r) => r.scopeKind === query.scopeKind); if (query.scopeId) out = out.filter((r) => r.scopeId === query.scopeId); if (query.externalId) out = out.filter((r) => r.externalId === query.externalId); if (query.offset) out = out.slice(query.offset); if (query.limit) out = out.slice(0, query.limit); return out; }, }, projects: { async list(input) { requireCapability(manifest, capabilitySet, "projects.read"); const companyId = requireCompanyId(input?.companyId); let out = [...projects.values()]; out = out.filter((project) => project.companyId === companyId); if (input?.offset) out = out.slice(input.offset); if (input?.limit) out = out.slice(0, input.limit); return out; }, async get(projectId, companyId) { requireCapability(manifest, capabilitySet, "projects.read"); const project = projects.get(projectId); return isInCompany(project, companyId) ? project : null; }, async listWorkspaces(projectId, companyId) { requireCapability(manifest, capabilitySet, "project.workspaces.read"); if (!isInCompany(projects.get(projectId), companyId)) return []; return projectWorkspaces.get(projectId) ?? []; }, async getPrimaryWorkspace(projectId, companyId) { requireCapability(manifest, capabilitySet, "project.workspaces.read"); if (!isInCompany(projects.get(projectId), companyId)) return null; const workspaces = projectWorkspaces.get(projectId) ?? []; return workspaces.find((workspace) => workspace.isPrimary) ?? null; }, async getWorkspaceForIssue(issueId, companyId) { requireCapability(manifest, capabilitySet, "project.workspaces.read"); const issue = issues.get(issueId); if (!isInCompany(issue, companyId)) return null; const projectId = (issue as unknown as Record)?.projectId as string | undefined; if (!projectId) return null; if (!isInCompany(projects.get(projectId), companyId)) return null; const workspaces = projectWorkspaces.get(projectId) ?? []; return workspaces.find((workspace) => workspace.isPrimary) ?? null; }, managed: { async get(projectKey, companyId) { requireCapability(manifest, capabilitySet, "projects.managed"); const declaration = manifest.projects?.find((project) => project.projectKey === projectKey); if (!declaration) { return { pluginKey: manifest.id, resourceKind: "project", resourceKey: projectKey, companyId, projectId: null, project: null, status: "missing", }; } const externalId = `${manifest.id}:project:${projectKey}`; const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource" && entity.scopeKind === "company" && entity.scopeId === companyId && entity.externalId === externalId ); const existingProject = existingEntity ? projects.get(String(existingEntity.data?.projectId ?? "")) : null; if (existingProject && isInCompany(existingProject, companyId)) { return { pluginKey: manifest.id, resourceKind: "project", resourceKey: projectKey, companyId, projectId: existingProject.id, project: existingProject, status: "resolved", }; } const now = new Date(); const project = { id: `project-${projects.size + 1}`, companyId, urlKey: declaration.projectKey, goalId: null, goalIds: [], goals: [], name: declaration.displayName, description: declaration.description ?? null, status: declaration.status ?? "in_progress", leadAgentId: null, targetDate: null, color: declaration.color ?? null, env: null, pauseReason: null, pausedAt: null, executionWorkspacePolicy: null, codebase: { workspaceId: null, repoUrl: null, repoRef: null, defaultRef: null, repoName: null, localFolder: null, managedFolder: `/tmp/${declaration.projectKey}`, effectiveLocalFolder: `/tmp/${declaration.projectKey}`, origin: "managed_checkout", }, workspaces: [], primaryWorkspace: null, managedByPlugin: { id: `managed-${projects.size + 1}`, pluginId: manifest.id, pluginKey: manifest.id, pluginDisplayName: manifest.displayName, resourceKind: "project", resourceKey: projectKey, defaultsJson: { displayName: declaration.displayName, settings: declaration.settings ?? {} }, createdAt: now, updatedAt: now, }, archivedAt: null, createdAt: now, updatedAt: now, } as Project; projects.set(project.id, project); const externalKey = `managed_resource|company|${companyId}|${externalId}`; const nowIso = now.toISOString(); const record: PluginEntityRecord = { id: randomUUID(), entityType: "managed_resource", scopeKind: "company", scopeId: companyId, externalId, title: declaration.displayName, status: null, data: { resourceKind: "project", resourceKey: projectKey, projectId: project.id }, createdAt: nowIso, updatedAt: nowIso, }; entities.set(record.id, record); entityExternalIndex.set(externalKey, record.id); return { pluginKey: manifest.id, resourceKind: "project", resourceKey: projectKey, companyId, projectId: project.id, project, status: "created", }; }, async reconcile(projectKey, companyId) { return this.get(projectKey, companyId); }, async reset(projectKey, companyId) { const resolved = await this.get(projectKey, companyId); return { ...resolved, status: resolved.project ? "reset" : resolved.status }; }, }, }, routines: { managed: { async get(routineKey, companyId) { requireCapability(manifest, capabilitySet, "routines.managed"); const declaration = manifest.routines?.find((routine) => routine.routineKey === routineKey); if (!declaration) { return { pluginKey: manifest.id, resourceKind: "routine", resourceKey: routineKey, companyId, routineId: null, routine: null, status: "missing", missingRefs: [], } satisfies PluginManagedRoutineResolution; } const externalId = `${manifest.id}:routine:${routineKey}`; const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource" && entity.scopeKind === "company" && entity.scopeId === companyId && entity.externalId === externalId ); const existingRoutine = existingEntity ? routines.get(String(existingEntity.data?.routineId ?? "")) : null; if (existingRoutine && isInCompany(existingRoutine, companyId)) { return { pluginKey: manifest.id, resourceKind: "routine", resourceKey: routineKey, companyId, routineId: existingRoutine.id, routine: existingRoutine, status: "resolved", missingRefs: [], } satisfies PluginManagedRoutineResolution; } return { pluginKey: manifest.id, resourceKind: "routine", resourceKey: routineKey, companyId, routineId: null, routine: null, status: "missing", missingRefs: [], } satisfies PluginManagedRoutineResolution; }, async reconcile(routineKey, companyId, overrides) { const existing = await this.get(routineKey, companyId); if (existing.routine) return existing; const declaration = manifest.routines?.find((routine) => routine.routineKey === routineKey); if (!declaration) return existing; const now = new Date(); const agentRef = declaration.assigneeRef; const projectRef = declaration.projectRef; const assigneeAgentId = overrides?.assigneeAgentId ?? (agentRef?.resourceKind === "agent" ? [...agents.values()].find((agent) => isInCompany(agent, companyId) && isManagedAgent(agent, agentRef.resourceKey))?.id : null) ?? null; const projectId = overrides?.projectId ?? (projectRef?.resourceKind === "project" ? [...projects.values()].find((project) => ( isInCompany(project, companyId) && project.managedByPlugin?.pluginKey === manifest.id && project.managedByPlugin?.resourceKey === projectRef.resourceKey ))?.id : null) ?? null; const missingRefs: NonNullable = []; if (agentRef && !assigneeAgentId) missingRefs.push({ ...agentRef, pluginKey: manifest.id }); if (projectRef && !projectId) missingRefs.push({ ...projectRef, pluginKey: manifest.id }); if (missingRefs.length > 0) { return { pluginKey: manifest.id, resourceKind: "routine", resourceKey: routineKey, companyId, routineId: null, routine: null, status: "missing_refs", missingRefs, } satisfies PluginManagedRoutineResolution; } const routine = { id: `routine-${routines.size + 1}`, companyId, projectId, goalId: declaration.goalId ?? null, parentIssueId: null, title: declaration.title, description: declaration.description ?? null, assigneeAgentId, priority: declaration.priority ?? "medium", status: declaration.status ?? (assigneeAgentId ? "active" : "paused"), concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active", catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed", variables: declaration.variables ?? [], latestRevisionId: null, latestRevisionNumber: 1, createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, lastTriggeredAt: null, lastEnqueuedAt: null, createdAt: now, updatedAt: now, managedByPlugin: { id: `managed-routine-${routines.size + 1}`, pluginId: manifest.id, pluginKey: manifest.id, pluginDisplayName: manifest.displayName, resourceKind: "routine", resourceKey: routineKey, defaultsJson: { title: declaration.title, issueTemplate: declaration.issueTemplate ?? null }, createdAt: now, updatedAt: now, }, } as Routine; routines.set(routine.id, routine); const nowIso = now.toISOString(); const record: PluginEntityRecord = { id: randomUUID(), entityType: "managed_resource", scopeKind: "company", scopeId: companyId, externalId: `${manifest.id}:routine:${routineKey}`, title: declaration.title, status: null, data: { resourceKind: "routine", resourceKey: routineKey, routineId: routine.id }, createdAt: nowIso, updatedAt: nowIso, }; entities.set(record.id, record); return { pluginKey: manifest.id, resourceKind: "routine", resourceKey: routineKey, companyId, routineId: routine.id, routine, status: "created", missingRefs: [], } satisfies PluginManagedRoutineResolution; }, async reset(routineKey, companyId, overrides) { const resolved = await this.reconcile(routineKey, companyId, overrides); return { ...resolved, status: resolved.routine ? "reset" : resolved.status } satisfies PluginManagedRoutineResolution; }, async update(routineKey, companyId, patch) { const resolved = await this.get(routineKey, companyId); if (!resolved.routine) throw new Error(`Managed routine not found: ${routineKey}`); const next = { ...resolved.routine, ...(patch.status !== undefined ? { status: patch.status } : {}), updatedAt: new Date(), }; routines.set(next.id, next); return next; }, async run(routineKey, companyId) { const resolved = await this.get(routineKey, companyId); if (!resolved.routine) throw new Error(`Managed routine not found: ${routineKey}`); const now = new Date(); const run = { id: `routine-run-${routineRuns.size + 1}`, companyId, routineId: resolved.routine.id, triggerId: null, source: "manual", status: "queued", triggeredAt: now, idempotencyKey: null, triggerPayload: null, dispatchFingerprint: null, linkedIssueId: null, coalescedIntoRunId: null, failureReason: null, completedAt: null, createdAt: now, updatedAt: now, } satisfies RoutineRun; routineRuns.set(run.id, run); routines.set(resolved.routine.id, { ...resolved.routine, lastTriggeredAt: now, lastEnqueuedAt: now, updatedAt: now, }); return run; }, }, }, skills: { managed: { async get(skillKey, companyId) { requireCapability(manifest, capabilitySet, "skills.managed"); const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey); if (!declaration) { return { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, companyId, skillId: null, skill: null, status: "missing", defaultDrift: null, } satisfies PluginManagedSkillResolution; } const externalId = `${manifest.id}:skill:${skillKey}`; const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource" && entity.scopeKind === "company" && entity.scopeId === companyId && entity.externalId === externalId ); const existingSkill = existingEntity?.data?.skill as CompanySkill | undefined; if (existingSkill && existingSkill.companyId === companyId) { return { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, companyId, skillId: existingSkill.id, skill: existingSkill, status: "resolved", defaultDrift: null, } satisfies PluginManagedSkillResolution; } return { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, companyId, skillId: null, skill: null, status: "missing", defaultDrift: null, } satisfies PluginManagedSkillResolution; }, async reconcile(skillKey, companyId) { const existing = await this.get(skillKey, companyId); if (existing.skill) return existing; const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey); if (!declaration) return existing; const now = new Date(); const skill = { id: randomUUID(), companyId, key: `plugin/${manifest.id.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}/${skillKey}`, slug: declaration.slug ?? skillKey, name: declaration.displayName, description: declaration.description ?? null, markdown: declaration.markdown ?? `# ${declaration.displayName}\n`, sourceType: "catalog", sourceLocator: null, sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "catalog", pluginManagedResource: { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, }, }, createdAt: now, updatedAt: now, } satisfies CompanySkill; const nowIso = now.toISOString(); const record: PluginEntityRecord = { id: randomUUID(), entityType: "managed_resource", scopeKind: "company", scopeId: companyId, externalId: `${manifest.id}:skill:${skillKey}`, title: declaration.displayName, status: null, data: { resourceKind: "skill", resourceKey: skillKey, skillId: skill.id, skill }, createdAt: nowIso, updatedAt: nowIso, }; entities.set(record.id, record); return { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, companyId, skillId: skill.id, skill, status: "created", defaultDrift: null, } satisfies PluginManagedSkillResolution; }, async reset(skillKey, companyId) { requireCapability(manifest, capabilitySet, "skills.managed"); const existing = await this.get(skillKey, companyId); const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey); if (!declaration) return existing; const now = new Date(); const skill = { id: existing.skill?.id ?? randomUUID(), companyId, key: `plugin/${manifest.id.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}/${skillKey}`, slug: declaration.slug ?? skillKey, name: declaration.displayName, description: declaration.description ?? null, markdown: declaration.markdown ?? `# ${declaration.displayName}\n`, sourceType: "catalog", sourceLocator: null, sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "catalog", pluginManagedResource: { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, }, }, createdAt: existing.skill?.createdAt ?? now, updatedAt: now, } satisfies CompanySkill; const nowIso = now.toISOString(); const existingEntity = [...entities.values()].find((entity) => entity.entityType === "managed_resource" && entity.scopeKind === "company" && entity.scopeId === companyId && entity.externalId === `${manifest.id}:skill:${skillKey}`, ); const record: PluginEntityRecord = { id: existingEntity?.id ?? randomUUID(), entityType: "managed_resource", scopeKind: "company", scopeId: companyId, externalId: `${manifest.id}:skill:${skillKey}`, title: declaration.displayName, status: null, data: { resourceKind: "skill", resourceKey: skillKey, skillId: skill.id, skill }, createdAt: existingEntity?.createdAt ?? nowIso, updatedAt: nowIso, }; entities.set(record.id, record); return { pluginKey: manifest.id, resourceKind: "skill", resourceKey: skillKey, companyId, skillId: skill.id, skill, status: "reset", defaultDrift: null, } satisfies PluginManagedSkillResolution; }, }, }, companies: { async list(input) { requireCapability(manifest, capabilitySet, "companies.read"); let out = [...companies.values()]; if (input?.offset) out = out.slice(input.offset); if (input?.limit) out = out.slice(0, input.limit); return out; }, async get(companyId) { requireCapability(manifest, capabilitySet, "companies.read"); return companies.get(companyId) ?? null; }, }, issues: { async list(input) { requireCapability(manifest, capabilitySet, "issues.read"); const companyId = requireCompanyId(input?.companyId); let out = [...issues.values()]; out = out.filter((issue) => issue.companyId === companyId); if (input?.projectId) out = out.filter((issue) => issue.projectId === input.projectId); if (input?.assigneeAgentId) out = out.filter((issue) => issue.assigneeAgentId === input.assigneeAgentId); if (input?.originKind) { if (input.originKind.startsWith("plugin:")) normalizePluginOriginKind(input.originKind); out = out.filter((issue) => issue.originKind === input.originKind); } if (input?.originKindPrefix) { const prefix = input.originKindPrefix; out = out.filter((issue) => typeof issue.originKind === "string" && issue.originKind.startsWith(prefix), ); } if (input?.originId) out = out.filter((issue) => issue.originId === input.originId); if (input?.status) out = out.filter((issue) => issue.status === input.status); if (input?.offset) out = out.slice(input.offset); if (input?.limit) out = out.slice(0, input.limit); return out; }, async get(issueId, companyId) { requireCapability(manifest, capabilitySet, "issues.read"); const issue = issues.get(issueId); return isInCompany(issue, companyId) ? issue : null; }, async create(input) { requireCapability(manifest, capabilitySet, "issues.create"); const now = new Date(); const originKind = normalizePluginOriginKind( input.surfaceVisibility === "plugin_operation" && !input.originKind ? pluginOperationIssueOriginKind(manifest.id) : input.originKind, ); const record: Issue = { id: randomUUID(), companyId: input.companyId, projectId: input.projectId ?? null, projectWorkspaceId: null, goalId: input.goalId ?? null, parentId: input.parentId ?? null, title: input.title, description: input.description ?? null, status: input.status ?? "todo", workMode: "standard", priority: input.priority ?? "medium", assigneeAgentId: input.assigneeAgentId ?? null, assigneeUserId: input.assigneeUserId ?? null, checkoutRunId: null, executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, createdByAgentId: null, createdByUserId: null, issueNumber: null, identifier: null, originKind, originId: input.originId ?? null, originRunId: input.originRunId ?? null, requestDepth: input.requestDepth ?? 0, billingCode: input.billingCode ?? null, assigneeAdapterOverrides: input.assigneeAdapterOverrides ?? null, executionWorkspaceId: input.executionWorkspaceId ?? null, executionWorkspacePreference: input.executionWorkspacePreference ?? null, executionWorkspaceSettings: input.executionWorkspaceSettings ?? null, startedAt: null, completedAt: null, cancelledAt: null, hiddenAt: null, createdAt: now, updatedAt: now, }; issues.set(record.id, record); if (input.blockedByIssueIds) blockedByIssueIds.set(record.id, [...new Set(input.blockedByIssueIds)]); return record; }, async update(issueId, patch, companyId) { requireCapability(manifest, capabilitySet, "issues.update"); const record = issues.get(issueId); if (!isInCompany(record, companyId)) throw new Error(`Issue not found: ${issueId}`); const { blockedByIssueIds: nextBlockedByIssueIds, ...issuePatch } = patch; if (issuePatch.originKind !== undefined) { issuePatch.originKind = normalizePluginOriginKind(issuePatch.originKind); } const updated: Issue = { ...record, ...issuePatch, updatedAt: new Date(), }; issues.set(issueId, updated); if (nextBlockedByIssueIds !== undefined) { blockedByIssueIds.set(issueId, [...new Set(nextBlockedByIssueIds)]); } return updated; }, async assertCheckoutOwner(input) { requireCapability(manifest, capabilitySet, "issues.checkout"); const record = issues.get(input.issueId); if (!isInCompany(record, input.companyId)) throw new Error(`Issue not found: ${input.issueId}`); if ( record.status !== "in_progress" || record.assigneeAgentId !== input.actorAgentId || (record.checkoutRunId !== null && record.checkoutRunId !== input.actorRunId) ) { throw new Error("Issue run ownership conflict"); } return { issueId: record.id, status: record.status, assigneeAgentId: record.assigneeAgentId, checkoutRunId: record.checkoutRunId, adoptedFromRunId: null, }; }, async requestWakeup(issueId, companyId) { requireCapability(manifest, capabilitySet, "issues.wakeup"); const record = issues.get(issueId); if (!isInCompany(record, companyId)) throw new Error(`Issue not found: ${issueId}`); if (!record.assigneeAgentId) throw new Error("Issue has no assigned agent to wake"); if (["backlog", "done", "cancelled"].includes(record.status)) { throw new Error(`Issue is not wakeable in status: ${record.status}`); } const unresolved = issueRelationSummary(issueId).blockedBy.filter((blocker) => blocker.status !== "done"); if (unresolved.length > 0) throw new Error("Issue is blocked by unresolved blockers"); return { queued: true, runId: randomUUID() }; }, async requestWakeups(issueIds, companyId) { requireCapability(manifest, capabilitySet, "issues.wakeup"); const results = []; for (const issueId of issueIds) { const record = issues.get(issueId); if (!isInCompany(record, companyId)) throw new Error(`Issue not found: ${issueId}`); if (!record.assigneeAgentId) throw new Error("Issue has no assigned agent to wake"); if (["backlog", "done", "cancelled"].includes(record.status)) { throw new Error(`Issue is not wakeable in status: ${record.status}`); } const unresolved = issueRelationSummary(issueId).blockedBy.filter((blocker) => blocker.status !== "done"); if (unresolved.length > 0) throw new Error("Issue is blocked by unresolved blockers"); results.push({ issueId, queued: true, runId: randomUUID() }); } return results; }, async listComments(issueId, companyId) { requireCapability(manifest, capabilitySet, "issue.comments.read"); if (!isInCompany(issues.get(issueId), companyId)) return []; return issueComments.get(issueId) ?? []; }, async createComment(issueId, body, companyId, options) { requireCapability(manifest, capabilitySet, "issue.comments.create"); const parentIssue = issues.get(issueId); if (!isInCompany(parentIssue, companyId)) { throw new Error(`Issue not found: ${issueId}`); } const now = new Date(); const comment: IssueComment = { id: randomUUID(), companyId: parentIssue.companyId, issueId, authorType: options?.authorAgentId ? "agent" : "system", authorAgentId: options?.authorAgentId ?? null, authorUserId: null, body, presentation: null, metadata: null, createdAt: now, updatedAt: now, }; const current = issueComments.get(issueId) ?? []; current.push(comment); issueComments.set(issueId, current); return comment; }, async createInteraction(issueId, interaction, companyId, options) { requireCapability(manifest, capabilitySet, "issue.interactions.create"); const parentIssue = issues.get(issueId); if (!isInCompany(parentIssue, companyId)) { throw new Error(`Issue not found: ${issueId}`); } const now = new Date(); const current = issueInteractions.get(issueId) ?? []; if (interaction.idempotencyKey) { const existing = current.find((entry) => entry.idempotencyKey === interaction.idempotencyKey); if (existing) return existing; } const created: IssueThreadInteraction = { id: randomUUID(), companyId: parentIssue.companyId, issueId, kind: interaction.kind, status: "pending", continuationPolicy: interaction.continuationPolicy ?? "wake_assignee", idempotencyKey: interaction.idempotencyKey ?? null, sourceCommentId: interaction.sourceCommentId ?? null, sourceRunId: interaction.sourceRunId ?? null, title: interaction.title ?? null, summary: interaction.summary ?? null, createdByAgentId: options?.authorAgentId ?? null, createdByUserId: null, payload: interaction.payload, result: null, createdAt: now, updatedAt: now, } as IssueThreadInteraction; current.push(created); issueInteractions.set(issueId, current); return created; }, async suggestTasks(issueId, interaction, companyId, options) { return this.createInteraction(issueId, { ...interaction, kind: "suggest_tasks" }, companyId, options) as Promise; }, async askUserQuestions(issueId, interaction, companyId, options) { return this.createInteraction(issueId, { ...interaction, kind: "ask_user_questions" }, companyId, options) as Promise; }, async requestConfirmation(issueId, interaction, companyId, options) { return this.createInteraction(issueId, { ...interaction, kind: "request_confirmation" }, companyId, options) as Promise; }, documents: { async list(issueId, companyId) { requireCapability(manifest, capabilitySet, "issue.documents.read"); if (!isInCompany(issues.get(issueId), companyId)) return []; return [...issueDocuments.values()] .filter((document) => document.issueId === issueId && document.companyId === companyId) .map(({ body: _body, ...summary }) => summary); }, async get(issueId, key, companyId) { requireCapability(manifest, capabilitySet, "issue.documents.read"); if (!isInCompany(issues.get(issueId), companyId)) return null; return issueDocuments.get(`${issueId}|${key}`) ?? null; }, async upsert(input) { requireCapability(manifest, capabilitySet, "issue.documents.write"); const parentIssue = issues.get(input.issueId); if (!isInCompany(parentIssue, input.companyId)) { throw new Error(`Issue not found: ${input.issueId}`); } const now = new Date(); const existing = issueDocuments.get(`${input.issueId}|${input.key}`); const document: IssueDocument = { id: existing?.id ?? randomUUID(), companyId: input.companyId, issueId: input.issueId, key: input.key, title: input.title ?? existing?.title ?? null, format: "markdown", latestRevisionId: randomUUID(), latestRevisionNumber: (existing?.latestRevisionNumber ?? 0) + 1, createdByAgentId: existing?.createdByAgentId ?? null, createdByUserId: existing?.createdByUserId ?? null, updatedByAgentId: null, updatedByUserId: null, lockedAt: existing?.lockedAt ?? null, lockedByAgentId: existing?.lockedByAgentId ?? null, lockedByUserId: existing?.lockedByUserId ?? null, createdAt: existing?.createdAt ?? now, updatedAt: now, body: input.body, }; issueDocuments.set(`${input.issueId}|${input.key}`, document); return document; }, async delete(issueId, _key, companyId) { requireCapability(manifest, capabilitySet, "issue.documents.write"); const parentIssue = issues.get(issueId); if (!isInCompany(parentIssue, companyId)) { throw new Error(`Issue not found: ${issueId}`); } issueDocuments.delete(`${issueId}|${_key}`); }, }, relations: { async get(issueId, companyId) { requireCapability(manifest, capabilitySet, "issue.relations.read"); if (!isInCompany(issues.get(issueId), companyId)) throw new Error(`Issue not found: ${issueId}`); return issueRelationSummary(issueId); }, async setBlockedBy(issueId, nextBlockedByIssueIds, companyId) { requireCapability(manifest, capabilitySet, "issue.relations.write"); if (!isInCompany(issues.get(issueId), companyId)) throw new Error(`Issue not found: ${issueId}`); blockedByIssueIds.set(issueId, [...new Set(nextBlockedByIssueIds)]); return issueRelationSummary(issueId); }, async addBlockers(issueId, blockerIssueIds, companyId) { requireCapability(manifest, capabilitySet, "issue.relations.write"); if (!isInCompany(issues.get(issueId), companyId)) throw new Error(`Issue not found: ${issueId}`); const next = new Set(blockedByIssueIds.get(issueId) ?? []); for (const blockerIssueId of blockerIssueIds) next.add(blockerIssueId); blockedByIssueIds.set(issueId, [...next]); return issueRelationSummary(issueId); }, async removeBlockers(issueId, blockerIssueIds, companyId) { requireCapability(manifest, capabilitySet, "issue.relations.write"); if (!isInCompany(issues.get(issueId), companyId)) throw new Error(`Issue not found: ${issueId}`); const removals = new Set(blockerIssueIds); blockedByIssueIds.set( issueId, (blockedByIssueIds.get(issueId) ?? []).filter((blockerIssueId) => !removals.has(blockerIssueId)), ); return issueRelationSummary(issueId); }, }, async getSubtree(issueId, companyId, options) { requireCapability(manifest, capabilitySet, "issue.subtree.read"); const root = issues.get(issueId); if (!isInCompany(root, companyId)) throw new Error(`Issue not found: ${issueId}`); const includeRoot = options?.includeRoot !== false; const allIds = [root.id]; let frontier = [root.id]; while (frontier.length > 0) { const children = [...issues.values()] .filter((issue) => issue.companyId === companyId && frontier.includes(issue.parentId ?? "")) .map((issue) => issue.id) .filter((id) => !allIds.includes(id)); allIds.push(...children); frontier = children; } const issueIds = includeRoot ? allIds : allIds.filter((id) => id !== root.id); const subtreeIssues = issueIds.map((id) => issues.get(id)).filter((candidate): candidate is Issue => Boolean(candidate)); return { rootIssueId: root.id, companyId, issueIds, issues: subtreeIssues, ...(options?.includeRelations ? { relations: Object.fromEntries(issueIds.map((id) => [id, issueRelationSummary(id)])) } : {}), ...(options?.includeDocuments ? { documents: Object.fromEntries(issueIds.map((id) => [id, []])) } : {}), ...(options?.includeActiveRuns ? { activeRuns: Object.fromEntries(issueIds.map((id) => [id, []])) } : {}), ...(options?.includeAssignees ? { assignees: {} } : {}), }; }, summaries: { async getOrchestration(input) { requireCapability(manifest, capabilitySet, "issues.orchestration.read"); const root = issues.get(input.issueId); if (!isInCompany(root, input.companyId)) throw new Error(`Issue not found: ${input.issueId}`); const subtreeIssueIds = [root.id]; if (input.includeSubtree) { let frontier = [root.id]; while (frontier.length > 0) { const children = [...issues.values()] .filter((issue) => issue.companyId === input.companyId && frontier.includes(issue.parentId ?? "")) .map((issue) => issue.id) .filter((id) => !subtreeIssueIds.includes(id)); subtreeIssueIds.push(...children); frontier = children; } } return { issueId: root.id, companyId: input.companyId, subtreeIssueIds, relations: Object.fromEntries(subtreeIssueIds.map((id) => [id, issueRelationSummary(id)])), approvals: [], runs: [], costs: { costCents: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, billingCode: input.billingCode ?? null, }, openBudgetIncidents: [], invocationBlocks: [], }; }, }, }, agents: { async list(input) { requireCapability(manifest, capabilitySet, "agents.read"); const companyId = requireCompanyId(input?.companyId); let out = [...agents.values()]; out = out.filter((agent) => agent.companyId === companyId); if (input?.status) out = out.filter((agent) => agent.status === input.status); if (input?.offset) out = out.slice(input.offset); if (input?.limit) out = out.slice(0, input.limit); return out; }, async get(agentId, companyId) { requireCapability(manifest, capabilitySet, "agents.read"); const agent = agents.get(agentId); return isInCompany(agent, companyId) ? agent : null; }, async pause(agentId, companyId) { requireCapability(manifest, capabilitySet, "agents.pause"); const cid = requireCompanyId(companyId); const agent = agents.get(agentId); if (!isInCompany(agent, cid)) throw new Error(`Agent not found: ${agentId}`); if (agent!.status === "terminated") throw new Error("Cannot pause terminated agent"); const updated: Agent = { ...agent!, status: "paused", updatedAt: new Date() }; agents.set(agentId, updated); return updated; }, async resume(agentId, companyId) { requireCapability(manifest, capabilitySet, "agents.resume"); const cid = requireCompanyId(companyId); const agent = agents.get(agentId); if (!isInCompany(agent, cid)) throw new Error(`Agent not found: ${agentId}`); if (agent!.status === "terminated") throw new Error("Cannot resume terminated agent"); if (agent!.status === "pending_approval") throw new Error("Pending approval agents cannot be resumed"); const updated: Agent = { ...agent!, status: "idle", updatedAt: new Date() }; agents.set(agentId, updated); return updated; }, async invoke(agentId, companyId, opts) { requireCapability(manifest, capabilitySet, "agents.invoke"); const cid = requireCompanyId(companyId); const agent = agents.get(agentId); if (!isInCompany(agent, cid)) throw new Error(`Agent not found: ${agentId}`); if ( agent!.status === "paused" || agent!.status === "terminated" || agent!.status === "pending_approval" ) { throw new Error(`Agent is not invokable in its current state: ${agent!.status}`); } return { runId: randomUUID() }; }, managed: { async get(agentKey, companyId) { requireCapability(manifest, capabilitySet, "agents.managed"); const cid = requireCompanyId(companyId); managedAgentDeclaration(agentKey); const agent = [...agents.values()].find((candidate) => candidate.companyId === cid && candidate.status !== "terminated" && isManagedAgent(candidate, agentKey), ) ?? null; return managedResolution(agentKey, cid, agent, agent ? "resolved" : "missing"); }, async reconcile(agentKey, companyId) { requireCapability(manifest, capabilitySet, "agents.managed"); const cid = requireCompanyId(companyId); const declaration = managedAgentDeclaration(agentKey); const existingAgent = [...agents.values()].find((candidate) => candidate.companyId === cid && candidate.status !== "terminated" && isManagedAgent(candidate, agentKey), ) ?? null; const existing = managedResolution(agentKey, cid, existingAgent, existingAgent ? "resolved" : "missing"); if (existing.agent) return existing; const now = new Date(); const created: Agent = { id: randomUUID(), companyId: cid, name: declaration.displayName, urlKey: declaration.displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), role: (declaration.role ?? "general") as Agent["role"], title: declaration.title ?? null, icon: declaration.icon ?? null, status: declaration.status ?? "idle", reportsTo: null, capabilities: declaration.capabilities ?? null, adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], adapterConfig: declaration.adapterConfig ?? {}, runtimeConfig: declaration.runtimeConfig ?? {}, budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, spentMonthlyCents: 0, pauseReason: null, pausedAt: null, permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, lastHeartbeatAt: null, metadata: managedAgentMetadata(agentKey), createdAt: now, updatedAt: now, }; agents.set(created.id, created); return managedResolution(agentKey, cid, created, "created"); }, async reset(agentKey, companyId) { requireCapability(manifest, capabilitySet, "agents.managed"); const cid = requireCompanyId(companyId); const declaration = managedAgentDeclaration(agentKey); let agent = [...agents.values()].find((candidate) => candidate.companyId === cid && candidate.status !== "terminated" && isManagedAgent(candidate, agentKey), ) ?? null; if (!agent) { const now = new Date(); agent = { id: randomUUID(), companyId: cid, name: declaration.displayName, urlKey: declaration.displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), role: (declaration.role ?? "general") as Agent["role"], title: declaration.title ?? null, icon: declaration.icon ?? null, status: declaration.status ?? "idle", reportsTo: null, capabilities: declaration.capabilities ?? null, adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], adapterConfig: declaration.adapterConfig ?? {}, runtimeConfig: declaration.runtimeConfig ?? {}, budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, spentMonthlyCents: 0, pauseReason: null, pausedAt: null, permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, lastHeartbeatAt: null, metadata: managedAgentMetadata(agentKey), createdAt: now, updatedAt: now, }; agents.set(agent.id, agent); } const resolved = managedResolution(agentKey, cid, agent, "resolved"); if (!resolved.agent) return resolved; const updated: Agent = { ...resolved.agent, name: declaration.displayName, role: (declaration.role ?? "general") as Agent["role"], title: declaration.title ?? null, icon: declaration.icon ?? null, capabilities: declaration.capabilities ?? null, adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], adapterConfig: declaration.adapterConfig ?? {}, runtimeConfig: declaration.runtimeConfig ?? {}, budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, metadata: managedAgentMetadata(agentKey, resolved.agent.metadata), updatedAt: new Date(), }; agents.set(updated.id, updated); return managedResolution(agentKey, cid, updated, "reset"); }, }, sessions: { async create(agentId, companyId, opts) { requireCapability(manifest, capabilitySet, "agent.sessions.create"); const cid = requireCompanyId(companyId); const agent = agents.get(agentId); if (!isInCompany(agent, cid)) throw new Error(`Agent not found: ${agentId}`); const session: AgentSession = { sessionId: randomUUID(), agentId, companyId: cid, status: "active", createdAt: new Date().toISOString(), }; sessions.set(session.sessionId, session); return session; }, async list(agentId, companyId) { requireCapability(manifest, capabilitySet, "agent.sessions.list"); const cid = requireCompanyId(companyId); return [...sessions.values()].filter( (s) => s.agentId === agentId && s.companyId === cid && s.status === "active", ); }, async sendMessage(sessionId, companyId, opts) { requireCapability(manifest, capabilitySet, "agent.sessions.send"); const session = sessions.get(sessionId); if (!session || session.status !== "active") throw new Error(`Session not found or closed: ${sessionId}`); if (session.companyId !== companyId) throw new Error(`Session not found: ${sessionId}`); if (opts.onEvent) { sessionEventCallbacks.set(sessionId, opts.onEvent); } return { runId: randomUUID() }; }, async close(sessionId, companyId) { requireCapability(manifest, capabilitySet, "agent.sessions.close"); const session = sessions.get(sessionId); if (!session) throw new Error(`Session not found: ${sessionId}`); if (session.companyId !== companyId) throw new Error(`Session not found: ${sessionId}`); session.status = "closed"; sessionEventCallbacks.delete(sessionId); }, }, }, goals: { async list(input) { requireCapability(manifest, capabilitySet, "goals.read"); const companyId = requireCompanyId(input?.companyId); let out = [...goals.values()]; out = out.filter((goal) => goal.companyId === companyId); if (input?.level) out = out.filter((goal) => goal.level === input.level); if (input?.status) out = out.filter((goal) => goal.status === input.status); if (input?.offset) out = out.slice(input.offset); if (input?.limit) out = out.slice(0, input.limit); return out; }, async get(goalId, companyId) { requireCapability(manifest, capabilitySet, "goals.read"); const goal = goals.get(goalId); return isInCompany(goal, companyId) ? goal : null; }, async create(input) { requireCapability(manifest, capabilitySet, "goals.create"); const now = new Date(); const record: Goal = { id: randomUUID(), companyId: input.companyId, title: input.title, description: input.description ?? null, level: input.level ?? "task", status: input.status ?? "planned", parentId: input.parentId ?? null, ownerAgentId: input.ownerAgentId ?? null, createdAt: now, updatedAt: now, }; goals.set(record.id, record); return record; }, async update(goalId, patch, companyId) { requireCapability(manifest, capabilitySet, "goals.update"); const record = goals.get(goalId); if (!isInCompany(record, companyId)) throw new Error(`Goal not found: ${goalId}`); const updated: Goal = { ...record, ...patch, updatedAt: new Date(), }; goals.set(goalId, updated); return updated; }, }, data: { register(key, handler) { dataHandlers.set(key, handler); }, }, actions: { register(key, handler) { actionHandlers.set(key, handler); }, }, streams: (() => { const channelCompanyMap = new Map(); return { open(channel: string, companyId: string) { channelCompanyMap.set(channel, companyId); }, emit(_channel: string, _event: unknown) { // No-op in test harness — events are not forwarded }, close(channel: string) { channelCompanyMap.delete(channel); }, }; })(), tools: { register(name, _decl, fn) { requireCapability(manifest, capabilitySet, "agent.tools.register"); toolHandlers.set(name, fn); }, }, metrics: { async write(name, value, tags) { requireCapability(manifest, capabilitySet, "metrics.write"); metrics.push({ name, value, tags }); }, }, telemetry: { async track(eventName, dimensions) { requireCapability(manifest, capabilitySet, "telemetry.track"); telemetry.push({ eventName, dimensions }); }, }, logger: { info(message, meta) { logs.push({ level: "info", message, meta }); }, warn(message, meta) { logs.push({ level: "warn", message, meta }); }, error(message, meta) { logs.push({ level: "error", message, meta }); }, debug(message, meta) { logs.push({ level: "debug", message, meta }); }, }, }; const harness: TestHarness = { ctx, seed(input) { for (const row of input.companies ?? []) companies.set(row.id, row); for (const row of input.projects ?? []) projects.set(row.id, row); for (const row of input.issues ?? []) { issues.set(row.id, row); if (row.blockedBy) { blockedByIssueIds.set(row.id, row.blockedBy.map((blocker) => blocker.id)); } } for (const row of input.issueComments ?? []) { const list = issueComments.get(row.issueId) ?? []; list.push(row); issueComments.set(row.issueId, list); } for (const row of input.agents ?? []) agents.set(row.id, row); for (const row of input.goals ?? []) goals.set(row.id, row); }, setConfig(config) { currentConfig = { ...config }; }, async emit(eventType, payload, base) { const event: PluginEvent = { eventId: base?.eventId ?? randomUUID(), eventType, companyId: base?.companyId ?? "test-company", occurredAt: base?.occurredAt ?? new Date().toISOString(), actorId: base?.actorId, actorType: base?.actorType, entityId: base?.entityId, entityType: base?.entityType, payload, }; for (const handler of events) { const exactMatch = handler.name === event.eventType; const wildcardPluginAll = handler.name === "plugin.*" && String(event.eventType).startsWith("plugin."); const wildcardPluginOne = String(handler.name).endsWith(".*") && String(event.eventType).startsWith(String(handler.name).slice(0, -1)); if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne) continue; if (!allowsEvent(handler.filter, event)) continue; await handler.fn(event); } }, async runJob(jobKey, partial = {}) { const handler = jobs.get(jobKey); if (!handler) throw new Error(`No job handler registered for '${jobKey}'`); await handler({ jobKey, runId: partial.runId ?? randomUUID(), trigger: partial.trigger ?? "manual", scheduledAt: partial.scheduledAt ?? new Date().toISOString(), }); }, async getData(key: string, params: Record = {}) { const handler = dataHandlers.get(key); if (!handler) throw new Error(`No data handler registered for '${key}'`); return await handler(params) as T; }, async performAction(key: string, params: Record = {}) { const handler = actionHandlers.get(key); if (!handler) throw new Error(`No action handler registered for '${key}'`); return await handler(params) as T; }, async executeTool(name: string, params: unknown, runCtx: Partial = {}) { const handler = toolHandlers.get(name); if (!handler) throw new Error(`No tool handler registered for '${name}'`); const ctxToPass: ToolRunContext = { agentId: runCtx.agentId ?? "agent-test", runId: runCtx.runId ?? randomUUID(), companyId: runCtx.companyId ?? "company-test", projectId: runCtx.projectId ?? "project-test", }; return await handler(params, ctxToPass) as T; }, getState(input) { return state.get(stateMapKey(input)); }, simulateSessionEvent(sessionId, event) { const cb = sessionEventCallbacks.get(sessionId); if (!cb) throw new Error(`No active session event callback for session: ${sessionId}`); cb({ ...event, sessionId }); }, logs, activity, metrics, telemetry, dbQueries, dbExecutes, }; return harness; } /** * Create an environment-aware test harness that wraps the base harness with * environment driver simulation and lifecycle event recording. * * Use this to test environment plugins through the full host contract: * validateConfig → probe → acquireLease → realizeWorkspace → execute → releaseLease. */ export function createEnvironmentTestHarness(options: EnvironmentTestHarnessOptions): EnvironmentTestHarness { const base = createTestHarness(options); const environmentEvents: EnvironmentEventRecord[] = []; const driver = options.environmentDriver; function record( type: EnvironmentEventRecord["type"], params: Record, result?: unknown, error?: string, ): EnvironmentEventRecord { const event: EnvironmentEventRecord = { type, driverKey: (params as { driverKey?: string }).driverKey ?? driver.driverKey, environmentId: (params as { environmentId?: string }).environmentId ?? "unknown", timestamp: new Date().toISOString(), params, result, error, }; environmentEvents.push(event); return event; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async function callHook( type: EnvironmentEventRecord["type"], hook: ((...args: any[]) => Promise) | undefined, params: unknown, hookName: string, ): Promise { if (!hook) { const err = `Environment driver '${driver.driverKey}' does not implement ${hookName}`; record(type, params as Record, undefined, err); throw new Error(err); } try { const result = await hook(params); record(type, params as Record, result); return result; } catch (e) { const msg = e instanceof Error ? e.message : String(e); record(type, params as Record, undefined, msg); throw e; } } const envHarness: EnvironmentTestHarness = { ...base, environmentEvents, async validateConfig(params) { return callHook("validateConfig", driver.onValidateConfig, params, "onValidateConfig"); }, async probe(params) { return callHook("probe", driver.onProbe, params, "onProbe"); }, async acquireLease(params) { return callHook("acquireLease", driver.onAcquireLease, params, "onAcquireLease"); }, async resumeLease(params) { return callHook("resumeLease", driver.onResumeLease, params, "onResumeLease"); }, async releaseLease(params) { return callHook("releaseLease", driver.onReleaseLease, params, "onReleaseLease"); }, async destroyLease(params) { return callHook("destroyLease", driver.onDestroyLease, params, "onDestroyLease"); }, async realizeWorkspace(params) { return callHook("realizeWorkspace", driver.onRealizeWorkspace, params, "onRealizeWorkspace"); }, async execute(params) { return callHook("execute", driver.onExecute, params, "onExecute"); }, }; return envHarness; }