@forgezero/agent 0.1.74 → 0.1.75

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 (42) hide show
  1. package/README.md +126 -17
  2. package/dist/agent-heartbeat.d.ts +25 -1
  3. package/dist/agent-heartbeat.js +45 -5
  4. package/dist/bootstrap.js +1 -1
  5. package/dist/community-rehearsal-host.js +208 -9
  6. package/dist/credential-schema.d.ts +1 -1
  7. package/dist/credential-schema.js +1 -1
  8. package/dist/definition.js +206 -7
  9. package/dist/deploy-actions.d.ts +7 -0
  10. package/dist/deploy-compiler.js +191 -10
  11. package/dist/deploy-file.js +206 -7
  12. package/dist/deploy-plan-runner.d.ts +3 -0
  13. package/dist/deploy-plan-runner.js +190 -9
  14. package/dist/deploy-plan.js +187 -8
  15. package/dist/deploy-providers.d.ts +19 -0
  16. package/dist/deploy.d.ts +89 -3
  17. package/dist/deploy.js +49 -2
  18. package/dist/deployment-connectivity.d.ts +63 -0
  19. package/dist/deployment-connectivity.js +148 -0
  20. package/dist/deployment-pull.d.ts +2 -0
  21. package/dist/deployment-targets.d.ts +9 -0
  22. package/dist/deployment-targets.js +141 -0
  23. package/dist/deployment.d.ts +69 -0
  24. package/dist/fz-agent.js +1111 -301
  25. package/dist/fz.js +220 -15
  26. package/dist/index.d.ts +1 -0
  27. package/dist/metal-bootstrap.js +1 -1
  28. package/dist/metal-helper-socket.js +210 -11
  29. package/dist/metal-provision.js +210 -11
  30. package/dist/operator-bootstrap.js +1 -1
  31. package/dist/platform-bootstrap-runtime.js +209 -10
  32. package/dist/platform-fleet-verification.js +515 -149
  33. package/dist/platform-genesis.js +206 -7
  34. package/dist/provision.js +465 -99
  35. package/dist/service-supervisor.d.ts +6 -0
  36. package/dist/software-helper.d.ts +3 -0
  37. package/dist/software-helper.js +465 -98
  38. package/dist/software.d.ts +4 -1
  39. package/dist/software.js +210 -8
  40. package/dist/ubuntu.js +206 -7
  41. package/dist/version.d.ts +1 -1
  42. package/package.json +12 -4
