@wpelevator/tug 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.
Files changed (49) hide show
  1. package/README.md +24 -22
  2. package/dist/agent-credential.d.ts +39 -0
  3. package/dist/agent-credential.js +135 -0
  4. package/dist/agent.d.ts +15 -9
  5. package/dist/agent.js +126 -104
  6. package/dist/backup-check.d.ts +11 -0
  7. package/dist/backup-check.js +10 -0
  8. package/dist/backup-config.d.ts +5 -0
  9. package/dist/backup-config.js +23 -3
  10. package/dist/backup-destination.d.ts +1 -1
  11. package/dist/backup-destination.js +1 -1
  12. package/dist/backup-restore.js +28 -8
  13. package/dist/backup-scheduler.d.ts +7 -1
  14. package/dist/backup-scheduler.js +21 -6
  15. package/dist/cli-build.js +70 -4
  16. package/dist/cli-config.d.ts +9 -5
  17. package/dist/cli-config.js +89 -34
  18. package/dist/cli-install.js +1 -1
  19. package/dist/cli-live-transport.d.ts +3 -2
  20. package/dist/cli-live-transport.js +9 -5
  21. package/dist/cli.js +276 -142
  22. package/dist/compose.d.ts +3 -1
  23. package/dist/compose.js +14 -2
  24. package/dist/control-credential.d.ts +12 -0
  25. package/dist/control-credential.js +46 -0
  26. package/dist/control.d.ts +18 -1
  27. package/dist/control.js +586 -202
  28. package/dist/credentials.d.ts +111 -0
  29. package/dist/{tokens.js → credentials.js} +70 -54
  30. package/dist/db.d.ts +20 -5
  31. package/dist/db.js +3 -3
  32. package/dist/diagnostics.d.ts +44 -0
  33. package/dist/diagnostics.js +65 -0
  34. package/dist/ingress.js +4 -3
  35. package/dist/main.js +35 -0
  36. package/dist/normalize.d.ts +6 -0
  37. package/dist/normalize.js +16 -2
  38. package/dist/pki.d.ts +119 -0
  39. package/dist/pki.js +339 -0
  40. package/dist/protocol.d.ts +53 -22
  41. package/dist/status-report.d.ts +11 -0
  42. package/dist/status-report.js +147 -0
  43. package/dist/templates/agent.env +1 -3
  44. package/dist/templates/control-compose.yml +4 -3
  45. package/dist/templates/control-install.sh +8 -2
  46. package/dist/tls-server.d.ts +11 -0
  47. package/dist/tls-server.js +23 -0
  48. package/package.json +10 -8
  49. package/dist/tokens.d.ts +0 -91
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  The name: tugboats pull ships into place, like the pull-based agents here; `tug` is the CLI.
4
4
 
5
- Deploy Docker Compose projects to one or more ordinary Linux VPSes through a small self-hosted control plane and a pull-based agent on each node, without Kubernetes or a hosted PaaS. What it is for, the deployment shapes it targets and the planned milestones are in the [Roadmap](docs/roadmap.md).
5
+ Deploy Docker Compose applications to one or more ordinary Linux VPSes through a small self-hosted control plane and a pull-based agent on each node, without Kubernetes or a hosted PaaS. Tug runs an agent on each node; the agents drive the node's Docker Engine API directly, and Compose is only used on your machine to read the file. What it is for, the deployment shapes it targets and the planned milestones are in the [Roadmap](docs/roadmap.md).
6
6
 
7
7
  ## Features
8
8
 
