π¦«ποΈ dispatch to foreman
π§ task enqueued
ββ priority = ?
ββ yieldage = ?
ββ leverage = ?
title
feat(ssm): DeclaredAwsSsmSshTunnel.ttl β a tunnel must not outlive its credential
description
.what
DeclaredAwsSsmSshTunnel has no declarable lifetime. a tunnel opened by git.grove.wake stays
open until its process dies, and there is no way to say "this tunnel must not outlive N hours."
the ask: a ttl field on the resource, so a tunnel past its ttl reads CLOSED and the next
apply respawns it fresh.
.why the resource is the right home
spawnedAt already exists on the object, and getOneSsmSshTunnel already reads it:
// DeclaredAwsSsmSshTunnel.d.ts
spawnedAt?: string | null; // @readonly β used with pid for reuse detection
static readonly: readonly ['pid', 'spawnedAt'];
so the resource ALREADY reasons about its own age. it records when it started and compares that
against a pid, to catch OS pid reuse. a ttl adds no new state and no new read β it adds one more
comparison against a value already in hand.
.the change, at the line it belongs on
getOneSsmSshTunnel.js already gates OPEN behind two live conditions:
const processAlive = isProcessAlive({ pid: cacheFile.pid });
const portReady = processAlive
? await isSshTunnelHealthy({ port: input.by.unique.from.port })
: false;
if (!processAlive || !portReady) return notconnected; // β the seam
a ttl is a third condition on that same line:
const withinTtl = !ttl
|| Date.now() - new Date(cacheFile.spawnedAt).getTime() < toMilliseconds(ttl);
if (!processAlive || !portReady || !withinTtl) return notconnected;
ttl as an IsoDuration ('PT12H'), to match the house glossary (rule.require.iso-time).
.why it converges with no reaper
a past-ttl tunnel reads CLOSED, so the very next declastruct apply sees a drift and respawns
it. there is no timer to install, no state file to keep, and no daemon to supervise β the age is
DERIVED from spawnedAt on each read.
this is the shape domain.terms/term=idle already validated in the grove's own hibernate clock:
a stored clock plus a reset unit was retired in favor of a derived read off an mtime the disk
already held. same move, same reason β a stored deadline can drift from the world, a derived one
cannot.
.the second half β the process should carry its own deadline
the get-side ttl makes a stale tunnel read CLOSED. it does not make the process die. for a
true timeout, setSsmSshTunnel must hand the spawned session-manager-plugin a deadline, so the
process terminates on its own at ttl whether or not anyone applies again.
both halves earn their keep, and they answer different questions:
| half |
answers |
without it |
| get-side ttl |
"is this tunnel still valid?" |
a stale tunnel reads OPEN and is reused |
| set-side deadline |
"when does this tunnel die?" |
the process leaks a local port + an ssm session forever |
.why 12h specifically, for the grove case
12h is not an arbitrary number β it is the life of the credential that opened the tunnel.
domain.terms/term=tier records that a permission set's sessionDuration caps at PT12H, which
is AWS's hard maximum, and that both everyday-reader and everyday-power sit at that cap. so
the SSO credential behind the tunnel's ssm session expires at 12h by construction.
a tunnel cannot honestly outlive the credential that opened it. today it tries: the process
stays alive, the local port stays bound, and the session behind it goes bad β so the failure
presents as alive but unusable, which is precisely the case isSshTunnelHealthy was written to
catch after the fact. a ttl converts that ragged, silent death into a clean, on-time one.
.the ask
- add
ttl?: IsoDuration | null to DeclaredAwsSsmSshTunnel (a settable field, not @readonly)
- gate the get's OPEN verdict on it, at the seam quoted above
- hand the deadline to the spawn in
setSsmSshTunnel, so the process self-terminates at ttl
- mirror it onto
DeclaredAwsSsmVpcTunnel β that object records no spawnedAt at all today,
so it cannot even observe its own age, and it has the same credential-expiry exposure
.the transferable shape
a resource that already records its own start time has already paid for a ttl. the field is
the cheap half; the comparison is one line. when you find a spawnedAt with no deadline beside
it, ask what the value was recorded FOR β here it was pid-reuse detection, and the same value
answers staleness for free.
and the sharper one, for any tunnel that fronts a credential:
a session must not outlive the credential that opened it. where the credential's life is a
declared number (term=tier: PT12H), the session's ttl is not a policy choice β it is that
number, and any longer is a promise the credential cannot keep.
π¦«ποΈ dispatch to foreman
title
feat(ssm): DeclaredAwsSsmSshTunnel.ttl β a tunnel must not outlive its credential
description
.what
DeclaredAwsSsmSshTunnelhas no declarable lifetime. a tunnel opened bygit.grove.wakestaysopen until its process dies, and there is no way to say "this tunnel must not outlive N hours."
the ask: a
ttlfield on the resource, so a tunnel past its ttl reads CLOSED and the nextapply respawns it fresh.
.why the resource is the right home
spawnedAtalready exists on the object, andgetOneSsmSshTunnelalready reads it:so the resource ALREADY reasons about its own age. it records when it started and compares that
against a pid, to catch OS pid reuse. a ttl adds no new state and no new read β it adds one more
comparison against a value already in hand.
.the change, at the line it belongs on
getOneSsmSshTunnel.jsalready gates OPEN behind two live conditions:a ttl is a third condition on that same line:
ttlas anIsoDuration('PT12H'), to match the house glossary (rule.require.iso-time)..why it converges with no reaper
a past-ttl tunnel reads CLOSED, so the very next
declastruct applysees a drift and respawnsit. there is no timer to install, no state file to keep, and no daemon to supervise β the age is
DERIVED from
spawnedAton each read.this is the shape
domain.terms/term=idlealready validated in the grove's own hibernate clock:a stored clock plus a reset unit was retired in favor of a derived read off an mtime the disk
already held. same move, same reason β a stored deadline can drift from the world, a derived one
cannot.
.the second half β the process should carry its own deadline
the get-side ttl makes a stale tunnel read CLOSED. it does not make the process die. for a
true timeout,
setSsmSshTunnelmust hand the spawned session-manager-plugin a deadline, so theprocess terminates on its own at ttl whether or not anyone applies again.
both halves earn their keep, and they answer different questions:
.why 12h specifically, for the grove case
12h is not an arbitrary number β it is the life of the credential that opened the tunnel.
domain.terms/term=tierrecords that a permission set'ssessionDurationcaps atPT12H, whichis AWS's hard maximum, and that both
everyday-readerandeveryday-powersit at that cap. sothe SSO credential behind the tunnel's ssm session expires at 12h by construction.
a tunnel cannot honestly outlive the credential that opened it. today it tries: the process
stays alive, the local port stays bound, and the session behind it goes bad β so the failure
presents as alive but unusable, which is precisely the case
isSshTunnelHealthywas written tocatch after the fact. a ttl converts that ragged, silent death into a clean, on-time one.
.the ask
ttl?: IsoDuration | nulltoDeclaredAwsSsmSshTunnel(a settable field, not@readonly)setSsmSshTunnel, so the process self-terminates at ttlDeclaredAwsSsmVpcTunnelβ that object records nospawnedAtat all today,so it cannot even observe its own age, and it has the same credential-expiry exposure
.the transferable shape
and the sharper one, for any tunnel that fronts a credential: