@dudousxd/nestjs-agent-dashboard 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { ActorSpendRow, AgentGovernanceQueries, ActorDirectory, AgentPricingStore, GovernanceRange, ModelSpendRow, UsageTrendPoint, ThreadSpendRow, RunMetrics, RunAgentBreakdownRow, RunErrorBreakdownRow, RunTrendPoint, RecentRunRow, ToolCallActivityRow, ThreadActivityRow, CurrentModelPrice, ModelPriceInput } from '@dudousxd/nestjs-agent-core';
1
+ import { ActorSpendRow, AgentGovernanceQueries, ActorDirectory, AgentPricingStore, AgentApprovalPort, GovernanceRange, ModelSpendRow, UsageTrendPoint, ThreadSpendRow, RunMetrics, RunAgentBreakdownRow, RunErrorBreakdownRow, RunTrendPoint, RecentRunRow, ToolCallActivityRow, PendingApprovalRow, ToolStatRow, ThreadActivityRow, CurrentModelPrice, ActorResolver, ModelPriceInput } from '@dudousxd/nestjs-agent-core';
2
2
  export { ActorDirectory } from '@dudousxd/nestjs-agent-core';
3
3
  import { Observable } from 'rxjs';
4
4
  import { DynamicModule, Type, CanActivate, StreamableFile } from '@nestjs/common';
