@forgezero/agent 0.1.31 → 0.1.33
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.
- package/README.md +60 -5
- package/dist/agent-heartbeat.d.ts +12 -2
- package/dist/agent-heartbeat.js +416 -60
- package/dist/agent-update-helper.d.ts +41 -2
- package/dist/agent-update-helper.js +361 -43
- package/dist/agent-update.d.ts +1 -0
- package/dist/agent-update.js +34 -2
- package/dist/attestation-client.d.ts +2 -0
- package/dist/cli/agent-install.d.ts +6 -0
- package/dist/cli/genesis.d.ts +7 -48
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/session-store.d.ts +22 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment.d.ts +3 -0
- package/dist/egress-policy.d.ts +50 -0
- package/dist/fz-agent.js +5088 -1699
- package/dist/fz.js +10334 -7613
- package/dist/git-egress.d.ts +35 -0
- package/dist/index.d.ts +19 -2
- package/dist/metal-helper-socket.js +12 -1
- package/dist/metal-provision.d.ts +2 -0
- package/dist/metal-provision.js +12 -1
- package/dist/migration-pull.d.ts +2 -0
- package/dist/migration-pull.js +18 -6
- package/dist/node-vault.d.ts +2 -0
- package/dist/node-vault.js +2 -1
- package/dist/provision.d.ts +13 -1
- package/dist/provision.js +540 -52
- package/dist/provisioning-pull.d.ts +2 -0
- package/dist/provisioning-pull.js +20 -7
- package/dist/telemetry-runtime.d.ts +20 -0
- package/dist/telemetry.d.ts +56 -0
- package/dist/version.d.ts +1 -1
- package/package.json +10 -10
|
@@ -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
|
@@ -29,11 +29,16 @@ export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './
|
|
|
29
29
|
export { materializeWarpMdm, renderWarpMdm } from './warp-config';
|
|
30
30
|
export { compareVersions, restoreAgentRelease, selectAgentRelease, stageAgentRelease, validateAgentRelease, DEFAULT_AGENT_RELEASE_ROOT, DEFAULT_AGENT_UPDATE_SOCKET, MAX_AGENT_TARBALL_BYTES } from './agent-update';
|
|
31
31
|
export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResult } from './agent-update';
|
|
32
|
-
export { activateAgentRelease, probeAgentSocket, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
33
|
-
export type { AgentUpdateRequest, AgentUpdateResponse } from './agent-update-helper';
|
|
32
|
+
export { activateAgentRelease, probeAgentSocket, readAgentUpdateReceipt, recoverInterruptedAgentUpdate, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_JOURNAL, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
33
|
+
export type { AgentUpdateOutcome, AgentUpdateReceipt, AgentUpdateRequest, AgentUpdateResponse } from './agent-update-helper';
|
|
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';
|
|
@@ -57,6 +62,8 @@ export interface AgentConfig {
|
|
|
57
62
|
/** systemd-owned listener fd; preserves the application endpoint across replacement. */
|
|
58
63
|
listenFd?: number;
|
|
59
64
|
seedPath?: string;
|
|
65
|
+
/** Explicit development-only permission to persist a seed file. */
|
|
66
|
+
allowFileSeed?: boolean;
|
|
60
67
|
/** Prefer this already-unsealed systemd credential over a persistent file. */
|
|
61
68
|
seedCredential?: string;
|
|
62
69
|
/** Injected by tests or an embedding process; never persisted by the agent. */
|
|
@@ -92,6 +99,16 @@ export interface AgentConfig {
|
|
|
92
99
|
export declare const DEFAULT_SOCKET_PATH = "/run/forgezero/vault.sock";
|
|
93
100
|
export declare const DEFAULT_SEED_PATH = "/var/lib/forgezero/node.seed";
|
|
94
101
|
export declare const DEFAULT_SEED_CREDENTIAL = "agent-seed";
|
|
102
|
+
/**
|
|
103
|
+
* Resolve identity custody without a silent production downgrade. A missing
|
|
104
|
+
* systemd credential is an outage to fix, not permission to create plaintext
|
|
105
|
+
* identity material under /var/lib.
|
|
106
|
+
*/
|
|
107
|
+
export declare function configuredAgentSeed(options: {
|
|
108
|
+
credential?: string;
|
|
109
|
+
path?: string;
|
|
110
|
+
allowFileSeed?: boolean;
|
|
111
|
+
}): Uint8Array;
|
|
95
112
|
export declare const DEFAULT_ENROLMENT_STATE_PATH = "/var/lib/forgezero/enrolment.json";
|
|
96
113
|
/** Resolve the one descriptor systemd passes for the application Vault socket. */
|
|
97
114
|
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[];
|
package/dist/metal-provision.js
CHANGED
|
@@ -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) {
|
package/dist/migration-pull.d.ts
CHANGED
|
@@ -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';
|
package/dist/migration-pull.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
119
|
+
const renew = () => post(options, "renew", {
|
|
118
120
|
migrationKey: claim.migrationKey,
|
|
119
121
|
claimToken: claim.claimToken
|
|
120
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/node-vault.d.ts
CHANGED
|
@@ -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): {
|
package/dist/node-vault.js
CHANGED
|
@@ -261,7 +261,8 @@ function startNodeVaultSync(cache, options = {}) {
|
|
|
261
261
|
const tick = () => {
|
|
262
262
|
if (stopped || active)
|
|
263
263
|
return;
|
|
264
|
-
|
|
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
|
});
|
package/dist/provision.d.ts
CHANGED
|
@@ -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
|
*
|