-
Notifications
You must be signed in to change notification settings - Fork 0
feat(kernel): shared @wave-av/kernel client (Phase A slice 1) #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
6f3a9ca
96fce3f
14742f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 WAVE Inc. | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| { | ||
| "name": "@wave-av/kernel", | ||
| "version": "0.1.0", | ||
| "description": "WAVE shared Kernel cloud-browser client — one typed client on @onkernel/sdk, api.onkernel.com, telemetry-ready", | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js", | ||
| "require": "./dist/index.cjs", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "./telemetry": { | ||
| "types": "./dist/telemetry.d.ts", | ||
| "import": "./dist/telemetry.js", | ||
| "require": "./dist/telemetry.cjs", | ||
| "default": "./dist/telemetry.js" | ||
| } | ||
| }, | ||
| "typesVersions": { | ||
| "*": { | ||
| "telemetry": [ | ||
| "./dist/telemetry.d.ts" | ||
| ] | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "build": "tsup src/index.ts src/telemetry.ts --format esm,cjs && tsc -p tsconfig.json --emitDeclarationOnly", | ||
| "dev": "tsup src/index.ts --format esm,cjs --dts --watch", | ||
| "lint": "eslint src/", | ||
| "type-check": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "prepublishOnly": "npm run build" | ||
| }, | ||
| "keywords": [ | ||
| "wave", | ||
| "kernel", | ||
| "cloud-browser", | ||
| "browser", | ||
| "onkernel", | ||
| "telemetry" | ||
| ], | ||
| "license": "Apache-2.0", | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "provenance": true, | ||
| "registry": "https://registry.npmjs.org/" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "README.md", | ||
| "LICENSE" | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/wave-av/sdks.git", | ||
| "directory": "sdk-typescript/packages/kernel" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/wave-av/sdks/issues" | ||
| }, | ||
| "homepage": "https://wave.online/developers/kernel", | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "dependencies": { | ||
| "@onkernel/sdk": "^0.78.0", | ||
| "zod": "^3.23.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.0.3", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Node.js 25 reached end-of-life June 1, 2026 and was never promoted to LTS (odd-numbered release). The active LTS lines as of mid-2026 are Node 22 (maintenance) and 24 (active), with 26 as the current non-LTS line. Pinning dev types to the EOL 25.x line also doesn't align with the package's own 🤖 Prompt for AI Agents |
||
| "tsup": "^8.0.0", | ||
| "typescript": "^5.9.0", | ||
| "vitest": "^4.1.4" | ||
| }, | ||
| "author": "WAVE Online, LLC <sdk@wave.online>" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { WaveKernel } from '../client.js'; | ||
| import { KernelApiError, KernelErrorCode, isKernelApiError } from '../errors.js'; | ||
| import { withTelemetry, WAVE_TELEMETRY_DEFAULT } from '../telemetry.js'; | ||
|
|
||
| describe('WaveKernel', () => { | ||
| it('constructs with an explicit apiKey without throwing (no network)', () => { | ||
| expect(() => new WaveKernel({ apiKey: 'test-key' })).not.toThrow(); | ||
| }); | ||
|
|
||
| it('exposes the SDK resource surface', () => { | ||
| const kernel = new WaveKernel({ apiKey: 'test-key' }); | ||
| expect(kernel.browsers).toBeDefined(); | ||
| expect(kernel.invocations).toBeDefined(); | ||
| expect(kernel.browserPools).toBeDefined(); | ||
| expect(kernel.credentials).toBeDefined(); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| describe('withTelemetry', () => { | ||
| it('enables console/network/page and leaves screenshot undefined', () => { | ||
| const params = withTelemetry({}); | ||
| expect(params.telemetry.enabled).toBe(true); | ||
| expect(params.telemetry.browser?.console?.enabled).toBe(true); | ||
| expect(params.telemetry.browser?.network?.enabled).toBe(true); | ||
| expect(params.telemetry.browser?.page?.enabled).toBe(true); | ||
| expect(params.telemetry.browser?.screenshot).toBeUndefined(); | ||
| }); | ||
|
Comment on lines
+61
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add a custom-category telemetry test. The helper accepts category overrides, but this suite only exercises the default argument. Verify that disabling a default category and enabling 🤖 Prompt for AI Agents |
||
|
|
||
| it('does not enable screenshot in the default categories', () => { | ||
| expect(WAVE_TELEMETRY_DEFAULT.screenshot).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('KernelApiError', () => { | ||
| it('timeout() sets a code', () => { | ||
| const err = KernelApiError.timeout(30000); | ||
| expect(err.code).toBe(KernelErrorCode.TIMEOUT); | ||
| expect(err.code).toBeTruthy(); | ||
| expect(isKernelApiError(err)).toBe(true); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /** | ||
| * WAVE shared Kernel cloud-browser client. | ||
| * | ||
| * `WaveKernel` is the ONE Kernel entrypoint for all of WAVE (law: | ||
| * kernel-substrate-governed). Every WAVE consumer — adk, render, and the | ||
| * split-out wave-surfer-connect homes — talks to Kernel through this single | ||
| * typed wrapper over `@onkernel/sdk`, never the raw SDK. That gives us one | ||
| * place to add telemetry, resilience, and auth without touching callers. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win CI is failing: private repo/product name referenced in doc comments across 3 files. The
🧰 Tools🪛 GitHub Actions: public-repo-guard / 0_Secrets + content policy.txt[error] 6-6: Guard_PRIVATE_REPOS violation: Reference to a private WAVE repo/product detected. Remove it, or annotate the line with '# guard:allow '. 🪛 GitHub Actions: public-repo-guard / Secrets + content policy[error] 6-6: CI failed: Reference to a private WAVE repo/product (configured via GUARD_PRIVATE_REPOS) detected. Remove it or annotate the line with '# guard:allow '. 📍 Affects 3 files
🤖 Prompt for AI AgentsSource: Pipeline failures |
||
| * | ||
| * Base URL is the SDK default (`https://api.onkernel.com/`); we never point at | ||
| * the legacy `api.kernel.sh` host. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { WaveKernel } from '@wave-av/kernel'; | ||
| * | ||
| * const kernel = new WaveKernel({ apiKey: process.env.KERNEL_API_KEY }); | ||
| * const browser = await kernel.browsers.create({}); | ||
| * ``` | ||
| */ | ||
| import { Kernel } from '@onkernel/sdk'; | ||
| import type { ResilienceHooks } from './resilience.js'; | ||
|
|
||
| /** Default request timeout in milliseconds. */ | ||
| export const DEFAULT_TIMEOUT_MS = 30_000; | ||
|
|
||
| /** Configuration for {@link WaveKernel}. */ | ||
| export interface WaveKernelConfig { | ||
| /** Kernel API key. Defaults to `process.env.KERNEL_API_KEY`. */ | ||
| apiKey?: string; | ||
| /** Override the API base URL. Defaults to the SDK default (`https://api.onkernel.com/`). */ | ||
| baseURL?: string; | ||
| /** Optional project scope applied to all requests. */ | ||
| projectID?: string; | ||
| /** Request timeout in milliseconds. Defaults to {@link DEFAULT_TIMEOUT_MS}. */ | ||
| timeoutMs?: number; | ||
| /** | ||
| * Optional resilience hooks (circuit breaker, signer). Inert in slice 1 — | ||
| * see {@link ResilienceHooks} and `src/resilience.ts`. | ||
| */ | ||
| resilience?: ResilienceHooks; | ||
| } | ||
|
Comment on lines
+28
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the SDK package files and the client under review.
git ls-files | rg '(^|/)(api\.md|client\.ts|package\.json)$|`@onkernel/sdk`|sdk-typescript/packages/kernel/src/client\.ts'
# Find any direct references to projectID, defaultHeaders, and Kernel constructor usage.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
'projectID|defaultHeaders|new Kernel\(' \
sdk-typescript packages . 2>/dev/null | sed -n '1,220p'
# If a vendored SDK doc/type file exists in the repo, show the relevant section.
if [ -f node_modules/@onkernel/sdk/api.md ]; then
echo '--- node_modules/@onkernel/sdk/api.md ---'
rg -n 'class Kernel|interface .*Kernel|projectID|defaultHeaders|constructor' node_modules/@onkernel/sdk/api.md | sed -n '1,220p'
fiRepository: wave-av/sdks Length of output: 3038 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Read the kernel package manifest to see how `@onkernel/sdk` is pinned.
cat -n sdk-typescript/packages/kernel/package.json | sed -n '1,220p'
# Check whether the installed SDK type docs exist locally and inspect the constructor shape.
if [ -f node_modules/@onkernel/sdk/api.md ]; then
echo '--- api.md excerpt ---'
rg -n 'class Kernel|constructor|projectID|defaultHeaders|fetchOptions|environment|logLevel|apiKey' \
node_modules/@onkernel/sdk/api.md | sed -n '1,220p'
else
echo 'node_modules/@onkernel/sdk/api.md not present'
fi
# Also inspect any local package metadata for the SDK itself if present in the repo.
git ls-files | rg '(^|/)`@onkernel/sdk`|(^|/)sdk/package\.json$|(^|/)api\.md$'Repository: wave-av/sdks Length of output: 2740 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- sdk-typescript/packages/kernel/src/client.ts ---'
cat -n sdk-typescript/packages/kernel/src/client.ts | sed -n '1,220p'
echo '--- references to X-Kernel-Project-Id / defaultHeaders in repo ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
'X-Kernel-Project-Id|defaultHeaders|projectID' \
sdk-typescript | sed -n '1,220p'
echo '--- lockfile / workspace references to `@onkernel/sdk` ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
'"`@onkernel/sdk`"|`@onkernel/sdk`' \
sdk-typescript package-lock.json pnpm-lock.yaml yarn.lock .npmrc . 2>/dev/null | sed -n '1,220p'Repository: wave-av/sdks Length of output: 7615 🌐 Web query:
💡 Result: In the Citations:
Scope Kernel requests via 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * The single shared WAVE Kernel client. Wraps a private `@onkernel/sdk` | ||
| * {@link Kernel} instance and re-exposes its full 0.78 resource surface as | ||
| * typed passthrough getters so callers never touch the raw SDK. | ||
| */ | ||
| export class WaveKernel { | ||
| private readonly kernel: Kernel; | ||
|
|
||
| /** Resilience hooks captured at construction (inert in slice 1). */ | ||
| readonly resilience?: ResilienceHooks; | ||
|
|
||
| constructor(config: WaveKernelConfig = {}) { | ||
| this.resilience = config.resilience; | ||
| this.kernel = new Kernel({ | ||
| apiKey: config.apiKey ?? process.env.KERNEL_API_KEY, | ||
| // Only override baseURL when explicitly provided so the SDK default | ||
| // (https://api.onkernel.com/) is used otherwise. | ||
| ...(config.baseURL !== undefined ? { baseURL: config.baseURL } : {}), | ||
| ...(config.projectID !== undefined ? { projectID: config.projectID } : {}), | ||
| timeout: config.timeoutMs ?? DEFAULT_TIMEOUT_MS, | ||
| }); | ||
| } | ||
|
|
||
| /** Escape hatch to the underlying SDK client. Prefer the typed getters below. */ | ||
| get raw(): Kernel { | ||
| return this.kernel; | ||
| } | ||
|
|
||
| /** Cloud browser sessions: create / retrieve / update / list / curl / loadExtensions. */ | ||
| get browsers() { | ||
| return this.kernel.browsers; | ||
| } | ||
|
|
||
| /** App invocations: create / retrieve / update / list / follow. */ | ||
| get invocations() { | ||
| return this.kernel.invocations; | ||
| } | ||
|
|
||
| /** Browser pools: create / update / list / delete / acquire / release. */ | ||
| get browserPools() { | ||
| return this.kernel.browserPools; | ||
| } | ||
|
|
||
| /** Browser profiles: create / retrieve / update / list / delete. */ | ||
| get profiles() { | ||
| return this.kernel.profiles; | ||
| } | ||
|
|
||
| /** Stored credentials: create / retrieve / update / list / delete / totpCode. */ | ||
| get credentials() { | ||
| return this.kernel.credentials; | ||
| } | ||
|
|
||
| /** Credential providers: create / retrieve / update / list / delete / listItems / test. */ | ||
| get credentialProviders() { | ||
| return this.kernel.credentialProviders; | ||
| } | ||
|
|
||
| /** API key management. */ | ||
| get apiKeys() { | ||
| return this.kernel.apiKeys; | ||
| } | ||
|
|
||
| /** Project management. */ | ||
| get projects() { | ||
| return this.kernel.projects; | ||
| } | ||
|
|
||
| /** Auth resource. */ | ||
| get auth() { | ||
| return this.kernel.auth; | ||
| } | ||
|
|
||
| /** Proxy configuration. */ | ||
| get proxies() { | ||
| return this.kernel.proxies; | ||
| } | ||
|
|
||
| /** App deployments. */ | ||
| get deployments() { | ||
| return this.kernel.deployments; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * Kernel error taxonomy. Mirrors the WSC taxonomy | ||
| * (wave-surfer-connect/src/lib/kernel/client.ts) so consumers can migrate | ||
| * onto the shared client without rewriting error handling. | ||
| */ | ||
|
|
||
| /** Stable, machine-readable Kernel error codes. */ | ||
| export const KernelErrorCode = { | ||
| API_ERROR: 'KERNEL_API_ERROR', | ||
| CIRCUIT_OPEN: 'KERNEL_CIRCUIT_OPEN', | ||
| RATE_LIMITED: 'KERNEL_RATE_LIMITED', | ||
| SESSION_NOT_FOUND: 'KERNEL_SESSION_NOT_FOUND', | ||
| POOL_EXHAUSTED: 'KERNEL_POOL_EXHAUSTED', | ||
| TIMEOUT: 'KERNEL_TIMEOUT', | ||
| } as const; | ||
|
|
||
| export type KernelErrorCodeValue = | ||
| (typeof KernelErrorCode)[keyof typeof KernelErrorCode]; | ||
|
|
||
| /** Base error for all Kernel client failures. */ | ||
| export class KernelApiError extends Error { | ||
| /** Machine-readable error code (see {@link KernelErrorCode}). */ | ||
| readonly code: string; | ||
| /** Optional structured context for the error. */ | ||
| readonly context: Record<string, unknown>; | ||
|
|
||
| constructor( | ||
| message: string, | ||
| code: string = KernelErrorCode.API_ERROR, | ||
| context: Record<string, unknown> = {}, | ||
| ) { | ||
| super(message); | ||
| this.name = 'KernelApiError'; | ||
| this.code = code; | ||
| this.context = context; | ||
| Error.captureStackTrace?.(this, this.constructor); | ||
| } | ||
|
|
||
| /** The circuit breaker is open — requests are being shed. */ | ||
| static circuitOpen(serviceName = 'kernel'): CircuitOpenError { | ||
| return new CircuitOpenError( | ||
| `Circuit breaker is open for ${serviceName}`, | ||
| { serviceName }, | ||
| ); | ||
| } | ||
|
|
||
| /** The request was rate limited. */ | ||
| static rateLimited(retryAfterMs?: number): KernelApiError { | ||
| const suffix = | ||
| retryAfterMs !== undefined ? ` Retry after ${retryAfterMs}ms` : ''; | ||
| return new KernelApiError( | ||
| `Rate limited.${suffix}`, | ||
| KernelErrorCode.RATE_LIMITED, | ||
| retryAfterMs !== undefined ? { retryAfterMs } : {}, | ||
| ); | ||
| } | ||
|
|
||
| /** The referenced browser session does not exist. */ | ||
| static sessionNotFound(sessionId: string): KernelApiError { | ||
| return new KernelApiError( | ||
| `Browser session not found: ${sessionId}`, | ||
| KernelErrorCode.SESSION_NOT_FOUND, | ||
| { sessionId }, | ||
| ); | ||
| } | ||
|
|
||
| /** The referenced browser pool has no available capacity. */ | ||
| static poolExhausted(poolId: string): KernelApiError { | ||
| return new KernelApiError( | ||
| `Browser pool exhausted: ${poolId}`, | ||
| KernelErrorCode.POOL_EXHAUSTED, | ||
| { poolId }, | ||
| ); | ||
| } | ||
|
|
||
| /** The operation exceeded its time budget. */ | ||
| static timeout(durationMs: number): KernelApiError { | ||
| return new KernelApiError( | ||
| `Operation timed out after ${durationMs}ms`, | ||
| KernelErrorCode.TIMEOUT, | ||
| { durationMs }, | ||
| ); | ||
| } | ||
|
|
||
| toJSON(): Record<string, unknown> { | ||
| return { | ||
| name: this.name, | ||
| message: this.message, | ||
| code: this.code, | ||
| context: this.context, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** Raised when the circuit breaker is open and requests are shed fast. */ | ||
| export class CircuitOpenError extends KernelApiError { | ||
| constructor( | ||
| message = 'Circuit breaker is open', | ||
| context: Record<string, unknown> = {}, | ||
| ) { | ||
| super(message, KernelErrorCode.CIRCUIT_OPEN, context); | ||
| this.name = 'CircuitOpenError'; | ||
| } | ||
| } | ||
|
|
||
| /** Type guard for {@link KernelApiError} (and its subclasses). */ | ||
| export function isKernelApiError(error: unknown): error is KernelApiError { | ||
| return error instanceof KernelApiError; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
License mismatch: package.json says Apache-2.0, LICENSE file is MIT.
The
LICENSEfile added in this PR contains the full MIT License text withCopyright (c) 2026 WAVE Inc., butpackage.jsondeclares"license": "Apache-2.0". This is a real SPDX/legal mismatch for a package published withpublishConfig.access: "public"— pick one license and align both files before publishing.🤖 Prompt for AI Agents