API / @aihu/agent-service

@aihu/agent-service

Agents & governance

Service-side agent runtime (server-hosted agent endpoints).

version
0.3.0
exports
38
values
5
types
33
01

createAgentService

functionagent
function createAgentService(options?: AgentServiceOptions): AgentService

Create an `AgentService`.

02

decideEmission

functionagent
function decideEmission( principal: Principal, query: EmissionQuery, deps?: EmissionDeps, ): EmissionDecision

Decide whether `principal` may receive the emission this query describes.

03

isScopeValue

functionagent
function isScopeValue(value: unknown): value is ExtractScopeValue

Is this axis value the `{ scope }` shape?

04

resolvePrincipal

functionagent
async function resolvePrincipal( source: PrincipalSource, deps?: PrincipalGateDeps, ): Promise<Principal>

Resolve THE principal for one request (spec §4.1 order): 1.

05

surfaceCallPolicy

functionagent
function surfaceCallPolicy(meta: AgentMetadata | undefined): ExtractCallValue

Read the `extract.call` policy off a compiled `AgentMetadata` entry (the agent-meta artifact of the Phase 1 fan-out; rides the spec-mandated open index signature).

06

AgentManifest

interfaceagent
interface AgentManifest {
  tools: AgentToolEntry[]
}

MCP multi-tool manifest aggregating all registered agents.

07

AgentService

interfaceagent
interface AgentService {
  /** Returns the aggregated manifest for all registered agents. */
  getManifest(): AgentManifest
  /**
   * Routes a tool call to the correct agent binding.
   * `toolName` format: `"<tag>/<actionName>"`.
   *
   * v0.3.0: full live-dispatch per RFC §5. Error ordering invariant:
   * 404 → 401 → 403 → 429 (Amendment 4 timing-channel invariant).
   *
   * The gate is ASYNC end-to-end (#420): scoped/rate-limited tools require a
   * signature-verified JWT (via `AuthPlugin.verify`, `crypto.subtle`-based),
   * and verification completes before any scope or rate-limit consult.
   *
   * @param toolName - `"<tag>/<actionName>"` or `"<tag>/<signalName>"`.
   * @param params - Action arguments or signal write value.
   * @param requestContext - Auth context. `jwt` is the credential and is
   *   required for scoped/rate-limited components; `userId` is a hint the
   *   gate never trusts (see {@link RequestContext}).
   */
  handleToolCall(
    toolName: string,
    params: unknown,
    requestContext?: RequestContext,
  ): Promise<unknown>
  /**
   * Run the security gate WITHOUT dispatching. Returns `{ authorized: true }`
   * when the call would be allowed, or the same `{ error, code, jsonrpc }`
   * rejection envelope `handleToolCall` would return (404/401/403/429). Shares
   * the single gate implementation with `handleToolCall`, so the error-ordering
   * invariant is identical. Used by `@aihu/agent-server`'s capability-bridge
   * path to gate on the server while delegating execution to the visible
   * browser instance.
   */
  authorize(toolName: string, params: unknown, requestContext?: RequestContext): Promise<unknown>
  /**
   * Returns a fetch-compatible middleware function.
   * Handles `POST /__aihu/tools/call` with `{ tool, params }` JSON body.
   * Returns `null` for non-matching requests (pass-through).
   */
  asMiddleware(): (req: Request) => Promise<Response | null>
}

The service handle returned by `createAgentService`.

08

AgentServiceOptions

