@gethelio/proxy 0.7.0 → 0.9.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.
package/dist/cli.js CHANGED
@@ -3277,6 +3277,7 @@ var GovernedForwarder = class {
3277
3277
  let result;
3278
3278
  let approvalOutcome;
3279
3279
  let approvalWaitMs = 0;
3280
+ let approvalContext;
3280
3281
  let rateLimitResult;
3281
3282
  let spendLimitResult;
3282
3283
  let forwardingError;
@@ -3306,6 +3307,7 @@ var GovernedForwarder = class {
3306
3307
  result = approvalResult.result;
3307
3308
  approvalOutcome = approvalResult.outcome;
3308
3309
  approvalWaitMs = approvalResult.approvalWaitMs;
3310
+ approvalContext = approvalResult.approvalContext;
3309
3311
  }
3310
3312
  } else if (decision.action === "rate_limit") {
3311
3313
  if (!this.rateLimiter) {
@@ -3358,6 +3360,7 @@ var GovernedForwarder = class {
3358
3360
  dependencyResult,
3359
3361
  evidenceBlocked,
3360
3362
  approvalOutcome,
3363
+ approvalContext,
3361
3364
  rateLimitResult,
3362
3365
  spendLimitResult,
3363
3366
  isDryRun,
@@ -3379,6 +3382,16 @@ var GovernedForwarder = class {
3379
3382
  request.signal
3380
3383
  );
3381
3384
  const approvalWaitMs = performance.now() - approvalStart;
3385
+ const ticket = outcome.ticketId ? router.getTicket(outcome.ticketId) : void 0;
3386
+ const denialReason = outcome.status === "denied" && outcome.reason ? outcome.reason : void 0;
3387
+ const approvalContext = outcome.ticketId && (denialReason || ticket?.escalated_at) ? {
3388
+ ticket_id: outcome.ticketId,
3389
+ ...denialReason ? { denial_reason: denialReason } : {},
3390
+ ...ticket?.escalated_at ? {
3391
+ escalated_at: ticket.escalated_at,
3392
+ escalated_to: [...ticket.escalated_to ?? []]
3393
+ } : {}
3394
+ } : void 0;
3382
3395
  let result;
3383
3396
  if (outcome.status === "approved" || outcome.status === "break_glass") {
3384
3397
  if (request.signal?.aborted) {
@@ -3407,7 +3420,7 @@ var GovernedForwarder = class {
3407
3420
  result = makeErrorResult(request, POLICY_DENIED, message, { ...feedback });
3408
3421
  }
3409
3422
  }
3410
- return { result, outcome, approvalWaitMs };
3423
+ return { result, outcome, approvalWaitMs, approvalContext };
3411
3424
  }
3412
3425
  async handleRateLimit(request, decision, toolName) {
3413
3426
  const limiter = this.rateLimiter;
@@ -3587,7 +3600,7 @@ var GovernedForwarder = class {
3587
3600
  wasForwardedUpstream(decision, approvalOutcome, rateLimitResult, spendLimitResult) {
3588
3601
  return decision.action === "allow" || approvalOutcome?.status === "approved" || approvalOutcome?.status === "break_glass" || approvalOutcome?.status === "timeout" && this.approvalRouter?.defaultOnTimeout === "allow" || rateLimitResult?.allowed === true || spendLimitResult?.allowed === true;
3589
3602
  }
3590
- writeAuditRecord(request, timestamp, toolName, toolArguments, decision, result, totalDurationMs, approvalWaitMs, flaggedDestructive, evidenceResult, dependencyResult, evidenceBlocked, approvalOutcome, rateLimitResult, spendLimitResult, isDryRun, forwardingError, drift) {
3603
+ writeAuditRecord(request, timestamp, toolName, toolArguments, decision, result, totalDurationMs, approvalWaitMs, flaggedDestructive, evidenceResult, dependencyResult, evidenceBlocked, approvalOutcome, approvalContext, rateLimitResult, spendLimitResult, isDryRun, forwardingError, drift) {
3591
3604
  if (!this.auditWriter) return;
3592
3605
  const wasForwarded = this.wasForwardedUpstream(
3593
3606
  decision,
@@ -3622,6 +3635,12 @@ var GovernedForwarder = class {
3622
3635
  }
3623
3636
  };
3624
3637
  }
3638
+ if (approvalContext) {
3639
+ evidenceChain = {
3640
+ ...evidenceChain ?? {},
3641
+ approval: { ...approvalContext }
3642
+ };
3643
+ }
3625
3644
  if (rateLimitResult) {
3626
3645
  evidenceChain = {
3627
3646
  ...evidenceChain ?? {},
@@ -4445,6 +4464,8 @@ function clampInt(value, fallback, min, max) {
4445
4464
 
4446
4465
  // src/audit/store.ts
4447
4466
  var DRIFT_EVENT_DECISIONS_SQL = "('tool_drift', 'tool_drift_reverted')";
4467
+ var EXPORT_MAX_RECORDS = 1e4;
4468
+ var LIST_MAX_PAGE_SIZE = 1e3;
4448
4469
  var CREATE_TABLE_DDL = `
4449
4470
  CREATE TABLE IF NOT EXISTS audit_records (
4450
4471
  id TEXT PRIMARY KEY,
@@ -4748,15 +4769,28 @@ var AuditStore = class {
4748
4769
  const row = this.db.prepare("SELECT * FROM audit_records WHERE id = ?").get(id);
4749
4770
  return row ? deserializeRow(row) : void 0;
4750
4771
  }
4751
- /** Query records with filters and pagination. */
4772
+ /** Query records with filters and pagination. Capped at 1,000 per page. */
4752
4773
  list(filters = {}, pagination = {}) {
4753
- const { clause, params } = buildWhereClause(filters);
4754
- const limit = clamp(pagination.limit ?? 50, 1, 1e3);
4774
+ const limit = clamp(pagination.limit ?? 50, 1, LIST_MAX_PAGE_SIZE);
4755
4775
  const offset = Math.max(pagination.offset ?? 0, 0);
4756
4776
  const order = pagination.order === "asc" ? "ASC" : "DESC";
4777
+ return this.query(filters, limit, offset, order);
4778
+ }
4779
+ /**
4780
+ * Query records for bulk export. Unlike `list()`, which enforces the
4781
+ * dashboard's 1,000-row page cap, this path allows up to
4782
+ * {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
4783
+ * (ascending `created_at`), so a capped export keeps the earliest records.
4784
+ */
4785
+ listForExport(filters = {}, limit) {
4786
+ const clamped = clamp(limit ?? EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS);
4787
+ return this.query(filters, clamped, 0, "ASC");
4788
+ }
4789
+ query(filters, limit, offset, order) {
4790
+ const { clause, params } = buildWhereClause(filters);
4757
4791
  const { total } = this.db.prepare(`SELECT COUNT(*) as total FROM audit_records ${clause}`).get(...params);
4758
4792
  const rows = this.db.prepare(
4759
- `SELECT * FROM audit_records ${clause} ORDER BY created_at ${order} LIMIT ? OFFSET ?`
4793
+ `SELECT * FROM audit_records ${clause} ORDER BY created_at ${order}, rowid ${order} LIMIT ? OFFSET ?`
4760
4794
  ).all(...params, limit, offset);
4761
4795
  return {
4762
4796
  records: rows.map(deserializeRow),
@@ -5255,6 +5289,7 @@ var EvidenceStore = class _EvidenceStore {
5255
5289
  // src/evidence/api.ts
5256
5290
  import { Hono as Hono5 } from "hono";
5257
5291
  import { bodyLimit } from "hono/body-limit";
5292
+ import { HTTPException } from "hono/http-exception";
5258
5293
  import { z as z5 } from "zod";
5259
5294
 
5260
5295
  // src/auth/bearer.ts
@@ -5455,6 +5490,11 @@ function createSidebandApp(store, options = {}) {
5455
5490
  const app = new Hono5();
5456
5491
  const sdkToken = options.token && options.token.length > 0 ? options.token : void 0;
5457
5492
  const adapterToken = options.adapterToken && options.adapterToken.length > 0 ? options.adapterToken : void 0;
5493
+ app.onError((err, c) => {
5494
+ if (err instanceof HTTPException) return err.getResponse();
5495
+ console.error("[helio] Unhandled sideband API error:", err);
5496
+ return c.json({ error: "Internal server error" }, 500);
5497
+ });
5458
5498
  app.use("*", async (c, next) => {
5459
5499
  const origin = c.req.header("origin");
5460
5500
  if (origin) {
@@ -5567,6 +5607,7 @@ var MAX_PENDING_BYTES = 64 * 1024 * 1024;
5567
5607
  var MAX_SENDER_KEYS = 5e4;
5568
5608
  var MAX_EVIDENCE_ENTRIES = 16;
5569
5609
  var MAX_EVIDENCE_BYTES = 64 * 1024;
5610
+ var MAX_VERSION_LOG_LINES_PER_ORIGIN = 5;
5570
5611
  var SWEEP_INTERVAL_MS2 = 3e4;
5571
5612
  var GovernanceService = class {
5572
5613
  policy;
@@ -5584,6 +5625,13 @@ var GovernanceService = class {
5584
5625
  maxSenderKeys;
5585
5626
  /** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
5586
5627
  senderKeys = /* @__PURE__ */ new Set();
5628
+ /**
5629
+ * Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
5630
+ * the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
5631
+ * other path updates existing entries and skips unknown origins, so the
5632
+ * registry shares the origin cap instead of adding a second growth vector.
5633
+ */
5634
+ adapters = /* @__PURE__ */ new Map();
5587
5635
  pending = /* @__PURE__ */ new Map();
5588
5636
  tombstones = /* @__PURE__ */ new Map();
5589
5637
  caches = /* @__PURE__ */ new Map();
@@ -5641,6 +5689,7 @@ var GovernanceService = class {
5641
5689
  return { status: 503, body: { error: "evaluation_backlog_full" } };
5642
5690
  }
5643
5691
  const cache = this.cacheFor(req.origin);
5692
+ this.recordAdapterSeen(req.origin, req.adapter_version);
5644
5693
  const toolName = req.tool.name;
5645
5694
  const hasDefinition = definitionProvided(req.tool);
5646
5695
  if (hasDefinition) {
@@ -5832,6 +5881,7 @@ var GovernanceService = class {
5832
5881
  }
5833
5882
  let approvalStatus = null;
5834
5883
  let approvedBy = null;
5884
+ let approvalContext;
5835
5885
  if (entry.approvalTicketId) {
5836
5886
  const ticket = this.getTicketStatus(entry.approvalTicketId);
5837
5887
  const status = ticket?.status;
@@ -5840,6 +5890,16 @@ var GovernanceService = class {
5840
5890
  }
5841
5891
  approvalStatus = status;
5842
5892
  approvedBy = ticket.resolved_by ?? null;
5893
+ if (ticket.denial_reason || ticket.escalated_at) {
5894
+ approvalContext = {
5895
+ ticket_id: entry.approvalTicketId,
5896
+ ...ticket.denial_reason ? { denial_reason: ticket.denial_reason } : {},
5897
+ ...ticket.escalated_at ? {
5898
+ escalated_at: ticket.escalated_at,
5899
+ escalated_to: [...ticket.escalated_to ?? []]
5900
+ } : {}
5901
+ };
5902
+ }
5843
5903
  }
5844
5904
  if (req.actual_amount !== void 0) {
5845
5905
  if (!Number.isFinite(req.actual_amount) || req.actual_amount < 0) {
@@ -5876,6 +5936,7 @@ var GovernanceService = class {
5876
5936
  limitsChain,
5877
5937
  approvalStatus,
5878
5938
  approvedBy,
5939
+ approvalContext,
5879
5940
  upstreamError: req.status === "error" ? req.error ?? "tool call failed" : null,
5880
5941
  upstreamResponse: req.result ?? null,
5881
5942
  upstreamLatencyMs: req.duration_ms ?? null
@@ -5887,6 +5948,7 @@ var GovernanceService = class {
5887
5948
  finalizedBy: "audit",
5888
5949
  expiresAtMs: this.now() + this.ttlMs
5889
5950
  });
5951
+ this.touchAdapter(entry.origin);
5890
5952
  const body = { ok: true, audit_record_id: auditId };
5891
5953
  if (evidenceOutcomes) body["evidence"] = evidenceOutcomes;
5892
5954
  return { status: 201, body };
@@ -5952,6 +6014,7 @@ var GovernanceService = class {
5952
6014
  if (reserved) {
5953
6015
  return { status: 400, body: { error: "reserved_metadata_key", key: reserved } };
5954
6016
  }
6017
+ this.touchAdapter(req.origin);
5955
6018
  const evaluationId = randomUUID4();
5956
6019
  const toolName = `install:${req.package.source ?? "pkg"}:${req.package.name}`;
5957
6020
  const verdict = this.evaluateInstall(req);
@@ -6016,6 +6079,65 @@ var GovernanceService = class {
6016
6079
  };
6017
6080
  }
6018
6081
  // -------------------------------------------------------------------------
6082
+ // Adapter liveness registry (issue #126)
6083
+ // -------------------------------------------------------------------------
6084
+ /** Wire-ready liveness entries, most recently seen first. */
6085
+ listAdapters() {
6086
+ return [...this.adapters.entries()].sort(([oa, a], [ob, b]) => b.lastSeenMs - a.lastSeenMs || oa.localeCompare(ob)).map(([origin, state]) => ({
6087
+ origin,
6088
+ adapter_version: state.adapterVersion,
6089
+ first_seen: new Date(state.firstSeenMs).toISOString(),
6090
+ last_seen: new Date(state.lastSeenMs).toISOString()
6091
+ }));
6092
+ }
6093
+ /** Insert-or-refresh on the /evaluate path (the only insert site). */
6094
+ recordAdapterSeen(origin, version) {
6095
+ const normalized = version && version.length <= 64 ? version : void 0;
6096
+ const now = this.now();
6097
+ const existing = this.adapters.get(origin);
6098
+ if (!existing) {
6099
+ const state = {
6100
+ adapterVersion: normalized ?? null,
6101
+ firstSeenMs: now,
6102
+ lastSeenMs: now,
6103
+ versionLogCount: 0
6104
+ };
6105
+ this.adapters.set(origin, state);
6106
+ if (normalized !== void 0) this.logVersionEvent(origin, state, null, normalized);
6107
+ return;
6108
+ }
6109
+ existing.lastSeenMs = Math.max(existing.lastSeenMs, now);
6110
+ if (normalized !== void 0 && normalized !== existing.adapterVersion) {
6111
+ this.logVersionEvent(origin, existing, existing.adapterVersion, normalized);
6112
+ existing.adapterVersion = normalized;
6113
+ }
6114
+ }
6115
+ /** Refresh-only for paths without an origin budget gate (install-scan, audit). */
6116
+ touchAdapter(origin) {
6117
+ const existing = this.adapters.get(origin);
6118
+ if (!existing) return;
6119
+ existing.lastSeenMs = Math.max(existing.lastSeenMs, this.now());
6120
+ }
6121
+ /**
6122
+ * Log a version sighting/change, capped per origin per boot. Both origin and
6123
+ * version are caller-controlled free text, so both are JSON-escaped — a
6124
+ * newline or control character must not be able to forge extra log lines
6125
+ * (the route's origin regex does not protect direct embedders).
6126
+ */
6127
+ logVersionEvent(origin, state, from, to) {
6128
+ if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) return;
6129
+ state.versionLogCount += 1;
6130
+ if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) {
6131
+ console.error(
6132
+ `[helio] adapter origin ${JSON.stringify(origin)}: suppressing further version logs after ${String(MAX_VERSION_LOG_LINES_PER_ORIGIN)}`
6133
+ );
6134
+ return;
6135
+ }
6136
+ console.error(
6137
+ from === null ? `[helio] adapter origin ${JSON.stringify(origin)} reports version ${JSON.stringify(to)}` : `[helio] adapter origin ${JSON.stringify(origin)} version changed ${JSON.stringify(from)} -> ${JSON.stringify(to)}`
6138
+ );
6139
+ }
6140
+ // -------------------------------------------------------------------------
6019
6141
  // POST /approval/:id/resolve
6020
6142
  // -------------------------------------------------------------------------
6021
6143
  resolveApproval(ticketId, req) {
@@ -6117,6 +6239,7 @@ var GovernanceService = class {
6117
6239
  this.tombstones.clear();
6118
6240
  this.caches.clear();
6119
6241
  this.senderKeys.clear();
6242
+ this.adapters.clear();
6120
6243
  this.pendingBytes = 0;
6121
6244
  }
6122
6245
  // -------------------------------------------------------------------------
@@ -6281,6 +6404,9 @@ var GovernanceService = class {
6281
6404
  if (args.sidebandUnreported) {
6282
6405
  evidenceChain = { ...evidenceChain ?? {}, sideband: { unreported: true } };
6283
6406
  }
6407
+ if (args.approvalContext) {
6408
+ evidenceChain = { ...evidenceChain ?? {}, approval: { ...args.approvalContext } };
6409
+ }
6284
6410
  const record = {
6285
6411
  timestamp: args.timestampIso,
6286
6412
  session_id: args.sessionId,
@@ -7367,6 +7493,7 @@ import { readFileSync } from "fs";
7367
7493
  import { join } from "path";
7368
7494
  import { randomUUID as randomUUID6 } from "crypto";
7369
7495
  import { Hono as Hono8 } from "hono";
7496
+ import { HTTPException as HTTPException2 } from "hono/http-exception";
7370
7497
  import { z as z8 } from "zod";
7371
7498
  import { cors } from "hono/cors";
7372
7499
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -7397,7 +7524,10 @@ var CSV_HEADERS = [
7397
7524
  "dry_run",
7398
7525
  "created_at",
7399
7526
  "environment",
7400
- "matched_rule_index"
7527
+ "matched_rule_index",
7528
+ "record_kind",
7529
+ "origin",
7530
+ "metadata"
7401
7531
  ];
7402
7532
  var FORMULA_PREFIXES = /^[=+\-@\t\r]/;
7403
7533
  function csvEscape(value) {
@@ -7543,7 +7673,7 @@ var feedQuerySchema = z8.object({
7543
7673
  });
7544
7674
  var auditExportQuerySchema = z8.object({
7545
7675
  format: z8.preprocess((value) => value === "csv" ? "csv" : "json", z8.enum(["json", "csv"])),
7546
- limit: clampedQueryInt(1e4, 1, 1e4),
7676
+ limit: clampedQueryInt(EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS),
7547
7677
  tool: optionalQueryString,
7548
7678
  decision: optionalQueryString,
7549
7679
  reason: optionalQueryString,
@@ -7561,7 +7691,7 @@ var auditExportQuerySchema = z8.object({
7561
7691
  sender_id: optionalQueryString
7562
7692
  });
7563
7693
  var auditQuerySchema = z8.object({
7564
- limit: clampedQueryInt(50, 1, 1e3),
7694
+ limit: clampedQueryInt(50, 1, LIST_MAX_PAGE_SIZE),
7565
7695
  offset: clampedQueryInt(0, 0, Number.MAX_SAFE_INTEGER),
7566
7696
  tool: optionalQueryString,
7567
7697
  decision: optionalQueryString,
@@ -7633,6 +7763,16 @@ function shouldSetSecureCookie(url, xForwardedProto) {
7633
7763
  if (xForwardedProto?.toLowerCase() === "https") return true;
7634
7764
  return new URL(url).protocol === "https:";
7635
7765
  }
7766
+ function isPrivateIpv4(host) {
7767
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
7768
+ if (!match) return false;
7769
+ const a = Number(match[1]);
7770
+ const b = Number(match[2]);
7771
+ const c = Number(match[3]);
7772
+ const d = Number(match[4]);
7773
+ if (a > 255 || b > 255 || c > 255 || d > 255) return false;
7774
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
7775
+ }
7636
7776
  function createDashboardAppWithLifecycle(deps, options) {
7637
7777
  const {
7638
7778
  auditStore,
@@ -7641,11 +7781,17 @@ function createDashboardAppWithLifecycle(deps, options) {
7641
7781
  rateLimiter,
7642
7782
  spendLimiter,
7643
7783
  evidenceStore,
7644
- eventBus
7784
+ eventBus,
7785
+ adapterLiveness
7645
7786
  } = deps;
7646
7787
  const apiSecret = options?.apiSecret;
7647
7788
  const sessionStore = apiSecret ? new DashboardSessionStore({ secret: apiSecret, ttlMs: SESSION_TTL_MS }) : void 0;
7648
7789
  const app = new Hono8();
7790
+ app.onError((err, c) => {
7791
+ if (err instanceof HTTPException2) return err.getResponse();
7792
+ console.error("[helio] Unhandled dashboard API error:", err);
7793
+ return c.json({ error: "Internal server error" }, 500);
7794
+ });
7649
7795
  app.use(
7650
7796
  "*",
7651
7797
  cors({
@@ -7655,7 +7801,7 @@ function createDashboardAppWithLifecycle(deps, options) {
7655
7801
  const url = new URL(origin);
7656
7802
  const h = url.hostname;
7657
7803
  if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0") return origin;
7658
- if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(h)) return origin;
7804
+ if (isPrivateIpv4(h)) return origin;
7659
7805
  } catch {
7660
7806
  }
7661
7807
  return null;
@@ -7791,7 +7937,7 @@ function createDashboardAppWithLifecycle(deps, options) {
7791
7937
  channel_id: query.channel_id,
7792
7938
  sender_id: query.sender_id
7793
7939
  };
7794
- const result = auditStore.list(filters, { limit, order: "asc" });
7940
+ const result = auditStore.listForExport(filters, limit);
7795
7941
  if (format === "csv") {
7796
7942
  const csv = recordsToCsv(result.records);
7797
7943
  return new Response(csv, {
@@ -7855,6 +8001,9 @@ function createDashboardAppWithLifecycle(deps, options) {
7855
8001
  spend_limits: spendLimiter.listKeyStates()
7856
8002
  });
7857
8003
  });
8004
+ app.get("/api/adapters", (c) => {
8005
+ return c.json({ adapters: adapterLiveness?.listAdapters() ?? [] });
8006
+ });
7858
8007
  app.get("/api/analytics", (c) => {
7859
8008
  const query = analyticsQuerySchema.parse(c.req.query());
7860
8009
  const now = /* @__PURE__ */ new Date();
@@ -8385,6 +8534,7 @@ async function startCommand(configPath, options) {
8385
8534
  let sidebandToken;
8386
8535
  let sidebandTokenSource;
8387
8536
  let adapterToken;
8537
+ let adapterTokenSource;
8388
8538
  let governanceService;
8389
8539
  if (config.sdk.enabled) {
8390
8540
  sidebandToken = process.env["HELIO_SDK_TOKEN"];
@@ -8399,6 +8549,9 @@ async function startCommand(configPath, options) {
8399
8549
  if (!adapterToken || adapterToken.length === 0) {
8400
8550
  adapterToken = randomBytes2(32).toString("hex");
8401
8551
  process.env["HELIO_ADAPTER_TOKEN"] = adapterToken;
8552
+ adapterTokenSource = "generated";
8553
+ } else {
8554
+ adapterTokenSource = "env";
8402
8555
  }
8403
8556
  governanceService = new GovernanceService({
8404
8557
  policy,
@@ -8429,7 +8582,10 @@ async function startCommand(configPath, options) {
8429
8582
  rateLimiter,
8430
8583
  spendLimiter,
8431
8584
  evidenceStore,
8432
- eventBus
8585
+ eventBus,
8586
+ // Adapter liveness for GET /api/adapters (issue #126); undefined
8587
+ // unless the SDK sideband is enabled → endpoint serves an empty list.
8588
+ adapterLiveness: governanceService
8433
8589
  },
8434
8590
  {
8435
8591
  apiSecret: config.dashboard.api_secret,
@@ -8461,15 +8617,14 @@ async function startCommand(configPath, options) {
8461
8617
  if (sidebandHandle) {
8462
8618
  console.error(`SDK sideband listening on http://${config.sdk.host}:${String(config.sdk.port)}`);
8463
8619
  if (sidebandToken) {
8464
- const source = sidebandTokenSource === "env" ? "reusing HELIO_SDK_TOKEN from environment" : "generated per-boot HELIO_SDK_TOKEN";
8465
8620
  console.error(
8466
- `SDK token (${source}; pass as HELIO_SDK_TOKEN env var to your SDK clients):
8621
+ sidebandTokenSource === "env" ? "SDK token: reusing HELIO_SDK_TOKEN from environment (value not shown)" : `SDK token (generated per-boot HELIO_SDK_TOKEN; pass as HELIO_SDK_TOKEN env var to your SDK clients):
8467
8622
  ${sidebandToken}`
8468
8623
  );
8469
8624
  }
8470
8625
  if (adapterToken) {
8471
8626
  console.error(
8472
- `Adapter token (governance routes; pass as HELIO_ADAPTER_TOKEN to your adapter):
8627
+ adapterTokenSource === "env" ? "Adapter token: reusing HELIO_ADAPTER_TOKEN from environment (value not shown)" : `Adapter token (generated per-boot HELIO_ADAPTER_TOKEN; governance routes; pass as HELIO_ADAPTER_TOKEN to your adapter):
8473
8628
  ${adapterToken}`
8474
8629
  );
8475
8630
  }
@@ -8600,6 +8755,14 @@ async function validateCommand(configPath) {
8600
8755
  }
8601
8756
  }
8602
8757
  async function exportCommand(opts) {
8758
+ const parsedLimit = Number(opts.limit);
8759
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
8760
+ console.error(
8761
+ `Error: --limit must be an integer between 1 and ${String(EXPORT_MAX_RECORDS)} (got "${opts.limit}")`
8762
+ );
8763
+ process.exit(1);
8764
+ }
8765
+ const limit = Math.min(parsedLimit, EXPORT_MAX_RECORDS);
8603
8766
  let config;
8604
8767
  try {
8605
8768
  config = await loadConfig(opts.config);
@@ -8618,7 +8781,7 @@ async function exportCommand(opts) {
8618
8781
  // No cleanup timer for one-shot CLI
8619
8782
  });
8620
8783
  try {
8621
- const result = store.list(
8784
+ const result = store.listForExport(
8622
8785
  {
8623
8786
  tool_name: opts.tool,
8624
8787
  policy_decision: opts.decision,
@@ -8627,7 +8790,7 @@ async function exportCommand(opts) {
8627
8790
  from: opts.from,
8628
8791
  to: opts.to
8629
8792
  },
8630
- { limit: Number(opts.limit), order: "asc" }
8793
+ limit
8631
8794
  );
8632
8795
  if (opts.format === "csv") {
8633
8796
  writeCsv(result.records);
@@ -8699,5 +8862,5 @@ program.command("start").description("Load config and start the proxy server").o
8699
8862
  );
8700
8863
  program.command("init").description("Scaffold a helio.yaml config file with commented defaults").option("-o, --output <path>", "Output file path", DEFAULT_CONFIG_PATH).option("-f, --force", "Overwrite existing file", false).action((opts) => initCommand(opts.output, opts.force));
8701
8864
  program.command("validate").description("Validate a helio.yaml config file").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).action((opts) => validateCommand(opts.config));
8702
- program.command("export").description("Export audit records to JSON or CSV").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).option("-f, --format <format>", "Output format: json or csv", "json").option("--tool <name>", "Filter by tool name").option("--decision <decision>", "Filter by policy decision").option("--reason <reason>", "Filter by block reason").option("--session <id>", "Filter by session ID").option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max records to export", "1000").action((opts) => exportCommand(opts));
8865
+ program.command("export").description("Export audit records to JSON or CSV").option("-c, --config <path>", "Path to helio.yaml", DEFAULT_CONFIG_PATH).option("-f, --format <format>", "Output format: json or csv", "json").option("--tool <name>", "Filter by tool name").option("--decision <decision>", "Filter by policy decision").option("--reason <reason>", "Filter by block reason").option("--session <id>", "Filter by session ID").option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max records to export (up to 10000)", "1000").action((opts) => exportCommand(opts));
8703
8866
  program.parse();