Skip to content

Latest commit

 

History

History
281 lines (217 loc) · 17.1 KB

AGENTS.md

File metadata and controls

281 lines (217 loc) · 17.1 KB

AGENTS.md

Agent onboarding guide for the @project-kessel/kessel-sdk Node.js SDK. This file is the entry point for any AI agent (Claude, Cursor, CodeRabbit, etc.) working in this repository.

Guidelines Index

Directory-local GUIDELINES.md files contain detailed, domain-specific rules. Read the relevant file before making changes in that area.

  • src/kessel/auth/GUIDELINES.md -- OAuth2 credentials, OIDC discovery, token caching, AuthRequest
  • src/kessel/inventory/GUIDELINES.md -- ClientBuilder, per-version wiring, generated vs hand-written code
  • src/kessel/rbac/GUIDELINES.md -- REST workspace helpers, resource/subject factories, paginated streaming
  • src/kessel/console/GUIDELINES.md -- Red Hat identity header parsing, principal extraction
  • examples/GUIDELINES.md -- Runnable integration examples, file naming, script registration

What This Repository Is

A TypeScript gRPC SDK for Project Kessel. The SDK provides:

  • Generated gRPC client stubs for the Kessel Inventory API
  • A fluent ClientBuilder pattern for client construction
  • OAuth 2.0 Client Credentials authentication (via optional oauth4webapi dependency)
  • RBAC workspace REST helpers
  • Console identity header parsing and principal extraction
  • A promisifyClient utility that converts callback-based gRPC clients to Promise-based

Published as @project-kessel/kessel-sdk on npm. Requires Node >= 20.

Project Structure

src/
  kessel/
    auth/               # Hand-written. OAuth2 client credentials, OIDC discovery
      __tests__/
    console/            # Hand-written. Red Hat identity header parsing
      __tests__/
    grpc/               # Hand-written. gRPC call credentials helper
      __tests__/
    inventory/
      index.ts          # Hand-written. ClientBuilder base class, clientBuilderForStub()
      v1/               # Generated (health.ts) + hand-written (index.ts)
      v1beta1/          # Generated + hand-written (index.ts). Legacy, NOT in package exports
      v1beta2/          # Generated + hand-written (index.ts). Current primary API
        __tests__/
    rbac/
      v2.ts             # Hand-written. REST workspace helpers, resource/subject factories
      __tests__/
  google/               # Generated. google.protobuf and google.api types
  buf/validate/         # Generated. buf validation types
  promisify.ts          # Hand-written. Proxy-based gRPC client promisification
examples/               # Integration examples (NOT automated tests). Require live server
  builder/              # Examples using ClientBuilder + buildAsync()
  vanilla/              # Examples using raw gRPC stubs + callbacks
  rbac/                 # Examples for RBAC workspace operations
  console/              # Examples for console identity helpers
dist/                   # Build output (gitignored contents, committed structure)
  cjs/                  # CommonJS build
  esm/                  # ES module build
  types/                # TypeScript declarations
tsc-alias-replacers/    # ESM import path fixer for protobufjs/minimal

Generated vs Hand-Written Code

This is the most important architectural distinction in the repo.

Generated code (never hand-edit):

  • All .ts files in src/kessel/inventory/v1/, v1beta1/, v1beta2/ EXCEPT index.ts
  • All files in src/google/ and src/buf/
  • Identified by the header: // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
  • Contains /* eslint-disable */ at the top
  • Regenerated automatically every 6 hours via .github/workflows/buf-generate.yml from upstream protos at buf.build/project-kessel/inventory-api
  • To regenerate manually: buf generate && npm run prettier

ts-proto generation options (in buf.gen.yaml):

  • outputServices=grpc-js -- generates @grpc/grpc-js-compatible client/server stubs
  • outputServices=generic-definitions -- emits *Definition objects for generic service metadata
  • useOptionals=all -- every protobuf field becomes field?: Type | undefined
  • esModuleInterop=true, env=node, outputExtensions=true
  • Do not change these options without verifying that ClientBuilder, promisifyClient, and all examples still compile