interfaceagent
interface AgentServiceOptions {
  /**
   * Explicit list of agent metadata entries.
   * When omitted, reads the global registry via `getAgentMetadata`.
   */
  manifests?: AgentMetadata[]
  /**
   * Optional auth plugin. When absent and a component uses `$scope`,
   * `handleToolCall` returns 401 AUTH_MISSING (fail-closed, Amendment 2).
   */
  authPlugin?: AuthPlugin
  /**
   * Optional rate-limit plugin. Fail-closed, matching `authPlugin`: when a
   * component declares `$rate-limit` and this is ABSENT, the gate returns 429
   * `RATE_LIMIT_MISSING` rather than dispatching. A component that declares no
   * `$rate-limit` is unaffected and dispatches normally.
   *
   * "Optional" therefore means "un-rate-limited components need no plugin" — it
   * is NOT optional for a component that asks to be rate-limited.
   */
  rateLimitPlugin?: RateLimitPlugin
  /**
   * Getter for the `componentInstanceRegistry` from `@aihu/arbor/mount`.
   * Injected at construction time to avoid a circular import.
   * When absent, all `handleToolCall` calls return 404 (no live instance).
   *
   * The registry is consumed READ-ONLY here (only `.get(tag)` is called),
   * so the getter is typed as `ReadonlyMap` — a zero-cost type-level lock that
   * prevents external mutation of the live registry without snapshotting it.
   */
  getRegistry?: () => ReadonlyMap<string, LiveBinding[]>
  /**
   * Optional per-request auth resolver. When provided, `asMiddleware()` calls
   * it to build the `RequestContext` (userId + raw jwt) passed to
   * `handleToolCall`, enabling scoped tools over the bundled HTTP path. When
   * absent, `asMiddleware()` passes no context (fail-closed: scoped tools 401).
   * Injected to keep agent-service auth-library-agnostic and avoid a circular
   * `@aihu/auth` dep. The host wires `getAuthState` (`@aihu/auth/server`).
   */
  resolveAuth?: (req: Request) => RequestContext | Promise<RequestContext>
  /**
   * Optional auth-discovery URL (e.g. the deployment's
   * `/.well-known/oauth-protected-resource`) included as `authDiscoveryUrl`
   * in every 401 envelope the gate returns, so an agent that is refused
   * knows WHERE to obtain a credential. Purely informational — never a
   * policy input. When unset, 401 envelopes carry no discovery field.
   */
  authDiscoveryUrl?: string
  /**
   * GX Phase 4 (#466, spec §4.6): the live-entitlement registry handle —
   * pass the SAME `createGovernedRegistry()` instance (`@aihu/server`) the
   * server router was constructed with, so one scope has one live meaning on
   * both axes. When present, `runGate` gains one stage between the scope meet
   * and the rate limit: every scope in the met set is consulted through the
   * shared `check` (deny → 403 `ENTITLEMENT_DENIED`; resolver failure → 503
   * `ENTITLEMENT_UNAVAILABLE` + Retry-After — the §4.3 ladder in the tool
   * envelope). ABSENT ⇒ byte-identical behavior to today (the same injected
   * posture as `resolveAuth`: no ambient state, trivially testable).
   */
  entitlements?: EntitlementsHandle
}

Options for `createAgentService`.

09

AgentToolEntry

interfaceagent
interface AgentToolEntry {
  /** The custom-element tag name (tool identifier prefix). */
  name: string
  /** Custom-element tag, identical to `name`. */
  tag: string
  /** Input fields declared on the component. */
  inputs: Record<string, import('@aihu/agent').InputSchema>
  /** Callable actions declared on the component. */
  actions: Record<string, import('@aihu/agent').ActionSchema>
}

Per-tool entry in an `AgentManifest`.

10

AnonymousPrincipal

interfaceagent
interface AnonymousPrincipal {
  readonly class: 'anonymous'
  /** Declared-crawler tier from the shared bot registry, or null. */
  readonly uaTier: AnonymousUaTier | null
  /** Why no verified principal could be produced (the AUTH_* ladder input). */
  readonly credentialFailure: CredentialFailure
}

An unauthenticated requester — possibly a declared crawler.

11

AuthPlugin

interfaceagent
interface AuthPlugin {
  /**
   * Verify that the JWT carries the required scope claim.
   * Returns true if the claim is present, false otherwise.
   *
   * SECURITY (#420): the gate calls this only AFTER `verify` has confirmed
   * the token's signature, so implementations may decode without their own
   * signature check — the claims they read are already authenticated. This
   * method alone is NOT sufficient for a scoped tool: a plugin without
   * `verify` is treated as unable to verify and the gate fails closed (401).
   */
  checkScope(jwt: string, scope: string): boolean
  /**
   * Signature-verify `jwt` and return its claims, or `null` when the token
   * is malformed, forged, or otherwise unverifiable. Never throws.
   *
   * This is THE single verified-claims source for the gate (#420): the
   * rate-limit key derives from the returned `sub`, and the scope check runs
   * only against a token this method has authenticated. Async because
   * verification uses `crypto.subtle` (e.g. `@aihu/auth/server`'s HMAC path).
   *
   * OPTIONAL so existing third-party `AuthPlugin` implementations still
   * typecheck — but a plugin without it cannot serve scoped or rate-limited
   * tools: the gate FAILS CLOSED (401 `AUTH_UNVERIFIABLE`) rather than fall
   * back to caller-supplied identity.
   */
  verify?(jwt: string): Promise<VerifiedClaims | null>
}

