@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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="referrer" content="no-referrer" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<title>Helio Dashboard</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-0ylAcvX3.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-DZKoV0Vx.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/dist/index.d.ts
CHANGED
|
@@ -921,7 +921,7 @@ interface AuditQueryFilters {
|
|
|
921
921
|
}
|
|
922
922
|
/** Pagination options for list queries. */
|
|
923
923
|
interface AuditPaginationOptions {
|
|
924
|
-
/** Maximum number of records to return (default: 50, max:
|
|
924
|
+
/** Maximum number of records to return (default: 50, max: 1,000 — `LIST_MAX_PAGE_SIZE`). */
|
|
925
925
|
readonly limit?: number;
|
|
926
926
|
/** Number of records to skip (default: 0). */
|
|
927
927
|
readonly offset?: number;
|
|
@@ -946,7 +946,7 @@ interface AuditTimeBucket {
|
|
|
946
946
|
interface AuditAggregateStats {
|
|
947
947
|
/** Total number of records in the time range. */
|
|
948
948
|
readonly total: number;
|
|
949
|
-
/** Total records that resolved without a block (`block_reason IS NULL`). */
|
|
949
|
+
/** Total records that resolved without a block (`block_reason IS NULL`), excluding drift events. */
|
|
950
950
|
readonly allowed_total: number;
|
|
951
951
|
/** Total records that resolved with a block (`block_reason IS NOT NULL`). */
|
|
952
952
|
readonly blocked_total: number;
|
|
@@ -986,6 +986,17 @@ interface AuditStoreOptions {
|
|
|
986
986
|
readonly cleanupIntervalMs?: number;
|
|
987
987
|
}
|
|
988
988
|
|
|
989
|
+
/**
|
|
990
|
+
* Maximum records a single bulk export may return. Shared by the dashboard
|
|
991
|
+
* export route schema and the CLI export command so the advertised cap and
|
|
992
|
+
* the store's actual cap cannot diverge.
|
|
993
|
+
*/
|
|
994
|
+
declare const EXPORT_MAX_RECORDS = 10000;
|
|
995
|
+
/**
|
|
996
|
+
* Maximum page size for paginated `list()` reads. Shared with the dashboard
|
|
997
|
+
* audit route schema for the same reason as {@link EXPORT_MAX_RECORDS}.
|
|
998
|
+
*/
|
|
999
|
+
declare const LIST_MAX_PAGE_SIZE = 1000;
|
|
989
1000
|
/**
|
|
990
1001
|
* SQLite-backed audit record store.
|
|
991
1002
|
*
|
|
@@ -1032,8 +1043,16 @@ declare class AuditStore {
|
|
|
1032
1043
|
insertBatch(records: ReadonlyArray<Omit<AuditRecord, 'id' | 'created_at'>>, onError?: (record: Omit<AuditRecord, 'id' | 'created_at'>, err: unknown) => void, ids?: ReadonlyArray<string>, onPersist?: (record: Omit<AuditRecord, 'id' | 'created_at'>, id: string) => void): number;
|
|
1033
1044
|
/** Get a single record by ID, or undefined if not found. */
|
|
1034
1045
|
get(id: string): AuditRecord | undefined;
|
|
1035
|
-
/** Query records with filters and pagination. */
|
|
1046
|
+
/** Query records with filters and pagination. Capped at 1,000 per page. */
|
|
1036
1047
|
list(filters?: AuditQueryFilters, pagination?: AuditPaginationOptions): AuditListResult;
|
|
1048
|
+
/**
|
|
1049
|
+
* Query records for bulk export. Unlike `list()`, which enforces the
|
|
1050
|
+
* dashboard's 1,000-row page cap, this path allows up to
|
|
1051
|
+
* {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
|
|
1052
|
+
* (ascending `created_at`), so a capped export keeps the earliest records.
|
|
1053
|
+
*/
|
|
1054
|
+
listForExport(filters?: AuditQueryFilters, limit?: number): AuditListResult;
|
|
1055
|
+
private query;
|
|
1037
1056
|
/** Count records matching the given filters. */
|
|
1038
1057
|
count(filters?: AuditQueryFilters): number;
|
|
1039
1058
|
/** Get aggregate statistics for a time range. */
|
|
@@ -1962,6 +1981,17 @@ interface ServiceResult {
|
|
|
1962
1981
|
readonly status: number;
|
|
1963
1982
|
readonly body: Record<string, unknown>;
|
|
1964
1983
|
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Per-origin adapter liveness, wire-ready for the dashboard's
|
|
1986
|
+
* `GET /api/adapters` (issue #126). ISO-8601 timestamps; `adapter_version`
|
|
1987
|
+
* stays null until an /evaluate supplies one.
|
|
1988
|
+
*/
|
|
1989
|
+
interface AdapterLivenessEntry {
|
|
1990
|
+
readonly origin: string;
|
|
1991
|
+
readonly adapter_version: string | null;
|
|
1992
|
+
readonly first_seen: string;
|
|
1993
|
+
readonly last_seen: string;
|
|
1994
|
+
}
|
|
1965
1995
|
interface GovernanceServiceOptions {
|
|
1966
1996
|
readonly policy: CompiledPolicy;
|
|
1967
1997
|
readonly environment?: string;
|
|
@@ -2001,6 +2031,13 @@ declare class GovernanceService {
|
|
|
2001
2031
|
private readonly maxSenderKeys;
|
|
2002
2032
|
/** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
|
|
2003
2033
|
private readonly senderKeys;
|
|
2034
|
+
/**
|
|
2035
|
+
* Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
|
|
2036
|
+
* the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
|
|
2037
|
+
* other path updates existing entries and skips unknown origins, so the
|
|
2038
|
+
* registry shares the origin cap instead of adding a second growth vector.
|
|
2039
|
+
*/
|
|
2040
|
+
private readonly adapters;
|
|
2004
2041
|
private readonly pending;
|
|
2005
2042
|
private readonly tombstones;
|
|
2006
2043
|
private readonly caches;
|
|
@@ -2031,6 +2068,19 @@ declare class GovernanceService {
|
|
|
2031
2068
|
installScan(req: InstallScanInput): ServiceResult;
|
|
2032
2069
|
/** First-match-wins evaluation of the compiled install policy (issue #13). */
|
|
2033
2070
|
private evaluateInstall;
|
|
2071
|
+
/** Wire-ready liveness entries, most recently seen first. */
|
|
2072
|
+
listAdapters(): AdapterLivenessEntry[];
|
|
2073
|
+
/** Insert-or-refresh on the /evaluate path (the only insert site). */
|
|
2074
|
+
private recordAdapterSeen;
|
|
2075
|
+
/** Refresh-only for paths without an origin budget gate (install-scan, audit). */
|
|
2076
|
+
private touchAdapter;
|
|
2077
|
+
/**
|
|
2078
|
+
* Log a version sighting/change, capped per origin per boot. Both origin and
|
|
2079
|
+
* version are caller-controlled free text, so both are JSON-escaped — a
|
|
2080
|
+
* newline or control character must not be able to forge extra log lines
|
|
2081
|
+
* (the route's origin regex does not protect direct embedders).
|
|
2082
|
+
*/
|
|
2083
|
+
private logVersionEvent;
|
|
2034
2084
|
resolveApproval(ticketId: string, req: ResolveApprovalInput): ServiceResult;
|
|
2035
2085
|
sweep(): void;
|
|
2036
2086
|
/**
|
|
@@ -2338,6 +2388,14 @@ interface DashboardAppDeps {
|
|
|
2338
2388
|
readonly spendLimiter: SpendLimiter;
|
|
2339
2389
|
readonly evidenceStore: EvidenceStore;
|
|
2340
2390
|
readonly eventBus: DashboardEventBus;
|
|
2391
|
+
/**
|
|
2392
|
+
* Adapter liveness source for `GET /api/adapters` (issue #126) — a narrow
|
|
2393
|
+
* view of the SDK sideband's GovernanceService. Absent when the SDK
|
|
2394
|
+
* sideband is disabled; the endpoint then serves an empty list.
|
|
2395
|
+
*/
|
|
2396
|
+
readonly adapterLiveness?: {
|
|
2397
|
+
listAdapters(): AdapterLivenessEntry[];
|
|
2398
|
+
};
|
|
2341
2399
|
}
|
|
2342
2400
|
/** Options for the dashboard API. */
|
|
2343
2401
|
interface DashboardAppOptions {
|
|
@@ -2356,4 +2414,4 @@ interface DashboardAppOptions {
|
|
|
2356
2414
|
*/
|
|
2357
2415
|
declare function createDashboardApp(deps: DashboardAppDeps, options?: DashboardAppOptions): Hono;
|
|
2358
2416
|
|
|
2359
|
-
export { type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type CompilePoliciesResult, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
|
|
2417
|
+
export { type AdapterLivenessEntry, type ApprovalAppOptions, type ApprovalChannel, type ApprovalOutcome, ApprovalQueue, type ApprovalQueueOptions, ApprovalRouter, type ApprovalRouterOptions, type ApprovalStatus, type ApprovalTicket, type AuditAggregateStats, type AuditInput, type AuditListResult, type AuditPaginationOptions, type AuditQueryFilters, type AuditRecord, AuditStore, type AuditStoreOptions, type AuditTimeBucket, AuditWriter, type AuditWriterOptions, type CompilePoliciesResult, type CompiledPolicy, type CompiledPolicyRule, ConfigError, type CreateAppOptions, type DashboardAppDeps, type DashboardAppOptions, DashboardEventBus, type DashboardEventType, type DashboardEvents, EXPORT_MAX_RECORDS, type EvaluateInput, type EvidenceEntry, EvidenceStore, type EvidenceStoreOptions, GovernanceConfigError, GovernanceService, type GovernanceServiceOptions, GovernedForwarder, type GovernedForwarderOptions, type HelioConfig, type InstallScanInput, LIST_MAX_PAGE_SIZE, type MatchContext, type PolicyDecision, PolicyParseError, QueueChannel, type RateLimitCheckParams, type RateLimitKeyState, type RateLimitResult, RateLimiter, type RateLimiterOptions, type ResolveApprovalInput, type ServerHandle, type SessionState, type SlackActionAppOptions, SlackChannel, type SlackChannelOptions, type SpendLimitCheckParams, type SpendLimitKeyState, type SpendLimitResult, SpendLimiter, type SpendLimiterOptions, SseUpstreamForwarder, type SseUpstreamForwarderOptions, StdioForwarder, type StdioForwarderOptions, StreamableHttpForwarder, type StreamableHttpForwarderOptions, UpstreamForwarder, type UpstreamForwarderOptions, VERSION, WebhookChannel, type WebhookChannelOptions, type WireDecision, compilePolicies, createApp, createApprovalApp, createChannels, createDashboardApp, createSidebandApp, createSlackActionApp, evaluatePolicy, loadConfig, matchRule, startServer, startSidebandServer };
|
package/dist/index.js
CHANGED
|
@@ -3141,6 +3141,7 @@ var GovernedForwarder = class {
|
|
|
3141
3141
|
let result;
|
|
3142
3142
|
let approvalOutcome;
|
|
3143
3143
|
let approvalWaitMs = 0;
|
|
3144
|
+
let approvalContext;
|
|
3144
3145
|
let rateLimitResult;
|
|
3145
3146
|
let spendLimitResult;
|
|
3146
3147
|
let forwardingError;
|
|
@@ -3170,6 +3171,7 @@ var GovernedForwarder = class {
|
|
|
3170
3171
|
result = approvalResult.result;
|
|
3171
3172
|
approvalOutcome = approvalResult.outcome;
|
|
3172
3173
|
approvalWaitMs = approvalResult.approvalWaitMs;
|
|
3174
|
+
approvalContext = approvalResult.approvalContext;
|
|
3173
3175
|
}
|
|
3174
3176
|
} else if (decision.action === "rate_limit") {
|
|
3175
3177
|
if (!this.rateLimiter) {
|
|
@@ -3222,6 +3224,7 @@ var GovernedForwarder = class {
|
|
|
3222
3224
|
dependencyResult,
|
|
3223
3225
|
evidenceBlocked,
|
|
3224
3226
|
approvalOutcome,
|
|
3227
|
+
approvalContext,
|
|
3225
3228
|
rateLimitResult,
|
|
3226
3229
|
spendLimitResult,
|
|
3227
3230
|
isDryRun,
|
|
@@ -3243,6 +3246,16 @@ var GovernedForwarder = class {
|
|
|
3243
3246
|
request.signal
|
|
3244
3247
|
);
|
|
3245
3248
|
const approvalWaitMs = performance.now() - approvalStart;
|
|
3249
|
+
const ticket = outcome.ticketId ? router.getTicket(outcome.ticketId) : void 0;
|
|
3250
|
+
const denialReason = outcome.status === "denied" && outcome.reason ? outcome.reason : void 0;
|
|
3251
|
+
const approvalContext = outcome.ticketId && (denialReason || ticket?.escalated_at) ? {
|
|
3252
|
+
ticket_id: outcome.ticketId,
|
|
3253
|
+
...denialReason ? { denial_reason: denialReason } : {},
|
|
3254
|
+
...ticket?.escalated_at ? {
|
|
3255
|
+
escalated_at: ticket.escalated_at,
|
|
3256
|
+
escalated_to: [...ticket.escalated_to ?? []]
|
|
3257
|
+
} : {}
|
|
3258
|
+
} : void 0;
|
|
3246
3259
|
let result;
|
|
3247
3260
|
if (outcome.status === "approved" || outcome.status === "break_glass") {
|
|
3248
3261
|
if (request.signal?.aborted) {
|
|
@@ -3271,7 +3284,7 @@ var GovernedForwarder = class {
|
|
|
3271
3284
|
result = makeErrorResult(request, POLICY_DENIED, message, { ...feedback });
|
|
3272
3285
|
}
|
|
3273
3286
|
}
|
|
3274
|
-
return { result, outcome, approvalWaitMs };
|
|
3287
|
+
return { result, outcome, approvalWaitMs, approvalContext };
|
|
3275
3288
|
}
|
|
3276
3289
|
async handleRateLimit(request, decision, toolName) {
|
|
3277
3290
|
const limiter = this.rateLimiter;
|
|
@@ -3451,7 +3464,7 @@ var GovernedForwarder = class {
|
|
|
3451
3464
|
wasForwardedUpstream(decision, approvalOutcome, rateLimitResult, spendLimitResult) {
|
|
3452
3465
|
return decision.action === "allow" || approvalOutcome?.status === "approved" || approvalOutcome?.status === "break_glass" || approvalOutcome?.status === "timeout" && this.approvalRouter?.defaultOnTimeout === "allow" || rateLimitResult?.allowed === true || spendLimitResult?.allowed === true;
|
|
3453
3466
|
}
|
|
3454
|
-
writeAuditRecord(request, timestamp, toolName, toolArguments, decision, result, totalDurationMs, approvalWaitMs, flaggedDestructive, evidenceResult, dependencyResult, evidenceBlocked, approvalOutcome, rateLimitResult, spendLimitResult, isDryRun, forwardingError, drift) {
|
|
3467
|
+
writeAuditRecord(request, timestamp, toolName, toolArguments, decision, result, totalDurationMs, approvalWaitMs, flaggedDestructive, evidenceResult, dependencyResult, evidenceBlocked, approvalOutcome, approvalContext, rateLimitResult, spendLimitResult, isDryRun, forwardingError, drift) {
|
|
3455
3468
|
if (!this.auditWriter) return;
|
|
3456
3469
|
const wasForwarded = this.wasForwardedUpstream(
|
|
3457
3470
|
decision,
|
|
@@ -3486,6 +3499,12 @@ var GovernedForwarder = class {
|
|
|
3486
3499
|
}
|
|
3487
3500
|
};
|
|
3488
3501
|
}
|
|
3502
|
+
if (approvalContext) {
|
|
3503
|
+
evidenceChain = {
|
|
3504
|
+
...evidenceChain ?? {},
|
|
3505
|
+
approval: { ...approvalContext }
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3489
3508
|
if (rateLimitResult) {
|
|
3490
3509
|
evidenceChain = {
|
|
3491
3510
|
...evidenceChain ?? {},
|
|
@@ -4536,6 +4555,7 @@ var EvidenceStore = class _EvidenceStore {
|
|
|
4536
4555
|
// src/evidence/api.ts
|
|
4537
4556
|
import { Hono as Hono5 } from "hono";
|
|
4538
4557
|
import { bodyLimit } from "hono/body-limit";
|
|
4558
|
+
import { HTTPException } from "hono/http-exception";
|
|
4539
4559
|
import { z as z5 } from "zod";
|
|
4540
4560
|
|
|
4541
4561
|
// src/auth/bearer.ts
|
|
@@ -4736,6 +4756,11 @@ function createSidebandApp(store, options = {}) {
|
|
|
4736
4756
|
const app = new Hono5();
|
|
4737
4757
|
const sdkToken = options.token && options.token.length > 0 ? options.token : void 0;
|
|
4738
4758
|
const adapterToken = options.adapterToken && options.adapterToken.length > 0 ? options.adapterToken : void 0;
|
|
4759
|
+
app.onError((err, c) => {
|
|
4760
|
+
if (err instanceof HTTPException) return err.getResponse();
|
|
4761
|
+
console.error("[helio] Unhandled sideband API error:", err);
|
|
4762
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
4763
|
+
});
|
|
4739
4764
|
app.use("*", async (c, next) => {
|
|
4740
4765
|
const origin = c.req.header("origin");
|
|
4741
4766
|
if (origin) {
|
|
@@ -4848,6 +4873,7 @@ var MAX_PENDING_BYTES = 64 * 1024 * 1024;
|
|
|
4848
4873
|
var MAX_SENDER_KEYS = 5e4;
|
|
4849
4874
|
var MAX_EVIDENCE_ENTRIES = 16;
|
|
4850
4875
|
var MAX_EVIDENCE_BYTES = 64 * 1024;
|
|
4876
|
+
var MAX_VERSION_LOG_LINES_PER_ORIGIN = 5;
|
|
4851
4877
|
var SWEEP_INTERVAL_MS2 = 3e4;
|
|
4852
4878
|
var GovernanceService = class {
|
|
4853
4879
|
policy;
|
|
@@ -4865,6 +4891,13 @@ var GovernanceService = class {
|
|
|
4865
4891
|
maxSenderKeys;
|
|
4866
4892
|
/** Distinct sender_id limit keys with live state (reservation registry, issue #13). */
|
|
4867
4893
|
senderKeys = /* @__PURE__ */ new Set();
|
|
4894
|
+
/**
|
|
4895
|
+
* Per-origin adapter liveness (issue #126). New origins are inserted ONLY on
|
|
4896
|
+
* the /evaluate path, which sits behind the MAX_ORIGINS cache gate — every
|
|
4897
|
+
* other path updates existing entries and skips unknown origins, so the
|
|
4898
|
+
* registry shares the origin cap instead of adding a second growth vector.
|
|
4899
|
+
*/
|
|
4900
|
+
adapters = /* @__PURE__ */ new Map();
|
|
4868
4901
|
pending = /* @__PURE__ */ new Map();
|
|
4869
4902
|
tombstones = /* @__PURE__ */ new Map();
|
|
4870
4903
|
caches = /* @__PURE__ */ new Map();
|
|
@@ -4922,6 +4955,7 @@ var GovernanceService = class {
|
|
|
4922
4955
|
return { status: 503, body: { error: "evaluation_backlog_full" } };
|
|
4923
4956
|
}
|
|
4924
4957
|
const cache = this.cacheFor(req.origin);
|
|
4958
|
+
this.recordAdapterSeen(req.origin, req.adapter_version);
|
|
4925
4959
|
const toolName = req.tool.name;
|
|
4926
4960
|
const hasDefinition = definitionProvided(req.tool);
|
|
4927
4961
|
if (hasDefinition) {
|
|
@@ -5113,6 +5147,7 @@ var GovernanceService = class {
|
|
|
5113
5147
|
}
|
|
5114
5148
|
let approvalStatus = null;
|
|
5115
5149
|
let approvedBy = null;
|
|
5150
|
+
let approvalContext;
|
|
5116
5151
|
if (entry.approvalTicketId) {
|
|
5117
5152
|
const ticket = this.getTicketStatus(entry.approvalTicketId);
|
|
5118
5153
|
const status = ticket?.status;
|
|
@@ -5121,6 +5156,16 @@ var GovernanceService = class {
|
|
|
5121
5156
|
}
|
|
5122
5157
|
approvalStatus = status;
|
|
5123
5158
|
approvedBy = ticket.resolved_by ?? null;
|
|
5159
|
+
if (ticket.denial_reason || ticket.escalated_at) {
|
|
5160
|
+
approvalContext = {
|
|
5161
|
+
ticket_id: entry.approvalTicketId,
|
|
5162
|
+
...ticket.denial_reason ? { denial_reason: ticket.denial_reason } : {},
|
|
5163
|
+
...ticket.escalated_at ? {
|
|
5164
|
+
escalated_at: ticket.escalated_at,
|
|
5165
|
+
escalated_to: [...ticket.escalated_to ?? []]
|
|
5166
|
+
} : {}
|
|
5167
|
+
};
|
|
5168
|
+
}
|
|
5124
5169
|
}
|
|
5125
5170
|
if (req.actual_amount !== void 0) {
|
|
5126
5171
|
if (!Number.isFinite(req.actual_amount) || req.actual_amount < 0) {
|
|
@@ -5157,6 +5202,7 @@ var GovernanceService = class {
|
|
|
5157
5202
|
limitsChain,
|
|
5158
5203
|
approvalStatus,
|
|
5159
5204
|
approvedBy,
|
|
5205
|
+
approvalContext,
|
|
5160
5206
|
upstreamError: req.status === "error" ? req.error ?? "tool call failed" : null,
|
|
5161
5207
|
upstreamResponse: req.result ?? null,
|
|
5162
5208
|
upstreamLatencyMs: req.duration_ms ?? null
|
|
@@ -5168,6 +5214,7 @@ var GovernanceService = class {
|
|
|
5168
5214
|
finalizedBy: "audit",
|
|
5169
5215
|
expiresAtMs: this.now() + this.ttlMs
|
|
5170
5216
|
});
|
|
5217
|
+
this.touchAdapter(entry.origin);
|
|
5171
5218
|
const body = { ok: true, audit_record_id: auditId };
|
|
5172
5219
|
if (evidenceOutcomes) body["evidence"] = evidenceOutcomes;
|
|
5173
5220
|
return { status: 201, body };
|
|
@@ -5233,6 +5280,7 @@ var GovernanceService = class {
|
|
|
5233
5280
|
if (reserved) {
|
|
5234
5281
|
return { status: 400, body: { error: "reserved_metadata_key", key: reserved } };
|
|
5235
5282
|
}
|
|
5283
|
+
this.touchAdapter(req.origin);
|
|
5236
5284
|
const evaluationId = randomUUID2();
|
|
5237
5285
|
const toolName = `install:${req.package.source ?? "pkg"}:${req.package.name}`;
|
|
5238
5286
|
const verdict = this.evaluateInstall(req);
|
|
@@ -5297,6 +5345,65 @@ var GovernanceService = class {
|
|
|
5297
5345
|
};
|
|
5298
5346
|
}
|
|
5299
5347
|
// -------------------------------------------------------------------------
|
|
5348
|
+
// Adapter liveness registry (issue #126)
|
|
5349
|
+
// -------------------------------------------------------------------------
|
|
5350
|
+
/** Wire-ready liveness entries, most recently seen first. */
|
|
5351
|
+
listAdapters() {
|
|
5352
|
+
return [...this.adapters.entries()].sort(([oa, a], [ob, b]) => b.lastSeenMs - a.lastSeenMs || oa.localeCompare(ob)).map(([origin, state]) => ({
|
|
5353
|
+
origin,
|
|
5354
|
+
adapter_version: state.adapterVersion,
|
|
5355
|
+
first_seen: new Date(state.firstSeenMs).toISOString(),
|
|
5356
|
+
last_seen: new Date(state.lastSeenMs).toISOString()
|
|
5357
|
+
}));
|
|
5358
|
+
}
|
|
5359
|
+
/** Insert-or-refresh on the /evaluate path (the only insert site). */
|
|
5360
|
+
recordAdapterSeen(origin, version) {
|
|
5361
|
+
const normalized = version && version.length <= 64 ? version : void 0;
|
|
5362
|
+
const now = this.now();
|
|
5363
|
+
const existing = this.adapters.get(origin);
|
|
5364
|
+
if (!existing) {
|
|
5365
|
+
const state = {
|
|
5366
|
+
adapterVersion: normalized ?? null,
|
|
5367
|
+
firstSeenMs: now,
|
|
5368
|
+
lastSeenMs: now,
|
|
5369
|
+
versionLogCount: 0
|
|
5370
|
+
};
|
|
5371
|
+
this.adapters.set(origin, state);
|
|
5372
|
+
if (normalized !== void 0) this.logVersionEvent(origin, state, null, normalized);
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, now);
|
|
5376
|
+
if (normalized !== void 0 && normalized !== existing.adapterVersion) {
|
|
5377
|
+
this.logVersionEvent(origin, existing, existing.adapterVersion, normalized);
|
|
5378
|
+
existing.adapterVersion = normalized;
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
/** Refresh-only for paths without an origin budget gate (install-scan, audit). */
|
|
5382
|
+
touchAdapter(origin) {
|
|
5383
|
+
const existing = this.adapters.get(origin);
|
|
5384
|
+
if (!existing) return;
|
|
5385
|
+
existing.lastSeenMs = Math.max(existing.lastSeenMs, this.now());
|
|
5386
|
+
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Log a version sighting/change, capped per origin per boot. Both origin and
|
|
5389
|
+
* version are caller-controlled free text, so both are JSON-escaped — a
|
|
5390
|
+
* newline or control character must not be able to forge extra log lines
|
|
5391
|
+
* (the route's origin regex does not protect direct embedders).
|
|
5392
|
+
*/
|
|
5393
|
+
logVersionEvent(origin, state, from, to) {
|
|
5394
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) return;
|
|
5395
|
+
state.versionLogCount += 1;
|
|
5396
|
+
if (state.versionLogCount > MAX_VERSION_LOG_LINES_PER_ORIGIN) {
|
|
5397
|
+
console.error(
|
|
5398
|
+
`[helio] adapter origin ${JSON.stringify(origin)}: suppressing further version logs after ${String(MAX_VERSION_LOG_LINES_PER_ORIGIN)}`
|
|
5399
|
+
);
|
|
5400
|
+
return;
|
|
5401
|
+
}
|
|
5402
|
+
console.error(
|
|
5403
|
+
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)}`
|
|
5404
|
+
);
|
|
5405
|
+
}
|
|
5406
|
+
// -------------------------------------------------------------------------
|
|
5300
5407
|
// POST /approval/:id/resolve
|
|
5301
5408
|
// -------------------------------------------------------------------------
|
|
5302
5409
|
resolveApproval(ticketId, req) {
|
|
@@ -5398,6 +5505,7 @@ var GovernanceService = class {
|
|
|
5398
5505
|
this.tombstones.clear();
|
|
5399
5506
|
this.caches.clear();
|
|
5400
5507
|
this.senderKeys.clear();
|
|
5508
|
+
this.adapters.clear();
|
|
5401
5509
|
this.pendingBytes = 0;
|
|
5402
5510
|
}
|
|
5403
5511
|
// -------------------------------------------------------------------------
|
|
@@ -5562,6 +5670,9 @@ var GovernanceService = class {
|
|
|
5562
5670
|
if (args.sidebandUnreported) {
|
|
5563
5671
|
evidenceChain = { ...evidenceChain ?? {}, sideband: { unreported: true } };
|
|
5564
5672
|
}
|
|
5673
|
+
if (args.approvalContext) {
|
|
5674
|
+
evidenceChain = { ...evidenceChain ?? {}, approval: { ...args.approvalContext } };
|
|
5675
|
+
}
|
|
5565
5676
|
const record = {
|
|
5566
5677
|
timestamp: args.timestampIso,
|
|
5567
5678
|
session_id: args.sessionId,
|
|
@@ -5748,6 +5859,8 @@ function clampInt(value, fallback, min, max) {
|
|
|
5748
5859
|
|
|
5749
5860
|
// src/audit/store.ts
|
|
5750
5861
|
var DRIFT_EVENT_DECISIONS_SQL = "('tool_drift', 'tool_drift_reverted')";
|
|
5862
|
+
var EXPORT_MAX_RECORDS = 1e4;
|
|
5863
|
+
var LIST_MAX_PAGE_SIZE = 1e3;
|
|
5751
5864
|
var CREATE_TABLE_DDL = `
|
|
5752
5865
|
CREATE TABLE IF NOT EXISTS audit_records (
|
|
5753
5866
|
id TEXT PRIMARY KEY,
|
|
@@ -6051,15 +6164,28 @@ var AuditStore = class {
|
|
|
6051
6164
|
const row = this.db.prepare("SELECT * FROM audit_records WHERE id = ?").get(id);
|
|
6052
6165
|
return row ? deserializeRow(row) : void 0;
|
|
6053
6166
|
}
|
|
6054
|
-
/** Query records with filters and pagination. */
|
|
6167
|
+
/** Query records with filters and pagination. Capped at 1,000 per page. */
|
|
6055
6168
|
list(filters = {}, pagination = {}) {
|
|
6056
|
-
const
|
|
6057
|
-
const limit = clamp(pagination.limit ?? 50, 1, 1e3);
|
|
6169
|
+
const limit = clamp(pagination.limit ?? 50, 1, LIST_MAX_PAGE_SIZE);
|
|
6058
6170
|
const offset = Math.max(pagination.offset ?? 0, 0);
|
|
6059
6171
|
const order = pagination.order === "asc" ? "ASC" : "DESC";
|
|
6172
|
+
return this.query(filters, limit, offset, order);
|
|
6173
|
+
}
|
|
6174
|
+
/**
|
|
6175
|
+
* Query records for bulk export. Unlike `list()`, which enforces the
|
|
6176
|
+
* dashboard's 1,000-row page cap, this path allows up to
|
|
6177
|
+
* {@link EXPORT_MAX_RECORDS} in a single call. Always oldest-first
|
|
6178
|
+
* (ascending `created_at`), so a capped export keeps the earliest records.
|
|
6179
|
+
*/
|
|
6180
|
+
listForExport(filters = {}, limit) {
|
|
6181
|
+
const clamped = clamp(limit ?? EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS);
|
|
6182
|
+
return this.query(filters, clamped, 0, "ASC");
|
|
6183
|
+
}
|
|
6184
|
+
query(filters, limit, offset, order) {
|
|
6185
|
+
const { clause, params } = buildWhereClause(filters);
|
|
6060
6186
|
const { total } = this.db.prepare(`SELECT COUNT(*) as total FROM audit_records ${clause}`).get(...params);
|
|
6061
6187
|
const rows = this.db.prepare(
|
|
6062
|
-
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order} LIMIT ? OFFSET ?`
|
|
6188
|
+
`SELECT * FROM audit_records ${clause} ORDER BY created_at ${order}, rowid ${order} LIMIT ? OFFSET ?`
|
|
6063
6189
|
).all(...params, limit, offset);
|
|
6064
6190
|
return {
|
|
6065
6191
|
records: rows.map(deserializeRow),
|
|
@@ -7231,6 +7357,7 @@ import { readFileSync } from "fs";
|
|
|
7231
7357
|
import { join } from "path";
|
|
7232
7358
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
7233
7359
|
import { Hono as Hono8 } from "hono";
|
|
7360
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
7234
7361
|
import { z as z8 } from "zod";
|
|
7235
7362
|
import { cors } from "hono/cors";
|
|
7236
7363
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -7261,7 +7388,10 @@ var CSV_HEADERS = [
|
|
|
7261
7388
|
"dry_run",
|
|
7262
7389
|
"created_at",
|
|
7263
7390
|
"environment",
|
|
7264
|
-
"matched_rule_index"
|
|
7391
|
+
"matched_rule_index",
|
|
7392
|
+
"record_kind",
|
|
7393
|
+
"origin",
|
|
7394
|
+
"metadata"
|
|
7265
7395
|
];
|
|
7266
7396
|
var FORMULA_PREFIXES = /^[=+\-@\t\r]/;
|
|
7267
7397
|
function csvEscape(value) {
|
|
@@ -7407,7 +7537,7 @@ var feedQuerySchema = z8.object({
|
|
|
7407
7537
|
});
|
|
7408
7538
|
var auditExportQuerySchema = z8.object({
|
|
7409
7539
|
format: z8.preprocess((value) => value === "csv" ? "csv" : "json", z8.enum(["json", "csv"])),
|
|
7410
|
-
limit: clampedQueryInt(
|
|
7540
|
+
limit: clampedQueryInt(EXPORT_MAX_RECORDS, 1, EXPORT_MAX_RECORDS),
|
|
7411
7541
|
tool: optionalQueryString,
|
|
7412
7542
|
decision: optionalQueryString,
|
|
7413
7543
|
reason: optionalQueryString,
|
|
@@ -7425,7 +7555,7 @@ var auditExportQuerySchema = z8.object({
|
|
|
7425
7555
|
sender_id: optionalQueryString
|
|
7426
7556
|
});
|
|
7427
7557
|
var auditQuerySchema = z8.object({
|
|
7428
|
-
limit: clampedQueryInt(50, 1,
|
|
7558
|
+
limit: clampedQueryInt(50, 1, LIST_MAX_PAGE_SIZE),
|
|
7429
7559
|
offset: clampedQueryInt(0, 0, Number.MAX_SAFE_INTEGER),
|
|
7430
7560
|
tool: optionalQueryString,
|
|
7431
7561
|
decision: optionalQueryString,
|
|
@@ -7497,6 +7627,16 @@ function shouldSetSecureCookie(url, xForwardedProto) {
|
|
|
7497
7627
|
if (xForwardedProto?.toLowerCase() === "https") return true;
|
|
7498
7628
|
return new URL(url).protocol === "https:";
|
|
7499
7629
|
}
|
|
7630
|
+
function isPrivateIpv4(host) {
|
|
7631
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
7632
|
+
if (!match) return false;
|
|
7633
|
+
const a = Number(match[1]);
|
|
7634
|
+
const b = Number(match[2]);
|
|
7635
|
+
const c = Number(match[3]);
|
|
7636
|
+
const d = Number(match[4]);
|
|
7637
|
+
if (a > 255 || b > 255 || c > 255 || d > 255) return false;
|
|
7638
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
7639
|
+
}
|
|
7500
7640
|
function createDashboardAppWithLifecycle(deps, options) {
|
|
7501
7641
|
const {
|
|
7502
7642
|
auditStore,
|
|
@@ -7505,11 +7645,17 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7505
7645
|
rateLimiter,
|
|
7506
7646
|
spendLimiter,
|
|
7507
7647
|
evidenceStore,
|
|
7508
|
-
eventBus
|
|
7648
|
+
eventBus,
|
|
7649
|
+
adapterLiveness
|
|
7509
7650
|
} = deps;
|
|
7510
7651
|
const apiSecret = options?.apiSecret;
|
|
7511
7652
|
const sessionStore = apiSecret ? new DashboardSessionStore({ secret: apiSecret, ttlMs: SESSION_TTL_MS }) : void 0;
|
|
7512
7653
|
const app = new Hono8();
|
|
7654
|
+
app.onError((err, c) => {
|
|
7655
|
+
if (err instanceof HTTPException2) return err.getResponse();
|
|
7656
|
+
console.error("[helio] Unhandled dashboard API error:", err);
|
|
7657
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
7658
|
+
});
|
|
7513
7659
|
app.use(
|
|
7514
7660
|
"*",
|
|
7515
7661
|
cors({
|
|
@@ -7519,7 +7665,7 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7519
7665
|
const url = new URL(origin);
|
|
7520
7666
|
const h = url.hostname;
|
|
7521
7667
|
if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0") return origin;
|
|
7522
|
-
if (
|
|
7668
|
+
if (isPrivateIpv4(h)) return origin;
|
|
7523
7669
|
} catch {
|
|
7524
7670
|
}
|
|
7525
7671
|
return null;
|
|
@@ -7655,7 +7801,7 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7655
7801
|
channel_id: query.channel_id,
|
|
7656
7802
|
sender_id: query.sender_id
|
|
7657
7803
|
};
|
|
7658
|
-
const result = auditStore.
|
|
7804
|
+
const result = auditStore.listForExport(filters, limit);
|
|
7659
7805
|
if (format === "csv") {
|
|
7660
7806
|
const csv = recordsToCsv(result.records);
|
|
7661
7807
|
return new Response(csv, {
|
|
@@ -7719,6 +7865,9 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7719
7865
|
spend_limits: spendLimiter.listKeyStates()
|
|
7720
7866
|
});
|
|
7721
7867
|
});
|
|
7868
|
+
app.get("/api/adapters", (c) => {
|
|
7869
|
+
return c.json({ adapters: adapterLiveness?.listAdapters() ?? [] });
|
|
7870
|
+
});
|
|
7722
7871
|
app.get("/api/analytics", (c) => {
|
|
7723
7872
|
const query = analyticsQuerySchema.parse(c.req.query());
|
|
7724
7873
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -7889,10 +8038,12 @@ export {
|
|
|
7889
8038
|
AuditWriter,
|
|
7890
8039
|
ConfigError,
|
|
7891
8040
|
DashboardEventBus,
|
|
8041
|
+
EXPORT_MAX_RECORDS,
|
|
7892
8042
|
EvidenceStore,
|
|
7893
8043
|
GovernanceConfigError,
|
|
7894
8044
|
GovernanceService,
|
|
7895
8045
|
GovernedForwarder,
|
|
8046
|
+
LIST_MAX_PAGE_SIZE,
|
|
7896
8047
|
PolicyParseError,
|
|
7897
8048
|
QueueChannel,
|
|
7898
8049
|
RateLimiter,
|