feat(agent): auto-renew the host cert in the native run-loop (A5)

Wire createCertRotator/renewCert into superviseNative so the frp-client cert
renews silently at ~2/3 TTL over mTLS /renew and frpc restarts onto the fresh
leaf; failures retry with backoff, never crash the supervisor. mTLS transport
has a 15s timeout + 64KB response cap. 281 tests pass.
This commit is contained in:
Yaojia Wang
2026-07-18 13:32:05 +02:00
parent fff011bb7f
commit 9a5909f672
8 changed files with 815 additions and 6 deletions

View File

@@ -145,4 +145,44 @@ describe('superviseFrpc (B4/H4 — restart-on-exit backoff)', () => {
await stopping
expect(spawn).toHaveBeenCalledTimes(1)
})
it('restartChild() kills the running child so the loop respawns onto the fresh cert files', async () => {
const flush = (): Promise<void> => new Promise((r) => setImmediate(r))
const children = [makeChild(), makeChild()]
let n = 0
const spawn: SpawnFrpc = () => children[n++]!.child
const handle = superviseFrpc('/frpc', '/toml', {
spawn,
sleep: async () => {},
logger: silentLogger,
now: () => 1_000_000, // fixed clock: run counts as unstable, but sleep is a no-op → respawn is immediate
})
await flush()
expect(handle.isChildAlive()).toBe(true) // child[0]
handle.restartChild() // A5: rotator calls this after a cert rotation to reload the new leaf
expect(children[0]!.killed).toBe(true)
await flush()
expect(n).toBe(2) // child[1] spawned — frpc now reads the rotated cert on disk
expect(handle.isChildAlive()).toBe(true)
await handle.stop()
})
it('restartChild() after stop is a no-op (no spawn past shutdown)', async () => {
const first = makeChild()
let n = 0
const spawn: SpawnFrpc = vi.fn(() => (n++ === 0 ? first.child : makeChild().child))
const handle = superviseFrpc('/frpc', '/toml', {
spawn,
sleep: async () => {},
logger: silentLogger,
})
await Promise.resolve()
const stopping = handle.stop()
first.exit(0)
await stopping
handle.restartChild() // must not resurrect the supervisor
expect(spawn).toHaveBeenCalledTimes(1)
})
})