9
- - Control API with bearer-token auth, scoped `read`/`deploy`/`admin` API tokens for CI and teammates, an audit trail naming who acted, and SQLite metadata through Node's built-in driver.
10
- - Nodes join over SSH (`node-install`) or enroll themselves with a single `docker run` (`node-enroll`), with one agent token per node. Agents only dial out, so nodes behind NAT work.
9
+ - One mutual-TLS port: every operator, CI job and node authenticates with a certificate from the control plane's private CA, and control authorizes it by public key. Scoped `read`/`deploy`/`admin` operator credentials for CI and teammates, an audit trail naming who acted, and SQLite metadata through Node's built-in driver.
10
+ - Nodes join over SSH (`node-install`) or enroll themselves with a single `docker run` after `node-prepare-enrollment`, each with its own certificate and an immutable node ID. Agents only dial out, so nodes behind NAT work.
11
11
  - Agents pull desired state from control, reconcile containers through the Docker Engine API, run TCP/HTTP/Docker health checks and report status. An outbound WebSocket wakes them as soon as control queues work and carries live `exec`, followed logs, `ps` and `inspect` sessions, with long-poll and periodic-poll fallbacks for ordinary work.
12
12
  - `x-tug` placement of services and volumes on nodes, with immutable desired-state records per deployment.
13
13
  - Build application images on control from uploaded source, or locally with `deploy --build-local`. Agents download saved images directly from control. Published images are pinned to registry digests at deployment creation.
@@ -15,22 +15,22 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
15
15
  - Deployment convergence, per-project operation locks, cancellation, rollback, per-service rollout events and log fetching through the agents.
