@forgezero/agent 0.1.2 → 0.1.10

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 (55) hide show
  1. package/README.md +70 -4
  2. package/dist/attestation-client.d.ts +22 -0
  3. package/dist/attestation-client.test.d.ts +1 -0
  4. package/dist/cli/agent-install.d.ts +82 -0
  5. package/dist/cli/agent-install.test.d.ts +1 -0
  6. package/dist/cli/custody.d.ts +35 -0
  7. package/dist/cli/genesis.d.ts +79 -0
  8. package/dist/cli/index.d.ts +12 -0
  9. package/dist/cli/options.test.d.ts +1 -0
  10. package/dist/cli/run.d.ts +92 -0
  11. package/dist/cli/run.test.d.ts +1 -0
  12. package/dist/compute.d.ts +122 -0
  13. package/dist/compute.js +150 -0
  14. package/dist/compute.test.d.ts +1 -0
  15. package/dist/control.d.ts +57 -0
  16. package/dist/control.test.d.ts +1 -0
  17. package/dist/definition.d.ts +34 -0
  18. package/dist/definition.js +159 -0
  19. package/dist/definition.test.d.ts +1 -0
  20. package/dist/deployment-pull.d.ts +61 -0
  21. package/dist/deployment-pull.test.d.ts +1 -0
  22. package/dist/deployment-runner.d.ts +23 -0
  23. package/dist/deployment-runner.js +199 -0
  24. package/dist/deployment-runner.test.d.ts +1 -0
  25. package/dist/deployment-watch.d.ts +36 -0
  26. package/dist/deployment-watch.test.d.ts +1 -0
  27. package/dist/deployment.d.ts +100 -0
  28. package/dist/deployment.test.d.ts +1 -0
  29. package/dist/fz-agent.js +2934 -182
  30. package/dist/fz.js +1270 -0
  31. package/dist/guest-enrolment.d.ts +29 -0
  32. package/dist/guest-enrolment.js +88 -0
  33. package/dist/guest-enrolment.test.d.ts +1 -0
  34. package/dist/index.d.ts +50 -4
  35. package/dist/metal-helper-socket.d.ts +15 -0
  36. package/dist/metal-helper-socket.js +1123 -0
  37. package/dist/metal-helper-socket.test.d.ts +1 -0
  38. package/dist/metal-isolation.d.ts +14 -0
  39. package/dist/metal-isolation.test.d.ts +1 -0
  40. package/dist/metal-provision.d.ts +85 -0
  41. package/dist/metal-provision.js +1014 -0
  42. package/dist/metal-provision.test.d.ts +1 -0
  43. package/dist/node-vault.d.ts +24 -0
  44. package/dist/node-vault.js +211 -0
  45. package/dist/node-vault.test.d.ts +1 -0
  46. package/dist/provision.d.ts +50 -2
  47. package/dist/provision.js +286 -12
  48. package/dist/provisioning-pull.d.ts +75 -0
  49. package/dist/provisioning-pull.js +188 -0
  50. package/dist/provisioning-pull.test.d.ts +1 -0
  51. package/dist/signed-node-http.d.ts +14 -0
  52. package/dist/snp-attestation.d.ts +18 -0
  53. package/dist/snp-attestation.test.d.ts +1 -0
  54. package/dist/socket.d.ts +4 -23
  55. package/package.json +27 -9