@@ -0,0 +1,148 @@
1
+ // src/deployment-connectivity.ts
2
+ import { createHash } from "node:crypto";
3
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
6
+ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
7
+ var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
8
+ var NAME = /^[a-z][a-z0-9._-]{0,99}$/;
9
+ var idFor = (key) => createHash("sha256").update(key).digest("hex").slice(0, 20);
10
+ var checked = async (host, argv, label) => {
11
+ const result = await host.exec(argv);
12
+ if (result.exitCode !== 0)
13
+ throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
14
+ return result.output.trim();
15
+ };
16
+ async function sealWithSystemd(name, path, value) {
17
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
18
+ const next = `${path}.next`;
19
+ const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
20
+ stdin: "pipe",
21
+ stdout: "pipe",
22
+ stderr: "pipe",
23
+ env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
24
+ });
25
+ child.stdin.write(value);
26
+ child.stdin.end();
27
+ const [stdout, stderr, exitCode] = await Promise.all([
28
+ new Response(child.stdout).text(),
29
+ new Response(child.stderr).text(),
30
+ child.exited
31
+ ]);
32
+ if (exitCode !== 0)
33
+ throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
34
+ renameSync(next, path);
35
+ }
36
+ var defaultHost = {
37
+ seal: sealWithSystemd,
38
+ write(path, content, mode) {
39
+ mkdirSync(dirname(path), { recursive: true, mode: 493 });
40
+ const next = `${path}.next`;
41
+ writeFileSync(next, content, { mode });
42
+ renameSync(next, path);
43
+ },
44
+ async exec(argv) {
45
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
46
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
47
+ return { exitCode, output: `${stdout}${stderr}` };
48
+ },
49
+ sleep: (ms) => Bun.sleep(ms)
50
+ };
51
+ function validate(request) {
52
+ if (!request.key || request.key.length > 256)
53
+ throw new Error("deployment connectivity key is invalid");
54
+ const publicIntent = request.intent.public;
55
+ const privateIntent = request.intent.private;
56
+ if (publicIntent?.mode === "cloudflare-tunnel") {
57
+ if (!NAME.test(publicIntent.credential) || !HOSTNAME.test(publicIntent.hostname) || !NAME.test(publicIntent.tunnelKey) || publicIntent.service.protocol !== "http" || !Number.isInteger(publicIntent.service.port) || publicIntent.service.port < 1 || publicIntent.service.port > 65535 || !request.capabilities.public || !UUID.test(request.capabilities.public.tunnelId) || !TOKEN.test(request.capabilities.public.connectorToken)) {
58
+ throw new Error("public deployment connectivity is malformed");
59
+ }
60
+ } else if (request.capabilities.public)
61
+ throw new Error("unexpected public deployment capability");
62
+ if (privateIntent?.mode === "cloudflare-warp") {
63
+ if (!NAME.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
64
+ throw new Error("private deployment connectivity is malformed");
65
+ }
66
+ } else if (request.capabilities.private)
67
+ throw new Error("unexpected private deployment capability");
68
+ }
69
+ async function applyDeploymentConnectivity(request, host = defaultHost) {
70
+ validate(request);
71
+ const id = idFor(request.key);
72
+ const evidence = { key: request.key };
73
+ if (request.intent.public?.mode === "cloudflare-tunnel") {
74
+ const capability = request.capabilities.public;
75
+ const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
76
+ const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
77
+ const unit = `forgezero-cloudflared-${id}.service`;
78
+ const unitPath = `/etc/systemd/system/${unit}`;
79
+ const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
80
+ await host.seal(credentialName, credentialPath, capability.connectorToken);
81
+ host.write(unitPath, `[Unit]
82
+ Description=ForgeZero deployment Tunnel ${id}
83
+ After=network-online.target
84
+ Wants=network-online.target
85
+
86
+ [Service]
87
+ Type=simple
88
+ DynamicUser=yes
89
+ LoadCredentialEncrypted=${credentialName}:${credentialPath}
90
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
91
+ Restart=always
92
+ RestartSec=5
93
+ NoNewPrivileges=true
94
+ PrivateTmp=true
95
+ ProtectSystem=strict
96
+ ProtectHome=true
97
+
98
+ [Install]
99
+ WantedBy=multi-user.target
100
+ `, 420);
101
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
102
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
103
+ await checked(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
104
+ evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
105
+ }
106
+ if (request.intent.private?.mode === "cloudflare-warp") {
107
+ const capability = request.capabilities.private;
108
+ const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
109
+ const unit = "forgezero-deployment-mesh.service";
110
+ await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
111
+ host.write(`/etc/systemd/system/${unit}`, `[Unit]
112
+ Description=ForgeZero deployment Mesh/WARP connector
113
+ Requires=warp-svc.service
114
+ After=network-online.target warp-svc.service
115
+ Wants=network-online.target
116
+
117
+ [Service]
118
+ Type=oneshot
119
+ RemainAfterExit=yes
120
+ LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
121
+ ExecStart=/usr/local/bin/fz-agent mesh-config
122
+ NoNewPrivileges=true
123
+ PrivateTmp=true
124
+ ProtectSystem=strict
125
+ ProtectHome=true
126
+ ReadWritePaths=/var/lib/cloudflare-warp
127
+
128
+ [Install]
129
+ WantedBy=multi-user.target
130
+ `, 420);
131
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
132
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
133
+ for (let attempt = 0;attempt < 10; attempt += 1) {
134
+ const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
135
+ if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
136
+ evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
137
+ break;
138
+ }
139
+ await host.sleep(1000);
140
+ }
141
+ if (!evidence.private)
142
+ throw new Error("WARP connector readiness failed");
143
+ }
144
+ return evidence;
145
+ }
146
+ export {
147
+ applyDeploymentConnectivity
148
+ };
@@ -10,6 +10,8 @@ export interface RemoteDeploymentClaim {
10
10
  attempt: number;
11
11
  /** True on the one target deterministically elected for this release. */
12
12
  releaseExecutor: boolean;
13
+ assignment?: NonNullable<import('./deployment').DeploymentRequest['assignment']>;
14
+ connectivityCapabilities?: import('./deployment-connectivity').DeploymentConnectivityCapabilities;
13
15
  source: {
14
16
  repository: string;
15
17
  branch: string;
@@ -0,0 +1,9 @@
1
+ import type { JsonPrimitive } from './deploy';
2
+ import type { DeploymentPlan } from './deploy-plan';
3
+ import type { ResolvedDeploymentTarget } from './deployment';
4
+ export declare class DeploymentInputError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ /** Resolve stable logical slots without selecting a hostname or mutating capacity. */
8
+ export declare function resolveDeploymentTargets(plan: DeploymentPlan, supplied?: Readonly<Record<string, unknown>>): ResolvedDeploymentTarget[];
9
+ export declare function resolveDeploymentInputs(plan: DeploymentPlan, supplied?: Readonly<Record<string, unknown>>): Record<string, JsonPrimitive>;
@@ -0,0 +1,141 @@
1
+ // src/deployment-targets.ts
2
+ class DeploymentInputError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "DeploymentInputError";
6
+ }
7
+ }
8
+ var HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
9
+ function inputValue(name, definition, supplied) {
10
+ let value = supplied ?? definition.default;
11
+ if (value === undefined) {
12
+ if (definition.required)
13
+ throw new DeploymentInputError(`deployment input ${name} is required`);
14
+ return;
15
+ }
16
+ if (typeof value === "string") {
17
+ const text = value;
18
+ if (definition.type === "boolean" && (text === "true" || text === "false"))
19
+ value = text === "true";
20
+ if ((definition.type === "integer" || definition.type === "number") && text.trim() !== "")
21
+ value = Number(text);
22
+ }
23
+ if (definition.type === "boolean" && typeof value !== "boolean")
24
+ throw new DeploymentInputError(`deployment input ${name} must be boolean`);
25
+ if (definition.type === "integer" && (!Number.isSafeInteger(value) || Number(value) < (definition.minimum ?? Number.MIN_SAFE_INTEGER) || Number(value) > (definition.maximum ?? Number.MAX_SAFE_INTEGER)))
26
+ throw new DeploymentInputError(`deployment input ${name} must be a bounded integer`);
27
+ if (definition.type === "number" && (typeof value !== "number" || !Number.isFinite(value) || value < (definition.minimum ?? -Number.MAX_VALUE) || value > (definition.maximum ?? Number.MAX_VALUE)))
28
+ throw new DeploymentInputError(`deployment input ${name} must be a bounded number`);
29
+ if (definition.type === "enum" && (typeof value !== "string" || !definition.values.includes(value)))
30
+ throw new DeploymentInputError(`deployment input ${name} must be one of its declared values`);
31
+ if (definition.type === "hostname" && (typeof value !== "string" || !HOSTNAME.test(value)))
32
+ throw new DeploymentInputError(`deployment input ${name} must be a hostname`);
33
+ if (definition.type === "string") {
34
+ if (typeof value !== "string" || value.length < (definition.minimumLength ?? 0) || value.length > (definition.maximumLength ?? 4096))
35
+ throw new DeploymentInputError(`deployment input ${name} must be a bounded string`);
36
+ if (definition.pattern !== undefined && !new RegExp(definition.pattern).test(value))
37
+ throw new DeploymentInputError(`deployment input ${name} does not match its pattern`);
38
+ }
39
+ return value;
40
+ }
41
+ function resolveDeploymentTargets(plan, supplied = {}) {
42
+ const inputs = resolveDeploymentInputs(plan, supplied);
43
+ const stringCoordinate = (value, label) => {
44
+ const resolved = typeof value === "string" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
45
+ if (typeof resolved !== "string" || resolved.length < 1 || resolved.length > 253) {
46
+ throw new DeploymentInputError(`${label} did not resolve to a bounded string`);
47
+ }
48
+ return resolved.toLowerCase();
49
+ };
50
+ const integerCoordinate = (value, label, minimum, maximum) => {
51
+ const resolved = typeof value === "number" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
52
+ if (!Number.isSafeInteger(resolved) || Number(resolved) < minimum || Number(resolved) > maximum) {
53
+ throw new DeploymentInputError(`${label} did not resolve to a bounded integer`);
54
+ }
55
+ return Number(resolved);
56
+ };
57
+ const credentialSource = (alias, label) => {
58
+ const source = plan.spec.credentials?.[alias]?.source.name;
59
+ if (!source)
60
+ throw new DeploymentInputError(`${label} did not resolve to a declared Vault credential`);
61
+ return source;
62
+ };
63
+ return Object.entries(plan.spec.targets).map(([name, target]) => {
64
+ const desired = typeof target.cardinality.desired === "number" ? target.cardinality.desired : target.cardinality.desired.$ref.startsWith("inputs.") ? inputs[target.cardinality.desired.$ref.slice("inputs.".length)] : undefined;
65
+ if (!Number.isSafeInteger(desired) || Number(desired) < target.cardinality.minimum || Number(desired) > target.cardinality.maximum) {
66
+ throw new DeploymentInputError(`target ${name} desired cardinality did not resolve to a bounded integer`);
67
+ }
68
+ let connectivity;
69
+ if (target.connectivity) {
70
+ const privateNetwork = target.connectivity.private;
71
+ const publicNetwork = target.connectivity.public;
72
+ connectivity = {
73
+ ...privateNetwork ? { private: privateNetwork.mode === "cloudflare-warp" ? {
74
+ mode: "cloudflare-warp",
75
+ credential: credentialSource(privateNetwork.credential, `target ${name} WARP credential`),
76
+ network: stringCoordinate(privateNetwork.network, `target ${name} WARP network`)
77
+ } : { mode: privateNetwork.mode } } : {},
78
+ ...publicNetwork ? { public: publicNetwork.mode === "cloudflare-tunnel" ? {
79
+ mode: "cloudflare-tunnel",
80
+ credential: credentialSource(publicNetwork.credential, `target ${name} Tunnel credential`),
81
+ zone: stringCoordinate(publicNetwork.zone, `target ${name} Tunnel zone`),
82
+ hostname: publicNetwork.hostname.mode === "static" ? {
83
+ mode: "static",
84
+ label: stringCoordinate(publicNetwork.hostname.label, `target ${name} Tunnel label`)
85
+ } : {
86
+ mode: "indexed",
87
+ prefix: stringCoordinate(publicNetwork.hostname.prefix, `target ${name} Tunnel prefix`),
88
+ startAt: publicNetwork.hostname.startAt ?? 1
89
+ },
90
+ service: (() => {
91
+ const applications = Object.values(plan.spec.components).filter((component) => component.target === name && component.kind === "application" && component.network?.public?.mode === "cloudflare-tunnel");
92
+ if (applications.length !== 1)
93
+ throw new DeploymentInputError(`target ${name} Tunnel requires exactly one public application`);
94
+ const port = applications[0].network.ingress?.stablePort;
95
+ if (!Number.isInteger(port))
96
+ throw new DeploymentInputError(`target ${name} Tunnel application requires a stable ingress port`);
97
+ return { protocol: "http", port: Number(port) };
98
+ })()
99
+ } : { mode: "disabled" } } : {}
100
+ };
101
+ }
102
+ return {
103
+ name,
104
+ profiles: [...target.selector.profiles],
105
+ operatingSystem: { ...target.selector.operatingSystem },
106
+ confidentialCompute: target.selector.confidentialCompute ?? "disabled",
107
+ labels: { ...target.selector.labels ?? {} },
108
+ minimum: target.cardinality.minimum,
109
+ desired: Number(desired),
110
+ maximum: target.cardinality.maximum,
111
+ allocation: {
112
+ ...target.allocation,
113
+ resources: {
114
+ cpuCores: integerCoordinate(target.allocation.resources.cpuCores, `target ${name} cpuCores`, 1, 1024),
115
+ memoryMiB: integerCoordinate(target.allocation.resources.memoryMiB, `target ${name} memoryMiB`, 128, 4194304),
116
+ storageGiB: integerCoordinate(target.allocation.resources.storageGiB, `target ${name} storageGiB`, 1, 1048576)
117
+ }
118
+ },
119
+ ...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
120
+ ...connectivity ? { connectivity } : {}
121
+ };
122
+ });
123
+ }
124
+ function resolveDeploymentInputs(plan, supplied = {}) {
125
+ for (const name of Object.keys(supplied))
126
+ if (!(name in (plan.spec.inputs ?? {}))) {
127
+ throw new DeploymentInputError(`deployment input ${name} is not declared`);
128
+ }
129
+ const inputs = {};
130
+ for (const [name, definition] of Object.entries(plan.spec.inputs ?? {})) {
131
+ const value = inputValue(name, definition, supplied[name]);
132
+ if (value !== undefined)
133
+ inputs[name] = value;
134
+ }
135
+ return inputs;
136
+ }
137
+ export {
138
+ resolveDeploymentTargets,
139
+ resolveDeploymentInputs,
140
+ DeploymentInputError
141
+ };
@@ -9,6 +9,7 @@ import type { SecretCache } from './cache';
9
9
  import type { AttestationSource } from './socket';
