@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
113 lines • 5.59 kB
JavaScript
/**
* Pick the effective {@link ActorResolver} for a turn: an agent's own `actorResolver` (from its
* {@link AgentDefinition}) wins over the module-global resolver, letting different agents read the
* caller from different places (e.g. one that reads the actor from the HTTP body). Falls back to
* `globalResolver` when the agent is unknown (`undefined` definition) or declares none — identical to
* the single-resolver behavior before per-agent overrides existed.
*/
export function resolveActorResolver(globalResolver, definition) {
return definition?.actorResolver ?? globalResolver;
}
/**
* The default {@link ActorResolver} the provider installs when `config/agent.ts` sets no
* `actorResolver`. It throws on every request rather than inventing an identity — security by
* default. Wire a real resolver (e.g. {@link AuthActorResolver}) or the opt-in
* {@link HeaderActorResolver} via `defineConfig({ actorResolver })`.
*/
export class UnconfiguredActorResolver {
resolve() {
throw new Error('No actorResolver configured. @adonis-agora/agent refuses to fabricate a caller identity. ' +
'Set defineConfig({ actorResolver }) in config/agent.ts with a resolver that reads your ' +
'authenticated principal (e.g. AuthActorResolver over ctx.auth.user), or the built-in ' +
'HeaderActorResolver behind a trusted gateway.');
}
}
/** Read a string header off an Adonis-ish `HttpContext` without trusting its concrete shape. */
function readHeader(ctx, name) {
const request = ctx.request;
const value = request?.header?.(name);
return typeof value === 'string' ? value : undefined;
}
/**
* A development / gateway {@link ActorResolver} that trusts request headers:
* `x-actor-id` (required), `x-actor-role` (comma-separated → `roles`), `x-tenant-ref`.
*
* **Security:** it throws when `x-actor-id` is absent — it never fabricates an identity and never
* grants a default role (an actor with no `x-actor-role` gets `roles: []`, i.e. no tools). Trusting
* client headers is only safe behind a gateway that strips and re-sets them from an authenticated
* principal. Real deployments should provide their own resolver (or {@link AuthActorResolver}) that
* reads a verified session/JWT instead.
*/
export class HeaderActorResolver {
resolve(ctx) {
const id = readHeader(ctx, 'x-actor-id');
if (id === undefined || id.length === 0) {
throw new Error('HeaderActorResolver: missing x-actor-id header. No default actor is fabricated. ' +
'Send x-actor-id from a trusted gateway, or configure defineConfig({ actorResolver }) ' +
'with a resolver that reads your authenticated principal.');
}
const roles = (readHeader(ctx, 'x-actor-role') ?? '')
.split(',')
.map((role) => role.trim())
.filter((role) => role.length > 0);
const tenantRef = readHeader(ctx, 'x-tenant-ref');
return {
id,
roles,
...(tenantRef !== undefined ? { tenantRef } : {}),
};
}
}
/**
* Reads Adonis's authenticated principal (`ctx.auth.user`, from `@adonisjs/auth`) into an
* {@link Actor}. The agent routes run no auth middleware, so when no guard has tried the request
* yet the resolver checks it itself — `ctx.auth.check()` (or `checkUsing(guards)`), which reads a
* session cookie or a bearer token and never throws for an anonymous request. Fail-closed: throws
* (the routes answer `401`) when no user is authenticated rather than fabricating one. Pass a
* `toActor` mapper when your user model doesn't expose `id`/`roles`/`tenantRef` directly (e.g. to
* pull roles from a relation). This is the resolver most apps wire in `config/agent.ts`.
*/
export class AuthActorResolver {
options;
constructor(options = {}) {
this.options = options;
}
async resolve(ctx) {
const auth = ctx?.auth;
let user = auth?.user;
if ((user === undefined || user === null) &&
auth !== undefined &&
!auth.authenticationAttempted) {
const guards = this.options.guards;
if (guards !== undefined && guards.length > 0 && typeof auth.checkUsing === 'function') {
await auth.checkUsing(guards);
}
else if (typeof auth.check === 'function') {
await auth.check();
}
user = auth.user;
}
if (user === undefined || user === null) {
throw new Error('AuthActorResolver: no authenticated user on ctx.auth.user. Install @adonisjs/auth (its ' +
'guard is checked on each request), or authenticate the request in a middleware before ' +
'it reaches the agent routes; no identity is fabricated.');
}
if (this.options.toActor !== undefined) {
return this.options.toActor(user);
}
const record = user;
if (record.id === undefined || record.id === null) {
throw new Error('AuthActorResolver: ctx.auth.user has no id; pass a toActor mapper.');
}
const roles = Array.isArray(record.roles)
? record.roles.filter((role) => typeof role === 'string')
: [];
const tenantRef = typeof record.tenantRef === 'string' ? record.tenantRef : undefined;
return {
id: String(record.id),
roles,
...(tenantRef !== undefined ? { tenantRef } : {}),
};
}
}
//# sourceMappingURL=actor-resolver.js.map