@forgezero/agent 0.1.32 → 0.1.34

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.
@@ -0,0 +1,35 @@
1
+ export type GitNetworkProtocol = 'https' | 'ssh';
2
+ export interface GitNetworkTarget {
3
+ protocol: GitNetworkProtocol;
4
+ hostname: string;
5
+ port: number;
6
+ /** The exact name (and optional port) whose SSH key was accepted by the operator. */
7
+ hostKeyAlias?: string;
8
+ }
9
+ export interface PinnedGitTarget extends GitNetworkTarget {
10
+ /** Public addresses captured in one DNS answer and reused for the whole Git operation. */
11
+ addresses: readonly string[];
12
+ }
13
+ export type GitHostResolver = (hostname: string) => Promise<readonly string[]>;
14
+ export declare class GitEgressError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ /**
18
+ * True only for ordinary globally routable unicast destinations.
19
+ *
20
+ * This intentionally rejects documentation, benchmarking, transition,
21
+ * multicast and address-translation ranges as well as RFC1918/link-local
22
+ * space. A Git forge has no operational reason to live on one of them, and
23
+ * allowing them turns a repository URL into a network probe.
24
+ */
25
+ export declare function isPublicGitAddress(value: string): boolean;
26
+ /** Parse the only remote protocols the credential-bearing Agent permits. */
27
+ export declare function gitNetworkTarget(repository: string): GitNetworkTarget | null;
28
+ export declare const resolveSystemGitHost: GitHostResolver;
29
+ /**
30
+ * Resolve once, reject the complete answer if any address is not public, and
31
+ * return only the captured addresses. Git must then be forced to use these
32
+ * values rather than resolving the hostname again.
33
+ */
34
+ export declare function resolvePinnedGitTarget(repository: string, resolveHost?: GitHostResolver): Promise<PinnedGitTarget | null>;
35
+ export declare const curlResolveValue: (target: PinnedGitTarget) => string;
package/dist/index.d.ts CHANGED
@@ -34,6 +34,11 @@ export type { AgentUpdateOutcome, AgentUpdateReceipt, AgentUpdateRequest, AgentU
34
34
  export { DEFAULT_SOFTWARE_HELPER_SOCKET, requestSoftware, startSoftwareHelper, SOFTWARE_HELPER_GROUP, SOFTWARE_HELPER_UNIT_PATH } from './software-helper';
35
35
  export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements, OS_CATALOG, SOFTWARE_CATALOG } from './software';
36
36
  export type { CatalogStatus, DeploymentChannel, OsCatalogEntry, SoftwareCatalogEntry, SoftwareCommandResult, SoftwareExec, SoftwareId, SoftwareObservation, SoftwareRequirement } from './software';
37
+ export { AGENT_EGRESS_TABLE, BLOCKED_IPV4, BLOCKED_IPV6, SYSTEMD_RESOLVED_ADDRESS, SYSTEMD_RESOLVED_STUB, applyAgentEgressPolicy, normalizeEgressTcpPorts, renderAgentEgressNft, superviseAgentEgressPolicy, systemdAgentEgressDirectives, verifyAgentEgressPolicy } from './egress-policy';
38
+ export type { EgressCommand, EgressCommandResult } from './egress-policy';
39
+ export type { LoopbackEgressGrant } from './egress-policy';
40
+ export { AGENT_TELEMETRY_EVENTS, AGENT_TELEMETRY_OPERATIONS, AGENT_TELEMETRY_OUTCOMES, createAgentTelemetry, resolveAgentTelemetryConfig } from './telemetry';
41
+ export type { AgentOperationObservation, AgentTelemetry, AgentTelemetryConfig, AgentTelemetryEvent, AgentTelemetryOperation, AgentTelemetryOutcome, AgentTelemetryState } from './telemetry';
37
42
  export { heartbeatAgentOnce, observeAgentHost, startAgentHeartbeat } from './agent-heartbeat';
38
43
  export type { AgentHeartbeatOptions, AgentHeartbeatResponse, AgentObservation } from './agent-heartbeat';
