agent-shim 7.14.0 → 8.0.1

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.
@@ -664,10 +664,12 @@ interface LayoutPaths {
664
664
  readonly categoriesLocalFile: string;
665
665
  /** Path to the claude-shim.json marker recording where `agent-shim shim enable` last placed a `claude`-named copy of this executable, and how. */
666
666
  readonly claudeShimFile: string;
667
- /** Directory holding the headroom daemon's coordination state: state.json, the start lock, and the session registry. */
667
+ /** Directory holding the headroom daemon's coordination state: the state file, the start lock, the session registry, and the socket directory. */
668
668
  readonly headroomDir: string;
669
- /** Path to the headroom supervisor's state.json: pids, port, version, allowlist hash, last error. */
669
+ /** Path to the headroom supervisor's state file: pids, socket path, version, allowlist hash, last error. Its name carries `HEADROOM_STATE_SCHEMA_VERSION`, so a supervisor from a release with another state schema keeps its own file instead of contending for this one. */
670
670
  readonly headroomStateFile: string;
671
+ /** Directory, mode 0700, holding the headroom daemon's unix socket: one `<supervisor-pid>.sock` per supervisor generation. Only its owner can enter it, which is what authenticates the door's hop to the daemon. */
672
+ readonly headroomSocketDir: string;
671
673
  /** Path to the exclusive-create marker guarding "who spawns the supervisor" so concurrent launches start at most one. */
672
674
  readonly headroomLockFile: string;
673
675
  /** Directory holding one `<launcher-pid>.json` session-registry entry per live launch routed through headroom. */
@@ -684,7 +686,7 @@ interface LayoutPaths {
684
686
  readonly frontdoorCaCertFile: string;
685
687
  /** Path to the front door's CA private key, mode 0600: whoever can read it can impersonate the door to every routed child. */
686
688
  readonly frontdoorCaKeyFile: string;
687
- /** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS file plus the front door's CA, one file per distinct combination, for children that must keep trusting both. */
689
+ /** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS (for a routed child) or HEADROOM_CA_BUNDLE (for the headroom daemon) file plus the front door's CA, one file per distinct combination, for processes that must keep trusting both. */
688
690
  readonly frontdoorCaBundlesDir: string;
689
691
  /** Path to the front door's state.json: its supervisor pid, its port, and the sticky port it last served on. */
690
692
  readonly frontdoorStateFile: string;
@@ -1157,8 +1159,8 @@ interface LogPort {
1157
1159
  }
1158
1160
  /** The running headroom daemon one launch routed through, as `HeadroomPort.ensure` reports it. The front door owns the session's routing; this names the daemon the door's headroom hop forwards to, and the project identity headroom scopes memory to. */
1159
1161
  interface HeadroomUp {
1160
- /** The loopback port the headroom daemon listens on: what the door's headroom hop reads live and `HEADROOM_PROXY_URL` names for anything else that wants the daemon. */
1161
- readonly port: number;
1162
+ /** The unix socket the headroom daemon serves on, inside the owner-only socket directory: what the door's headroom hop dials. The child never reaches the daemon itself, so this is reported, never exported to its environment. */
1163
+ readonly socketPath: string;
1162
1164
  /** The project identity headroom scopes memory state to: the git repo root of the launch directory, or the directory itself outside a repository. */
1163
1165
  readonly projectId: string;
1164
1166
  }
@@ -1468,7 +1470,7 @@ declare function mintLeaf(ca: CaMaterial, names: readonly string[], now: Readonl
1468
1470
  /** Mints each host's leaf once from the CA and reuses it, because generating a 2048-bit RSA keypair costs noticeable time and nothing about the certificate changes between sessions. */
1469
1471
  declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
1470
1472
  /**
1471
- * Persistence for the CA: generated once on the machine's first headroom start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
1473
+ * Persistence for the CA: generated once on the machine's first front-door start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
1472
1474
  */
1473
1475
  interface ConnectCertStore {
1474
1476
  /** The persisted CA, or undefined when none exists yet (or only half of it does: a cert without its key can sign nothing, so it counts as absent). */
@@ -1784,21 +1786,39 @@ interface EnsureHeadroomPorts {
1784
1786
  /**
1785
1787
  * Brings the headroom daemon up for this launch, or finds it already running, and registers this launcher pid in its session registry (the fact that keeps the daemon alive and defers drift restarts until this launch is done).
1786
1788
  *
1787
- * The exclusive-create start lock decides which of several concurrent launches spawns the one supervisor: its holder spawns and keeps waiting like everyone else, everyone else waits on state.json, and a lock whose holder has died is removed and retried. Readiness is "state names a live supervisor, a live headroom pid, and a port", which the supervisor only writes after its own `/readyz` probe has passed, so polling state alone never mistakes a bound-but-not-ready port for a usable daemon.
1789
+ * The exclusive-create start lock decides which of several concurrent launches spawns the one supervisor: its holder spawns and keeps waiting like everyone else, everyone else waits on the state file, and a lock whose holder has died is removed and retried. Readiness is "state names a live supervisor, a live headroom pid, and a socket path", which the supervisor only writes after its own `/readyz` probe over that socket has passed, so polling state alone never mistakes a bound-but-not-ready socket for a usable daemon.
1788
1790
  *
1789
- * Throws `HeadroomStartError` (naming the daemon log path and any recorded `lastError`) when the daemon is not up within `HEADROOM_START_TIMEOUT_MS`, or when a live supervisor has recorded a fatal error.
1791
+ * Throws `HeadroomStartError` (naming the daemon log path and any recorded `lastError`) when the daemon is not up within `HEADROOM_START_TIMEOUT_MS`, or when a live supervisor has recorded a fatal error (a platform without unix sockets and a socket directory that fails the owner-only check are recorded this way, so the launch fails with the reason).
1790
1792
  */
1791
1793
  declare function ensureHeadroom(params: {
1792
1794
  readonly paths: LayoutPaths;
1793
1795
  readonly launcherPid: number;
1794
1796
  readonly ports: EnsureHeadroomPorts;
1795
1797
  }): {
1796
- readonly port: number;
1798
+ readonly socketPath: string;
1797
1799
  };
1798
1800
 
1799
1801
  /** The token-saving settings of the `headroom` config block, without its install and lifecycle fields (`source`, `idleShutdownMinutes`). */
1800
1802
  type HeadroomSettings = Pick<HeadroomGlobalConfig, "mode" | "targetRatio" | "ccr" | "rolloutChannel" | "interceptToolResults" | "readMaturation">;
1801
1803
 
1804
+ /** What an `lstat` (which never follows symlinks) reports about one path, narrowed to what authenticating the socket needs. */
1805
+ interface SocketPathStat {
1806
+ readonly kind: "dir" | "socket" | "symlink" | "other";
1807
+ /** The owning user id. */
1808
+ readonly uid: number;
1809
+ /** The permission bits, without the file type bits. */
1810
+ readonly mode: number;
1811
+ }
1812
+ /** The platform facts and the one filesystem call the socket checks need, injected so every refusal is unit-testable without a real socket, directory or second user. */
1813
+ interface HeadroomSocketTrustPorts {
1814
+ /** `process.platform` in the real implementation. */
1815
+ readonly platform: string;
1816
+ /** This process's user id. Only called on a platform with unix sockets, which all have POSIX user ids. */
1817
+ readonly currentUid: () => number;
1818
+ /** Stats one path without following symlinks, or returns undefined when nothing is there. */
1819
+ readonly lstat: (target: string) => SocketPathStat | undefined;
1820
+ }
1821
+
1802
1822
  /** Everything the supervisor needs to run the daemon, resolved from the global config and the filesystem before it starts. */
1803
1823
  interface HeadroomSupervisorConfig {
1804
1824
  /** The install spec as configured (`headroom.source`), already defaulted. */
@@ -1826,20 +1846,18 @@ interface SupervisorPorts {
1826
1846
  * Zombie-aware liveness, and for a daemon this supervisor spawned, exit-event-backed: the implementation keeps the spawned ChildProcess handle, consumes its exit event (which is also what reaps it), and reports the pid as not running from that moment. A bare signal-0 check is not enough, because a defunct process still answers it.
1827
1847
  */
1828
1848
  readonly isRunning: (pid: number) => boolean;
1829
- /** Returns a free loopback port. Async because the only reliable way to reserve one is to bind and release a socket. */
1830
- readonly freePort: () => Promise<number>;
1831
- /** Whether nothing is currently listening on one specific loopback port: the check that decides whether the sticky `lastPort` can be reused. The same bind-and-release technique `freePort` uses, applied to a number the caller already cares about. */
1832
- readonly isPortFree: (port: number) => Promise<boolean>;
1849
+ /** The platform facts and owner-only checks the daemon's socket directory is prepared and verified with (see `prepareHeadroomSocket`). */
1850
+ readonly socketTrust: HeadroomSocketTrustPorts;
1833
1851
  /**
1834
- * Starts `headroom proxy` bound to `port` with the given allowlist, returning its pid. Output goes to the daemon log. The implementation must keep the ChildProcess handle and attach an exit listener (detaching the process is fine; unref'ing a child you still need events from is not, since without the listener nothing reaps it and it lingers as a zombie).
1852
+ * Starts `headroom proxy` serving on the unix socket `socketPath` (and on no TCP port) with the given allowlist, returning its pid. Output goes to the daemon log. The implementation must keep the ChildProcess handle and attach an exit listener (detaching the process is fine; unref'ing a child you still need events from is not, since without the listener nothing reaps it and it lingers as a zombie).
1835
1853
  */
1836
- readonly spawnHeadroom: (port: number, allowlist: readonly string[], settings: Readonly<HeadroomSettings>) => number;
1854
+ readonly spawnHeadroom: (socketPath: string, allowlist: readonly string[], settings: Readonly<HeadroomSettings>) => number;
1837
1855
  /** Reads the headroom config block as it stands on disk now. The supervisor calls it every tick, so a changed `source` or setting is noticed while the daemon runs and applied by the same quiet-registry restart an allowlist change gets. */
1838
1856
  readonly readConfig: () => HeadroomSupervisorConfig;
1839
1857
  /** Stops a process the supervisor owns, escalating SIGTERM to SIGKILL on a bounded timeout (see `stopSupervisedProcess`). */
1840
1858
  readonly stopProcess: (pid: number) => void;
1841
- /** True once `GET /readyz` on the port succeeds. */
1842
- readonly ready: (port: number) => Promise<boolean>;
1859
+ /** True once `GET /readyz` over the unix socket succeeds. */
1860
+ readonly ready: (socketPath: string) => Promise<boolean>;
1843
1861
  /** Runs `uv tool install <spec>`, logging output to the daemon log. Returns ok=false with the error when `uv` is absent or the install fails. */
1844
1862
  readonly install: (spec: string) => {
1845
1863
  readonly ok: boolean;
@@ -2644,7 +2662,7 @@ interface RunDoctorParams {
2644
2662
  readonly run?: RunPort;
2645
2663
  /** `process.platform` in real use; the Keychain check only ever runs when this is `"darwin"`. */
2646
2664
  readonly platform: string;
2647
- /** The headroom daemon's state.json plus a zombie-aware liveness predicate for the pids it names (a defunct daemon holds no port but still answers signal 0). Omit the raw text when the daemon has never run; that is a pass, not a failure. */
2665
+ /** The headroom daemon's state.json plus a zombie-aware liveness predicate for the pids it names (a defunct daemon serves nothing but still answers signal 0). Omit the raw text when the daemon has never run; that is a pass, not a failure. */
2648
2666
  readonly headroom: {
2649
2667
  readonly state: DoctorFileInput;
2650
2668
  readonly isRunning: (pid: number) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "7.14.0",
3
+ "version": "8.0.1",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",