@gethelio/proxy 0.6.0 → 0.8.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 ?? {},
|
|
@@ -5255,6 +5274,7 @@ var EvidenceStore = class _EvidenceStore {
|
|
|
5255
5274
|
// src/evidence/api.ts
|
|
5256
5275
|
import { Hono as Hono5 } from "hono";
|
|
5257
5276
|
import { bodyLimit } from "hono/body-limit";
|
|
5277
|
+
import { HTTPException } from "hono/http-exception";
|
|
5258
5278
|
import { z as z5 } from "zod";
|
|
5259
5279
|
|
|
5260
5280
|
// src/auth/bearer.ts
|
|
@@ -5455,6 +5475,11 @@ function createSidebandApp(store, options = {}) {
|
|
|
5455
5475
|
const app = new Hono5();
|
|
5456
5476
|
const sdkToken = options.token && options.token.length > 0 ? options.token : void 0;
|
|
5457
5477
|
const adapterToken = options.adapterToken && options.adapterToken.length > 0 ? options.adapterToken : void 0;
|
|
5478
|
+
app.onError((err, c) => {
|
|
5479
|
+
if (err instanceof HTTPException) return err.getResponse();
|
|
5480
|
+
console.error("[helio] Unhandled sideband API error:", err);
|
|
5481
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
5482
|
+
});
|
|
5458
5483
|
app.use("*", async (c, next) => {
|
|
5459
5484
|
const origin = c.req.header("origin");
|
|
5460
5485
|
if (origin) {
|
|
@@ -5832,6 +5857,7 @@ var GovernanceService = class {
|
|
|
5832
5857
|
}
|
|
5833
5858
|
let approvalStatus = null;
|
|
5834
5859
|
let approvedBy = null;
|
|
5860
|
+
let approvalContext;
|
|
5835
5861
|
if (entry.approvalTicketId) {
|
|
5836
5862
|
const ticket = this.getTicketStatus(entry.approvalTicketId);
|
|
5837
5863
|
const status = ticket?.status;
|
|
@@ -5840,6 +5866,16 @@ var GovernanceService = class {
|
|
|
5840
5866
|
}
|
|
5841
5867
|
approvalStatus = status;
|
|
5842
5868
|
approvedBy = ticket.resolved_by ?? null;
|
|
5869
|
+
if (ticket.denial_reason || ticket.escalated_at) {
|
|
5870
|
+
approvalContext = {
|
|
5871
|
+
ticket_id: entry.approvalTicketId,
|
|
5872
|
+
...ticket.denial_reason ? { denial_reason: ticket.denial_reason } : {},
|
|
5873
|
+
...ticket.escalated_at ? {
|
|
5874
|
+
escalated_at: ticket.escalated_at,
|
|
5875
|
+
escalated_to: [...ticket.escalated_to ?? []]
|
|
5876
|
+
} : {}
|
|
5877
|
+
};
|
|
5878
|
+
}
|
|
5843
5879
|
}
|
|
5844
5880
|
if (req.actual_amount !== void 0) {
|
|
5845
5881
|
if (!Number.isFinite(req.actual_amount) || req.actual_amount < 0) {
|
|
@@ -5876,6 +5912,7 @@ var GovernanceService = class {
|
|
|
5876
5912
|
limitsChain,
|
|
5877
5913
|
approvalStatus,
|
|
5878
5914
|
approvedBy,
|
|
5915
|
+
approvalContext,
|
|
5879
5916
|
upstreamError: req.status === "error" ? req.error ?? "tool call failed" : null,
|
|
5880
5917
|
upstreamResponse: req.result ?? null,
|
|
5881
5918
|
upstreamLatencyMs: req.duration_ms ?? null
|
|
@@ -6281,6 +6318,9 @@ var GovernanceService = class {
|
|
|
6281
6318
|
if (args.sidebandUnreported) {
|
|
6282
6319
|
evidenceChain = { ...evidenceChain ?? {}, sideband: { unreported: true } };
|
|
6283
6320
|
}
|
|
6321
|
+
if (args.approvalContext) {
|
|
6322
|
+
evidenceChain = { ...evidenceChain ?? {}, approval: { ...args.approvalContext } };
|
|
6323
|
+
}
|
|
6284
6324
|
const record = {
|
|
6285
6325
|
timestamp: args.timestampIso,
|
|
6286
6326
|
session_id: args.sessionId,
|
|
@@ -7367,6 +7407,7 @@ import { readFileSync } from "fs";
|
|
|
7367
7407
|
import { join } from "path";
|
|
7368
7408
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
7369
7409
|
import { Hono as Hono8 } from "hono";
|
|
7410
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
7370
7411
|
import { z as z8 } from "zod";
|
|
7371
7412
|
import { cors } from "hono/cors";
|
|
7372
7413
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -7633,6 +7674,16 @@ function shouldSetSecureCookie(url, xForwardedProto) {
|
|
|
7633
7674
|
if (xForwardedProto?.toLowerCase() === "https") return true;
|
|
7634
7675
|
return new URL(url).protocol === "https:";
|
|
7635
7676
|
}
|
|
7677
|
+
function isPrivateIpv4(host) {
|
|
7678
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
7679
|
+
if (!match) return false;
|
|
7680
|
+
const a = Number(match[1]);
|
|
7681
|
+
const b = Number(match[2]);
|
|
7682
|
+
const c = Number(match[3]);
|
|
7683
|
+
const d = Number(match[4]);
|
|
7684
|
+
if (a > 255 || b > 255 || c > 255 || d > 255) return false;
|
|
7685
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
7686
|
+
}
|
|
7636
7687
|
function createDashboardAppWithLifecycle(deps, options) {
|
|
7637
7688
|
const {
|
|
7638
7689
|
auditStore,
|
|
@@ -7646,6 +7697,11 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7646
7697
|
const apiSecret = options?.apiSecret;
|
|
7647
7698
|
const sessionStore = apiSecret ? new DashboardSessionStore({ secret: apiSecret, ttlMs: SESSION_TTL_MS }) : void 0;
|
|
7648
7699
|
const app = new Hono8();
|
|
7700
|
+
app.onError((err, c) => {
|
|
7701
|
+
if (err instanceof HTTPException2) return err.getResponse();
|
|
7702
|
+
console.error("[helio] Unhandled dashboard API error:", err);
|
|
7703
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
7704
|
+
});
|
|
7649
7705
|
app.use(
|
|
7650
7706
|
"*",
|
|
7651
7707
|
cors({
|
|
@@ -7655,7 +7711,7 @@ function createDashboardAppWithLifecycle(deps, options) {
|
|
|
7655
7711
|
const url = new URL(origin);
|
|
7656
7712
|
const h = url.hostname;
|
|
7657
7713
|
if (h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0") return origin;
|
|
7658
|
-
if (
|
|
7714
|
+
if (isPrivateIpv4(h)) return origin;
|
|
7659
7715
|
} catch {
|
|
7660
7716
|
}
|
|
7661
7717
|
return null;
|
|
@@ -8045,6 +8101,13 @@ function warnIfDashboardOpenMode(config, log = console.error) {
|
|
|
8045
8101
|
);
|
|
8046
8102
|
return true;
|
|
8047
8103
|
}
|
|
8104
|
+
function warnIfNoEnforcement(policy, log = console.error) {
|
|
8105
|
+
if (policy.rules.length > 0 || policy.defaultAction !== "allow" || policy.dryRun) return false;
|
|
8106
|
+
log(
|
|
8107
|
+
'[helio] No policy rules are loaded and the default action is "allow" - Helio is recording an audit trail but NOT blocking anything. Add rules under `policies:` in helio.yaml to start enforcing (see docs/policies.md).'
|
|
8108
|
+
);
|
|
8109
|
+
return true;
|
|
8110
|
+
}
|
|
8048
8111
|
|
|
8049
8112
|
// src/crash-drain.ts
|
|
8050
8113
|
var hooks = [];
|
|
@@ -8117,18 +8180,6 @@ upstream:
|
|
|
8117
8180
|
# headers:
|
|
8118
8181
|
# Authorization: "Bearer \${UPSTREAM_TOKEN}"
|
|
8119
8182
|
|
|
8120
|
-
# Operator dashboard + approval REST API. Bound to 127.0.0.1 by default \u2014 do
|
|
8121
|
-
# not change to 0.0.0.0 without putting an authenticating reverse proxy in
|
|
8122
|
-
# front. dashboard.api_secret is the manual dashboard login secret and also
|
|
8123
|
-
# supports machine Bearer auth for sideband API clients. Store it safely; it
|
|
8124
|
-
# stays valid until you rotate it. Rotate by editing this file and restarting
|
|
8125
|
-
# (or hot-reloading) the proxy. Rotation invalidates active dashboard sessions.
|
|
8126
|
-
dashboard:
|
|
8127
|
-
enabled: true
|
|
8128
|
-
port: 3100
|
|
8129
|
-
host: 127.0.0.1
|
|
8130
|
-
api_secret: "${apiSecret}"
|
|
8131
|
-
|
|
8132
8183
|
# listen:
|
|
8133
8184
|
# port: 3000
|
|
8134
8185
|
# host: 127.0.0.1
|
|
@@ -8149,6 +8200,18 @@ dashboard:
|
|
|
8149
8200
|
# retention: 90d
|
|
8150
8201
|
# include_responses: true
|
|
8151
8202
|
|
|
8203
|
+
# Operator dashboard + approval REST API. Bound to 127.0.0.1 by default \u2014 do
|
|
8204
|
+
# not change to 0.0.0.0 without putting an authenticating reverse proxy in
|
|
8205
|
+
# front. dashboard.api_secret is the manual dashboard login secret and also
|
|
8206
|
+
# supports machine Bearer auth for sideband API clients. Store it safely; it
|
|
8207
|
+
# stays valid until you rotate it. Rotate by editing this file and restarting
|
|
8208
|
+
# (or hot-reloading) the proxy. Rotation invalidates active dashboard sessions.
|
|
8209
|
+
dashboard:
|
|
8210
|
+
enabled: true
|
|
8211
|
+
port: 3100
|
|
8212
|
+
host: 127.0.0.1
|
|
8213
|
+
api_secret: "${apiSecret}"
|
|
8214
|
+
|
|
8152
8215
|
# sdk:
|
|
8153
8216
|
# enabled: false
|
|
8154
8217
|
# port: 3200
|
|
@@ -8444,6 +8507,7 @@ async function startCommand(configPath, options) {
|
|
|
8444
8507
|
console.error(
|
|
8445
8508
|
`Policies: ${String(ruleCount)} rule${ruleCount !== 1 ? "s" : ""} loaded (default: ${policy.defaultAction})`
|
|
8446
8509
|
);
|
|
8510
|
+
warnIfNoEnforcement(policy);
|
|
8447
8511
|
if (config.upstream.transport === "stdio") {
|
|
8448
8512
|
console.error(`Upstream: ${config.upstream.command ?? ""} (stdio)`);
|
|
8449
8513
|
} else {
|