Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sdk-typescript/packages/kernel/LICENSE
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.
79 changes: 79 additions & 0 deletions sdk-typescript/packages/kernel/package.json
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",

Copy link
Copy Markdown

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 LICENSE file added in this PR contains the full MIT License text with Copyright (c) 2026 WAVE Inc., but package.json declares "license": "Apache-2.0". This is a real SPDX/legal mismatch for a package published with publishConfig.access: "public" — pick one license and align both files before publishing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-typescript/packages/kernel/package.json` at line 45, Align the package
metadata and LICENSE contents by choosing one license: either change the
package.json license field from Apache-2.0 to MIT to match the existing LICENSE
file, or replace the LICENSE text with the complete Apache-2.0 license and
preserve the corresponding metadata. Ensure both declarations match before
publishing.

"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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

@types/node pins an EOL, non-LTS Node major.

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 engines.node: ">=18.0.0" floor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-typescript/packages/kernel/package.json` at line 73, Update the
`@types/node` development dependency in package.json from the EOL Node 25 line to
an active LTS-compatible major, preferably the Node 24 type definitions, while
preserving the package's engines.node >=18.0.0 compatibility.

"tsup": "^8.0.0",
"typescript": "^5.9.0",
"vitest": "^4.1.4"
},
"author": "WAVE Online, LLC <sdk@wave.online>"
}
42 changes: 42 additions & 0 deletions sdk-typescript/packages/kernel/src/__tests__/client.test.ts
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();
});
Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 screenshot are preserved in the returned telemetry configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-typescript/packages/kernel/src/__tests__/client.test.ts` around lines 20
- 28, Extend the withTelemetry test suite with a custom-category case that
passes category overrides to withTelemetry, disabling a default category and
enabling screenshot. Assert that both overrides are preserved in the returned
telemetry configuration while retaining the existing default-behavior test.


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);
});
});
126 changes: 126 additions & 0 deletions sdk-typescript/packages/kernel/src/client.ts
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 public-repo-guard workflow blocks on all three sites for naming the internal wave-surfer-connect repo in shipped package doc comments. Same root cause everywhere — either de-identify the reference or add the guard's allow-annotation.

  • sdk-typescript/packages/kernel/src/client.ts#L4-L8: rewrite "the split-out wave-surfer-connect homes" to avoid naming the private repo, or annotate line 6 with # guard:allow <reason>.
  • sdk-typescript/packages/kernel/src/resilience.ts#L9-L9: rewrite the "port from wave-surfer-connect" TODO to avoid naming the private repo, or annotate with # guard:allow <reason>.
  • sdk-typescript/packages/kernel/src/errors.ts#L1-L6: rewrite "Mirrors the WSC taxonomy (wave-surfer-connect/src/lib/kernel/client.ts)" to avoid naming the private repo, or annotate with # guard:allow <reason>.
🧰 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
  • sdk-typescript/packages/kernel/src/client.ts#L4-L8 (this comment)
  • sdk-typescript/packages/kernel/src/resilience.ts#L9-L9
  • sdk-typescript/packages/kernel/src/errors.ts#L1-L6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-typescript/packages/kernel/src/client.ts` around lines 4 - 8, De-identify
the private repository name from shipped package documentation in
sdk-typescript/packages/kernel/src/client.ts lines 4-8,
sdk-typescript/packages/kernel/src/resilience.ts line 9, and
sdk-typescript/packages/kernel/src/errors.ts lines 1-6 by replacing each
reference with a generic description; alternatively, add the required guard
allow-annotation with a justification at each site.

Source: 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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'
fi

Repository: 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:

@onkernel/sdk 0.78.0 Kernel constructor options projectID defaultHeaders X-Kernel-Project-Id

💡 Result:

In the @onkernel/sdk, the Kernel client constructor accepts a defaultHeaders configuration option to set custom HTTP headers for all requests [1]. To scope the client to a specific project, you can pass the X-Kernel-Project-Id header within this object [1]. Note that the X-Kernel-Project-Id header requires a project ID (e.g., proj_abc123), not a project name [2][3]. Usage example: import Kernel from '@onkernel/sdk'; const kernel = new Kernel({ defaultHeaders: { 'X-Kernel-Project-Id': 'proj_abc123' }, }); // All subsequent requests made by this client instance are scoped to the specified project. const browser = await kernel.browsers.create; Additionally, you can override this header on a per-request basis by passing headers in the request options [1]: const other = await kernel.browsers.create( {}, { headers: { 'X-Kernel-Project-Id': 'proj_def456' } },); It is important to ensure you are providing the project ID, as the API middleware resolves this header by ID only; providing a project name will result in a 404 project_not_found error [2]. If you are using a project-scoped API key, it is already bound to a project server-side, and providing a conflicting X-Kernel-Project-Id header will lead to a 403 Forbidden error [1].

Citations:


Scope Kernel requests via defaultHeaders, not projectID. @onkernel/sdk uses defaultHeaders for client-wide headers, and project scoping is done with X-Kernel-Project-Id; projectID is not the constructor option here and won’t scope requests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk-typescript/packages/kernel/src/client.ts` around lines 28 - 42, Update
WaveKernelConfig and the client initialization to use a defaultHeaders option
for client-wide headers, including X-Kernel-Project-Id for project scoping,
instead of treating projectID as a constructor option. Remove or stop using
projectID in the request configuration and preserve the existing header behavior
for callers that provide defaultHeaders.


/**
* 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;
}
}
109 changes: 109 additions & 0 deletions sdk-typescript/packages/kernel/src/errors.ts
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;
}
Loading
Loading