Optional auth plugin injected into `createAgentService`.

12

EmissionDeps

interfaceagent
interface EmissionDeps {
  /**
   * Scope-membership predicate. The tool-call path injects
   * `(s) => authPlugin.checkScope(jwt, s) === true` so the refactor is
   * byte-for-byte the check the gate always ran (third-party `checkScope`
   * semantics preserved). Default: membership in `principal.scopes` — the
   * claims-derived set the emission taps use, where no raw JWT exists.
   */
  readonly hasScope?: ((scope: string) => boolean) | undefined
}
13

EntitlementMemo

interfaceagent
interface EntitlementMemo {
  /** @internal scope → in-flight/settled verdict for this request. */
  readonly verdicts: Map<string, Promise<EntitlementVerdict>>
}

The per-request memo (spec §4.4 layer 1, always on): a scope is resolved at most once per (request, scope) no matter how many surfaces/members consult it.

14

EntitlementsHandle

interfaceagent
interface EntitlementsHandle {
  /** One fresh per-request memo (spec §4.4 layer 1). */
  createMemo(): EntitlementMemo
  /**
   * THE single live check, both axes (spec §4.6). Memoized through `memo`
   * when supplied; TTL-cached per (scope, sub) when the registration
   * configured `cache:` (positive verdicts only — negatives are NEVER cached,
   * ratified Q2). A scope with no live resolver (unregistered or
   * `'token-only'`) returns `'granted'` — the step is a no-op and the static
   * meet's verdict stands.
   *
   * `url` is the request URL when the transport has one (threaded into
   * `EntitlementContext.request`); the call axis may omit it.
   */
  check(
    scope: string,
    principal: EntitledPrincipal,
    memo?: EntitlementMemo,
    url?: URL,
  ): Promise<EntitlementVerdict>
}

The structural handle `runGate` consults (spec §4.6).

15

ExtractScopeValue

interfaceagent
interface ExtractScopeValue {
  readonly scope: string
}

The `{ scope: 'x' }` value shape shared by both axes.

16

HumanSessionPrincipal

interfaceagent
interface HumanSessionPrincipal {
  readonly class: 'human-session'
  readonly sub: string
  readonly scopes: readonly string[]
}
17

LiveBinding

interfaceagent
interface LiveBinding {
  /** Stable root ID from the mount scope (spec §2.7 path prefix). */
  readonly rootId: number
  /** Custom-element tag name identifying the component type. */
  readonly tag: string
  /** Read the current value of a named signal (prop or computed). */
  getSignal(name: string): unknown
  /** Write a value to a named writable signal (prop only). */
  setSignal(name: string, value: unknown): void
  /** Invoke a named action with arguments, returning the result. */
  callAction(name: string, args: unknown[]): Promise<unknown>
  /** The `$scope` claim string, or null when absent. */
  scope(): string | null
  /** The `$rate-limit` spec string (e.g. `'100/min'`), or null when absent. */
  rateLimit(): string | null
  /** Disposal function: removes this binding from the registry. Returns true on success. */
  dispose$: () => boolean
}

A live binding for one mounted component instance.

18

PrincipalGateDeps

interfaceagent
interface PrincipalGateDeps {
  /** The verifying auth plugin (same instance the tool gate uses). */
  readonly authPlugin?: AuthPlugin | undefined
  /**
   * Anonymous-UA classifier — wire `classifyBotUserAgent` from
   * `@aihu/plugin-agent-readiness` (the single shared bot registry). Absent →
   * every anonymous principal is unclassified (`uaTier: null`), which is the
   * fail-open-for-HUMANS / decided-later posture: `read` compliance values
   * treat unclassified as an anonymous human, and nothing downstream acts on
   * `read` decisions in Phase 2 anyway.
   */
  readonly classifyUserAgent?: ((userAgent: string) => AnonymousUaTier | null) | undefined
}
19

PrincipalSource