10
10
  import { type CapacityCalibration, type CapacityCalibrationOptions } from './capacity-calibration';
11
11
  import { type BootstrapBundleManifest } from './bootstrap-bundle';
12
+ import type { DeploymentConnectivityCapabilities, DeploymentConnectivityEvidence, DeploymentConnectivityIntent, DeploymentConnectivityRequest } from './deployment-connectivity';
12
13
  export interface CommandInput {
13
14
  argv: readonly string[];
14
15
  cwd?: string;
@@ -24,6 +25,20 @@ export interface DeploymentRequest {
24
25
  revision?: string;
25
26
  /** True only for the target deterministically elected for release-scoped steps. */
26
27
  releaseExecutor?: boolean;
28
+ /** Exact control-plane placement for this compute. Absence is allowed only for one unambiguous bootstrap target. */
29
+ assignment?: {
30
+ targetName: string;
31
+ slot: number;
32
+ definitionDigest: string;
33
+ operatingSystem: ResolvedDeploymentTarget['operatingSystem'];
34
+ confidentialCompute: ResolvedDeploymentTarget['confidentialCompute'];
35
+ execution: ResolvedDeploymentTarget['allocation']['execution'];
36
+ sharing: ResolvedDeploymentTarget['allocation']['sharing'];
37
+ resources: ResolvedDeploymentTarget['allocation']['resources'];
38
+ connectivity?: DeploymentConnectivityIntent;
39
+ };
40
+ /** Connector-scoped, one-claim capabilities. Never the project Cloudflare management token. */
41
+ connectivityCapabilities?: DeploymentConnectivityCapabilities;
27
42
  }
28
43
  export interface DeploymentResult {
29
44
  key: string;
@@ -49,11 +64,63 @@ export interface DeploymentResult {
49
64
  export interface ResolvedDeploymentTarget {
50
65
  name: string;
51
66
  profiles: readonly string[];
67
+ operatingSystem: {
68
+ id: 'ubuntu';
69
+ version: '24.04' | '26.04';
70
+ architecture: 'x64';
71
+ };
52
72
  confidentialCompute: 'required' | 'preferred' | 'disabled';
53
73
  labels: Readonly<Record<string, string>>;
54
74
  minimum: number;
55
75
  desired: number;
56
76
  maximum: number;
77
+ allocation: {
78
+ execution: 'native' | 'oci-runc' | 'oci-kata-qemu-snp';
79
+ sharing: 'exclusive' | 'shared';
80
+ resources: {
81
+ cpuCores: number;
82
+ memoryMiB: number;
83
+ storageGiB: number;
84
+ };
85
+ existing: 'prefer' | 'require';
86
+ };
87
+ provisioning?: {
88
+ planKey: string;
89
+ regionKey: string;
90
+ imageKey: string;
91
+ environmentKey: string;
92
+ ownership: 'platform' | 'tenant-metal';
93
+ metalHostname?: string;
94
+ maxMonthlySpendMinor: string;
95
+ };
96
+ connectivity?: {
97
+ private?: {
98
+ mode: 'disabled' | 'private-lan';
99
+ } | {
100
+ mode: 'cloudflare-warp';
101
+ credential: string;
102
+ network: string;
103
+ };
104
+ public?: {
105
+ mode: 'disabled';
106
+ } | {
107
+ mode: 'cloudflare-tunnel';
108
+ credential: string;
109
+ zone: string;
110
+ hostname: {
111
+ mode: 'static';
112
+ label: string;
113
+ } | {
114
+ mode: 'indexed';
115
+ prefix: string;
116
+ startAt: number;
117
+ };
118
+ service: {
119
+ protocol: 'http';
120
+ port: number;
121
+ };
122
+ };
123
+ };
57
124
  }
58
125
  /**
59
126
  * How the credential-bearing agent may read one server-owned Git source.
@@ -121,6 +188,8 @@ export interface DeploymentOptions {
121
188
  buildContainer?: (request: ContainerBuildRequest) => Promise<ContainerBuildResult>;
122
189
  /** Root-owned bounded container/Nginx blue-green activation. */
123
190
  activateContainer?: (request: ContainerActivateRequest) => Promise<ServiceActivationState>;
191
+ /** Root-owned Tunnel/WARP convergence using connector-scoped capabilities only. */
192
+ applyConnectivity?: (request: DeploymentConnectivityRequest) => Promise<DeploymentConnectivityEvidence>;
124
193
  /** Agent-owned directory; definitions cannot choose or overwrite this path. */
125
194
  capacityEvidenceDirectory?: string;
126
195
  /** Test/embedding seam. Production runs the bounded loopback GET calibrator. */