Generated message shape: Every protobuf message type exports a TypeScript interface and a companion object with encode, decode, fromJSON, toJSON, create, and fromPartial methods, plus DeepPartial<T> and Exact<P, I> helper types (repeated per file -- this is normal for ts-proto). Generated enums use numeric values with an UNRECOGNIZED = -1 sentinel.

Hand-written code (where your changes go):

  • src/kessel/inventory/index.ts -- ClientBuilder abstract class and clientBuilderForStub() factory
  • src/kessel/inventory/*/index.ts -- Per-version wiring (imports generated client, exports ClientBuilder)
  • src/kessel/auth/index.ts -- OAuth2 client credentials
  • src/kessel/console/index.ts -- Red Hat identity header parsing
  • src/kessel/grpc/index.ts -- gRPC call credentials helper
  • src/kessel/rbac/v2.ts -- RBAC REST helpers
  • src/promisify.ts -- Client promisification utility
  • All __tests__/ files

Build System

  • npm run build -- Runs CJS, ESM, and type declaration builds in parallel (npm-run-all)
  • npm run build:cjs -- TypeScript to CommonJS, injects {"type":"commonjs"} into dist/cjs/package.json
  • npm run build:esm -- TypeScript to ESM, runs tsc-alias to fix import paths, injects {"type":"module"} into dist/esm/package.json
  • npm run build:types -- Declaration-only emit to dist/types/
  • npm test -- Jest with ts-jest
  • npm run lint -- ESLint with auto-fix (covers src and examples)
  • npm run lint:check -- ESLint without auto-fix (used in CI)
  • npm run prettier -- Prettier with auto-fix
  • npm run prettier:check -- Prettier without auto-fix (used in CI)

CI (.github/workflows/build.yml) runs on every push/PR to main, testing Node 20, 22, and 24. All four checks must pass: lint, prettier, build, test.

Dependabot (.github/dependabot.yml) checks both npm and GitHub Actions dependencies daily. Review and merge security updates promptly.

Naming Conventions

Files

  • Generated protobuf files use snake_case.ts matching the proto file name (e.g., check_request.ts, resource_reference.ts, inventory_service.ts)
  • Hand-written module entry points are always index.ts
  • Test files inside __tests__/ match the source file name (e.g., __tests__/index.ts tests ../index.ts, __tests__/v2.ts tests ../v2.ts)
  • No .test.ts suffix is used when tests are in __tests__/ directories

Variables and functions

  • camelCase for functions, methods, variables, and properties (e.g., buildAsync, oauth2CallCredentials, fetchOIDCDiscovery)
  • Exported factory functions use camelCase verbs (e.g., clientBuilderForStub, principalSubject, workspaceResource)
  • Constants use UPPER_SNAKE_CASE (e.g., EXPIRATION_WINDOW_MILLI, DEFAULT_PAGE_LIMIT, WORKSPACE_ENDPOINT)

Types and classes

  • PascalCase for interfaces, classes, types, and enums (e.g., ClientBuilder, OAuth2ClientCredentials, CheckRequest, SubjectReference)
  • Generated service clients follow Kessel*ServiceClient pattern (e.g., KesselInventoryServiceClient, KesselInventoryHealthServiceClient)
  • Generated enums use UPPER_SNAKE_CASE values with an UNRECOGNIZED = -1 sentinel

Exports

  • Per-version index.ts files export a single ClientBuilder const (v1, v1beta2) or multiple named builders (v1beta1)
  • The ClientBuilder export is the result of calling clientBuilderForStub(GeneratedServiceClient)

Code Style

TypeScript

  • Target: ES2020. Module: ES2022. Module resolution: node
  • strict: true but strictNullChecks: false -- protobuf fields are all optional (field?: Type | undefined), so null checks are disabled project-wide
  • Unused variables/args prefixed with _ are allowed (eslint rule argsIgnorePattern: "^_")
  • @typescript-eslint/no-explicit-any is error in source, off in test files
  • Prettier and ESLint are both enforced; run npm run prettier && npm run lint before committing

Imports

  • Source code uses relative imports (e.g., import { OAuth2ClientCredentials } from "../auth")
  • Examples and consumers use package subpath imports (e.g., import { ClientBuilder } from "@project-kessel/kessel-sdk/kessel/inventory/v1beta2")
  • Tests use relative imports from source, never package imports
  • There is no root/barrel export -- consumers must import from specific subpaths

Patterns

  • Builder pattern with fluent API for client construction (method chaining returns this)
  • Factory function pattern: clientBuilderForStub() creates concrete builder classes from generated stubs
  • Proxy-based promisification: promisifyClient() uses Proxy to lazily wrap callback methods
  • Optional dependency via dynamic import(): oauth4webapi is loaded only when auth is needed
  • Object.freeze() on returned token response objects (immutable by convention)
  • Async generator pattern for paginated streaming (see listWorkspaces in rbac/v2.ts)

Error Handling Conventions

These conventions apply across all modules. See individual GUIDELINES.md files for module-specific error details.

  • No custom error classes. The SDK uses plain Error and @grpc/grpc-js ServiceError. Do not define custom error types.
  • Let errors propagate. Do not catch errors inside SDK source to suppress them. Let them reach the caller.
  • Three distinct code spaces. Do not conflate gRPC ServiceError.code (gRPC status enum, e.g. 5 = NOT_FOUND), google.rpc.Status.code (in bulk response per-item errors), and HTTP status codes (in RBAC fetch responses). They are separate systems.
  • Bulk per-item errors. checkBulk, checkSelfBulk, and checkForUpdateBulk use a per-item error model. A successful RPC does not mean every item succeeded -- always check pair.error on each CheckBulkResponsePair. pair.item and pair.error are mutually exclusive.
  • ALLOWED_FALSE is not an error. It is a valid authorization response. ALLOWED_UNSPECIFIED (0) typically means the field was not set and should be handled defensively.

Consistency Modes

Every check and list request accepts an optional consistency field that trades latency for freshness:

Mode When to use Latency
minimizeLatency: true Read-heavy dashboards, UI filtering Lowest
atLeastAsFresh: { token } Read-after-write with a known token Medium
atLeastAsAcknowledged: true Must see all acknowledged writes Higher
Omitted Server default (typically minimizeLatency) Varies

For reportResource, writeVisibility controls the write side: MINIMIZE_LATENCY returns quickly (subsequent checks may not reflect the write), while IMMEDIATE waits for acknowledgment. For security-critical checks immediately after permission changes, use atLeastAsAcknowledged or atLeastAsFresh with a token from the write response.

Testing Conventions

Tests use Jest with ts-jest. See individual GUIDELINES.md files for module-specific test patterns.

Assertion patterns

  • toBe for primitives and identity checks; toEqual for deep object comparison
  • rejects.toThrow("message") for async error assertions; expect(() => ...).toThrow("message") for sync
  • Verify mock call counts explicitly: expect(mock).toHaveBeenCalledTimes(n)
  • Use toBeFalsy() (not toBe(false)) for checks that may return undefined or false

What not to test

  • Generated protobuf types (auto-generated by buf generate)
  • The promisify.ts utility internals (tested indirectly through ClientBuilder tests)
  • The dist/ output (tested through the CI build step)

Commit Message Conventions

The project uses conventional-ish commits but does not enforce them with tooling:

  • chore: bump version to X.Y.Z for version bumps
  • Descriptive imperative titles for feature/fix PRs (e.g., "Add example for running a check_bulk request")
  • Regenerate protobuf files from buf for automated protobuf PRs
  • Dependabot PRs use the default Bump <package> from X to Y format

PR Expectations

  • All CI checks must pass: lint, prettier, build, test (Node 20/22/24)
  • Do not edit generated files -- changes to protobuf types must go through the upstream proto definitions at buf.build/project-kessel/inventory-api
  • Review automated buf-generate PRs for unexpected changes to security-sensitive types (CheckRequest, Consistency, SubjectReference, etc.)
  • New public API surfaces (new version directories, new exported modules) require:
    1. A hand-written index.ts with ClientBuilder wiring
    2. Corresponding entries in package.json exports (require/import/types triple + wildcard)
    3. Tests in a colocated __tests__/ directory
  • Keep the runtime dependency count minimal (currently: @grpc/grpc-js required, oauth4webapi optional)

Maintaining Examples

See examples/GUIDELINES.md for full conventions on file naming, script structure, imports, environment variables, and npm script registration.

When public API surface is added or changed, update the examples/ directory so that examples stay accurate and demonstrate current usage. Add examples for new API operations, new subpath exports, and changed method signatures. Remove examples for removed API surface.

Common Pitfalls

  1. Editing generated files: Files with // Code generated by protoc-gen-ts_proto. DO NOT EDIT. are overwritten every 6 hours by CI. Your changes will be lost. Fix issues in the upstream .proto definitions instead.

  2. Forgetting package.json exports: Adding a new module under src/kessel/ without adding the corresponding exports entry in package.json means consumers cannot import it. Each public subpath needs require, import, and types conditions, plus a wildcard entry for deep imports.

  3. Importing from package root: There is no root export (@project-kessel/kessel-sdk). Always import from a specific subpath.

  4. Mixing auth with insecure channels: The builder throws "Invalid credential configuration: can not authenticate with insecure channel" if you combine call credentials with insecure(). This is validated at configuration time, not at call time.

  5. Creating multiple client instances: Each build()/buildAsync() call creates a new gRPC client with its own HTTP/2 channel. Create one client and reuse it.

  6. Assuming streaming methods are promisified: buildAsync() only promisifies unary methods. Streaming methods (streamedListObjects, streamedListSubjects) remain as-is because they already return AsyncIterable.

  7. Testing with package imports instead of relative imports: Tests must use relative imports from source (../index), not package subpath imports (@project-kessel/kessel-sdk/...). Package imports are only used in examples/.

  8. Forgetting strictNullChecks is off: The tsconfig has strictNullChecks: false. Code that works here may fail in consumer projects with stricter settings. Be aware when writing type signatures.

  9. Treating a successful bulk RPC as fully successful: Bulk endpoints use per-item errors. Always iterate response.pairs and check pair.error on each entry. See the Error Handling Conventions section.

  10. Not closing gRPC clients: @grpc/grpc-js clients hold open HTTP/2 connections. Call client.close() during graceful shutdown. Failing to close clients in short-lived processes (CLI tools, serverless functions) can cause the Node.js process to hang on exit.

  11. Needing gRPC channel options with ClientBuilder: ClientBuilder does not expose @grpc/grpc-js channel options (keepalive, message size). If you need these, use the vanilla constructor with ClientOptions as the third argument.

Architecture: How the Modules Connect

Consumer code
  |
  |-- imports from package subpaths (e.g., kessel/inventory/v1beta2)
  |
  v
index.ts (hand-written, per version)
  |-- calls clientBuilderForStub(GeneratedServiceClient) from inventory/index.ts
  |
  v
ClientBuilder (abstract, in inventory/index.ts)
  |-- .build()      --> raw gRPC client (generated stub)
  |-- .buildAsync() --> Proxy-wrapped client (promisify.ts)
  |-- .oauth2ClientAuthenticated() --> uses oauth2CallCredentials (grpc/index.ts)
  |                                        |
  |                                        v
  |                               OAuth2ClientCredentials (auth/index.ts)
  |                                   |-- dynamic import("oauth4webapi")
  |                                   |-- token caching + refresh
  |
  v
Generated gRPC stubs (protoc-gen-ts_proto output)
  |-- use @grpc/grpc-js for transport
  |-- use protobufjs/minimal for serialization

Console module (console/index.ts)
  |-- parses x-rh-identity header
  |-- extracts principal via principalSubject (rbac/v2.ts)

RBAC module (rbac/v2.ts) -- separate from the gRPC path
  |-- uses native fetch() for REST calls
  |-- uses AuthRequest interface for HTTP auth
  |-- uses generated protobuf types for request/response shapes
  |-- provides async generator for paginated streaming via gRPC

The key insight is that inventory/index.ts is the bridge layer: it provides the ClientBuilder base class and clientBuilderForStub() factory that connect hand-written auth/config logic to generated gRPC stubs. Each API version's index.ts simply calls this factory with its specific generated client class.