16
16
  - Opt-in zero-downtime rolling rollouts for ingress-fronted services (`x-tug.rollout`) and automatic rollback of failed deployments (`x-tug.rollback: auto`). See [Compose extensions](docs/compose.md#rolling-rollouts).
17
17
  - Private published images use encrypted project credentials for digest resolution and Engine API pulls.
18
- - Optional WireGuard mesh between nodes, health-aware cross-node service names and a managed Caddy ingress with automatic HTTPS, each opt-in. See [Networking](docs/networking.md).
18
+ - Optional WireGuard mesh between nodes, health-aware cross-node service names (hosts-file entries injected into containers) and a managed Caddy ingress with automatic HTTPS, each opt-in. See [Networking](docs/networking.md).
19
19
  - Private-by-default published ports: generated Compose binds them to the overlay IP, loopback or the node address, never every interface unless you write `0.0.0.0`. See [Networking](docs/networking.md#published-ports-private-by-default).
20
- - Control-plane backup and restore, and `tug doctor` diagnostics.
20
+ - Workload backups of service commands and volumes with restic, a separate control-plane backup and restore, and `tug doctor` diagnostics for certificate expiry, node connectivity and clock skew.
21
21
 
22
22
  ## How it fits together
23
23
 
24
24
  ```text
25
- tug CLI / CI (API token)
25
+ tug CLI / CI (operator credential)
26
26
  |
27
- | HTTPS: deploy, status, logs, exec
27
+ | mTLS: deploy, status, logs, exec
28
28
  v
29
29
  +-------------------------------------+
30
- | Control plane |
30
+ | Control plane (one mTLS port) |
31
31
  | Control API (role-checked) |
32
32
  | SQLite: desired states, secrets |
33
- | image builds, saved images |
33
+ | private CA, image builds |
34
34
  +-------------------------------------+
35
35
  ^ ^
36
36
  | agents dial out: work, status,
@@ -39,7 +39,7 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
39
39
  +------+------------+ +------+------------------+
40
40
  | Node app1 | | Node app2 (ingress node)|
41
41
  | Agent |<->| Agent |
42
- | Docker Compose | | Docker Compose |
42
+ | Docker Engine | | Docker Engine |
43
43
  | services | | services |
44
44
  | | | managed Caddy ingress |
45
45
  +-------------------+ +-------------------------+
@@ -48,7 +48,7 @@ Deploy Docker Compose projects to one or more ordinary Linux VPSes through a sma
48
48
  SSH is used only by the CLI to install the control plane and nodes.
49
49
  ```
50
50
 
51
- Operators and CI talk to the control API. The control plane stores desired state and builds images; each agent dials out to fetch the work for its node, applies it with Docker Compose and reports health back, so nodes behind NAT need no inbound ports. SSH is used only to install control and agents. The optional WireGuard mesh and Caddy ingress are described in [Networking](docs/networking.md), and every flow in detail in [Architecture](docs/architecture.md).
51
+ Operators and CI talk to the control API. The control plane stores desired state and builds images; each agent dials out to fetch the work for its node, applies it through the Docker Engine API and reports health back, so nodes behind NAT need no inbound ports. SSH is used only to install the control plane and its nodes. The optional WireGuard mesh and Caddy ingress are described in [Networking](docs/networking.md), and every flow in detail in [Architecture](docs/architecture.md).
52
52
 
53
53
  ## Quick start
54
54
 
@@ -60,29 +60,31 @@ npm install -g @wpelevator/tug
60
60
 
61
61
  The CLI installs the matching `ghcr.io/wpelevator/tug:<version>` image on your hosts.
62
62
 
63
- 1. Install the control plane: `tug control-install root@control.example.com --install-docker`. The flag installs missing Docker Engine and Compose on supported Debian, Ubuntu, Fedora and RHEL-compatible hosts; omit it when Docker is ready. Put HTTPS in front of the control API as described in [Control plane](docs/control-plane.md).
64
- 2. Point the CLI at it: `tug init --control-url https://control.example.com --token "$TUG_TOKEN"`. It writes `tug.yml` (commit it, and list your servers in it) and keeps the token in the git-ignored `tug.local.yml`; see [CLI reference](docs/cli.md#config-file).
63
+ 1. Install the control plane: `tug control-install root@control.example.com --public-url https://control.example.com:8443 --install-docker`. The flag installs missing Docker Engine and the Compose plugin that runs the control container on supported Debian, Ubuntu, Fedora and RHEL-compatible hosts; omit it when Docker is ready. Control serves mutual TLS directly on its published port (`8443`); do not terminate TLS in front of it ([Control plane](docs/control-plane.md#authentication-and-pki)).
64
+ 2. Point the CLI at it: `tug init tug-root.credential.json` (the file `control-install` wrote). It writes `tug.yml` (commit it, and list your servers in it), copies the credential to a `0600` file in `~/.config/tug/credentials/` and points the git-ignored `tug.local.yml` at it; see [CLI reference](docs/cli.md#config-file).
65
65
  3. Set the secrets your services need: `tug secret-set DB_PASSWORD`.
66
- 4. Add nodes with `tug node-install root@app1.example.com --name app1 --install-docker`, or `tug node-enroll --name app1` and run the printed Docker command. For a bare cloud host, `tug node-enroll --name app1 --install-docker` prints cloud-init user-data.
67
- 5. Deploy: `tug deploy -f docker-compose.yml`. It waits until every node reports the new version healthy.
68
- 6. Operate with `tug status`, `events`, `exec`, `run`, `logs`, `ps`, `inspect`, `restart`, `rollback`, `backup-run`, `restore` and `doctor`.
66
+ 4. Add nodes with `tug node-install root@app1.example.com --name app1 --install-docker`, or run `tug node-prepare-enrollment --name app1` and then the printed Docker command on the host. Nodes need Docker Engine only. For a bare cloud host, `tug node-prepare-enrollment --name app1 --install-docker` prints cloud-init user-data.
67
+ 5. Deploy: `tug deploy -f docker-compose.yml`. It waits until every node reports the new desired-state revision healthy.
68
+ 6. Operate with `tug status`, `events`, `exec`, `run`, `logs`, `ps`, `inspect`, `restart`, `rollback`, `backup-run`, `restore` and `doctor`. Create more operator credentials for CI and teammates with `tug credential-create`.
69
69
 
70
70
  ## Documentation
71
71
 
72
+ - [Terminology](docs/terminology.md): core concepts, the naming review (which recommendations are implemented), industry comparisons and the remaining proposed interface improvements.
72
73
  - [Architecture](docs/architecture.md): components, reconciliation, workflow diagrams, secrets and runtime paths.
73
74
  - [Compose extensions](docs/compose.md): `x-tug` placement, health checks, volumes and limitations.
74
- - [Control plane](docs/control-plane.md): install, TLS, image delivery, backup and restore.
75
- - [Backups](docs/backups.md): interim WordPress backup sidecar with restic, per-node variants, and a restore drill until `x-tug.backup` (M11).
76
- - [Access control](docs/access-control.md): scoped API tokens, roles, audit trail and a GitHub Actions deploy example.
77
- - [Networking](docs/networking.md): WireGuard mesh, service DNS, ingress and TLS, firewall notes.
78
- - [Nodes](docs/nodes.md): SSH install, self-service enrollment, registration and status.
75
+ - [Control plane](docs/control-plane.md): install, mutual TLS and the private CA, image delivery, control-plane backup and restore.
76
+ - [Backups](docs/backups.md): scheduled workload backups with `x-tug.backup` (restic), restore, recipes for common databases, and a restore drill. See also the [WordPress example](examples/wordpress/README.md).
77
+ - [Access control](docs/access-control.md): certificate identity classes, scoped operator credentials, roles, audit trail and a GitHub Actions deploy example.
78
+ - [Security](docs/security.md): deployment trust boundaries, security audit findings and validation limits.
79
+ - [Networking](docs/networking.md): WireGuard mesh, cross-node service names, ingress and TLS, firewall notes.
80
+ - [Nodes](docs/nodes.md): node names and IDs, SSH install, self-service enrollment, certificate renewal and recovery, registration and status.
79
81
  - [CLI reference](docs/cli.md): every `tug` command and option.
80
82
  - [Configuration](docs/configuration.md): CLI, control and agent environment variables.
81
83
  - [Development](docs/development.md): local setup, CI, Compose stack, host mode and the integration test.
82
84
  - [Testing](docs/testing.md): test layers, rollout metrics, mocked test backlog, and the [Caddy/PHP and static-file benchmark](docs/testing.md#caddy-proxy-overhead-benchmark).
83
85
  - [Observability](docs/observability.md): what can be observed today, and the proposed logs, metrics and alerting.
84
86
  - [Releases](docs/releases.md): changesets, npm and Docker image publishing.
85
- - [Standalone binary](docs/standalone-binary.md): proposed Bun-compiled CLI binaries and Homebrew distribution.
87
+ - [Standalone binary](docs/standalone-binary.md): proposed Bun-compiled CLI binaries and Homebrew distribution, with the mTLS dependency findings.
86
88
  - [Roadmap](docs/roadmap.md): goals, principles, the reference WordPress topology and planned milestones.
87
89
 
88
90
  ## Development
@@ -0,0 +1,39 @@
1
+ import { Agent } from 'undici';
2
+ import type { Credential, PersistHooks } from './pki.ts';
3
+ import type { IdentityResponse } from './protocol.ts';
4
+ /** The agent's persisted identity: the common credential plus the node it names. */
5
+ export interface AgentDocument extends Credential {
6
+ nodeId: string;
7
+ nodeName: string;
8
+ }
9
+ export declare const agentFile: (stateDir: string) => string;
10
+ export declare const pendingFile: (stateDir: string) => string;
11
+ export declare function loadAgentDocument(stateDir: string): Promise<AgentDocument | null>;
12
+ /** `agent identity`: reuse or generate the pending key and return its CSR. Safe to repeat. */
13
+ export declare function agentIdentityCsr(stateDir: string): Promise<string>;
14
+ /** Validates an identity response against the pending key and durably installs it as the agent document. */
15
+ export declare function installIdentity(stateDir: string, response: IdentityResponse, hooks?: PersistHooks): Promise<AgentDocument>;
16
+ /** `agent install-cert`: reads an identity response from stdin text. */
17
+ export declare function installCertFromText(stateDir: string, text: string): Promise<AgentDocument>;
18
+ export declare function describeMissingIdentity(): string;
19
+ export type HttpClient = (url: string, init?: RequestInit) => Promise<Response>;
20
+ /**
21
+ * A client pinned to the credential's CA that presents its certificate. `connectUrl` only changes where the TCP
22
+ * connection goes; the server name is still verified against the credential's public host. Redirects are refused.
23
+ */
24
+ export declare function createTransport(credential: Credential, connectUrl?: string): {
25
+ http: HttpClient;
26
+ base: string;
27
+ dispatcher: Agent;
28
+ close: () => Promise<void>;
29
+ };
30
+ export declare function certificateWindow(certPem: string): {
31
+ notBefore: number;
32
+ notAfter: number;
33
+ };
34
+ /** When to renew: two thirds of the lifetime plus up to ten percent random jitter, so a fleet does not sign together. */
35
+ export declare function renewalTime(certPem: string, random?: number): number;
36
+ /** Sends a CSR for the existing key, validates the result, saves it durably and returns the new document. */
37
+ export declare function renewCertificate(stateDir: string, doc: AgentDocument, post: (csr: string) => Promise<{
38
+ certificate: string;
39
+ }>, hooks?: PersistHooks): Promise<AgentDocument>;
@@ -0,0 +1,135 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { Agent, fetch as undiciFetch } from 'undici';
5
+ import { clientTlsOptions, createCsr, generateKeyPair, identityUri, parseCredential, validateInstalledCertificate, writeFileDurable, } from "./pki.js";
6
+ export const agentFile = (stateDir) => path.join(stateDir, 'agent.json');
7
+ export const pendingFile = (stateDir) => path.join(stateDir, 'agent-pending.json');
8
+ const NO_AGENT_IDENTITY = 'this node has no identity: run `tug node-install`, `tug node-prepare-enrollment` or `tug node-repair-identity` from an operator machine';
9
+ async function readJson(file) {
10
+ let text;
11
+ try {
12
+ text = await fs.readFile(file, 'utf8');
13
+ }
14
+ catch (e) {
15
+ if (e.code === 'ENOENT')
16
+ return null;
17
+ throw e;
18
+ }
19
+ try {
20
+ return JSON.parse(text);
21
+ }
22
+ catch (e) {
23
+ throw new Error(`${file} is corrupt; run \`tug node-repair-identity\` to replace this node's identity`, {
24
+ cause: e,
25
+ });
26
+ }
27
+ }
28
+ export async function loadAgentDocument(stateDir) {
29
+ const raw = await readJson(agentFile(stateDir));
30
+ if (raw === null)
31
+ return null;
32
+ try {
33
+ const credential = parseCredential(raw);
34
+ const { nodeId, nodeName } = raw;
35
+ if (typeof nodeId !== 'string' || typeof nodeName !== 'string')
36
+ throw new Error('missing node identity');
37
+ return { ...credential, nodeId, nodeName };
38
+ }
39
+ catch (e) {
40
+ throw new Error(`${agentFile(stateDir)} is invalid (${e instanceof Error ? e.message : String(e)}); run \`tug node-repair-identity\``, { cause: e });
41
+ }
42
+ }
43
+ async function pendingKey(stateDir) {
44
+ const existing = (await readJson(pendingFile(stateDir)));
45
+ if (existing?.key)
46
+ return existing.key;
47
+ const keys = await generateKeyPair();
48
+ await writeFileDurable(pendingFile(stateDir), JSON.stringify({ v: 1, key: keys.privateKeyPem }));
49
+ return keys.privateKeyPem;
50
+ }
51
+ /** `agent identity`: reuse or generate the pending key and return its CSR. Safe to repeat. */
52
+ export async function agentIdentityCsr(stateDir) {
53
+ return createCsr(await pendingKey(stateDir));
54
+ }
55
+ /** Validates an identity response against the pending key and durably installs it as the agent document. */
56
+ export async function installIdentity(stateDir, response, hooks = {}) {
57
+ const pending = (await readJson(pendingFile(stateDir)));
58
+ if (!pending?.key)
59
+ throw new Error('no pending key: run `agent identity` first');
60
+ validateInstalledCertificate({
61
+ cert: response.certificate,
62
+ ca: response.ca,
63
+ keyPem: pending.key,
64
+ uri: identityUri('node', response.node.id),
65
+ });
66
+ const doc = {
67
+ ...parseCredential({
68
+ v: 1,
69
+ url: response.url,
70
+ ca: response.ca,
71
+ cert: response.certificate,
72
+ key: pending.key,
73
+ }),
74
+ nodeId: response.node.id,
75
+ nodeName: response.node.name,
76
+ };
77
+ await writeFileDurable(agentFile(stateDir), JSON.stringify(doc), 0o600, hooks);
78
+ await fs.rm(pendingFile(stateDir), { force: true });
79
+ return doc;
80
+ }
81
+ /** `agent install-cert`: reads an identity response from stdin text. */
82
+ export async function installCertFromText(stateDir, text) {
83
+ return installIdentity(stateDir, JSON.parse(text));
84
+ }
85
+ export function describeMissingIdentity() {
86
+ return NO_AGENT_IDENTITY;
87
+ }
88
+ /**
89
+ * A client pinned to the credential's CA that presents its certificate. `connectUrl` only changes where the TCP
90
+ * connection goes; the server name is still verified against the credential's public host. Redirects are refused.
91
+ */
92
+ export function createTransport(credential, connectUrl) {
93
+ const dispatcher = new Agent({ connect: clientTlsOptions(credential) });
94
+ const base = new URL(connectUrl ?? credential.url).origin;
95
+ const http = (url, init) => {
96
+ let request = init;
97
+ // The runtime's FormData is not the undici package's FormData, which would stringify it. Serialize it with the
98
+ // runtime's own Response and stream the result.
99
+ if (typeof FormData !== 'undefined' && init?.body instanceof FormData) {
100
+ const encoded = new Response(init.body);
101
+ const headers = new Headers(init.headers);
102
+ headers.set('content-type', encoded.headers.get('content-type') ?? 'multipart/form-data');
103
+ request = { ...init, headers, body: encoded.body, duplex: 'half' };
104
+ }
105
+ return undiciFetch(url, {
106
+ ...request,
107
+ redirect: 'error',
108
+ dispatcher,
109
+ });
110
+ };
111
+ return { http, base, dispatcher, close: () => dispatcher.close() };
112
+ }
113
+ export function certificateWindow(certPem) {
114
+ const x = new crypto.X509Certificate(certPem);
115
+ return { notBefore: Date.parse(x.validFrom), notAfter: Date.parse(x.validTo) };
116
+ }
117
+ /** When to renew: two thirds of the lifetime plus up to ten percent random jitter, so a fleet does not sign together. */
118
+ export function renewalTime(certPem, random = Math.random()) {
119
+ const { notBefore, notAfter } = certificateWindow(certPem);
120
+ const life = notAfter - notBefore;
121
+ return notBefore + (life * 2) / 3 + random * life * 0.1;
122
+ }
123
+ /** Sends a CSR for the existing key, validates the result, saves it durably and returns the new document. */
124
+ export async function renewCertificate(stateDir, doc, post, hooks = {}) {
125
+ const { certificate } = await post(await createCsr(doc.key));
126
+ validateInstalledCertificate({
127
+ cert: certificate,
128
+ ca: doc.ca,
129
+ keyPem: doc.key,
130
+ uri: identityUri('node', doc.nodeId),
131
+ });
132
+ const next = { ...doc, cert: certificate };
133
+ await writeFileDurable(agentFile(stateDir), JSON.stringify(next), 0o600, hooks);
134
+ return next;
135
+ }
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { WebSocket } from 'ws';
3
+ import type { AgentDocument } from './agent-credential.ts';
4
+ import type { Credential } from './pki.ts';
3
5
  import type { ContainerSummary, DockerApi } from './docker-api.ts';
4
6
  import type { AgentLiveCommand, AgentLiveFrame } from './protocol.ts';
5
7
  import type { LiveEngineOptions } from './live.ts';
@@ -13,7 +15,8 @@ export interface AgentContext {
13
15
  stateDir: string;
14
16
  apply: boolean;
15
17
  controlUrl: string;
16
- token: string | undefined;
18
+ /** The node credential presented to control; absent only in tests with a fake `http`. */
19
+ credential?: Credential;
17
20
  /** Runs `wg` / `ip` for the mesh. */
18
21
  host?: HostRunner;
19
22
  /** Manage the WireGuard mesh (generate a key, report it, reconcile `wg0`). */
@@ -22,14 +25,14 @@ export interface AgentContext {
22
25
  /** Host port range (`LOW-HIGH`) for rolling-rollout revisions; default 20000-20999. */
23
26
  rolloutPorts?: string;
24
27
  }
25
- export declare function defaultContext(): AgentContext;
28
+ export declare function defaultContext(doc?: AgentDocument | undefined): AgentContext;
26
29
  export type AgentLiveControlFrame = AgentLiveCommand;
27
30
  export type AgentLiveOutputFrame = AgentLiveFrame;
28
31
  interface LiveChannelHandlers {
29
32
  onLive?: (frame: AgentLiveControlFrame) => void | Promise<void>;
30
33
  onDisconnect?: () => void;
31
34
  }
32
- type WebSocketFactory = (url: string, token: string) => WebSocket;
35
+ type WebSocketFactory = (url: string, credential: Credential | undefined) => WebSocket;
33
36
  /** Keeps one outbound stream open and falls back to a bounded HTTP wait when an intermediary rejects WebSockets. */
34
37
  export declare class AgentWakeChannel {
35
38
  private readonly ctx;
@@ -66,6 +69,13 @@ export declare class AgentLiveOperations {
66
69
  }
67
70
  /** The container's address on the project network, which health checks dial. */
68
71
  export declare function containerIp(ctx: AgentContext, project: string, container: string): Promise<string>;
72
+ /** True when this process runs inside a container: Docker, containerd, Podman or Kubernetes. */
73
+ export declare function runsInContainer(): boolean;
74
+ /**
75
+ * Health checks dial the container's address on the project network. An agent that runs as a plain host process
76
+ * cannot reach it on a machine where Docker lives in a VM (macOS, Windows), and the bare timeout does not say why.
77
+ */
78
+ export declare function outsideContainerHint(health: HealthCheck, inContainer?: boolean): string;
69
79
  export declare function waitForHealth(ctx: AgentContext, project: string, container: string, service: string, health: HealthCheck | null | undefined): Promise<void>;
70
80
  export { parseDuration };
71
81
  export interface PublicListener {
@@ -89,10 +99,6 @@ export declare function reconcile(desiredState: DesiredState, ctx?: AgentContext
89
99
  /** Reports this node's WireGuard public key when control has not seen the current one yet. */
90
100
  export declare function reportWgKey(ctx: AgentContext): Promise<void>;
91
101
  export declare function pollOnce(ctx?: AgentContext): Promise<ProjectStatus[]>;
92
- interface Creds {
93
- agentToken: string;
94
- nodeName?: string;
95
- controlUrl?: string;
96
- }
97
- export declare function loadOrEnroll(): Promise<Creds>;
102
+ /** Loads the persisted identity, or enrolls with a join key. Without either the agent refuses to start. */
103
+ export declare function loadOrEnroll(): Promise<AgentDocument>;
98
104
  export declare function main(): Promise<void>;