39
44
  export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
@@ -43,6 +48,8 @@ export { attestNodeOnce, startNodeAttestation } from './attestation-client';
43
48
  export type { NodeAttestationOptions } from './attestation-client';
44
49
  export { applyMetalIsolation, metalGuestSliceUnit, metalHousekeepingDropIn } from './metal-isolation';
45
50
  export { assertSupportedGuestImage, SUPPORTED_GUEST_IMAGE } from './ubuntu';
51
+ export { calibrateHttpConcurrency, localCalibrationEndpoint } from './capacity-calibration';
52
+ export type { CapacityCalibration, CapacityCalibrationOptions, CapacityStage } from './capacity-calibration';
46
53
  /**
47
54
  * The node seed, on disk, owner-only.
48
55
  *
@@ -57,6 +64,8 @@ export interface AgentConfig {
57
64
  /** systemd-owned listener fd; preserves the application endpoint across replacement. */
58
65
  listenFd?: number;
59
66
  seedPath?: string;
67
+ /** Explicit development-only permission to persist a seed file. */
68
+ allowFileSeed?: boolean;
60
69
  /** Prefer this already-unsealed systemd credential over a persistent file. */
61
70
  seedCredential?: string;
62
71
  /** Injected by tests or an embedding process; never persisted by the agent. */
@@ -92,6 +101,16 @@ export interface AgentConfig {
92
101
  export declare const DEFAULT_SOCKET_PATH = "/run/forgezero/vault.sock";
93
102
  export declare const DEFAULT_SEED_PATH = "/var/lib/forgezero/node.seed";
94
103
  export declare const DEFAULT_SEED_CREDENTIAL = "agent-seed";
104
+ /**
105
+ * Resolve identity custody without a silent production downgrade. A missing
106
+ * systemd credential is an outage to fix, not permission to create plaintext
107
+ * identity material under /var/lib.
108
+ */
109
+ export declare function configuredAgentSeed(options: {
110
+ credential?: string;
111
+ path?: string;
112
+ allowFileSeed?: boolean;
113
+ }): Uint8Array;
95
114
  export declare const DEFAULT_ENROLMENT_STATE_PATH = "/var/lib/forgezero/enrolment.json";
96
115
  /** Resolve the one descriptor systemd passes for the application Vault socket. */
97
116
  export declare function systemdListenFd(environment?: Record<string, string | undefined>, pid?: number): number | undefined;
@@ -166,6 +166,7 @@ import {
166
166
  writeFileSync
167
167
  } from "node:fs";
168
168
  import { dirname, isAbsolute, join } from "node:path";
169
+ import { isIP } from "node:net";
169
170
  var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
170
171
  var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
171
172
  var SHA256 = /^[a-f0-9]{64}$/;
@@ -215,6 +216,14 @@ function validateMetalProfile(profile) {
215
216
  throw new MetalProvisionError("metal paths must be absolute");
216
217
  }
217
218
  new URL(profile.apiUrl);
219
+ let telemetryEndpoint;
220
+ try {
221
+ telemetryEndpoint = new URL(profile.agentTelemetryEndpoint);
222
+ } catch {
223
+ throw new MetalProvisionError("Agent telemetry endpoint must be an absolute public HTTPS URL");
224
+ }
225
+ if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
226
+ throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
218
227
  const imageKeys = Object.keys(profile.images);
219
228
  if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
220
229
  throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
@@ -331,7 +340,9 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
331
340
  content: ${base64(content)}
332
341
  `;
333
342
  function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
343
+ validateMetalProfile(profile);
334
344
  const agentBun = "/usr/local/lib/forgezero/bun";
345
+ const telemetryEndpoint = `'${profile.agentTelemetryEndpoint.replace(/'/g, `'\\''`)}'`;
335
346
  const attestationSetup = attested ? `# The report device is not part of the encryption path, so a guest can appear
336
347
  # healthy and encrypted while attestation is silently impossible. Install and
337
348
  # load the driver shipped by the one pinned image before the agent starts. Do
@@ -355,7 +366,7 @@ fi
355
366
  if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
356
367
  env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
357
368
  fi
358
- env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
369
+ env FZ_API=${profile.apiUrl} OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_AGENT_EGRESS_ENFORCE=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
359
370
  `;
360
371
  }
361
372
  function cloudInit(profile, claim, manifest) {
@@ -27,6 +27,8 @@ export interface MetalProvisionProfile {
27
27
  /** One-time compatibility source for pre-Agent platform guests. */
28
28
  legacyStateDir?: string;
29
29
  apiUrl: string;
30
+ /** Root-owned collector coordinate inherited by guests, never claim-controlled. */
31
+ agentTelemetryEndpoint: string;
30
32
  images: Record<string, MetalImage>;
31
33
  /** Explicit host-observed pools. No pool means no dynamic guest authority. */
32
34
  cpuPools: readonly MetalCpuPool[];
@@ -166,6 +166,7 @@ import {
166
166
  writeFileSync
167
167
  } from "node:fs";
168
168
  import { dirname, isAbsolute, join } from "node:path";
169
+ import { isIP } from "node:net";
169
170
  var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
170
171
  var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
171
172
  var SHA256 = /^[a-f0-9]{64}$/;
@@ -215,6 +216,14 @@ function validateMetalProfile(profile) {
215
216
  throw new MetalProvisionError("metal paths must be absolute");
216
217
  }
217
218
  new URL(profile.apiUrl);
219
+ let telemetryEndpoint;
220
+ try {
221
+ telemetryEndpoint = new URL(profile.agentTelemetryEndpoint);
222
+ } catch {
223
+ throw new MetalProvisionError("Agent telemetry endpoint must be an absolute public HTTPS URL");
224
+ }
225
+ if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
226
+ throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
218
227
  const imageKeys = Object.keys(profile.images);
219
228
  if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
220
229
  throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
@@ -331,7 +340,9 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
331
340
  content: ${base64(content)}
332
341
  `;
333
342
  function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
343
+ validateMetalProfile(profile);
334
344
  const agentBun = "/usr/local/lib/forgezero/bun";
345
+ const telemetryEndpoint = `'${profile.agentTelemetryEndpoint.replace(/'/g, `'\\''`)}'`;
335
346
  const attestationSetup = attested ? `# The report device is not part of the encryption path, so a guest can appear
336
347
  # healthy and encrypted while attestation is silently impossible. Install and
337
348
  # load the driver shipped by the one pinned image before the agent starts. Do
@@ -355,7 +366,7 @@ fi
355
366
  if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
356
367
  env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
357
368
  fi
358
- env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
369
+ env FZ_API=${profile.apiUrl} OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_AGENT_EGRESS_ENFORCE=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
359
370
  `;
360
371
  }
361
372
  function cloudInit(profile, claim, manifest) {
@@ -1,4 +1,5 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
2
3
  export type MigrationNetwork = 'private-lan' | 'cloudflare-warp';
3
4
  export type MigrationEvidenceProfile = 'forgezero-platform' | 'tenant-managed';
4
5
  export type MigrationAction = 'network-ready' | 'database-member-ready' | 'api-ready' | 'source-drained' | 'source-stopped';
@@ -37,6 +38,7 @@ export interface MigrationPullOptions {
37
38
  setTimer?: (callback: () => void, ms: number) => unknown;
38
39
  clearTimer?: (handle: unknown) => void;
39
40
  onEvent?: (event: string, detail?: unknown) => void;
41
+ telemetry?: AgentOperationTelemetry;
40
42
  }
41
43
  export type MigrationPullResult = {
42
44
  status: 'idle';
@@ -74,6 +74,7 @@ class MigrationClaimLostError extends Error {
74
74
  this.name = "MigrationClaimLostError";
75
75
  }
76
76
  }
77
+ var remoteOutcome = (cause) => cause instanceof SignedNodeHttpError && cause.status < 500 ? "refused" : "retryable";
77
78
  var post = (options, operation, body) => postSignedNode(options, `v1/node/migrations/${operation}`, body);
78
79
  async function complete(options, body) {
79
80
  const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
@@ -94,7 +95,8 @@ async function complete(options, body) {
94
95
  throw last;
95
96
  }
96
97
  async function pullMigrationOnce(options) {
97
- const response = await post(options, "claim", {});
98
+ const claimWork = () => post(options, "claim", {});
99
+ const response = options.telemetry ? await options.telemetry.observe("migration.claim", claimWork, (value) => value.claim ? "success" : "idle", remoteOutcome) : await claimWork();
98
100
  if (!response.claim)
99
101
  return { status: "idle" };
100
102
  const claim = response.claim;
@@ -114,10 +116,11 @@ async function pullMigrationOnce(options) {
114
116
  if (stopped || renewal)
115
117
  return;
116
118
  let retry;
117
- renewal = post(options, "renew", {
119
+ const renew = () => post(options, "renew", {
118
120
  migrationKey: claim.migrationKey,
119
121
  claimToken: claim.claimToken
120
- }).then((value) => {
122
+ });
123
+ renewal = (options.telemetry ? options.telemetry.observe("migration.renew", renew, () => "success", remoteOutcome) : renew()).then((value) => {
121
124
  expires = value.claimExpiresAtTs;
122
125
  }).catch((cause) => {
123
126
  if (cause instanceof SignedNodeHttpError && cause.status < 500) {
@@ -135,7 +138,8 @@ async function pullMigrationOnce(options) {
135
138
  schedule();
136
139
  let evidence;
137
140
  try {
138
- evidence = await options.run(claim);
141
+ const run = () => options.run(claim);
142
+ evidence = options.telemetry ? await options.telemetry.observe("migration.run", run) : await run();
139
143
  } catch (cause) {
140
144
  stopped = true;
141
145
  clearTimer(timer);
@@ -143,12 +147,16 @@ async function pullMigrationOnce(options) {
143
147
  if (lost)
144
148
  throw lost;
145
149
  const reason = (cause instanceof Error ? cause.message : String(cause)).slice(0, 2000);
146
- await complete(options, {
150
+ const acknowledge2 = () => complete(options, {
147
151
  migrationKey: claim.migrationKey,
148
152
  claimToken: claim.claimToken,
149
153
  ok: false,
150
154
  detail: reason
151
155
  });
156
+ if (options.telemetry) {
157
+ await options.telemetry.observe("migration.complete", acknowledge2, () => "success", remoteOutcome);
158
+ } else
159
+ await acknowledge2();
152
160
  return { status: "failed", claim, reason };
153
161
  }
154
162
  stopped = true;
@@ -156,12 +164,16 @@ async function pullMigrationOnce(options) {
156
164
  await renewal;
157
165
  if (lost)
158
166
  throw lost;
159
- await complete(options, {
167
+ const acknowledge = () => complete(options, {
160
168
  migrationKey: claim.migrationKey,
161
169
  claimToken: claim.claimToken,
162
170
  ok: true,
163
171
  evidence
164
172
  });
173
+ if (options.telemetry) {
174
+ await options.telemetry.observe("migration.complete", acknowledge, () => "success", remoteOutcome);
175
+ } else
176
+ await acknowledge();
165
177
  return { status: "completed", claim, evidence };
166
178
  }
167
179
  function startMigrationPull(options) {
@@ -1,6 +1,7 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import { type SecretCache } from './cache';
3
3
  import { type SignedNodeHttpOptions } from './signed-node-http';
4
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
4
5
  export interface NodeVaultOptions extends Omit<SignedNodeHttpOptions, 'apiUrl'> {
5
6
  /** Tenant node base, for example https://api.example/api/t/acme. */
6
7
  apiUrl: string;
@@ -26,6 +27,7 @@ export interface NodeVaultSyncOptions {
26
27
  setTimer?: (callback: () => void, ms: number) => unknown;
27
28
  clearTimer?: (handle: unknown) => void;
28
29
  onEvent?: (event: string, detail?: unknown) => void;
30
+ telemetry?: AgentOperationTelemetry;
29
31
  }
30
32
  /** Poll rotation cursors without overlapping calls; stop waits for the active sync. */
31
33
  export declare function startNodeVaultSync(cache: SecretCache, options?: NodeVaultSyncOptions): {
@@ -261,7 +261,8 @@ function startNodeVaultSync(cache, options = {}) {
261
261
  const tick = () => {
262
262
  if (stopped || active)
263
263
  return;
264
- active = cache.sync().then((result) => options.onEvent?.("synced", result)).catch((cause) => options.onEvent?.("sync-failed", cause)).finally(() => {
264
+ const sync = () => cache.sync();
265
+ active = (options.telemetry ? options.telemetry.observe("vault.sync", sync) : sync()).then((result) => options.onEvent?.("synced", result)).catch((cause) => options.onEvent?.("sync-failed", cause)).finally(() => {
265
266
  active = null;
266
267
  schedule();
267
268
  });
@@ -92,6 +92,12 @@ export interface UnitOptions {
92
92
  deploymentCredentials?: Record<string, string>;
93
93
  /** Enable authenticated outbound deployment claims after enrolment. */
94
94
  pullDeployments?: boolean;
95
+ /** Production/deployed service boundary. Omit for an ordinary local developer install. */
96
+ enforceEgress?: boolean;
97
+ /** Provisioning-owned runner health endpoints; repository commands cannot change them. */
98
+ runnerLoopbackPorts?: readonly number[];
99
+ /** Vetted public TCP surface for project commands. Defaults to HTTPS only. */
100
+ runnerPublicTcpPorts?: readonly number[];
95
101
  /** Enable PQ-authenticated lifecycle claims through a constrained root helper. */
96
102
  pullMigrations?: boolean;
97
103
  /** Root-owned declarative lifecycle profile; never supplied by a migration claim. */
@@ -121,6 +127,8 @@ export interface UnitOptions {
121
127
  nodeLabel?: string;
122
128
  /** Stable API ingress identity bound by the signed one-time enrolment. */
123
129
  nodeHostname?: string;
130
+ /** Provisioning-owned public HTTPS OTLP coordinate. Production fails closed when absent. */
131
+ telemetryEndpoint?: string;
124
132
  }
125
133
  export declare const DEPLOYMENT_RUNNER_USER = "forgezero-runner";
126
134
  export declare const DEPLOYMENT_GROUP = "forgezero-deploy";
@@ -135,6 +143,10 @@ export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero
135
143
  export declare const LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
136
144
  export declare const WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
137
145
  export declare const WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
146
+ export declare const AGENT_EGRESS_UNIT_PATH = "/etc/systemd/system/forgezero-agent-egress.service";
147
+ export declare const DEFAULT_RUNNER_PUBLIC_TCP_PORTS: readonly [443];
148
+ /** Root-owned policy monitor. The credential-bearing Agent is bound to it. */
149
+ export declare function agentEgressUnit(options: Pick<UnitOptions, 'binPath' | 'user' | 'repository' | 'pullDeployments' | 'runnerLoopbackPorts' | 'runnerPublicTcpPorts'>): string;
138
150
  /** Root may execute only package-owned OS strategies selected by id + version. */
139
151
  export declare function softwareHelperUnit(options: Pick<UnitOptions, 'binPath'>): string;
140
152
  /** Fixed root boundary for verified, health-gated Agent replacement. */
@@ -165,7 +177,7 @@ export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath'
165
177
  * the oneshot skip; a failed exchange keeps the encrypted token for a retry.
166
178
  */
167
179
  export declare function agentEnrolmentUnit(options: UnitOptions): string;
168
- export declare function deploymentRunnerUnit(options: Pick<UnitOptions, 'binPath' | 'deployRoot' | 'user'>): string;
180
+ export declare function deploymentRunnerUnit(options: Pick<UnitOptions, 'binPath' | 'deployRoot' | 'user' | 'enforceEgress' | 'runnerLoopbackPorts'>): string;
169
181
  /**
170
182
  * A systemd unit for the agent.
171
183
  *