Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions hypaware-core/plugins-workspace/ai-gateway/src/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,17 +432,23 @@ async function prepareInterception(ctx, config, upstreams, liveState) {
if (!config.proxyMode) {
// Proxy mode is off, but a CA on disk means a client was attached in proxy
// mode at some point and may still have `HTTPS_PROXY` pointing here. Serve
// blind tunnels so its egress keeps working, and say so loudly: the repair
// is a re-attach (or a detach), and nothing else on the machine will
// volunteer that.
// blind tunnels so its egress keeps working, and say so loudly: nothing
// else on the machine will volunteer that this install is in the degraded
// state. A plain re-attach is NOT the remedy: attach leaves the CA where
// it is on purpose (the trust is offered back, never taken), so the file
// that puts the install in this state survives the re-attach. The
// remedies that land are removing the CA (`hyp detach claude --purge`,
// then re-attach) or turning `proxy_mode` back on.
// @ref LLP 0262#migration [constrained-by]: attach offers the CA back rather than removing it, so it cannot clear this state on its own
const stale = await readLocalCaInfo({ stateRoot: defaultStateRoot(ctx.env) })
if (stale) {
liveState.interceptionError = 'proxy_mode is off but a local CA is installed'
ctx.log.warn('aigw.proxy_mode_stale_ca', {
[Attr.PLUGIN]: PLUGIN_NAME,
ca_cert_path: stale.certPath,
reason: 'serving blind tunnels so an already-attached client keeps working; ' +
'run `hyp attach claude` to move it back to base-URL mode, or `hyp detach claude`',
're-attaching leaves this CA on disk, so run `hyp detach claude --purge` ' +
'and re-attach to clear the proxy residue, or turn proxy_mode back on',
})
return { tunnelOnly: true }
}
Expand Down
19 changes: 16 additions & 3 deletions test/plugins/ai-gateway-proxy-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,13 @@ test('proxy mode turned off with a CA still installed serves blind tunnels', asy
const echoPort = echoAddress && typeof echoAddress === 'object' ? echoAddress.port : 0
t.after(() => new Promise((resolve) => echo.close(() => resolve(undefined))))

/** @type {{ level: string, event: string }[]} */
/** @type {{ level: string, event: string, attrs: Record<string, unknown> }[]} */
const logged = []
/** @param {string} level */
const record = (level) => (/** @type {string} */ event) => logged.push({ level, event })
const record = (level) => (
/** @type {string} */ event,
/** @type {Record<string, unknown> | undefined} */ attrs
) => logged.push({ level, event, attrs: attrs ?? {} })
const ctx = /** @type {any} */ ({
// proxy_mode deliberately absent, as if the operator turned it back off.
config: {
Expand All @@ -576,7 +579,17 @@ test('proxy mode turned off with a CA still installed serves blind tunnels', asy
const details = /** @type {any} */ ((await source.status()).details)
assert.equal(details.proxy_mode, false)
assert.match(details.proxy_mode_error, /a local CA is installed/)
assert.equal(logged.some((l) => l.event === 'aigw.proxy_mode_stale_ca'), true)
const staleWarn = logged.find((l) => l.event === 'aigw.proxy_mode_stale_ca')
assert.ok(staleWarn, 'the stale-CA warning is emitted')

// The warning has to name a remedy that actually works. Attach deliberately
// leaves the CA on disk (it offers the trust back rather than taking it), so
// telling the operator to re-attach leaves the install exactly as degraded
// as it was, every time, until the CA is gone.
// @ref LLP 0262#migration [tests]: the stale-CA remedy cannot be a plain re-attach
const reason = String(staleWarn.attrs.reason ?? '')
assert.match(reason, /hyp detach claude --purge/)
assert.doesNotMatch(reason, /run `hyp attach claude` to move it back/)

// The tunnel is still served, so an already-attached client keeps its egress
// instead of losing all HTTPS. (The byte-level round trip is covered by
Expand Down
Loading