/** * @module @kb-labs/core-contracts * * Canonical execution request contracts. * This module must not depend on plugin-layer contracts. */ /** * Core execution descriptor. * * Plugin/runtime specific layers may provide richer descriptor types via * ExecutionRequest. */ interface ExecutionDescriptorCore { requestId?: string; tenantId?: string; pluginId?: string; pluginVersion?: string; handlerId?: string; [key: string]: unknown; } /** * Execution target type. * * - `platform` — execute on the platform (in-process or worker pool) * - `workspace-agent` — execute on a connected Workspace Agent (near the code) * - `environment` — execute in a specific provisioned environment (container) * * When `type` is omitted, RoutingBackend uses deployment-level routing config. */ type ExecutionTargetType = 'platform' | 'workspace-agent' | 'environment'; /** * Host selection strategy for workspace-agent target. * * - `pinned` — specific hostId required, error if offline * - `any-matching` — any host with matching capability + workspace * - `prefer-local` — prefer hostType='local', fallback to cloud * - `prefer-cloud` — prefer hostType='cloud', fallback to local */ type HostSelectionStrategy = 'pinned' | 'any-matching' | 'prefer-local' | 'prefer-cloud'; /** * Execution target affinity. * * Extended to support Workspace Agent routing (ADR-0052, ADR-0054). * Backwards compatible: existing code using only `environmentId` continues to work. */ interface ExecutionTarget { /** Target type. When omitted, resolved from routing config. */ type?: ExecutionTargetType; /** Specific environment/container ID */ environmentId?: string; /** Logical workspace for routing to correct Workspace Agent */ workspaceId?: string; /** Namespace for multi-tenancy */ namespace?: string; /** Working directory override */ workdir?: string; /** Pin to specific host */ hostId?: string; /** Host selection strategy (default: 'any-matching') */ hostSelection?: HostSelectionStrategy; /** Repo fingerprint for affinity routing */ repoFingerprint?: string; } /** * Workspace configuration. */ interface WorkspaceConfig { type?: 'local' | 'ephemeral'; cwd?: string; repo?: { url: string; ref: string; commit?: string; }; filter?: { include?: string[]; exclude?: string[]; }; snapshotId?: string; } /** * Artifacts collection configuration. */ interface ArtifactsConfig { outdir?: string; upload?: boolean; patterns?: string[]; } /** * Canonical execution request. * * TDescriptor allows upper layers to provide strongly typed descriptors * while keeping core-contracts plugin-agnostic. */ interface ExecutionRequest { /** Unique execution ID for this execution attempt */ executionId: string; /** Runtime descriptor required by the execution runtime */ descriptor: TDescriptor; /** Absolute plugin root */ pluginRoot: string; /** Handler reference relative to pluginRoot */ handlerRef: string; /** Optional named export from handler module */ exportName?: string; /** Input payload for handler */ input: unknown; /** Workspace strategy (default local) */ workspace?: WorkspaceConfig; /** Artifacts collection settings */ artifacts?: ArtifactsConfig; /** Timeout in milliseconds */ timeoutMs?: number; /** Optional target affinity */ target?: ExecutionTarget; /** Additional execution context */ context?: { tenantId?: string; traceId?: string; sessionId?: string; [key: string]: unknown; }; } /** * @module @kb-labs/core-contracts * * Canonical execution response contracts. */ interface ExecutionMeta { startTime: number; endTime: number; duration: number; pluginId: string; pluginVersion: string; handlerId?: string; requestId: string; tenantId?: string; } interface RunResult { ok: boolean; data?: T; error?: { name: string; message: string; code?: string; stack?: string; }; executionMeta: ExecutionMeta; } /** * Standardized execution-layer error codes. */ type ExecutionErrorCode = 'TIMEOUT' | 'ABORTED' | 'PERMISSION_DENIED' | 'HANDLER_ERROR' | 'HANDLER_CONTRACT_ERROR' | 'HANDLER_NOT_FOUND' | 'WORKSPACE_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR' | 'QUEUE_FULL' | 'ACQUIRE_TIMEOUT' | 'WORKER_CRASHED' | 'WORKER_UNHEALTHY' | 'NO_HOST_AVAILABLE'; interface ExecutionError { message: string; code?: ExecutionErrorCode; stack?: string; details?: Record; [key: string]: unknown; } interface ExecutionMetadata { workerId?: string; workspaceId?: string; memoryUsedMB?: number; handlerWasWarmed?: boolean; backend?: 'in-process' | 'subprocess' | 'worker-pool' | 'remote'; executionMeta?: ExecutionMeta; target?: ExecutionTarget; [key: string]: unknown; } /** * Canonical execution result. */ interface ExecutionResult { ok: boolean; data?: unknown; error?: ExecutionError; executionTimeMs: number; artifactIds?: string[]; metadata?: ExecutionMetadata; [key: string]: unknown; } /** * Canonical response alias. */ type ExecutionResponse = ExecutionResult; interface ExecuteOptions { signal?: AbortSignal; pluginInvoker?: (pluginId: string, input?: unknown, options?: unknown) => Promise; [key: string]: unknown; } /** * @module @kb-labs/core-contracts * * Execution backend interfaces - contract for execution layer. */ /** * Health status for execution backend. */ interface HealthStatus { /** Backend is healthy */ healthy: boolean; /** Backend type */ backend: "in-process" | "subprocess" | "worker-pool" | "remote"; /** Additional health details */ details?: Record; } /** * Execution statistics. */ interface ExecutionStats { /** Total executions */ totalExecutions: number; /** Successful executions */ successCount: number; /** Failed executions */ errorCount: number; /** Average execution time in ms */ avgExecutionTimeMs: number; /** P95 execution time in ms */ p95ExecutionTimeMs?: number; /** P99 execution time in ms */ p99ExecutionTimeMs?: number; } /** * Execution backend interface. * * Implemented by backends in @kb-labs/plugin-execution. * Used by @kb-labs/core-runtime and CLI/REST hosts. */ interface IExecutionBackend { /** * Execute a plugin handler. * * Returns ExecutionResult on success, ExecutionError on failure. * Never throws - all errors are caught and returned as ExecutionError. */ execute(request: ExecutionRequest, options?: ExecuteOptions): Promise; /** * Get backend health status. */ health(): Promise; /** * Get execution statistics. */ stats(): Promise; /** * Shutdown backend and cleanup resources. */ shutdown(): Promise; } /** * @module @kb-labs/core-contracts * * Platform Gateway - Wire protocol for IPC/HTTP communication between workers and platform. */ /** * Vector query (duplicated from @kb-labs/core-platform to avoid dependency). */ interface VectorQuery { vector: number[]; topK?: number; filter?: Record; [key: string]: unknown; } /** * Vector search result (duplicated from @kb-labs/core-platform to avoid dependency). */ interface VectorSearchResult { id: string; score: number; metadata?: Record; [key: string]: unknown; } /** * Vector record (duplicated from @kb-labs/core-platform to avoid dependency). */ interface VectorRecord { id: string; vector: number[]; metadata?: Record; [key: string]: unknown; } /** * LLM options (duplicated from @kb-labs/core-platform to avoid dependency). */ interface LLMOptions { model?: string; temperature?: number; maxTokens?: number; [key: string]: unknown; } /** * LLM response (duplicated from @kb-labs/core-platform to avoid dependency). */ interface LLMResponse { text: string; usage?: { promptTokens: number; completionTokens: number; totalTokens: number; }; [key: string]: unknown; } /** * Request context - included in every gateway call for auth/correlation. */ interface RequestContext { /** Execution ID for correlation */ executionId: string; /** Tenant ID for multi-tenancy (optional) */ tenantId?: string; /** Trace ID for distributed tracing (optional) */ traceId?: string; /** Auth token for security (KB_PLATFORM_SOCKET_TOKEN) */ authToken: string; } /** * Platform Gateway - Wire protocol for IPC/HTTP communication. * * This is the low-level RPC interface that workers use to call platform services. * In-worker, this is wrapped by PlatformServices facade (cache.get() -> gateway.cacheGet()). * * Security: All requests must include auth token + correlation context. */ interface IPlatformGateway { /** * Cache operations (flat RPC methods) */ cacheGet(ctx: RequestContext, key: string): Promise; cacheSet(ctx: RequestContext, key: string, value: string, ttl?: number): Promise; cacheDelete(ctx: RequestContext, key: string): Promise; cacheClear(ctx: RequestContext, pattern?: string): Promise; /** * Vector operations */ vectorSearch(ctx: RequestContext, query: VectorQuery): Promise; vectorUpsert(ctx: RequestContext, vectors: VectorRecord[]): Promise; vectorDelete(ctx: RequestContext, ids: string[]): Promise; /** * LLM operations */ llmComplete(ctx: RequestContext, prompt: string, options?: LLMOptions): Promise; /** * Storage operations */ storageRead(ctx: RequestContext, path: string): Promise; storageWrite(ctx: RequestContext, path: string, data: Buffer): Promise; storageDelete(ctx: RequestContext, path: string): Promise; } /** * @module @kb-labs/core-contracts * * Subprocess runner interface - contract for running handlers in subprocesses. */ /** * Options for subprocess execution. */ interface SubprocessRunOptions { /** Plugin context descriptor */ descriptor: ExecutionDescriptorCore | unknown; /** Platform socket path (Unix socket) */ platformSocketPath: string; /** Platform auth token for gateway security */ platformAuthToken: string; /** Handler file path */ handlerPath: string; /** Handler export name (default: "default") */ exportName?: string; /** Input data for handler */ input: unknown; /** Timeout in milliseconds */ timeoutMs?: number; /** Abort signal for cancellation */ signal?: AbortSignal; /** Current working directory for handler execution */ cwd: string; /** Output directory (optional, defaults to ${cwd}/.kb/output) */ outdir?: string; /** Callback for real-time log streaming */ onLog?: (entry: { level: string; message: string; stream: 'stdout' | 'stderr'; lineNo: number; timestamp: string; meta?: Record; }) => void; } /** * Subprocess runner interface. * * Implemented by @kb-labs/plugin-runtime. * Used by @kb-labs/plugin-execution SubprocessBackend. */ interface ISubprocessRunner { /** * Run handler in subprocess with IPC communication. * * @throws {PluginError} Handler execution failed * @throws {TimeoutError} Execution exceeded timeout * @throws {AbortError} Execution was cancelled */ runInSubprocess(options: SubprocessRunOptions): Promise>; } /** * @module @kb-labs/core-contracts * * IExecutionTransport — abstraction for sending plugin execution requests * to a remote runtime server. * * RemoteBackend depends only on this interface, not on any specific * infrastructure (Gateway, TCP, gRPC, etc.). * * Implementations live outside core-contracts: * - GatewayDispatchTransport (@kb-labs/gateway-core) — via /internal/dispatch * - future: TcpTransport, GrpcTransport, ... */ /** * Result returned by transport after remote execution. */ interface TransportExecutionResult { /** Raw data returned by the handler */ data: unknown; } /** * Transport abstraction — sends an execution request to a remote runtime * and returns the result. * * The transport is responsible for: * - establishing the connection / routing * - serialising the request * - deserialising the response * - timeout handling * * It is NOT responsible for: * - handlerRef remapping (done by RemoteBackend before calling transport) * - retry logic (done by the caller) * - workspace / environment lifecycle */ interface IExecutionTransport { /** * Send an execution request and wait for the result. * Throws on transport-level errors (connection refused, timeout, etc.). */ execute(request: ExecutionRequest): Promise; } /** * @module @kb-labs/core-contracts/cancellation * * Cancellation types for execution pipeline (CC2). * Covers: user cancel, timeout, disconnect, system-level cancel. */ /** * Reason for cancellation. */ type CancellationReason = 'user' | 'timeout' | 'disconnect' | 'system'; /** * Cancel request — sent by client or system. */ interface CancelRequest { executionId: string; reason: CancellationReason; /** Who initiated (connectionId, userId, "system") */ initiator?: string; } /** * Handler for cancellation at any level (Gateway, Backend, Host Agent). */ interface ICancellationHandler { /** Cancel an execution. Returns true if successfully cancelled. */ cancel(request: CancelRequest): Promise; } /** * @module @kb-labs/core-contracts/execution-events * * Unified execution event types. * Generated once on the server, streamed to N clients via Gateway. */ /** * Union of all execution event types. */ type ExecutionEvent = ExecutionOutputEvent | ExecutionProgressEvent | ExecutionArtifactEvent | ExecutionErrorEvent | ExecutionRetryEvent | ExecutionCancelledEvent | ExecutionDoneEvent; /** * Output from handler execution (stdout/stderr). */ interface ExecutionOutputEvent { type: 'execution:output'; requestId: string; executionId: string; stream: 'stdout' | 'stderr'; data: string; timestamp: number; } /** * Progress update from handler execution. */ interface ExecutionProgressEvent { type: 'execution:progress'; requestId: string; executionId: string; step: number; total: number; label: string; timestamp: number; } /** * Artifact produced by handler execution. */ interface ExecutionArtifactEvent { type: 'execution:artifact'; requestId: string; executionId: string; name: string; mime: string; url: string; sizeBytes?: number; } /** * Error during handler execution. */ interface ExecutionErrorEvent { type: 'execution:error'; requestId: string; executionId: string; code: string; message: string; retryable: boolean; attempt?: number; maxAttempts?: number; } /** * Execution is being retried after a failure (CC3). * Emitted between attempts. */ interface ExecutionRetryEvent { type: 'execution:retry'; requestId: string; executionId: string; attempt: number; maxAttempts: number; delayMs: number; error: string; } /** * Execution was cancelled (CC2). * Emitted before execution:done with exitCode 130. */ interface ExecutionCancelledEvent { type: 'execution:cancelled'; requestId: string; executionId: string; reason: string; durationMs: number; } /** * Handler execution completed (success or failure). */ interface ExecutionDoneEvent { type: 'execution:done'; requestId: string; executionId: string; exitCode: number; durationMs: number; metadata?: Record; } /** * Event emitter interface for execution pipeline. * Backend calls emit(), Gateway translates to subscribers. */ interface IExecutionEventEmitter { emit(event: ExecutionEvent): void; onEvent(handler: (event: ExecutionEvent) => void): () => void; } /** * @module @kb-labs/core-contracts/retry * * Three retry levels (CC3): * * Level 1 (transport) — transparent, in HTTP/WS transport layer. * Retries on 503, network errors. Client doesn't know. * * Level 2 (execution) — configurable in ExecutionConfig.retry. * Wraps backend.execute() with retry + backoff. * Emits execution:retry events between attempts. * * Level 3 (checkpoint) — opt-in, implemented by handler. * Handler saves progress via ctx.checkpoint, resumes on retry. */ /** Canonical failure source. */ type FailureSource = 'execution' | 'command' | 'transport' | 'workflow'; /** Canonical failure taxonomy shared by execution, CLI and workflow hosts. */ type FailureKind = 'command' | 'network' | 'timeout' | 'rate_limit' | 'server' | 'validation' | 'configuration' | 'authentication' | 'authorization' | 'not_found' | 'cancelled' | 'infrastructure' | 'unknown'; /** Safety of repeating an operation after a failure. */ type RetrySafety = 'safe' | 'requires_idempotency' | 'never'; /** Structured failure information supplied by a plugin or execution layer. */ interface FailureInfo { message: string; code: string; kind?: FailureKind; source?: FailureSource; details?: Record; retryAfterMs?: number; } /** Classifier output used by all retry policies. */ interface ClassifiedFailure extends FailureInfo { kind: FailureKind; source: FailureSource; transient: boolean; retrySafety: RetrySafety; phase?: 'dispatch' | 'running' | 'response'; } /** Input context that prevents ambiguous transport failures from being retried blindly. */ interface FailureClassificationContext { source?: FailureSource; phase?: 'dispatch' | 'running' | 'response'; idempotent?: boolean; } /** Backward-compatible retry input. Prefer ClassifiedFailure for new code. */ interface RetryableError { code: string; message: string; retryable: boolean; } /** * Level 2: Execution retry policy. * Determines whether to retry and with what delay. */ interface IRetryPolicy { /** Maximum number of attempts (including first try). */ maxAttempts: number; /** Whether this error should be retried. */ shouldRetry(error: RetryableError, attempt: number): boolean; /** Delay in ms before next attempt (for backoff). */ getDelay(attempt: number): number; } /** Failure kinds allowed by a retry policy. */ type RetryableFailureKind = FailureKind; /** Shared retry configuration. maxAttempts includes the initial attempt. */ interface RetryPolicyConfig { maxAttempts: number; retryOn: RetryableFailureKind[]; neverRetryOn?: FailureKind[]; initialDelayMs: number; backoff: 'fixed' | 'linear' | 'exponential'; multiplier?: number; maxDelayMs: number; jitter?: number; respectRetryAfter?: boolean; requireIdempotencyForUnsafeFailures?: boolean; } interface RetryDecision { retry: boolean; reason: 'retryable' | 'attempts_exhausted' | 'policy_denied' | 'not_idempotent' | 'unsafe_phase'; delayMs: number; } /** * Level 3: Checkpoint interface for handler-level resume. * Available via ctx.checkpoint in handler execution context. */ interface ICheckpointContext { /** Get last saved checkpoint data. */ getCheckpoint(): Promise; /** Save checkpoint (survives retries, cleared on success). */ saveCheckpoint(data: T): Promise; /** Clear checkpoint (call on successful completion). */ clearCheckpoint(): Promise; } /** * Retry configuration for ExecutionConfig. */ interface ExecutionRetryConfig { /** Maximum attempts (including first try). @default 1 (no retry) */ maxAttempts?: number; /** Initial delay between retries in ms. @default 1000 */ initialDelayMs?: number; /** Backoff multiplier. @default 2 */ backoffMultiplier?: number; /** Maximum delay cap in ms. @default 30000 */ maxDelayMs?: number; /** Only retry errors with retryable=true. @default true */ onlyRetryable?: boolean; } /** * @module @kb-labs/core-contracts * * Host resolution abstraction for execution routing. * * Execution layer uses IHostResolver to find which host should handle * a request — without knowing about Gateway, WebSocket, or HTTP. */ /** * Result of host resolution. */ interface HostResolution { /** Resolved host identifier. */ hostId: string; /** Which strategy produced this result. */ strategy: HostSelectionStrategy; /** Namespace the host belongs to. */ namespaceId: string; } /** * Resolves an ExecutionTarget to a concrete host. * * Implementations may use Gateway REST API, local registry, config file, etc. * The execution layer only knows this interface — never the transport details. */ interface IHostResolver { /** * Resolve a target to a host. * * @returns HostResolution if a suitable host is found, null otherwise. * Null signals the caller to apply fallback policy. */ resolve(target: ExecutionTarget): Promise; } /** * @module @kb-labs/core-contracts/observability * Versioned observability contract shared by KB Labs services. */ declare const OBSERVABILITY_CONTRACT_VERSION = "1.0"; declare const OBSERVABILITY_SCHEMA = "kb.observability/1"; declare const OBSERVABILITY_CAPABILITIES: readonly ["httpMetrics", "eventLoopMetrics", "operationMetrics", "logCorrelation", "diagnosisHints", "llmContext"]; type ObservabilityCapability = (typeof OBSERVABILITY_CAPABILITIES)[number]; declare const CANONICAL_OBSERVABILITY_METRICS: readonly ["process_cpu_percent", "process_rss_bytes", "process_heap_used_bytes", "process_event_loop_lag_ms", "service_health_status", "service_restarts_total", "service_active_operations", "http_requests_total", "http_errors_total", "http_request_duration_ms", "service_operation_total", "service_operation_duration_ms"]; type CanonicalObservabilityMetric = (typeof CANONICAL_OBSERVABILITY_METRICS)[number]; declare const CANONICAL_SERVICE_LOG_FIELDS: readonly ["applicationId", "serviceId", "instanceId", "layer", "component", "requestId", "traceId", "spanId", "tenantId", "pluginId", "pluginVersion", "pluginKind", "operation", "http.route", "http.method", "http.url"]; type CanonicalServiceLogField = (typeof CANONICAL_SERVICE_LOG_FIELDS)[number]; declare const DIAGNOSTIC_REASON_CODES: readonly ["snapshot_stale", "snapshot_partial", "registry_restore_failed", "manifest_missing", "manifest_invalid", "manifest_load_timeout", "integrity_mismatch", "plugin_discovery_failed", "version_mismatch", "handler_not_found", "module_import_failed", "export_missing", "zod_validation_failed", "execution_host_unavailable", "execution_dispatch_failed", "route_validation_failed", "route_mount_failed", "ws_mount_failed", "registry_refresh_failed", "upstream_unavailable", "websocket_auth_failed", "websocket_hello_timeout", "websocket_handshake_invalid", "websocket_protocol_unsupported", "websocket_message_invalid", "adapter_call_rejected", "adapter_bridge_unavailable", "workspace_provision_failed", "workspace_provision_timeout", "worker_loop_error"]; type DiagnosticReasonCode = (typeof DIAGNOSTIC_REASON_CODES)[number]; type ServiceObservabilityState = "active" | "stale" | "dead" | "overloaded" | "partial_observability" | "unsupported_contract_version" | "insufficient_data"; type ServiceHealthStatus = "healthy" | "degraded" | "unhealthy"; type ObservabilityCheckStatus = "ok" | "warn" | "error"; interface ServiceDependencyDescriptor { serviceId: string; required: boolean; description?: string; } interface ServiceObservabilityDescribe { schema: typeof OBSERVABILITY_SCHEMA; contractVersion: typeof OBSERVABILITY_CONTRACT_VERSION; serviceId: string; instanceId: string; serviceType: string; version: string; environment: string; startedAt: string; dependencies: ServiceDependencyDescriptor[]; metricsEndpoint: string; healthEndpoint: string; logsSource: string; capabilities: ObservabilityCapability[]; metricFamilies: CanonicalObservabilityMetric[]; } interface ObservabilityCheck { id: string; status: ObservabilityCheckStatus; message?: string; latencyMs?: number; } interface ResourceSnapshot { cpuPercent?: number; rssBytes?: number; heapUsedBytes?: number; eventLoopLagMs?: number; activeOperations?: number; } interface ServiceOperationSample { operation: string; count?: number; avgDurationMs?: number; maxDurationMs?: number; errorCount?: number; } interface ServiceLogCorrelationContext { applicationId: string; serviceId: string; instanceId: string; layer: string; component?: string; requestId?: string; traceId?: string; spanId?: string; tenantId?: string; pluginId?: string; pluginVersion?: string; pluginKind?: string; operation?: string; "http.route"?: string; "http.method"?: string; "http.url"?: string; } interface ServiceObservabilityHealth { schema: typeof OBSERVABILITY_SCHEMA; contractVersion: typeof OBSERVABILITY_CONTRACT_VERSION; serviceId: string; instanceId: string; observedAt: string; status: ServiceHealthStatus; uptimeSec: number; metricsEndpoint: string; logsSource: string; capabilities: ObservabilityCapability[]; checks: ObservabilityCheck[]; snapshot?: ResourceSnapshot; topOperations?: ServiceOperationSample[]; state?: ServiceObservabilityState; meta?: Record; } interface DiagnosisSuspect { type: "route" | "operation" | "dependency" | "runtime" | "log-pattern" | "unknown"; id: string; confidence: number; reason: string; } interface DiagnosisEvidence { type: "metric" | "log" | "timeline" | "state" | "gap"; source: string; summary: string; ts?: string; data?: Record; } interface EvidenceBundle { service: Pick; snapshot?: ResourceSnapshot; checks?: ObservabilityCheck[]; topOperations?: ServiceOperationSample[]; suspects?: DiagnosisSuspect[]; evidence?: DiagnosisEvidence[]; dataGaps?: string[]; recommendedActions?: string[]; provenance?: Array<{ source: string; kind: "metrics" | "logs" | "health" | "events" | "derived"; collectedAt?: string; }>; } type ObservabilityValidationIssue = { path: string; message: string; }; type ObservabilityValidationResult = { ok: true; value: T; } | { ok: false; issues: ObservabilityValidationIssue[]; }; type ObservabilityMetricCompliance = { ok: boolean; present: CanonicalObservabilityMetric[]; missing: CanonicalObservabilityMetric[]; }; declare function isObservabilityCapability(value: unknown): value is ObservabilityCapability; declare function isCanonicalObservabilityMetric(value: unknown): value is CanonicalObservabilityMetric; declare function validateServiceObservabilityDescribe(value: unknown): ObservabilityValidationResult; declare function validateServiceObservabilityHealth(value: unknown): ObservabilityValidationResult; declare function extractPrometheusMetricFamilies(metricsText: string): string[]; declare function checkCanonicalObservabilityMetrics(metricsText: string): ObservabilityMetricCompliance; /** * @module @kb-labs/core-contracts/permissions * * Canonical permission strings (ADR-0020, CD-7). * * Both the gateway (`requirePermission(...)`) and Studio (`useCan(...)`) * import from this single source of truth. Typos become compile errors * instead of silent denies. * * Permissions are kept stable: renaming a value is a breaking change * because it appears in tokens, audit logs, and admin UIs. Add new * permissions instead of repurposing existing ones. */ /** * Canonical permission strings used across the platform. * * Naming: `:`. `verb` is one of `read` | `write` | a * resource-specific action. */ declare const PERMISSIONS: Readonly<{ /** Read users in your tenant (admin UI list view). */ readonly USERS_READ: "users:read"; /** Modify users in your tenant (enable/disable/role change). */ readonly USERS_WRITE: "users:write"; /** Read pending invites for your tenant. */ readonly INVITES_READ: "invites:read"; /** Create / revoke invites for your tenant. */ readonly INVITES_WRITE: "invites:write"; /** Register a new machine client (host). Replaces the old public `/auth/register`. */ readonly MACHINE_REGISTER: "machine:register"; }>; /** * A canonical permission string. * * Use this anywhere you accept a permission value: * * ```ts * function requirePermission(p: Permission) { ... } * requirePermission(PERMISSIONS.USERS_WRITE); // ok * requirePermission('typo:write'); // compile error * ``` */ type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS]; /** * @module @kb-labs/core-contracts/identity-provider * * The single public extension seam for authentication (ADR-0020, * principle #2). * * An identity provider verifies "this caller is `alice@acme.com`" and * nothing else. Everything downstream — looking up the local `User` * record, issuing tokens, creating session families, mapping to * memberships — stays inside the gateway. That asymmetry is the whole * point: replacing the door does not require rewriting the building. * * The built-in `email-password` provider implements this exact * contract; future Google/Okta/LDAP/SAML providers ship as additional * implementations without touching gateway internals. */ /** * Result of a single authentication attempt. * * On success, the provider only returns the canonical identity hints — * `email` is the join key to our internal `users` collection, and * `externalId` is whatever stable id the upstream IdP uses (its own * subject claim). On failure, `reason` is purposefully coarse: the * caller maps every failure mode to the same opaque `invalid_credentials` * response (ADR-0020, CD-8) to defend against email enumeration and * timing attacks. */ type IdentityResult = { ok: true; /** Canonical email (lowercased+trimmed by the provider, CD-4). */ email: string; /** Stable opaque id from the upstream IdP, if any. */ externalId?: string; /** Free-form attributes the upstream IdP may surface for ABAC use later. */ attributes?: Record; } | { ok: false; /** * - `invalid` — credentials format ok but didn't match. * - `disabled` — caller exists but the upstream marks them disabled. * - `unknown` — caller does not exist upstream. * * Callers must treat all three identically in user-facing responses * (CD-8). The distinction is for **internal logging only**. */ reason: 'invalid' | 'disabled' | 'unknown'; }; /** * Common members shared by every provider regardless of `kind`. * * `authenticate` lives here (and therefore on BOTH union members) on * purpose: a TypeScript union of object types intersects the parameter * types of methods that differ between members, so keeping * `authenticate(input: unknown)` identical on every member preserves the * ability to call `provider.authenticate(unknown)` on the union without * narrowing first (ADR-0020, DD-1). */ interface IdentityProviderBase { /** * Stable identifier registered with the provider registry. Surfaces as * `providerId` in `POST /auth/login`. Example values: `email-password`, * `google`, `okta`. */ readonly id: string; /** * Verify a single authentication attempt. * * `input` shape is provider-specific. For `kind: 'password'` it is * typically `{ email, password }`. For `kind: 'redirect'` it is the * provider-specific callback payload (authorization code + the stored * per-attempt session) after the user returned from the upstream IdP. * * Providers must: * - Lowercase + trim emails before any lookup (CD-4). * - Run a dummy compare on unknown-user paths so success and failure * take comparable time (CD-8 — enforced in the email-password * provider's tests; ports of this contract must do the same). * - Never throw on bad credentials; throws are reserved for genuine * infrastructure errors (DB down, upstream IdP 5xx). */ authenticate(input: unknown): Promise; } /** * A provider that authenticates with credentials presented directly to * the gateway (e.g. email + password). Studio renders a credential form. */ interface IPasswordIdentityProvider extends IdentityProviderBase { readonly kind: 'password'; } /** * Context the gateway hands a redirect provider when a browser begins an * authorization flow (`GET /auth/oauth/:id/start`). */ interface RedirectStartContext { /** * Opaque anti-CSRF / correlation token the gateway minted and persisted * in its OAuth state store. The provider embeds it in the upstream * authorize URL (`state` query param) verbatim. */ state: string; /** * Absolute callback URL the upstream IdP must redirect back to, derived * from the inbound request Host: * `https://{host}/api/auth/oauth/{id}/callback`. */ redirectUri: string; } /** * What a redirect provider returns when starting an authorization flow. */ interface RedirectStartResult { /** * Absolute URL on the upstream IdP. The gateway answers the browser * with `302 Location: `. */ redirectUrl: string; /** * Per-attempt secrets the provider needs back at the callback to finish * verification (OIDC `nonce`, PKCE `code_verifier`, ...). The gateway * persists this opaque blob alongside the state token and returns it to * `authenticate` at callback time. Never logged. */ session?: Record; } /** * A provider that delegates authentication to an upstream IdP via a * browser redirect (OAuth 2.0 / OIDC / SAML). Studio renders a * "Continue with X" button. */ interface IRedirectIdentityProvider extends IdentityProviderBase { readonly kind: 'redirect'; /** * Begin an authorization flow. Pure URL construction + per-attempt * secret generation — no network call. The gateway persists * `result.session` keyed by `ctx.state`, sets a state cookie, and 302s * the browser to `result.redirectUrl`. */ startAuthorization(ctx: RedirectStartContext): Promise; } /** * The provider contract. * * A discriminated union on `kind`. Narrow with `if (provider.kind === * 'redirect')` to reach `startAuthorization`; `authenticate` is callable * on the union directly (DD-1). * * Implementations are stateless from the caller's perspective; they get * their dependencies (the narrow ports below, HTTP clients to upstream * IdPs) wired via the factory at registration time. */ type IIdentityProvider = IPasswordIdentityProvider | IRedirectIdentityProvider; /** * Minimal read view of a local user, as a provider needs it. * * Deliberately a structural subset of the gateway's `User` so the * gateway's `UsersStore` satisfies {@link IdentityUserPort} without an * adapter, while a third-party provider package depends only on * `core/contracts` (never on gateway internals). */ interface IdentityUserRecord { userId: string; tenantId: string; /** Canonical (lowercased + trimmed) email. */ email: string; status: 'pending' | 'active' | 'disabled'; } /** * Minimal read view of a stored credential. * * Structural subset of the gateway's `Credential`. `hash` is opaque to * this contract (bcrypt hash for email-password; anything for others). */ interface IdentityCredentialRecord { userId: string; providerId: string; hash: string; } /** * Narrow read port over the users collection (DD-2). A provider may look * a user up by canonical email within its configured tenant; it may not * write, list, or mutate. Satisfied structurally by the gateway's * `UsersStore`. */ interface IdentityUserPort { findByEmailTenant(email: string, tenantId: string): Promise; } /** * Narrow read port over the credentials collection (DD-2). Satisfied * structurally by the gateway's `CredentialsStore`. */ interface IdentityCredentialPort { getCredential(userId: string, providerId: string): Promise; } /** * Structural minimum logger. * * Layer-0 `core/contracts` must not depend on `core-platform`'s * `ILogger`, so providers receive this three-method shape instead. A * real `ILogger` satisfies it structurally. */ interface IdentityProviderLogger { warn(message: string, ...args: unknown[]): void; info(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; } /** * Everything a provider factory receives at registration time (DD-2). * * The gateway loader builds this once per configured provider. Third-party * provider packages type their factory against this and nothing else. */ interface IdentityProviderDeps { /** Narrow read port over users. */ users: IdentityUserPort; /** Narrow read port over credentials. */ credentials: IdentityCredentialPort; /** The single tenant this gateway instance serves (subdomain-per-tenant). */ tenantId: string; /** bcrypt cost factor for password providers (ignored by redirect ones). */ bcryptCost: number; /** Structural-minimum logger. */ logger: IdentityProviderLogger; /** * Injected `fetch` for providers that talk to an upstream IdP. Defaults * to `globalThis.fetch` in the loader; tests inject a fake (DD-8). */ fetch?: typeof globalThis.fetch; } /** * The factory every provider package exports (DD-2/DD-3). * * `TConfig` is the provider's own validated config slice (each provider * owns its zod schema). The gateway loader calls this with the parsed * config and the shared deps. Built-in providers (`email-password`, * `oidc`) are registered through factories of this exact shape. */ type IdentityProviderFactory = (config: TConfig, deps: IdentityProviderDeps) => IIdentityProvider | Promise; /** * @module @kb-labs/core-contracts/policy * * Policy Decision Point (ADR-0020, principle #4). * * Tokens carry identity, never permissions. Authorization decisions live * entirely behind this contract: every authenticated handler calls * `policy.check(identity, action, resource?, ctx?)` and trusts the * decision. The stub implementation that ships with this iteration uses * a hard-coded RBAC mapping; the real engine (RBAC + ReBAC, see * https://app.clickup.com/t/869def338) replaces the implementation * without touching this contract or any caller. * * `enumeratePermissions(identity)` exists so Studio can build * permission-aware UI (`useCan(...)`) without inferring the set from the * presence/absence of `check` calls. The stub returns the subset of * canonical permissions for which `check` would currently allow. */ /** * An authenticated caller. Subject ≠ identity: a `Subject` is built by * the policy layer at decision time from `{ user, memberships, * attributes }`. The token only carries enough to look the user up. * * Note the **absence** of `role`, `scopes`, `permissions`: those never * appear in tokens (ADR-0020, principle #3). */ interface Identity { /** Internal stable user id (`users.userId`) for `type: 'user'`, or * machine-client id for `type: 'machine'`. */ userId: string; /** Tenant the identity belongs to. Cross-tenant access is denied at * the middleware layer before `check` is called. */ tenantId: string; type: 'user' | 'machine'; } /** * The thing being acted upon. `type` is the resource kind (`'user'`, * `'invite'`, `'workflow'`, ...); `id`/`tenantId` are the specific * instance when known. * * Most permission checks are coarse (e.g. "can this identity write * users in their own tenant"). `resource` is optional so callers can * pass `undefined` for tenant-wide checks. */ interface Resource { type: string; id?: string; tenantId?: string; } /** * Per-request attributes available to ABAC predicates in the future * (region, time, IP, etc.). Today the stub PDP ignores it; the contract * accepts it now so callers do not need to be retrofitted later. */ type PolicyContext = Record; /** * Decision from the PDP. `reason` on deny is for logs and admin debug * surfaces — user-facing responses just say "forbidden". */ type PolicyDecision = { allow: true; } | { allow: false; reason: string; }; /** * The single authorization seam in the platform. */ interface IPolicyDecisionPoint { /** * Decide whether `identity` may perform `action` on `resource` under * `ctx`. Must never throw on policy denial — throws are reserved for * infrastructure failures (store down, etc). * * `action` is a `Permission` value from the canonical enum, but typed * as `string` so future per-plugin permissions (e.g. * `plugin.foo.bar`) can flow without coupling to `core/contracts`. */ check(identity: Identity, action: string, resource?: Resource, ctx?: PolicyContext): Promise; /** * Return the set of permissions `identity` currently holds. * * Used by `GET /auth/permissions` to seed Studio's permission-aware * UI. Implementations should resolve the set from the same source as * `check`, so UI and backend decisions never disagree. * * Returned strings are canonical `Permission` values — the type is * `string[]` so future per-plugin permissions are not blocked by the * enum. */ enumeratePermissions(identity: Identity): Promise; } export { type ArtifactsConfig, CANONICAL_OBSERVABILITY_METRICS, CANONICAL_SERVICE_LOG_FIELDS, type CancelRequest, type CancellationReason, type CanonicalObservabilityMetric, type CanonicalServiceLogField, type ClassifiedFailure, DIAGNOSTIC_REASON_CODES, type DiagnosisEvidence, type DiagnosisSuspect, type DiagnosticReasonCode, type EvidenceBundle, type ExecuteOptions, type ExecutionArtifactEvent, type ExecutionCancelledEvent, type ExecutionDescriptorCore, type ExecutionDoneEvent, type ExecutionError, type ExecutionErrorCode, type ExecutionErrorEvent, type ExecutionEvent, type ExecutionMeta, type ExecutionMetadata, type ExecutionOutputEvent, type ExecutionProgressEvent, type ExecutionRequest, type ExecutionResponse, type ExecutionResult, type ExecutionRetryConfig, type ExecutionRetryEvent, type ExecutionStats, type ExecutionTarget, type ExecutionTargetType, type FailureClassificationContext, type FailureInfo, type FailureKind, type FailureSource, type HealthStatus, type HostResolution, type HostSelectionStrategy, type ICancellationHandler, type ICheckpointContext, type IExecutionBackend, type IExecutionEventEmitter, type IExecutionTransport, type IHostResolver, type IIdentityProvider, type IPasswordIdentityProvider, type IPlatformGateway, type IPolicyDecisionPoint, type IRedirectIdentityProvider, type IRetryPolicy, type ISubprocessRunner, type Identity, type IdentityCredentialPort, type IdentityCredentialRecord, type IdentityProviderDeps, type IdentityProviderFactory, type IdentityProviderLogger, type IdentityResult, type IdentityUserPort, type IdentityUserRecord, type LLMOptions, type LLMResponse, OBSERVABILITY_CAPABILITIES, OBSERVABILITY_CONTRACT_VERSION, OBSERVABILITY_SCHEMA, type ObservabilityCapability, type ObservabilityCheck, type ObservabilityCheckStatus, type ObservabilityMetricCompliance, type ObservabilityValidationIssue, type ObservabilityValidationResult, PERMISSIONS, type Permission, type PolicyContext, type PolicyDecision, type RedirectStartContext, type RedirectStartResult, type RequestContext, type Resource, type ResourceSnapshot, type RetryDecision, type RetryPolicyConfig, type RetrySafety, type RetryableError, type RetryableFailureKind, type RunResult, type ServiceDependencyDescriptor, type ServiceHealthStatus, type ServiceLogCorrelationContext, type ServiceObservabilityDescribe, type ServiceObservabilityHealth, type ServiceObservabilityState, type ServiceOperationSample, type SubprocessRunOptions, type TransportExecutionResult, type VectorQuery, type VectorRecord, type VectorSearchResult, type WorkspaceConfig, checkCanonicalObservabilityMetrics, extractPrometheusMetricFamilies, isCanonicalObservabilityMetric, isObservabilityCapability, validateServiceObservabilityDescribe, validateServiceObservabilityHealth };