interfaceagent
interface PrincipalSource {
  /** Raw JWT (with or without `Bearer ` prefix — the plugin's concern). */
  readonly jwt?: string | null | undefined
  /** `User-Agent` header, for anonymous crawler classification. */
  readonly userAgent?: string | null | undefined
  /**
   * A session the HOST already verified (e.g. `getAuthState` over the cookie
   * — which signature-verifies via the same `verifyJwt` primitive). `null`/
   * absent when there is no session. Never pass unverified cookie contents.
   */
  readonly session?: { readonly sub: string; readonly scopes: readonly string[] } | null | undefined
}

The credential material for one request, normalized.

20

RateLimitPlugin

interfaceagent
interface RateLimitPlugin {
  /**
   * Check and consume one unit of quota for `key`.
   * `rateSpec` is a string like `'100/min'`.
   * Returns false when quota is exhausted (caller should return 429).
   */
  checkRateLimit(rateSpec: string, key: string): boolean
}

Rate-limit store injected into `createAgentService`.

21

RequestContext

interfaceagent
interface RequestContext {
  /**
   * Caller-supplied identity hint. Retained for interface compatibility and
   * telemetry only — the gate ignores it for every policy decision. The
   * authoritative identity is the `sub` claim of the signature-verified `jwt`.
   */
  readonly userId: string | null | undefined
  /**
   * The raw JWT string. This is the sole credential: scoped and rate-limited
   * tools require it, and all claims (identity + scopes) are read from it
   * only after `AuthPlugin.verify` has checked its signature.
   */
  readonly jwt?: string | null
  /**
   * GX Phase 4 (#466, spec §4.4/§4.6): an OPTIONAL shared per-request
   * entitlement memo. A host serving one page request that both renders a
   * governed route and executes governed tool calls passes the same memo to
   * both transports so a scope is live-resolved at most once per request.
   * When absent, `runGate` creates a fresh memo per call. Never a policy
   * input — only a deduplication scope.
   */
  readonly entitlementMemo?: EntitlementMemo
}

Request context passed to `handleToolCall` for authorization and rate-limit checks.

22

ScopedAgentPrincipal

interfaceagent
interface ScopedAgentPrincipal {
  readonly class: 'scoped-agent'
  readonly sub: string
  readonly scopes: readonly string[]
  readonly claims: VerifiedClaims
}

A signature-verified agent principal whose claims carry ≥ 1 scope.

23

VerifiedAgentPrincipal

interfaceagent
interface VerifiedAgentPrincipal {
  readonly class: 'verified-agent'
  readonly sub: string
  readonly scopes: readonly string[]
  readonly claims: VerifiedClaims
}

A signature-verified agent principal (Bearer credential) with no scopes.

24

VerifiedClaims

interfaceagent
interface VerifiedClaims {
  /** Subject — the verified principal identity. */
  readonly sub?: string
  /** Space-separated scope string (RFC 6749 §3.3). */
  readonly scope?: string
  /** Array-form scopes (Auth0 convention). */
  readonly scp?: readonly string[]
  /** Array-form scopes (Okta convention). */
  readonly scopes?: readonly string[]
  readonly [key: string]: unknown
}

Claims decoded from a JWT whose signature HAS been verified.

25

ActionSchema

typeagent
type ActionSchema
26

AnonymousUaTier

typeagent
type AnonymousUaTier = 'searcher' | 'user-fetcher' | 'training-crawler'
27

CredentialFailure

typeagent
type CredentialFailure =
  /** No auth plugin is registered (→ AUTH_MISSING when a principal is required). */
  | 'no-auth-plugin'
  /** The registered plugin cannot signature-verify (no `verify`; → AUTH_UNVERIFIABLE). */
  | 'unverifiable-plugin'
  /** No credential was presented (→ AUTH_REQUIRED when a principal is required). */
  | 'no-credential'
  /** A credential was presented and `verify` rejected it (→ AUTH_INVALID). */
  | 'invalid-credential'
  /** The credential verified but carries no usable `sub` claim (→ AUTH_INVALID). */
  | 'no-subject'

Why this request resolved to anonymous rather than a verified principal.

28

EmissionDecision

typeagent
type EmissionDecision =
  | { readonly allow: true; readonly axis: 'call' | 'read'; readonly tier: EnforcementTier }
  | {
      readonly allow: false
      readonly axis: 'call' | 'read'
      readonly tier: EnforcementTier
      /**
       * 503 is produced ONLY by the GX Phase 4 live-entitlement stage
       * (`ENTITLEMENT_UNAVAILABLE`); `decideEmission` itself emits 401/403/404.
       */
      readonly code: 401 | 403 | 404 | 503
      readonly reason: EmissionDenyReason
      /** Human-readable message — for 401/403 this IS the envelope message. */
      readonly message: string
    }

