@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,150 @@
1
+ // src/compute.ts
2
+ class ComputeError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "ComputeError";
8
+ }
9
+ }
10
+ var NAME = /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/;
11
+ var unitName = (name) => {
12
+ if (!NAME.test(name)) {
13
+ throw new ComputeError("BAD_NAME", `"${name}" is not a usable guest name — lower case, digits and hyphens, 3-32 characters.`);
14
+ }
15
+ return `forgezero-guest@${name}.service`;
16
+ };
17
+ function qemuArgv(spec) {
18
+ if (spec.vcpu < 1 || spec.memoryGib < 1) {
19
+ throw new ComputeError("BAD_SPEC", "a guest needs at least 1 vCPU and 1 GiB");
20
+ }
21
+ const argv = [
22
+ "/usr/bin/qemu-system-x86_64",
23
+ "-name",
24
+ spec.name,
25
+ "-accel",
26
+ "kvm",
27
+ "-cpu",
28
+ "host",
29
+ "-m",
30
+ `${spec.memoryGib}G`,
31
+ "-smp",
32
+ String(spec.vcpu)
33
+ ];
34
+ if (spec.confidential) {
35
+ const { cbitpos, reducedPhysBits, policy } = spec.confidential;
36
+ argv.push("-machine", `q35,confidential-guest-support=snp,memory-backend=ram`, "-object", `memory-backend-memfd,id=ram,size=${spec.memoryGib}G,share=true`, "-object", `sev-snp-guest,id=snp,cbitpos=${cbitpos},reduced-phys-bits=${reducedPhysBits},policy=${policy}`, "-bios", "/usr/share/ovmf/OVMF.fd");
37
+ } else {
38
+ argv.push("-machine", "q35");
39
+ }
40
+ argv.push("-drive", `file=${spec.disk},format=raw,if=none,id=disk0,cache=none,aio=native`, "-device", "virtio-blk-pci,drive=disk0,iommu_platform=on", "-drive", `file=${spec.seed},format=raw,if=none,id=seed0,readonly=on`, "-device", "virtio-blk-pci,drive=seed0,iommu_platform=on", "-netdev", spec.tap ? `tap,id=net0,ifname=${spec.tap},script=no,downscript=no` : `bridge,id=net0,br=${spec.bridge}`, "-device", `virtio-net-pci,netdev=net0,mac=${spec.mac},iommu_platform=on`, "-display", "none");
41
+ if (spec.consoleLog)
42
+ argv.push("-serial", `file:${spec.consoleLog}`);
43
+ return argv;
44
+ }
45
+ function guestUnit(spec) {
46
+ const argv = qemuArgv(spec).map((part) => /[\s"']/.test(part) ? JSON.stringify(part) : part).join(" ");
47
+ if (spec.egress && !spec.tap) {
48
+ throw new ComputeError("BAD_SPEC", "shaped egress needs a stable tap device");
49
+ }
50
+ const tapSetup = spec.tap ? [
51
+ `ExecStartPre=-/usr/sbin/ip link del ${spec.tap}`,
52
+ `ExecStartPre=/usr/sbin/ip tuntap add dev ${spec.tap} mode tap`,
53
+ `ExecStartPre=/usr/sbin/ip link set ${spec.tap} master ${spec.bridge}`,
54
+ `ExecStartPre=/usr/sbin/ip link set ${spec.tap} up`,
55
+ ...spec.egress ? shapeEgressUnitDirectives(spec.tap, spec.egress.guaranteedMbps, spec.egress.burstMbps) : [],
56
+ `ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
57
+ ].join(`
58
+ `) : "";
59
+ return `[Unit]
60
+ Description=ForgeZero guest ${spec.name}
61
+ Documentation=https://www.forgezero.net
62
+ After=network-online.target
63
+ Wants=network-online.target
64
+
65
+ [Service]
66
+ Type=simple
67
+ Slice=forgezero-guests.slice
68
+ ${tapSetup}
69
+ ExecStart=${argv}
70
+ Restart=always
71
+ RestartSec=5
72
+ ${spec.allowedCpus ? `AllowedCPUs=${spec.allowedCpus}
73
+ ` : ""}${spec.allowedMemoryNodes ? `AllowedMemoryNodes=${spec.allowedMemoryNodes}
74
+ ` : ""}# The affinity belongs to the unit rather than only QEMU's vCPU threads. Its
75
+ # emulator and IO threads can otherwise run on a different tenant's cores.
76
+ # The agent is NOT the parent. Restarting or upgrading fz-agent must never stop
77
+ # a tenant's compute, which is the whole reason this is a unit rather than a
78
+ # child process.
79
+ KillMode=mixed
80
+ TimeoutStopSec=120
81
+
82
+ [Install]
83
+ WantedBy=multi-user.target
84
+ `;
85
+ }
86
+ function parseCensus(psOutput) {
87
+ const guests = [];
88
+ for (const line of psOutput.split(`
89
+ `)) {
90
+ const trimmed = line.trim();
91
+ if (!trimmed || !trimmed.includes("qemu-system"))
92
+ continue;
93
+ const pid = Number(trimmed.split(/\s+/)[0]);
94
+ const name = /-name\s+([^\s]+)/.exec(trimmed)?.[1];
95
+ if (!Number.isFinite(pid) || !name)
96
+ continue;
97
+ const disks = [...trimmed.matchAll(/file=([^,\s]+)/g)].map((match) => match[1]);
98
+ guests.push({ name, pid, disks });
99
+ }
100
+ return guests;
101
+ }
102
+ var deviceInUse = (census, device) => census.some((guest) => guest.disks.includes(device));
103
+ function shapeEgressCommands(tap, mbps, burstMbps = mbps * 2) {
104
+ if (!/^[a-z][a-z0-9]{0,14}$/.test(tap)) {
105
+ throw new ComputeError("BAD_SPEC", `"${tap}" is not a device name`);
106
+ }
107
+ if (mbps <= 0)
108
+ return [
109
+ `tc qdisc del dev ${tap} root 2>/dev/null || true`,
110
+ `tc qdisc del dev ${tap} ingress 2>/dev/null || true`
111
+ ];
112
+ const ceiling = `${Math.floor(Math.max(burstMbps, mbps))}mbit`;
113
+ return [
114
+ `tc qdisc del dev ${tap} root 2>/dev/null || true`,
115
+ `tc qdisc del dev ${tap} ingress 2>/dev/null || true`,
116
+ `tc qdisc add dev ${tap} handle ffff: ingress`,
117
+ `tc filter add dev ${tap} parent ffff: protocol all u32 match u32 0 0 action police rate ${ceiling} burst 16mb conform-exceed drop`
118
+ ];
119
+ }
120
+ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
121
+ if (!/^[a-z][a-z0-9]{0,14}$/.test(tap)) {
122
+ throw new ComputeError("BAD_SPEC", `"${tap}" is not a device name`);
123
+ }
124
+ if (!Number.isInteger(guaranteedMbps) || guaranteedMbps < 0 || !Number.isInteger(burstMbps) || burstMbps < guaranteedMbps) {
125
+ throw new ComputeError("BAD_SPEC", "egress rates must be whole numbers and burst must cover the guarantee");
126
+ }
127
+ const clear = [
128
+ `ExecStartPre=-/usr/sbin/tc qdisc del dev ${tap} root`,
129
+ `ExecStartPre=-/usr/sbin/tc qdisc del dev ${tap} ingress`
130
+ ];
131
+ if (guaranteedMbps === 0)
132
+ return clear;
133
+ return [
134
+ ...clear,
135
+ `ExecStartPre=/usr/sbin/tc qdisc add dev ${tap} handle ffff: ingress`,
136
+ `ExecStartPre=/usr/sbin/tc filter add dev ${tap} parent ffff: protocol all u32 match u32 0 0 action police rate ${burstMbps}mbit burst 16mb conform-exceed drop`
137
+ ];
138
+ }
139
+ var tapFor = (guestIndex) => `tap${guestIndex}`;
140
+ export {
141
+ unitName,
142
+ tapFor,
143
+ shapeEgressUnitDirectives,
144
+ shapeEgressCommands,
145
+ qemuArgv,
146
+ parseCensus,
147
+ guestUnit,
148
+ deviceInUse,
149
+ ComputeError
150
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { type Server } from 'node:net';
2
+ import type { DeploymentManager, DeploymentRequest, DeploymentResult } from './deployment';
3
+ export declare const DEFAULT_CONTROL_SOCKET = "/run/forgezero/control.sock";
4
+ export type ControlRequest = {
5
+ op: 'deploy';
6
+ request?: DeploymentRequest;
7
+ } | {
8
+ op: 'status';
9
+ } | {
10
+ op: 'pause';
11
+ } | {
12
+ op: 'resume';
13
+ } | {
14
+ op: 'pause-key';
15
+ key: string;
16
+ } | {
17
+ op: 'resume-key';
18
+ key: string;
19
+ } | {
20
+ op: 'stop-key';
21
+ key: string;
22
+ } | {
23
+ op: 'start-key';
24
+ key: string;
25
+ } | {
26
+ op: 'cancel';
27
+ id: string;
28
+ };
29
+ export type ControlResponse = {
30
+ ok: true;
31
+ op: 'deploy';
32
+ taskId: string;
33
+ result: DeploymentResult;
34
+ } | {
35
+ ok: true;
36
+ op: 'status';
37
+ queue: ReturnType<DeploymentManager['snapshot']>;
38
+ } | {
39
+ ok: true;
40
+ op: Exclude<ControlRequest['op'], 'deploy' | 'status' | 'stop-key'>;
41
+ changed: boolean;
42
+ } | {
43
+ ok: true;
44
+ op: 'stop-key';
45
+ changed: boolean;
46
+ removed: number;
47
+ } | {
48
+ ok: false;
49
+ error: {
50
+ code: string;
51
+ message: string;
52
+ };
53
+ };
54
+ export declare function handleControl(manager: DeploymentManager, request: ControlRequest): Promise<ControlResponse>;
55
+ /** Root/operator control is deliberately separate from the tenant vault socket. */
56
+ export declare function startControlServer(manager: DeploymentManager, socketPath?: string): Server;
57
+ export declare function requestControl(request: ControlRequest, socketPath?: string): Promise<ControlResponse>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import type { Pipeline, PipelineStep } from './pipeline';
2
+ export declare const PIPELINE_VERSION: 1;
3
+ export interface SoftwareRequirement {
4
+ name: string;
5
+ check: string;
6
+ install: string;
7
+ }
8
+ export interface PipelineRole {
9
+ name: string;
10
+ count: number;
11
+ software: readonly SoftwareRequirement[];
12
+ }
13
+ export interface DeployStep extends PipelineStep {
14
+ phase: 'build' | 'release' | 'migrate' | 'health';
15
+ /** The coordinator runs this on one selected node, never on every replica. */
16
+ once?: boolean;
17
+ /** Run only when every named non-secret deployment coordinate has this value. */
18
+ when?: Readonly<Record<string, string>>;
19
+ }
20
+ export interface DeployDefinition {
21
+ version: typeof PIPELINE_VERSION;
22
+ name: string;
23
+ requireAttestation?: boolean;
24
+ roles: readonly PipelineRole[];
25
+ steps: readonly DeployStep[];
26
+ }
27
+ export declare class DefinitionError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ /** Validate parsed YAML before any command from it is allowed to run. */
31
+ export declare function parseDeployDefinition(value: unknown): DeployDefinition;
32
+ export declare function phasePipeline(definition: DeployDefinition, phase: DeployStep['phase']): Pipeline;
33
+ /** Turn one role's declared software checks into the same executable pipeline shape. */
34
+ export declare function prerequisitePipeline(definition: DeployDefinition, roleName: string): Pipeline;
@@ -0,0 +1,159 @@
1
+ // src/definition.ts
2
+ var PIPELINE_VERSION = 1;
3
+
4
+ class DefinitionError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "DefinitionError";
8
+ }
9
+ }
10
+ var record = (value, where) => {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12
+ throw new DefinitionError(`${where} must be an object.`);
13
+ }
14
+ return value;
15
+ };
16
+ var text = (value, where) => {
17
+ if (typeof value !== "string" || value.trim() === "") {
18
+ throw new DefinitionError(`${where} must be a non-empty string.`);
19
+ }
20
+ return value;
21
+ };
22
+ var exactKeys = (value, allowed, where) => {
23
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
24
+ if (unknown.length > 0)
25
+ throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
26
+ };
27
+ var RESERVED_STEP_ENV = new Set([
28
+ "PATH",
29
+ "HOME",
30
+ "SHELL",
31
+ "PWD",
32
+ "BUN_INSTALL",
33
+ "NODE_OPTIONS",
34
+ "LD_PRELOAD",
35
+ "LD_LIBRARY_PATH",
36
+ "GIT_SSH",
37
+ "GIT_SSH_COMMAND"
38
+ ]);
39
+ function parseDeployDefinition(value) {
40
+ const root = record(value, "pipeline");
41
+ exactKeys(root, ["version", "name", "requireAttestation", "roles", "steps"], "pipeline");
42
+ if (root.version !== PIPELINE_VERSION) {
43
+ throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
44
+ }
45
+ if (!Array.isArray(root.roles) || root.roles.length === 0) {
46
+ throw new DefinitionError("pipeline.roles must contain at least one role.");
47
+ }
48
+ if (!Array.isArray(root.steps) || root.steps.length === 0) {
49
+ throw new DefinitionError("pipeline.steps must contain at least one step.");
50
+ }
51
+ const roles = root.roles.map((raw, index) => {
52
+ const role = record(raw, `roles[${index}]`);
53
+ exactKeys(role, ["name", "count", "software"], `roles[${index}]`);
54
+ const count = Number(role.count);
55
+ if (!Number.isSafeInteger(count) || count < 1) {
56
+ throw new DefinitionError(`roles[${index}].count must be a positive integer.`);
57
+ }
58
+ if (!Array.isArray(role.software)) {
59
+ throw new DefinitionError(`roles[${index}].software must be an array.`);
60
+ }
61
+ return {
62
+ name: text(role.name, `roles[${index}].name`),
63
+ count,
64
+ software: role.software.map((rawSoftware, softwareIndex) => {
65
+ const software = record(rawSoftware, `roles[${index}].software[${softwareIndex}]`);
66
+ exactKeys(software, ["name", "check", "install"], `roles[${index}].software[${softwareIndex}]`);
67
+ return {
68
+ name: text(software.name, `roles[${index}].software[${softwareIndex}].name`),
69
+ check: text(software.check, `roles[${index}].software[${softwareIndex}].check`),
70
+ install: text(software.install, `roles[${index}].software[${softwareIndex}].install`)
71
+ };
72
+ })
73
+ };
74
+ });
75
+ if (new Set(roles.map((role) => role.name)).size !== roles.length) {
76
+ throw new DefinitionError("pipeline.roles must have unique names.");
77
+ }
78
+ const phases = new Set(["build", "release", "migrate", "health"]);
79
+ const steps = root.steps.map((raw, index) => {
80
+ const step = record(raw, `steps[${index}]`);
81
+ exactKeys(step, ["name", "run", "phase", "secrets", "once", "always", "timeoutMs", "when"], `steps[${index}]`);
82
+ const phase = text(step.phase, `steps[${index}].phase`);
83
+ if (!phases.has(phase))
84
+ throw new DefinitionError(`steps[${index}].phase is not supported.`);
85
+ if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name) => typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)))) {
86
+ throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
87
+ }
88
+ if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
89
+ throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
90
+ }
91
+ if (Array.isArray(step.secrets) && step.secrets.some((name) => RESERVED_STEP_ENV.has(String(name)))) {
92
+ throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
93
+ }
94
+ const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
95
+ if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)) {
96
+ throw new DefinitionError(`steps[${index}].timeoutMs must be an integer from 1 to 86400000.`);
97
+ }
98
+ let when;
99
+ if (step.when !== undefined) {
100
+ const conditions = record(step.when, `steps[${index}].when`);
101
+ when = {};
102
+ for (const [name, expected] of Object.entries(conditions)) {
103
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || typeof expected !== "string" || expected.length === 0) {
104
+ throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
105
+ }
106
+ when[name] = expected;
107
+ }
108
+ if (Object.keys(when).length === 0)
109
+ throw new DefinitionError(`steps[${index}].when must not be empty.`);
110
+ }
111
+ return {
112
+ name: text(step.name, `steps[${index}].name`),
113
+ run: text(step.run, `steps[${index}].run`),
114
+ phase,
115
+ secrets: step.secrets,
116
+ once: step.once === true,
117
+ always: step.always === true,
118
+ timeoutMs,
119
+ when
120
+ };
121
+ });
122
+ if (new Set(steps.map((step) => step.name)).size !== steps.length) {
123
+ throw new DefinitionError("pipeline.steps must have unique names.");
124
+ }
125
+ return {
126
+ version: PIPELINE_VERSION,
127
+ name: text(root.name, "pipeline.name"),
128
+ requireAttestation: root.requireAttestation === true,
129
+ roles,
130
+ steps
131
+ };
132
+ }
133
+ function phasePipeline(definition, phase) {
134
+ return {
135
+ name: `${definition.name}:${phase}`,
136
+ requireAttestation: definition.requireAttestation,
137
+ steps: definition.steps.filter((step) => step.phase === phase)
138
+ };
139
+ }
140
+ function prerequisitePipeline(definition, roleName) {
141
+ const role = definition.roles.find((candidate) => candidate.name === roleName);
142
+ if (!role)
143
+ throw new DefinitionError(`pipeline role does not exist: ${roleName}.`);
144
+ return {
145
+ name: `${definition.name}:prerequisites:${role.name}`,
146
+ requireAttestation: definition.requireAttestation,
147
+ steps: role.software.map((software) => ({
148
+ name: `prepare ${software.name}`,
149
+ run: `${software.check} >/dev/null 2>&1 || { ${software.install}; ${software.check}; }`
150
+ }))
151
+ };
152
+ }
153
+ export {
154
+ prerequisitePipeline,
155
+ phasePipeline,
156
+ parseDeployDefinition,
157
+ PIPELINE_VERSION,
158
+ DefinitionError
159
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
+ import type { DeploymentManager, DeploymentResult, GitSourceAuth } from './deployment';
3
+ export interface RemoteDeploymentClaim {
4
+ runKey: string;
5
+ pipelineKey: string;
6
+ revision: string;
7
+ claimToken: string;
8
+ claimExpiresAtTs: number;
9
+ attempt: number;
10
+ source: {
11
+ repository: string;
12
+ branch: string;
13
+ role: string;
14
+ knownHosts?: string;
15
+ auth?: GitSourceAuth;
16
+ };
17
+ }
18
+ export interface DeploymentPullOptions {
19
+ apiUrl: string;
20
+ nodeKey: string;
21
+ keys: NodeKeyPair;
22
+ manager?: DeploymentManager;
23
+ /** Automatic computes construct one manager from the server-owned source coordinates. */
24
+ managerFor?: (claim: RemoteDeploymentClaim) => DeploymentManager;
25
+ fetch?: (input: URL, init: RequestInit) => Promise<Response>;
26
+ intervalMs?: number;
27
+ /** Concurrent claim workers. Same-key execution still serializes in the manager queue. */
28
+ parallelism?: number;
29
+ requestTimeoutMs?: number;
30
+ completionAttempts?: number;
31
+ sleep?: (ms: number) => Promise<void>;
32
+ now?: () => number;
33
+ renewRetryMs?: number;
34
+ setTimer?: (callback: () => void, ms: number) => unknown;
35
+ clearTimer?: (handle: unknown) => void;
36
+ onEvent?: (event: string, detail?: unknown) => void;
37
+ }
38
+ export type PullResult = {
39
+ status: 'idle';
40
+ } | {
41
+ status: 'deployed';
42
+ claim: RemoteDeploymentClaim;
43
+ result: DeploymentResult;
44
+ } | {
45
+ status: 'failed';
46
+ claim: RemoteDeploymentClaim;
47
+ reason: string;
48
+ };
49
+ /** Claim at most one job and await its exact deployment result before reporting. */
50
+ export declare function pullDeploymentOnce(options: DeploymentPullOptions): Promise<PullResult>;
51
+ /**
52
+ * Bounded outbound intake for the deployment manager.
53
+ *
54
+ * Claim workers may receive different pipeline keys concurrently. Managers
55
+ * still own execution ordering: a single pipeline key is serial even when two
56
+ * of its claims were fetched by different workers.
57
+ */
58
+ export declare function startDeploymentPull(options: DeploymentPullOptions): {
59
+ stop(): Promise<void>;
60
+ readonly active: boolean;
61
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { type Server } from 'node:net';
2
+ import type { CommandInput, CommandResult } from './deployment';
3
+ export declare const DEFAULT_DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
4
+ /**
5
+ * Execute tenant-controlled project commands under a separate Unix identity.
6
+ *
7
+ * This daemon has no LoadCredential directive and cannot open the owner-only
8
+ * agent socket. The credential-bearing agent may submit an exact command and
9
+ * only the secrets named for that step; the subprocess can steal those values,
10
+ * because it is the intended consumer, but cannot steal the node or Git key.
11
+ */
12
+ export declare function startDeploymentRunner(options: {
13
+ root: string;
14
+ home: string;
15
+ socketPath?: string;
16
+ /** systemd socket activation passes the agent-owned listener as fd 3. */
17
+ listenFd?: number;
18
+ exec?: (input: CommandInput, home: string) => Promise<CommandResult>;
19
+ }): {
20
+ server: Server;
21
+ stop(): Promise<void>;
22
+ };
23
+ export declare function requestDeploymentCommand(input: CommandInput, socketPath?: string): Promise<CommandResult>;