@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 +182 -19
- package/dist/dashboard-assets/assets/{index-DgywE2WQ.js → index-0ylAcvX3.js} +11 -11
- package/dist/dashboard-assets/index.html +1 -1
- package/dist/index.d.ts +62 -4
- package/dist/index.js +163 -12
- package/package.json +1 -1
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
|
|
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(
|
|
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,
|
|
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 (
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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();
|