@@ -0,0 +1,29 @@
1
+ import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
+ export interface GuestBinding {
3
+ nodeKey: string;
4
+ computeReference: string;
5
+ projectKey: string;
6
+ environmentKey: string;
7
+ tenantSlug: string;
8
+ }
9
+ export interface GuestEnrolmentOptions {
10
+ apiUrl: string;
11
+ /** Already-unsealed systemd credential. Production uses this path. */
12
+ token?: string;
13
+ /** Legacy/dev input; removed after a successful exchange. */
14
+ tokenPath?: string;
15
+ /** Removes the encrypted one-time source after the binding is durable. */
16
+ consume?: () => void | Promise<void>;
17
+ /** Non-secret coordinates needed to recover the tenant route after restart. */
18
+ statePath: string;
19
+ nodeKey: string;
20
+ keys: NodeKeyPair;
21
+ label?: string;
22
+ gitDeployPublicKey?: string;
23
+ fetch?: (input: URL, init: RequestInit) => Promise<Response>;
24
+ requestTimeoutMs?: number;
25
+ }
26
+ /** Load only routing coordinates. The node seed and enrolment token never enter this file. */
27
+ export declare function loadGuestBinding(path: string, expectedNodeKey: string): GuestBinding | null;
28
+ /** Exchange the cloud-init capability for the guest's permanent public identity. */
29
+ export declare function enrolGuestIdentity(options: GuestEnrolmentOptions): Promise<GuestBinding>;
@@ -0,0 +1,88 @@
1
+ // src/guest-enrolment.ts
2
+ import {
3
+ chmodSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ statSync,
9
+ unlinkSync,
10
+ writeFileSync
11
+ } from "node:fs";
12
+ import { dirname } from "node:path";
13
+ var validBinding = (value, expectedNodeKey) => {
14
+ if (!value || typeof value !== "object")
15
+ return false;
16
+ const row = value;
17
+ return ["nodeKey", "computeReference", "projectKey", "environmentKey", "tenantSlug"].every((key) => typeof row[key] === "string" && row[key].length > 0) && (!expectedNodeKey || row.nodeKey === expectedNodeKey);
18
+ };
19
+ function loadGuestBinding(path, expectedNodeKey) {
20
+ if (!existsSync(path))
21
+ return null;
22
+ const mode = statSync(path).mode & 511;
23
+ if ((mode & 63) !== 0) {
24
+ throw new Error(`guest enrolment state at ${path} is not private`);
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(readFileSync(path, "utf8"));
29
+ } catch {
30
+ throw new Error(`guest enrolment state at ${path} is malformed`);
31
+ }
32
+ if (!validBinding(parsed, expectedNodeKey)) {
33
+ throw new Error(`guest enrolment state at ${path} does not match this node identity`);
34
+ }
35
+ return parsed;
36
+ }
37
+ function persistGuestBinding(path, binding) {
38
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
39
+ const temporary = `${path}.next`;
40
+ writeFileSync(temporary, `${JSON.stringify(binding)}
41
+ `, { mode: 384 });
42
+ chmodSync(temporary, 384);
43
+ renameSync(temporary, path);
44
+ }
45
+ async function enrolGuestIdentity(options) {
46
+ const token = options.token?.trim() ?? (options.tokenPath ? readFileSync(options.tokenPath, "utf8").trim() : "");
47
+ if (!token.startsWith("fze_"))
48
+ throw new Error("guest enrolment credential is malformed");
49
+ const url = new URL(options.apiUrl);
50
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/compute/enrol`.replace(/\/+/g, "/");
51
+ url.search = "";
52
+ url.hash = "";
53
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
54
+ method: "POST",
55
+ headers: { "content-type": "application/json" },
56
+ body: JSON.stringify({
57
+ token,
58
+ label: options.label,
59
+ gitDeployPublicKey: options.gitDeployPublicKey,
60
+ publicKeys: {
61
+ ed25519: options.keys.ed25519.publicKey,
62
+ mlDsa: options.keys.mlDsa.publicKey
63
+ }
64
+ }),
65
+ signal: AbortSignal.timeout(options.requestTimeoutMs ?? 15000)
66
+ });
67
+ const payload = await response.json().catch(() => null);
68
+ if (!response.ok || !payload?.ok || payload.nodeKey !== options.nodeKey || !payload.computeReference || !payload.projectKey || !payload.environmentKey || !payload.tenantSlug) {
69
+ throw new Error(payload?.error?.message || `guest enrolment returned HTTP ${response.status}`);
70
+ }
71
+ const binding = {
72
+ nodeKey: payload.nodeKey,
73
+ computeReference: payload.computeReference,
74
+ projectKey: payload.projectKey,
75
+ environmentKey: payload.environmentKey,
76
+ tenantSlug: payload.tenantSlug
77
+ };
78
+ persistGuestBinding(options.statePath, binding);
79
+ if (options.consume)
80
+ await options.consume();
81
+ else if (options.tokenPath)
82
+ unlinkSync(options.tokenPath);
83
+ return binding;
84
+ }
85
+ export {
86
+ loadGuestBinding,
87
+ enrolGuestIdentity
88
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type NodeKeyPair } from '@forgezero/runtime/identity';
3
3
  import { startAgent, type AgentOptions, type AttestationSource } from './socket';
4
4
  import type { SecretCache } from './cache';
5
5
  /**
6
- * fz-agent — runs on the metal, driven over SSH before the platform exists and
6
+ * fz-agent — runs inside managed compute, driven locally before the platform exists and
7
7
  * over HTTPS after it does. One implementation, two front doors: bootstrap is
8
8
  * not a special case, it is the general case run first, because a tenant's own
9
9
  * bare metal has no ForgeZero on it either.
@@ -17,7 +17,7 @@ import type { SecretCache } from './cache';
17
17
  * ## What it serves
18
18
  *
19
19
  * A unix socket that SIGNS and never surrenders the key. `@forgezero/vault`
20
- * discovers `/run/forgezero.sock` and prefers it over `FORGEZERO_API_KEY`, so
20
+ * discovers `/run/forgezero/vault.sock` and prefers it over `FORGEZERO_API_KEY`, so
21
21
  * moving an application onto managed compute means deleting an environment
22
22
  * variable rather than changing a line of code — and a machine that used to hold
23
23
  * a signing seed now holds nothing an attacker can take.
@@ -25,11 +25,34 @@ import type { SecretCache } from './cache';
25
25
  * That preference was implemented in the client long before anything listened.
26
26
  * See `socket.ts` for why it signs rather than handing back a token.
27
27
  */
28
- export declare const VERSION = "0.1.2";
28
+ export declare const VERSION = "0.1.10";
29
29
  export { startAgent, handleRequest } from './socket';
30
30
  export type { AgentOptions, AttestationSource, Request, Response } from './socket';
31
31
  export { createSecretCache, CacheError } from './cache';
32
32
  export type { SecretCache, CacheOptions } from './cache';
33
+ export { createDeploymentManager, DeploymentError } from './deployment';
34
+ export type { DeploymentManager, DeploymentOptions, DeploymentRequest, DeploymentResult } from './deployment';
35
+ export { DEFAULT_CONTROL_SOCKET, requestControl, startControlServer } from './control';
36
+ export { pullDeploymentOnce, startDeploymentPull } from './deployment-pull';
37
+ export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from './deployment-pull';
38
+ export { readStaticDeploymentState, writeStaticDeploymentState, startStaticDeploymentWatch } from './deployment-watch';
39
+ export type { StaticDeploymentState, StaticDeploymentWatchOptions } from './deployment-watch';
40
+ export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
41
+ export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
42
+ export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
43
+ export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
44
+ export { createNodeVaultCache, startNodeVaultSync, tenantNodeApiUrl } from './node-vault';
45
+ export type { NodeVaultOptions, NodeVaultSyncOptions } from './node-vault';
46
+ export { allocateAddress, allocateCpuPool, cloudInit, guestNameFor, provisionMetalGuest, removeMetalGuest } from './metal-provision';
47
+ export { validateMetalProfile } from './metal-provision';
48
+ export type { GuestManifest, MetalCommandResult, MetalCpuPool, MetalExec, MetalProvisionProfile } from './metal-provision';
49
+ export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper } from './metal-helper-socket';
50
+ export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
51
+ export { createSnpAttestationSource } from './snp-attestation';
52
+ export type { SnpAttestationOptions } from './snp-attestation';
53
+ export { attestNodeOnce, startNodeAttestation } from './attestation-client';
54
+ export type { NodeAttestationOptions } from './attestation-client';
55
+ export { applyMetalIsolation, metalGuestSliceUnit, metalHousekeepingDropIn } from './metal-isolation';
33
56
  /**
34
57
  * The node seed, on disk, owner-only.
35
58
  *
@@ -42,6 +65,10 @@ export declare function loadOrCreateSeed(path: string): Uint8Array;
42
65
  export interface AgentConfig {
43
66
  socketPath?: string;
44
67
  seedPath?: string;
68
+ /** Prefer this already-unsealed systemd credential over a persistent file. */
69
+ seedCredential?: string;
70
+ /** Injected by tests or an embedding process; never persisted by the agent. */
71
+ seed?: Uint8Array;
45
72
  /** As the platform knows this node. Absent before enrolment. */
46
73
  nodeKey?: string;
47
74
  attestation?: AttestationSource;
@@ -70,8 +97,26 @@ export interface AgentConfig {
70
97
  * A tenant's application ships with `@forgezero/vault` and cannot be asked to
71
98
  * follow a path this repository moved.
72
99
  */
73
- export declare const DEFAULT_SOCKET_PATH = "/run/forgezero.sock";
100
+ export declare const DEFAULT_SOCKET_PATH = "/run/forgezero/vault.sock";
74
101
  export declare const DEFAULT_SEED_PATH = "/var/lib/forgezero/node.seed";
102
+ export declare const DEFAULT_SEED_CREDENTIAL = "agent-seed";
103
+ export declare const DEFAULT_ENROLMENT_STATE_PATH = "/var/lib/forgezero/enrolment.json";
104
+ /**
105
+ * Read a systemd credential made available only to this unit.
106
+ *
107
+ * `CREDENTIALS_DIRECTORY` is a private, read-only tmpfs populated by PID 1.
108
+ * The encrypted blob remains host-bound on disk and this plaintext disappears
109
+ * with the service. A configured credential never falls back to a disk file:
110
+ * doing so would turn a missing security control into a silent downgrade.
111
+ */
112
+ export declare function loadSeedCredential(name?: string, directory?: string | undefined): Uint8Array;
113
+ /** Read one text credential from PID 1's private tmpfs, never from an env value. */
114
+ export declare function loadTextCredential(name: string, directory?: string | undefined): string;
115
+ /** Explicit allow-list over systemd credentials exposed to checked-in pipeline steps. */
116
+ export declare function createSystemdDeploymentSecrets(names: string | undefined, directory?: string | undefined): {
117
+ has(name: string): boolean;
118
+ get(name: string): Promise<string>;
119
+ } | undefined;
75
120
  /**
76
121
  * Bring the agent up.
77
122
  *
@@ -83,4 +128,5 @@ export declare function runAgent(config?: AgentConfig): {
83
128
  server: ReturnType<typeof startAgent>;
84
129
  keys: NodeKeyPair;
85
130
  nodeKey: string;
131
+ setCache(cache: SecretCache): void;
86
132
  };
@@ -0,0 +1,15 @@
1
+ import { type Server } from 'node:net';
2
+ import { type MetalExec, type MetalProvisionProfile } from './metal-provision';
3
+ import type { ProvisionResult, RemoteProvisionClaim } from './provisioning-pull';
4
+ export declare const DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
5
+ export declare const spawnMetalCommand: MetalExec;
6
+ /** Root-owned server. Its fixed profile is never selectable by the caller. */
7
+ export declare function startMetalHelper(options: {
8
+ profile: MetalProvisionProfile;
9
+ socketPath?: string;
10
+ exec?: MetalExec;
11
+ }): {
12
+ server: Server;
13
+ stop(): Promise<void>;
14
+ };
15
+ export declare function requestMetalProvision(claim: RemoteProvisionClaim, socketPath?: string): Promise<ProvisionResult>;