@forgezero/agent 0.1.10 → 0.1.12
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 +36 -9
- package/dist/deployment.d.ts +1 -1
- package/dist/fz-agent.js +525 -490
- package/dist/fz.js +34 -12
- package/dist/guest-enrolment.d.ts +2 -1
- package/dist/guest-enrolment.js +81 -23
- package/dist/index.d.ts +5 -29
- package/dist/metal-helper-socket.js +117 -24
- package/dist/metal-provision.d.ts +2 -2
- package/dist/metal-provision.js +117 -24
- package/dist/node-vault.d.ts +9 -0
- package/dist/node-vault.js +53 -17
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +14 -4
- package/dist/provisioning-pull.d.ts +24 -1
- package/dist/provisioning-pull.js +30 -11
- package/dist/signed-node-http.d.ts +1 -1
- package/dist/socket.d.ts +11 -4
- package/dist/ubuntu.d.ts +16 -0
- package/dist/ubuntu.js +18 -0
- package/dist/version.d.ts +2 -0
- package/package.json +11 -6
- package/dist/attestation-client.test.d.ts +0 -1
- package/dist/cache.test.d.ts +0 -1
- package/dist/cli/agent-install.test.d.ts +0 -1
- package/dist/cli/options.test.d.ts +0 -1
- package/dist/cli/run.test.d.ts +0 -1
- package/dist/compute.test.d.ts +0 -1
- package/dist/control.test.d.ts +0 -1
- package/dist/definition.test.d.ts +0 -1
- package/dist/deployment-pull.test.d.ts +0 -1
- package/dist/deployment-runner.test.d.ts +0 -1
- package/dist/deployment-watch.d.ts +0 -36
- package/dist/deployment-watch.test.d.ts +0 -1
- package/dist/deployment.test.d.ts +0 -1
- package/dist/guest-enrolment.test.d.ts +0 -1
- package/dist/index.test.d.ts +0 -1
- package/dist/metal-helper-socket.test.d.ts +0 -1
- package/dist/metal-isolation.test.d.ts +0 -1
- package/dist/metal-provision.test.d.ts +0 -1
- package/dist/node-vault.test.d.ts +0 -1
- package/dist/pipeline.test.d.ts +0 -1
- package/dist/provisioning-pull.test.d.ts +0 -1
- package/dist/snp-attestation.test.d.ts +0 -1
- package/dist/socket.test.d.ts +0 -1
- package/dist/ssh-listen.test.d.ts +0 -1
- package/dist/ssh-server.test.d.ts +0 -1
- package/dist/subscribe.test.d.ts +0 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/signed-node-http.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
encodeSignatureHeader,
|
|
4
|
+
generateResponseRecipient,
|
|
5
|
+
openResponse,
|
|
6
|
+
RESPONSE_KEY_HEADER,
|
|
7
|
+
signRequest
|
|
8
|
+
} from "@forgezero/runtime/identity";
|
|
3
9
|
|
|
4
10
|
class SignedNodeHttpError extends Error {
|
|
5
11
|
status;
|
|
@@ -9,30 +15,28 @@ class SignedNodeHttpError extends Error {
|
|
|
9
15
|
this.name = "SignedNodeHttpError";
|
|
10
16
|
}
|
|
11
17
|
}
|
|
12
|
-
|
|
13
|
-
timestamp: envelope.timestamp,
|
|
14
|
-
nonce: envelope.nonce,
|
|
15
|
-
edSignature: envelope.edSignature,
|
|
16
|
-
mlDsaSignature: envelope.mlDsaSignature
|
|
17
|
-
})).toString("base64url");
|
|
18
|
-
async function postSignedNode(options, path, body) {
|
|
18
|
+
async function postSignedNode(options, path, body, sealedResponse = false) {
|
|
19
19
|
const url = new URL(options.apiUrl);
|
|
20
20
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
21
21
|
url.search = "";
|
|
22
22
|
url.hash = "";
|
|
23
23
|
const raw = JSON.stringify(body);
|
|
24
|
+
const recipient = sealedResponse ? generateResponseRecipient() : undefined;
|
|
24
25
|
const envelope = signRequest(options.keys, options.nodeKey, {
|
|
25
26
|
method: "POST",
|
|
26
27
|
path: url.pathname,
|
|
27
28
|
query: "",
|
|
28
|
-
body: raw
|
|
29
|
+
body: raw,
|
|
30
|
+
responseKey: recipient?.publicKey
|
|
29
31
|
});
|
|
32
|
+
const signature = encodeSignatureHeader(envelope);
|
|
30
33
|
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
31
34
|
method: "POST",
|
|
32
35
|
headers: {
|
|
33
36
|
"content-type": "application/json",
|
|
34
37
|
"x-fz-node": options.nodeKey,
|
|
35
|
-
"x-fz-signature":
|
|
38
|
+
"x-fz-signature": signature,
|
|
39
|
+
...recipient ? { [RESPONSE_KEY_HEADER]: recipient.publicKey } : {}
|
|
36
40
|
},
|
|
37
41
|
body: raw,
|
|
38
42
|
signal: AbortSignal.timeout(options.requestTimeoutMs ?? 15000)
|
|
@@ -43,6 +47,13 @@ async function postSignedNode(options, path, body) {
|
|
|
43
47
|
const reason = failure ? failure.error?.message ?? failure.message : undefined;
|
|
44
48
|
throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
|
|
45
49
|
}
|
|
50
|
+
if (recipient) {
|
|
51
|
+
try {
|
|
52
|
+
return await openResponse(recipient.secretKey, signature, payload);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new SignedNodeHttpError(502, "The node response was not sealed to this request.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
return payload;
|
|
47
58
|
}
|
|
48
59
|
|
|
@@ -123,7 +134,15 @@ async function pullProvisioningOnce(options) {
|
|
|
123
134
|
if (options.metalPreflight) {
|
|
124
135
|
const report = options.metalPreflight();
|
|
125
136
|
const accepted = await postSignedNode(options, "v1/metal/preflight", report);
|
|
126
|
-
options.
|
|
137
|
+
if (options.metalHostname && accepted.hostname !== options.metalHostname) {
|
|
138
|
+
throw new Error(`metal agent: configured inventory hostname ${options.metalHostname} is bound as ${accepted.hostname}; refusing work.`);
|
|
139
|
+
}
|
|
140
|
+
options.onEvent?.("preflight", {
|
|
141
|
+
...report,
|
|
142
|
+
hostname: accepted.hostname,
|
|
143
|
+
ready: accepted.ready,
|
|
144
|
+
state: accepted.state
|
|
145
|
+
});
|
|
127
146
|
if (!accepted.ready)
|
|
128
147
|
return { status: "idle" };
|
|
129
148
|
}
|
|
@@ -11,4 +11,4 @@ export declare class SignedNodeHttpError extends Error {
|
|
|
11
11
|
constructor(status: number, message: string);
|
|
12
12
|
}
|
|
13
13
|
/** One implementation of the hybrid-signed machine HTTP contract. */
|
|
14
|
-
export declare function postSignedNode<T>(options: SignedNodeHttpOptions, path: string, body: object): Promise<T>;
|
|
14
|
+
export declare function postSignedNode<T>(options: SignedNodeHttpOptions, path: string, body: object, sealedResponse?: boolean): Promise<T>;
|
package/dist/socket.d.ts
CHANGED
|
@@ -27,10 +27,11 @@ import type { SecretCache } from './cache';
|
|
|
27
27
|
*
|
|
28
28
|
* ## Peer credentials, not a shared secret
|
|
29
29
|
*
|
|
30
|
-
* Access control is the filesystem's: the socket is created
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* Access control is the filesystem's: the socket is created 0660 under the
|
|
31
|
+
* dedicated `forgezero-vault` service group. An application service account is
|
|
32
|
+
* admitted by group membership; unrelated users and the deployment runner are
|
|
33
|
+
* not. A token checked over the socket would have to live somewhere both sides
|
|
34
|
+
* can read, which is the problem this exists to remove rather than a solution.
|
|
34
35
|
*/
|
|
35
36
|
export declare class AgentError extends Error {
|
|
36
37
|
readonly code: string;
|
|
@@ -50,11 +51,15 @@ export type Request = {
|
|
|
50
51
|
nonce?: string;
|
|
51
52
|
} | {
|
|
52
53
|
op: 'get';
|
|
54
|
+
project: string;
|
|
55
|
+
environment: string;
|
|
53
56
|
name: string;
|
|
54
57
|
} | {
|
|
55
58
|
op: 'sync';
|
|
56
59
|
} | {
|
|
57
60
|
op: 'held';
|
|
61
|
+
project: string;
|
|
62
|
+
environment: string;
|
|
58
63
|
};
|
|
59
64
|
export type Response = {
|
|
60
65
|
ok: true;
|
|
@@ -125,6 +130,8 @@ export interface AgentOptions {
|
|
|
125
130
|
* database URL fails somewhere far from here.
|
|
126
131
|
*/
|
|
127
132
|
cache?: SecretCache;
|
|
133
|
+
/** Project boundary attached by the platform during one-time enrolment. */
|
|
134
|
+
projectKey?: string;
|
|
128
135
|
/** Every request, for the hash-chained journal. */
|
|
129
136
|
record?: (entry: {
|
|
130
137
|
op: string;
|
package/dist/ubuntu.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only guest operating system ForgeZero currently supports.
|
|
3
|
+
*
|
|
4
|
+
* A dated Canonical release directory and its published SHA-256 make image
|
|
5
|
+
* acquisition reproducible. `release/current` is deliberately forbidden: it
|
|
6
|
+
* changes underneath an already reviewed metal profile.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SUPPORTED_GUEST_IMAGE: Readonly<{
|
|
9
|
+
readonly key: "ubuntu-resolute-20260731";
|
|
10
|
+
readonly family: "ubuntu-26.04";
|
|
11
|
+
readonly version: "2026-07-31";
|
|
12
|
+
readonly label: "Ubuntu 26.04 LTS Resolute";
|
|
13
|
+
readonly url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img";
|
|
14
|
+
readonly sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05";
|
|
15
|
+
}>;
|
|
16
|
+
export declare function assertSupportedGuestImage(imageKey: string): void;
|
package/dist/ubuntu.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/ubuntu.ts
|
|
2
|
+
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
3
|
+
key: "ubuntu-resolute-20260731",
|
|
4
|
+
family: "ubuntu-26.04",
|
|
5
|
+
version: "2026-07-31",
|
|
6
|
+
label: "Ubuntu 26.04 LTS Resolute",
|
|
7
|
+
url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
|
|
8
|
+
sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
|
|
9
|
+
});
|
|
10
|
+
function assertSupportedGuestImage(imageKey) {
|
|
11
|
+
if (imageKey !== SUPPORTED_GUEST_IMAGE.key) {
|
|
12
|
+
throw new Error(`unsupported guest image ${imageKey}; ForgeZero currently supports only ${SUPPORTED_GUEST_IMAGE.key}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
assertSupportedGuestImage,
|
|
17
|
+
SUPPORTED_GUEST_IMAGE
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
|
|
3
3
|
"name": "@forgezero/agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.12",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"check": "tsc --noEmit",
|
|
8
|
-
"
|
|
8
|
+
"prebuild": "rm -rf dist",
|
|
9
|
+
"build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm --packages external && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm --packages external && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/deployment-runner.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
|
|
9
10
|
"prepublishOnly": "bun run check && bun run build"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
@@ -17,16 +18,16 @@
|
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"@forgezero/access": "^0.1.0",
|
|
20
|
-
"@forgezero/runtime": "^0.1.
|
|
21
|
-
"@forgezero/vault": "^0.1.
|
|
21
|
+
"@forgezero/runtime": "^0.1.3",
|
|
22
|
+
"@forgezero/vault": "^0.1.5",
|
|
22
23
|
"@noble/curves": "^2.2.0",
|
|
23
24
|
"@noble/hashes": "^2.2.0",
|
|
24
25
|
"@noble/post-quantum": "^0.6.1",
|
|
25
26
|
"@scure/bip39": "^2.2.0"
|
|
26
27
|
},
|
|
27
28
|
"bin": {
|
|
28
|
-
"fz-agent": "
|
|
29
|
-
"fz": "
|
|
29
|
+
"fz-agent": "dist/fz-agent.js",
|
|
30
|
+
"fz": "dist/fz.js"
|
|
30
31
|
},
|
|
31
32
|
"publishConfig": {
|
|
32
33
|
"access": "public"
|
|
@@ -90,6 +91,10 @@
|
|
|
90
91
|
"./metal-provision": {
|
|
91
92
|
"types": "./dist/metal-provision.d.ts",
|
|
92
93
|
"default": "./dist/metal-provision.js"
|
|
94
|
+
},
|
|
95
|
+
"./ubuntu": {
|
|
96
|
+
"types": "./dist/ubuntu.d.ts",
|
|
97
|
+
"default": "./dist/ubuntu.js"
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/cache.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/cli/run.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/compute.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/control.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { DeploymentManager } from './deployment';
|
|
2
|
-
export type StaticDeploymentOutcome = 'pending' | 'running' | 'deployed' | 'failed';
|
|
3
|
-
export interface StaticDeploymentState {
|
|
4
|
-
revision: string;
|
|
5
|
-
outcome: StaticDeploymentOutcome;
|
|
6
|
-
updatedAtTs: number;
|
|
7
|
-
detail?: string;
|
|
8
|
-
}
|
|
9
|
-
export interface StaticDeploymentWatchOptions {
|
|
10
|
-
manager: DeploymentManager;
|
|
11
|
-
statePath: string;
|
|
12
|
-
coordinator?: boolean;
|
|
13
|
-
intervalMs?: number;
|
|
14
|
-
readState?: () => StaticDeploymentState | null;
|
|
15
|
-
writeState?: (state: StaticDeploymentState) => void;
|
|
16
|
-
/** Active release on the first watcher boot, before a state file exists. */
|
|
17
|
-
currentRevision?: () => string | undefined;
|
|
18
|
-
setTimer?: (callback: () => void, ms: number) => unknown;
|
|
19
|
-
clearTimer?: (handle: unknown) => void;
|
|
20
|
-
now?: () => number;
|
|
21
|
-
onEvent?: (event: string, detail?: unknown) => void;
|
|
22
|
-
}
|
|
23
|
-
export declare function readStaticDeploymentState(path: string): StaticDeploymentState | null;
|
|
24
|
-
export declare function writeStaticDeploymentState(path: string, state: StaticDeploymentState): void;
|
|
25
|
-
/**
|
|
26
|
-
* Git is only an intake adapter. Every discovered revision is still executed
|
|
27
|
-
* by DeploymentManager and therefore by the common keyed async queue.
|
|
28
|
-
*
|
|
29
|
-
* The state file is business recovery state, not queue persistence: pending
|
|
30
|
-
* and running are written before execution, so a process crash re-submits the
|
|
31
|
-
* exact commit on boot; deployed is written only after the awaited result.
|
|
32
|
-
*/
|
|
33
|
-
export declare function startStaticDeploymentWatch(options: StaticDeploymentWatchOptions): {
|
|
34
|
-
stop(): Promise<void>;
|
|
35
|
-
readonly active: boolean;
|
|
36
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/pipeline.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/socket.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/subscribe.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|