The decision for one principal × one surface policy.

29

EmissionDenyReason

typeagent
type EmissionDenyReason =
  /** `call: 'none'` — the agent surface does not exist for anyone (404-shaped). */
  | 'SURFACE_UNAVAILABLE'
  /** The AUTH_* ladder (401): no plugin / unverifiable plugin / no / bad credential. */
  | 'AUTH_MISSING'
  | 'AUTH_UNVERIFIABLE'
  | 'AUTH_REQUIRED'
  | 'AUTH_INVALID'
  /** A required scope (surface or member) is not carried (403). */
  | 'SCOPE_DENIED'
  /** `read: 'human'` and the verified principal is an agent (403). */
  | 'HUMAN_ONLY'
  /** Compliance-tier: the declared crawler tier is refused by the `read` value (403). */
  | 'UA_REFUSED'
  /**
   * GX Phase 4 (#466, 70-governed-data-access §4.3/§5): the LIVE entitlement
   * stage. These two reasons are produced by the governed-boundary stage that
   * runs AFTER `decideEmission` — `decideEmission` itself NEVER returns them
   * (its contract stays sync/pure/static). Additive widening only.
   *
   * `ENTITLEMENT_DENIED` (403): the registered resolver answered `false` —
   * the principal's token carries the scope but the authority is not current.
   */
  | 'ENTITLEMENT_DENIED'
  /**
   * `ENTITLEMENT_UNAVAILABLE` (503 + Retry-After): the resolver threw or
   * exceeded its deadline. An outage is NEVER presented as a verdict — this is
   * fail-closed withholding, honestly labeled (spec §4.3).
   */
  | 'ENTITLEMENT_UNAVAILABLE'

Machine-readable refusal reasons.

30

EmissionQuery

typeagent
type EmissionQuery =
  | {
      readonly axis: 'call'
      /** The surface's `extract.call` value. */
      readonly value: ExtractCallValue
      /**
       * The member's own `$scope`, met with any surface `{ scope }` — BOTH
       * must pass (spec §3 R1). The surface value is a CEILING, never a
       * grant: it can only add requirements on top of the member's.
       */
      readonly memberScope?: string | null | undefined
      /** The member declares `$rate-limit` (forces a verified principal, as today). */
      readonly memberRateLimited?: boolean | undefined
    }
  | {
      readonly axis: 'read'
      /** The surface's `extract.read` value. */
      readonly value: ExtractReadValue
    }

One emission question: a surface's policy on ONE axis, plus (call axis) the member-level gates that MEET with it.

31

EnforcementTier

typeagent
type EnforcementTier = 'compliance' | 'hard'

Which enforcement tier a decision belongs to (spec §2.1 / §1).

32

EntitledPrincipal

typeagent
type EntitledPrincipal = Exclude<Principal, AnonymousPrincipal>

A principal that passed the static meet — the live resolver's contract assumes a verified principal and must never run for an anonymous request (spec §4.2: ordering static-first is both cheap and correct).

33

EntitlementVerdict

typeagent
type EntitlementVerdict = 'granted' | 'denied' | 'unavailable'

The three-way outcome of one live entitlement consult (spec §4.3): - `'granted'` — the resolver answered `true` (or the scope is registered `'token-only'` / carries no live resolver: step 3 is a no-op and the static meet's verdict stands).

34

ExtractCallValue

typeagent
type ExtractCallValue = 'none' | 'anonymous' | 'verified' | ExtractScopeValue

`call` (agent-callability) axis values — spec §2.1.

35

ExtractReadValue

typeagent
type ExtractReadValue =
  | 'all'
  | 'agents'
  | 'search'
  | 'none'
  | 'verified'
  | 'human'
  | ExtractScopeValue

`read` (crawl-visibility) axis values — spec §2.1.

36

InputSchema

typeagent
type InputSchema
37

Principal

typeagent
type Principal =
  | AnonymousPrincipal
  | VerifiedAgentPrincipal
  | ScopedAgentPrincipal
  | HumanSessionPrincipal

The one principal per request (spec §4.1).

38

PrincipalClass

typeagent
type PrincipalClass = Principal['class']

The four principal classes.