@@ -49,7 +49,8 @@ declare class DashboardService {
49
49
  private readonly queries;
50
50
  private readonly actorDirectory?;
51
51
  private readonly pricingStore?;
52
- constructor(queries: AgentGovernanceQueries, actorDirectory?: ActorDirectory | undefined, pricingStore?: AgentPricingStore | undefined);
52
+ private readonly approvalPort?;
53
+ constructor(queries: AgentGovernanceQueries, actorDirectory?: ActorDirectory | undefined, pricingStore?: AgentPricingStore | undefined, approvalPort?: AgentApprovalPort | undefined);
53
54
  /** Spend/usage overview for a day range: by-model + by-actor spend and the daily trend, in parallel. */
54
55
  spend(range: GovernanceRange): Promise<SpendOverview>;
55
56
  /** Top threads by cost for a day range (default 10, highest cost first). */
@@ -60,6 +61,19 @@ declare class DashboardService {
60
61
  recentRuns(limit: number): Promise<RecentRunRow[]>;
61
62
  /** Most recent tool calls (status/type/thread) for the Runs & tools activity feed. */
62
63
  recentToolCalls(limit: number): Promise<ToolCallActivityRow[]>;
64
+ /** Tool calls sitting `pending_approval`, oldest first, for the cross-thread approvals inbox. */
65
+ pendingApprovals(limit: number): Promise<PendingApprovalRow[]>;
66
+ /** Per-tool call/failure/rejection/latency rollup for a day range, for the Tools section. */
67
+ toolStats(range: GovernanceRange): Promise<ToolStatRow[]>;
68
+ /**
69
+ * Decide a pending HITL tool call from the console. Routes through the OPTIONAL
70
+ * {@link AGENT_APPROVAL_PORT} — bound by `@dudousxd/nestjs-agent` to the SAME signal path chat
71
+ * approvals use — so a 501 here (checked BEFORE body validation, same ordering as
72
+ * {@link upsertPrice}) means "no approval port bound", not "your body was invalid". `executedByRef`
73
+ * is an OPAQUE decider ref the caller resolved from the live request (see
74
+ * `AgentDashboardOptions.approvalActorRef`); omitted when the host didn't configure one.
75
+ */
76
+ decideApproval(toolCallId: string, body: unknown, executedByRef: string | undefined): Promise<void>;
63
77
  /** Most recent threads with rolled-up message/token counts. */
64
78
  recentThreads(limit: number): Promise<ThreadActivityRowWithLabel[]>;
65
79
  /**
@@ -92,7 +106,9 @@ declare class DashboardService {
92
106
  */
93
107
  declare class AgentApiController {
94
108
  private readonly dashboard;
95
- constructor(dashboard: DashboardService);
109
+ private readonly approvalActorRef;
110
+ private readonly actorResolver?;
111
+ constructor(dashboard: DashboardService, approvalActorRef: ((req: unknown) => string | undefined) | undefined, actorResolver?: ActorResolver | undefined);
96
112
  /** `{ byModel, byActor, trend }` for a day range (defaults to the last 30 days). */
97
113
  spend(from?: string, to?: string): Promise<SpendOverview>;
98
114
  /** Top threads by cost (default 10, max 200) for a day range (defaults to the last 30 days). */
@@ -103,6 +119,24 @@ declare class AgentApiController {
103
119
  runs(limit?: string): Promise<RecentRunRow[]>;
104
120
  /** Most recent tool calls (default 50, max 200) for the activity feed. */
105
121
  toolCalls(limit?: string): Promise<ToolCallActivityRow[]>;
122
+ /** Tool calls sitting `pending_approval` (default 50, max 200), oldest first — the approvals inbox. */
123
+ approvals(limit?: string): Promise<PendingApprovalRow[]>;
124
+ /**
125
+ * Decide a pending HITL tool call. Body `{ approved: boolean; reason?: string }`. 501s (via
126
+ * `DashboardService.decideApproval`) when no `AGENT_APPROVAL_PORT` is bound. `executedByRef`
127
+ * comes from {@link deciderRef} run against the live request.
128
+ */
129
+ decideApproval(toolCallId: string, body: unknown, req: unknown): Promise<void>;
130
+ /**
131
+ * WHO decided, as an opaque ref: an explicit `approvalActorRef` override wins outright (no
132
+ * resolver fallback, even when it returns `undefined`); otherwise the AgentModule-configured
133
+ * actor resolver — the same identity seam chat requests use. A resolver that throws is an
134
+ * unauthenticated/unreadable request, not an error to surface: the decision itself was already
135
+ * authorized by the dashboard's guards, so the ref is simply omitted.
136
+ */
137
+ private deciderRef;
138
+ /** Per-tool call/failure/rejection/latency rollup for a day range (defaults to the last 30 days). */
139
+ tools(from?: string, to?: string): Promise<ToolStatRow[]>;
106
140
  /** Most recent threads (default 50, max 200) with rolled-up counts. */
107
141
  threads(limit?: string): Promise<ThreadActivityRowWithLabel[]>;
108
142
  /**
@@ -153,7 +187,13 @@ interface AgentDashboardMountPathsOptions {
153
187
  */
154
188
  declare function agentDashboardMountPaths(options?: AgentDashboardMountPathsOptions): string[];
155
189
 
156
- interface AgentDashboardOptions {
190
+ /**
191
+ * `TReq` mirrors core's `ActorResolver<TReq>` convention: the transport request type
192
+ * {@link AgentDashboardOptions.approvalActorRef} receives. Defaults to `unknown` so untyped usage
193
+ * compiles unchanged; a host may narrow it (e.g. `forRoot<Request>({ approvalActorRef: ... })`)
194
+ * instead of writing its own `unknown`-narrowing type guard.
195
+ */
196
+ interface AgentDashboardOptions<TReq = unknown> {
157
197
  /**
158
198
  * Where the SPA (UI) is served. Default `/ai-gateway`. This is a page route — keep it out of an
159
199
  * `/api` prefix so it reads as a UI, not an endpoint.
@@ -182,6 +222,18 @@ interface AgentDashboardOptions {
182
222
  * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.
183
223
  */
184
224
  imports?: DynamicModule['imports'];
225
+ /**
226
+ * OVERRIDE for WHO is deciding a HITL approval — invoked on the incoming
227
+ * `POST <api>/approvals/:toolCallId` request to stamp `AgentApprovalPort`'s `opts.executedByRef`.
228
+ *
229
+ * DEFAULT (option omitted): the AgentModule-configured actor resolver (`AGENT_ACTOR_RESOLVER`) is
230
+ * consulted — the same identity seam chat requests use — and its actor id becomes the decider ref
231
+ * (a throwing resolver, i.e. an unauthenticated request, just omits the ref). Set this only when
232
+ * console auth differs from chat auth (e.g. the console sits behind a separate SSO whose principal
233
+ * the chat resolver can't read); returning `undefined` leaves `executedByRef` unset — the explicit
234
+ * override wins outright, with no resolver fallback.
235
+ */
236
+ approvalActorRef?: (req: TReq) => string | undefined;
185
237
  }
186
238
  /**
187
239
  * Holds the JSON API + SSE controller and its read service, mounted on its own path by `forRoot`.
@@ -192,9 +244,10 @@ interface AgentDashboardOptions {
192
244
  * the right `imports` to `forRoot`.
193
245
  */
194
246
  declare class AgentApiModule {
195
- static register(options: {
247
+ static register<TReq = unknown>(options: {
196
248
  imports?: DynamicModule['imports'];
197
249
  guards?: Type<CanActivate>[];
250
+ approvalActorRef?: (req: TReq) => string | undefined;
198
251
  }): DynamicModule;
199
252
  }
200
253
  /**
@@ -205,11 +258,31 @@ declare class AgentApiModule {
205
258
  * (global), which must provide `AGENT_GOVERNANCE_QUERIES` (bound by a store adapter). Front the
206
259
  * routes with the first-class `guards` option (plus `imports` for the guards' own dependencies) —
207
260
  * see {@link AgentDashboardOptions.guards}.
261
+ *
262
+ * Inertia hosts: the console is a full-page app, not an Inertia page. An in-app `<Link>` visit to
263
+ * `basePath` (an XHR carrying `X-Inertia`) is bounced with the protocol's own external-redirect
264
+ * mechanism — `409 Conflict` + `X-Inertia-Location: <the visited URL>` — so the Inertia client
265
+ * performs a full `window.location` load and the console renders normally. In-app links to the
266
+ * console therefore just work; no host-side special-casing needed.
208
267
  */
209
268
  declare class AgentDashboardModule {
210
- static forRoot(options?: AgentDashboardOptions): DynamicModule;
269
+ static forRoot<TReq = unknown>(options?: AgentDashboardOptions<TReq>): DynamicModule;
211
270
  }
212
271
 
272
+ /**
273
+ * The slice of the express request the Inertia bounce below reads — structural, so the package
274
+ * needs no express dependency (the API controller similarly takes `@Req() req: unknown`).
275
+ */
276
+ interface UiPageRequest {
277
+ headers: Record<string, string | string[] | undefined>;
278
+ /** Path + query as received (express `originalUrl`) — what the full-page visit must reload. */
279
+ originalUrl: string;
280
+ }
281
+ /** The slice of the express response the Inertia bounce writes (passthrough mode — Nest still sends). */
282
+ interface UiPageResponse {
283
+ status(code: number): unknown;
284
+ setHeader(name: string, value: string): unknown;
285
+ }
213
286
  /**
214
287
  * Serves the bundled AI-gateway console SPA at the configured base (+ hashed assets at
215
288
  * `<base>/assets`). The path comes from `RouterModule` (set by
@@ -220,7 +293,7 @@ declare class AgentUiController {
220
293
  private readonly apiBasePath;
221
294
  private readonly dir;
222
295
  constructor(basePath: string, apiBasePath: string);
223
- index(): string;
296
+ index(req: UiPageRequest, res: UiPageResponse): string;
224
297
  asset(file: string): StreamableFile;
225
298
  }
226
299
 
@@ -260,9 +333,33 @@ declare const AGENT_ACTOR_DIRECTORY: unique symbol;
260
333
  * tab/endpoints 501 with a clear message when nothing is bound.
261
334
  */
262
335
  declare const AGENT_PRICING_STORE: unique symbol;
336
+ /**
337
+ * Console-side HITL decisions (`AgentApprovalPort`), owned by `@dudousxd/nestjs-agent-core`.
338
+ * Re-declared here BY VALUE for the same reason as {@link AGENT_GOVERNANCE_QUERIES} above. Optional:
339
+ * bound by `@dudousxd/nestjs-agent` (which adapts it to the same signal path chat approvals use) —
340
+ * absent, `POST <api>/approvals/:toolCallId` 501s and the approvals inbox renders read-only.
341
+ */
342
+ declare const AGENT_APPROVAL_PORT: unique symbol;
343
+ /**
344
+ * The app's request-identity seam (`ActorResolver`), owned by `@dudousxd/nestjs-agent-core` and
345
+ * bound + exported by the (global) `AgentModule` from the host's `actorResolver` option. Re-declared
346
+ * here BY VALUE for the same reason as {@link AGENT_GOVERNANCE_QUERIES} above. Optional: the default
347
+ * decider attribution for `POST <api>/approvals/:toolCallId` — absent (or throwing, i.e.
348
+ * unauthenticated), `executedByRef` is simply omitted.
349
+ */
350
+ declare const AGENT_ACTOR_RESOLVER: unique symbol;
263
351
  /** DI token carrying the UI mount base (e.g. `/ai-gateway`). */
264
352
  declare const DASHBOARD_BASE_PATH: unique symbol;
265
353
  /** DI token carrying the JSON API base the SPA fetches from (e.g. `/ai-gateway/api`). */
266
354
  declare const DASHBOARD_API_PATH: unique symbol;
355
+ /**
356
+ * DI token carrying the host-provided {@link AgentDashboardOptions.approvalActorRef} extractor (or
357
+ * `undefined` when the host didn't set one — the controller then falls back to
358
+ * {@link AGENT_ACTOR_RESOLVER} for decider attribution). Threaded to `AgentApiModule` (where the API
359
+ * controller actually lives) same as the pattern above — `useValue` even when `undefined`, so
360
+ * injecting it needs no `@Optional()` (mirrors `AGENT_QUOTA_STORE`'s factory in
361
+ * `@dudousxd/nestjs-agent`).
362
+ */
363
+ declare const DASHBOARD_APPROVAL_ACTOR_REF: unique symbol;
267
364
 
268
- export { AGENT_ACTOR_DIRECTORY, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, type LiveAgentEvent, type ReliabilityOverview, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type WithActorLabel, agentDashboardMountPaths, parsePriceInput };
365
+ export { AGENT_ACTOR_DIRECTORY, AGENT_ACTOR_RESOLVER, AGENT_APPROVAL_PORT, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, DASHBOARD_API_PATH, DASHBOARD_APPROVAL_ACTOR_REF, DASHBOARD_BASE_PATH, DashboardService, type LiveAgentEvent, type ReliabilityOverview, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type WithActorLabel, agentDashboardMountPaths, parsePriceInput };
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/server/agent-api.controller.ts
5
- import { Body, Controller, Get, Post, Query, Sse } from "@nestjs/common";
5
+ import { Body, Controller, Get, HttpCode, Inject as Inject2, Optional as Optional2, Param, Post, Query, Req, Sse } from "@nestjs/common";
6
6
 
7
7
  // src/server/dashboard.service.ts
8
8
  import { subscribe, unsubscribe } from "node:diagnostics_channel";
@@ -10,27 +10,49 @@ import { channelName } from "@dudousxd/nestjs-diagnostics";
10
10
  import { Inject, Injectable, NotImplementedException, Optional } from "@nestjs/common";
11
11
  import { Observable as Observable2 } from "rxjs";
12
12
 
13
- // src/server/parse-price-input.ts
13
+ // src/server/parse-approval-decision.ts
14
14
  import { BadRequestException } from "@nestjs/common";
15
- function parsePriceInput(body) {
15
+ function parseApprovalDecision(body) {
16
16
  if (typeof body !== "object" || body === null) {
17
17
  throw new BadRequestException("Expected a JSON object body.");
18
18
  }
19
+ const { approved, reason } = body;
20
+ if (typeof approved !== "boolean") {
21
+ throw new BadRequestException('"approved" must be a boolean.');
22
+ }
23
+ if (reason !== void 0 && typeof reason !== "string") {
24
+ throw new BadRequestException('"reason" must be a string when present.');
25
+ }
26
+ return {
27
+ approved,
28
+ ...reason !== void 0 ? {
29
+ reason
30
+ } : {}
31
+ };
32
+ }
33
+ __name(parseApprovalDecision, "parseApprovalDecision");
34
+
35
+ // src/server/parse-price-input.ts
36
+ import { BadRequestException as BadRequestException2 } from "@nestjs/common";
37
+ function parsePriceInput(body) {
38
+ if (typeof body !== "object" || body === null) {
39
+ throw new BadRequestException2("Expected a JSON object body.");
40
+ }
19
41
  const { modelId, inputPricePer1m, outputPricePer1m, cacheWritePricePer1m, cacheReadPricePer1m } = body;
20
42
  if (typeof modelId !== "string" || modelId.trim().length === 0) {
21
- throw new BadRequestException('"modelId" must be a non-empty string.');
43
+ throw new BadRequestException2('"modelId" must be a non-empty string.');
22
44
  }
23
45
  if (!isFiniteNonNegative(inputPricePer1m)) {
24
- throw new BadRequestException('"inputPricePer1m" must be a non-negative number.');
46
+ throw new BadRequestException2('"inputPricePer1m" must be a non-negative number.');
25
47
  }
26
48
  if (!isFiniteNonNegative(outputPricePer1m)) {
27
- throw new BadRequestException('"outputPricePer1m" must be a non-negative number.');
49
+ throw new BadRequestException2('"outputPricePer1m" must be a non-negative number.');
28
50
  }
29
51
  if (cacheWritePricePer1m !== void 0 && !isFiniteNonNegative(cacheWritePricePer1m)) {
30
- throw new BadRequestException('"cacheWritePricePer1m" must be a non-negative number when present.');
52
+ throw new BadRequestException2('"cacheWritePricePer1m" must be a non-negative number when present.');
31
53
  }
32
54
  if (cacheReadPricePer1m !== void 0 && !isFiniteNonNegative(cacheReadPricePer1m)) {
33
- throw new BadRequestException('"cacheReadPricePer1m" must be a non-negative number when present.');
55
+ throw new BadRequestException2('"cacheReadPricePer1m" must be a non-negative number when present.');
34
56
  }
35
57
  return {
36
58
  modelId,
@@ -54,8 +76,11 @@ __name(isFiniteNonNegative, "isFiniteNonNegative");
54
76
  var AGENT_GOVERNANCE_QUERIES = Symbol.for("@dudousxd/nestjs-agent:governance-queries");
55
77
  var AGENT_ACTOR_DIRECTORY = Symbol.for("@dudousxd/nestjs-agent:actor-directory");
56
78
  var AGENT_PRICING_STORE = Symbol.for("@dudousxd/nestjs-agent:pricing-store");
79
+ var AGENT_APPROVAL_PORT = Symbol.for("@dudousxd/nestjs-agent:approval-port");
80
+ var AGENT_ACTOR_RESOLVER = Symbol.for("@dudousxd/nestjs-agent:actor-resolver");
57
81
  var DASHBOARD_BASE_PATH = Symbol.for("@dudousxd/nestjs-agent-dashboard:base-path");
58
82
  var DASHBOARD_API_PATH = Symbol.for("@dudousxd/nestjs-agent-dashboard:api-path");
83
+ var DASHBOARD_APPROVAL_ACTOR_REF = Symbol.for("@dudousxd/nestjs-agent-dashboard:approval-actor-ref");
59
84
 
60
85
  // src/server/dashboard.service.ts
61
86
  function _ts_decorate(decorators, target, key, desc) {
@@ -76,6 +101,7 @@ function _ts_param(paramIndex, decorator) {
76
101
  }
77
102
  __name(_ts_param, "_ts_param");
78
103
  var PRICING_STORE_UNBOUND_MESSAGE = "Pricing CRUD is unavailable: no AGENT_PRICING_STORE is bound. Bind a pricing store (e.g. MikroOrmPricingStore from @dudousxd/nestjs-agent-store-mikro-orm) to enable it.";
104
+ var APPROVAL_PORT_UNBOUND_MESSAGE = "Approve/reject is unavailable: no AGENT_APPROVAL_PORT is bound. Import AgentModule from @dudousxd/nestjs-agent alongside this dashboard to enable it \u2014 the approvals inbox stays read-only until then.";
79
105
  var AGENT_EVENTS = [
80
106
  "run.started",
81
107
  "message",
@@ -95,10 +121,12 @@ var DashboardService = class {
95
121
  queries;
96
122
  actorDirectory;
97
123
  pricingStore;
98
- constructor(queries, actorDirectory, pricingStore) {
124
+ approvalPort;
125
+ constructor(queries, actorDirectory, pricingStore, approvalPort) {
99
126
  this.queries = queries;
100
127
  this.actorDirectory = actorDirectory;
101
128
  this.pricingStore = pricingStore;
129
+ this.approvalPort = approvalPort;
102
130
  }
103
131
  /** Spend/usage overview for a day range: by-model + by-actor spend and the daily trend, in parallel. */
104
132
  async spend(range) {
@@ -142,6 +170,41 @@ var DashboardService = class {
142
170
  recentToolCalls(limit) {
143
171
  return this.queries.recentToolCalls(limit);
144
172
  }
173
+ /** Tool calls sitting `pending_approval`, oldest first, for the cross-thread approvals inbox. */
174
+ pendingApprovals(limit) {
175
+ return this.queries.pendingApprovals(limit);
176
+ }
177
+ /** Per-tool call/failure/rejection/latency rollup for a day range, for the Tools section. */
178
+ toolStats(range) {
179
+ return this.queries.toolStats(range);
180
+ }
181
+ /**
182
+ * Decide a pending HITL tool call from the console. Routes through the OPTIONAL
183
+ * {@link AGENT_APPROVAL_PORT} — bound by `@dudousxd/nestjs-agent` to the SAME signal path chat
184
+ * approvals use — so a 501 here (checked BEFORE body validation, same ordering as
185
+ * {@link upsertPrice}) means "no approval port bound", not "your body was invalid". `executedByRef`
186
+ * is an OPAQUE decider ref the caller resolved from the live request (see
187
+ * `AgentDashboardOptions.approvalActorRef`); omitted when the host didn't configure one.
188
+ */
189
+ async decideApproval(toolCallId, body, executedByRef) {
190
+ if (this.approvalPort === void 0) {
191
+ throw new NotImplementedException(APPROVAL_PORT_UNBOUND_MESSAGE);
192
+ }
193
+ const decision = parseApprovalDecision(body);
194
+ const opts = executedByRef !== void 0 ? {
195
+ executedByRef
196
+ } : {};
197
+ if (decision.approved) {
198
+ await this.approvalPort.approve(toolCallId, opts);
199
+ return;
200
+ }
201
+ await this.approvalPort.reject(toolCallId, {
202
+ ...opts,
203
+ ...decision.reason !== void 0 ? {
204
+ reason: decision.reason
205
+ } : {}
206
+ });
207
+ }
145
208
  /** Most recent threads with rolled-up message/token counts. */
146
209
  async recentThreads(limit) {
147
210
  const rows = await this.queries.recentThreads(limit);
@@ -227,11 +290,14 @@ DashboardService = _ts_decorate([
227
290
  _ts_param(1, Inject(AGENT_ACTOR_DIRECTORY)),
228
291
  _ts_param(2, Optional()),
229
292
  _ts_param(2, Inject(AGENT_PRICING_STORE)),
293
+ _ts_param(3, Optional()),
294
+ _ts_param(3, Inject(AGENT_APPROVAL_PORT)),
230
295
  _ts_metadata("design:type", Function),
231
296
  _ts_metadata("design:paramtypes", [
232
297
  typeof AgentGovernanceQueries === "undefined" ? Object : AgentGovernanceQueries,
233
298
  typeof ActorDirectory === "undefined" ? Object : ActorDirectory,
234
- typeof AgentPricingStore === "undefined" ? Object : AgentPricingStore
299
+ typeof AgentPricingStore === "undefined" ? Object : AgentPricingStore,
300
+ typeof AgentApprovalPort === "undefined" ? Object : AgentApprovalPort
235
301
  ])
236
302
  ], DashboardService);
237
303
 
@@ -281,8 +347,12 @@ var AgentApiController = class {
281
347
  __name(this, "AgentApiController");
282
348
  }
283
349
  dashboard;
284
- constructor(dashboard) {
350
+ approvalActorRef;
351
+ actorResolver;
352
+ constructor(dashboard, approvalActorRef, actorResolver) {
285
353
  this.dashboard = dashboard;
354
+ this.approvalActorRef = approvalActorRef;
355
+ this.actorResolver = actorResolver;
286
356
  }
287
357
  /** `{ byModel, byActor, trend }` for a day range (defaults to the last 30 days). */
288
358
  spend(from, to) {
@@ -304,6 +374,42 @@ var AgentApiController = class {
304
374
  toolCalls(limit) {
305
375
  return this.dashboard.recentToolCalls(parseLimit(limit, 50));
306
376
  }
377
+ /** Tool calls sitting `pending_approval` (default 50, max 200), oldest first — the approvals inbox. */
378
+ approvals(limit) {
379
+ return this.dashboard.pendingApprovals(parseLimit(limit, 50));
380
+ }
381
+ /**
382
+ * Decide a pending HITL tool call. Body `{ approved: boolean; reason?: string }`. 501s (via
383
+ * `DashboardService.decideApproval`) when no `AGENT_APPROVAL_PORT` is bound. `executedByRef`
384
+ * comes from {@link deciderRef} run against the live request.
385
+ */
386
+ async decideApproval(toolCallId, body, req) {
387
+ await this.dashboard.decideApproval(toolCallId, body, await this.deciderRef(req));
388
+ }
389
+ /**
390
+ * WHO decided, as an opaque ref: an explicit `approvalActorRef` override wins outright (no
391
+ * resolver fallback, even when it returns `undefined`); otherwise the AgentModule-configured
392
+ * actor resolver — the same identity seam chat requests use. A resolver that throws is an
393
+ * unauthenticated/unreadable request, not an error to surface: the decision itself was already
394
+ * authorized by the dashboard's guards, so the ref is simply omitted.
395
+ */
396
+ async deciderRef(req) {
397
+ if (this.approvalActorRef !== void 0) {
398
+ return this.approvalActorRef(req);
399
+ }
400
+ if (this.actorResolver === void 0) {
401
+ return void 0;
402
+ }
403
+ try {
404
+ return (await this.actorResolver.resolve(req)).id;
405
+ } catch {
406
+ return void 0;
407
+ }
408
+ }
409
+ /** Per-tool call/failure/rejection/latency rollup for a day range (defaults to the last 30 days). */
410
+ tools(from, to) {
411
+ return this.dashboard.toolStats(resolveRange(from, to));
412
+ }
307
413
  /** Most recent threads (default 50, max 200) with rolled-up counts. */
308
414
  threads(limit) {
309
415
  return this.dashboard.recentThreads(parseLimit(limit, 50));
@@ -381,6 +487,40 @@ _ts_decorate2([
381
487
  ]),
382
488
  _ts_metadata2("design:returntype", typeof Promise === "undefined" ? Object : Promise)
383
489
  ], AgentApiController.prototype, "toolCalls", null);
490
+ _ts_decorate2([
491
+ Get("approvals"),
492
+ _ts_param2(0, Query("limit")),
493
+ _ts_metadata2("design:type", Function),
494
+ _ts_metadata2("design:paramtypes", [
495
+ String
496
+ ]),
497
+ _ts_metadata2("design:returntype", typeof Promise === "undefined" ? Object : Promise)
498
+ ], AgentApiController.prototype, "approvals", null);
499
+ _ts_decorate2([
500
+ Post("approvals/:toolCallId"),
501
+ HttpCode(204),
502
+ _ts_param2(0, Param("toolCallId")),
503
+ _ts_param2(1, Body()),
504
+ _ts_param2(2, Req()),
505
+ _ts_metadata2("design:type", Function),
506
+ _ts_metadata2("design:paramtypes", [
507
+ String,
508
+ Object,
509
+ Object
510
+ ]),
511
+ _ts_metadata2("design:returntype", Promise)
512
+ ], AgentApiController.prototype, "decideApproval", null);
513
+ _ts_decorate2([
514
+ Get("tools"),
515
+ _ts_param2(0, Query("from")),
516
+ _ts_param2(1, Query("to")),
517
+ _ts_metadata2("design:type", Function),
518
+ _ts_metadata2("design:paramtypes", [
519
+ String,
520
+ String
521
+ ]),
522
+ _ts_metadata2("design:returntype", typeof Promise === "undefined" ? Object : Promise)
523
+ ], AgentApiController.prototype, "tools", null);
384
524
  _ts_decorate2([
385
525
  Get("threads"),
386
526
  _ts_param2(0, Query("limit")),
@@ -413,9 +553,14 @@ _ts_decorate2([
413
553
  ], AgentApiController.prototype, "stream", null);
414
554
  AgentApiController = _ts_decorate2([
415
555
  Controller(),
556
+ _ts_param2(1, Inject2(DASHBOARD_APPROVAL_ACTOR_REF)),
557
+ _ts_param2(2, Optional2()),
558
+ _ts_param2(2, Inject2(AGENT_ACTOR_RESOLVER)),
416
559
  _ts_metadata2("design:type", Function),
417
560
  _ts_metadata2("design:paramtypes", [
418
- typeof DashboardService === "undefined" ? Object : DashboardService
561
+ typeof DashboardService === "undefined" ? Object : DashboardService,
562
+ Object,
563
+ typeof ActorResolver === "undefined" ? Object : ActorResolver
419
564
  ])
420
565
  ], AgentApiController);
421
566
 
@@ -453,7 +598,7 @@ import { RouterModule } from "@nestjs/core";
453
598
  import { existsSync, readFileSync } from "node:fs";
454
599
  import { basename, extname, join, resolve, sep } from "node:path";
455
600
  import { fileURLToPath } from "node:url";
456
- import { Controller as Controller2, Get as Get2, Header, Inject as Inject2, NotFoundException, Param, StreamableFile } from "@nestjs/common";
601
+ import { Controller as Controller2, Get as Get2, Header, Inject as Inject3, NotFoundException, Param as Param2, Req as Req2, Res, StreamableFile } from "@nestjs/common";
457
602
  function _ts_decorate3(decorators, target, key, desc) {
458
603
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
459
604
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -498,7 +643,12 @@ var AgentUiController = class {
498
643
  }
499
644
  // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the classic
500
645
  // "stuck loading after a deploy"). The hashed assets below are immutable.
501
- index() {
646
+ index(req, res) {
647
+ if (req.headers["x-inertia"] !== void 0) {
648
+ res.status(409);
649
+ res.setHeader("X-Inertia-Location", req.originalUrl);
650
+ return "";
651
+ }
502
652
  const indexPath = join(this.dir, "index.html");
503
653
  if (!existsSync(indexPath)) {
504
654
  throw new NotFoundException("Dashboard is not built. Run the package build.");
@@ -525,14 +675,21 @@ _ts_decorate3([
525
675
  Get2(),
526
676
  Header("Content-Type", "text/html; charset=utf-8"),
527
677
  Header("Cache-Control", "no-store, must-revalidate"),
678
+ _ts_param3(0, Req2()),
679
+ _ts_param3(1, Res({
680
+ passthrough: true
681
+ })),
528
682
  _ts_metadata3("design:type", Function),
529
- _ts_metadata3("design:paramtypes", []),
683
+ _ts_metadata3("design:paramtypes", [
684
+ typeof UiPageRequest === "undefined" ? Object : UiPageRequest,
685
+ typeof UiPageResponse === "undefined" ? Object : UiPageResponse
686
+ ]),
530
687
  _ts_metadata3("design:returntype", String)
531
688
  ], AgentUiController.prototype, "index", null);
532
689
  _ts_decorate3([
533
690
  Get2("assets/:file"),
534
691
  Header("Cache-Control", "public, max-age=31536000, immutable"),
535
- _ts_param3(0, Param("file")),
692
+ _ts_param3(0, Param2("file")),
536
693
  _ts_metadata3("design:type", Function),
537
694
  _ts_metadata3("design:paramtypes", [
538
695
  String
@@ -541,8 +698,8 @@ _ts_decorate3([
541
698
  ], AgentUiController.prototype, "asset", null);
542
699
  AgentUiController = _ts_decorate3([
543
700
  Controller2(),
544
- _ts_param3(0, Inject2(DASHBOARD_BASE_PATH)),
545
- _ts_param3(1, Inject2(DASHBOARD_API_PATH)),
701
+ _ts_param3(0, Inject3(DASHBOARD_BASE_PATH)),
702
+ _ts_param3(1, Inject3(DASHBOARD_API_PATH)),
546
703
  _ts_metadata3("design:type", Function),
547
704
  _ts_metadata3("design:paramtypes", [
548
705
  String,
@@ -584,7 +741,13 @@ var AgentApiModule = class _AgentApiModule {
584
741
  ],
585
742
  providers: [
586
743
  DashboardService,
587
- ...options.guards ?? []
744
+ ...options.guards ?? [],
745
+ // `useValue` even when `options.approvalActorRef` is `undefined` — AgentApiController
746
+ // injects this WITHOUT `@Optional()` (same pattern as `AGENT_QUOTA_STORE`'s factory).
747
+ {
748
+ provide: DASHBOARD_APPROVAL_ACTOR_REF,
749
+ useValue: options.approvalActorRef
750
+ }
588
751
  ],
589
752
  exports: [
590
753
  DashboardService
@@ -616,6 +779,9 @@ var AgentDashboardModule = class _AgentDashboardModule {
616
779
  } : {},
617
780
  ...options.guards ? {
618
781
  guards: options.guards
782
+ } : {},
783
+ ...options.approvalActorRef ? {
784
+ approvalActorRef: options.approvalActorRef
619
785
  } : {}
620
786
  }),
621
787
  RouterModule.register([
@@ -656,6 +822,8 @@ AgentDashboardModule = _ts_decorate4([
656
822
  ], AgentDashboardModule);
657
823
  export {
658
824
  AGENT_ACTOR_DIRECTORY,
825
+ AGENT_ACTOR_RESOLVER,
826
+ AGENT_APPROVAL_PORT,
659
827
  AGENT_GOVERNANCE_QUERIES,
660
828
  AGENT_PRICING_STORE,
661
829
  AgentApiController,
@@ -663,6 +831,7 @@ export {
663
831
  AgentDashboardModule,
664
832
  AgentUiController,
665
833
  DASHBOARD_API_PATH,
834
+ DASHBOARD_APPROVAL_ACTOR_REF,
666
835
  DASHBOARD_BASE_PATH,
667
836
  DashboardService,
668
837
  agentDashboardMountPaths,