@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.
@@ -2,8 +2,11 @@
2
2
  import { createHash, timingSafeEqual, randomUUID } from "node:crypto";
3
3
  import {
4
4
  chmodSync,
5
+ closeSync,
5
6
  existsSync,
7
+ fsyncSync,
6
8
  mkdirSync,
9
+ openSync,
7
10
  readFileSync,
8
11
  readlinkSync,
9
12
  renameSync,
@@ -17,6 +20,25 @@ var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
17
20
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
18
21
  var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
19
22
  var REGISTRY = "registry.npmjs.org";
23
+ var syncPath = (path) => {
24
+ const descriptor = openSync(path, "r");
25
+ try {
26
+ fsyncSync(descriptor);
27
+ } finally {
28
+ closeSync(descriptor);
29
+ }
30
+ };
31
+ var syncReleaseDirectory = (directory) => {
32
+ for (const path of [
33
+ join(directory, "package.json"),
34
+ join(directory, "dist", "fz-agent.js"),
35
+ join(directory, "dist", "fz.js"),
36
+ join(directory, "dist"),
37
+ directory,
38
+ dirname(directory)
39
+ ])
40
+ syncPath(path);
41
+ };
20
42
  function validateAgentRelease(release) {
21
43
  if (release?.package !== "@forgezero/agent")
22
44
  throw new Error("agent update package is fixed");
@@ -135,16 +157,24 @@ async function stageAgentRelease(releaseInput, options) {
135
157
  ]
136
158
  }, "agent update extraction");
137
159
  await validateReleaseDirectory(unpacked, release, run);
138
- if (!existsSync(finalDirectory))
160
+ if (!existsSync(finalDirectory)) {
139
161
  renameSync(unpacked, finalDirectory);
140
- else
162
+ syncReleaseDirectory(finalDirectory);
163
+ } else
141
164
  await validateReleaseDirectory(finalDirectory, release, run);
142
165
  if (!existsSync(currentLink)) {
143
166
  throw new Error("agent update requires an active immutable release to roll back to");
144
167
  }
145
168
  const previousTarget = readlinkSync(currentLink);
169
+ if (previousTarget !== join("versions", options.currentVersion)) {
170
+ throw new Error("agent update current release does not match the running version");
171
+ }
172
+ if (!existsSync(join(root, previousTarget))) {
173
+ throw new Error("agent update rollback release is missing");
174
+ }
146
175
  return {
147
176
  version: release.version,
177
+ fromVersion: options.currentVersion,
148
178
  directory: finalDirectory,
149
179
  previousTarget,
150
180
  nextTarget: join("versions", release.version),
@@ -159,6 +189,7 @@ function selectAgentRelease(staged) {
159
189
  try {
160
190
  symlinkSync(staged.nextTarget, next);
161
191
  renameSync(next, staged.currentLink);
192
+ syncPath(dirname(staged.currentLink));
162
193
  } finally {
163
194
  rmSync(next, { force: true });
164
195
  }
@@ -168,6 +199,7 @@ function restoreAgentRelease(staged) {
168
199
  try {
169
200
  symlinkSync(staged.previousTarget, next);
170
201
  renameSync(next, staged.currentLink);
202
+ syncPath(dirname(staged.currentLink));
171
203
  } finally {
172
204
  rmSync(next, { force: true });
173
205
  }
@@ -1,6 +1,7 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import type { AttestationSource } from './socket';
3
3
  import { type SignedNodeHttpOptions } from './signed-node-http';
4
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
4
5
  export interface NodeAttestationOptions extends SignedNodeHttpOptions {
5
6
  keys: NodeKeyPair;
6
7
  source: AttestationSource;
@@ -10,6 +11,7 @@ export interface NodeAttestationOptions extends SignedNodeHttpOptions {
10
11
  setTimer?: (callback: () => void, ms: number) => unknown;
11
12
  clearTimer?: (handle: unknown) => void;
12
13
  onEvent?: (event: string, detail?: unknown) => void;
14
+ telemetry?: AgentOperationTelemetry;
13
15
  }
14
16
  /** One challenge, one PSP report, one signed presentation. */
15
17
  export declare function attestNodeOnce(options: NodeAttestationOptions): Promise<{
@@ -16,6 +16,8 @@ import { type Capabilities, type ProvisionPlan } from '../provision';
16
16
  export type { ProvisionPlan };
17
17
  /** Parse NAME=value pairs used only for non-secret unit coordinates and credential paths. */
18
18
  export declare function parseAssignments(value: string | undefined): Record<string, string> | undefined;
19
+ /** Parse an explicit provisioning-owned TCP allowlist; repository data never reaches this input. */
20
+ export declare function parseTcpPorts(value: string | undefined): readonly number[] | undefined;
19
21
  /**
20
22
  * Answer the capability checks by running their commands.
21
23
  *
@@ -50,6 +52,9 @@ export interface InstallOptions {
50
52
  deploymentEnvironment?: Record<string, string>;
51
53
  deploymentCredentials?: Record<string, string>;
52
54
  pullDeployments?: boolean;
55
+ enforceEgress?: boolean;
56
+ runnerLoopbackPorts?: readonly number[];
57
+ runnerPublicTcpPorts?: readonly number[];
53
58
  pullMigrations?: boolean;
54
59
  lifecycleProfilePath?: string;
55
60
  lifecycleHelperSocketPath?: string;
@@ -71,6 +76,7 @@ export interface InstallOptions {
71
76
  enrolStatePath?: string;
72
77
  nodeLabel?: string;
73
78
  nodeHostname?: string;
79
+ telemetryEndpoint?: string;
74
80
  }
75
81
  export declare function planInstall(options: InstallOptions): ProvisionPlan;
76
82
  /** Execute the same ordered plan the control plane executes over SSH. */
@@ -1,19 +1,5 @@
1
1
  import { type SealedShare } from '@forgezero/runtime/custody-share';
2
- import type { UsableIdentity } from './custody';
3
- /**
4
- * The genesis ceremony, driven from a terminal.
5
- *
6
- * Every value that matters is produced HERE and only sealed material crosses
7
- * the wire:
8
- *
9
- * phrase generated locally, printed once, never transmitted or stored
10
- * ssh key derived locally from an agent signature, never transmitted
11
- *
12
- * The API receives the derived 32-byte keys because that is what seals a share,
13
- * and it holds them for exactly the length of one request. Nothing here gives
14
- * the CLI a privileged path — it calls the same endpoints the browser does, and
15
- * a bypass that existed for bootstrapping would exist for an attacker too.
16
- */
2
+ /** Transport shared by the post-genesis vault unlock routine. */
17
3
  export interface Transport {
18
4
  (path: string, init?: {
19
5
  method?: string;
@@ -23,32 +9,6 @@ export interface Transport {
23
9
  body: unknown;
24
10
  }>;
25
11
  }
26
- export interface GenesisResult {
27
- ceremonyKey: string;
28
- phrase: string[];
29
- fingerprint: string;
30
- activated: boolean;
31
- }
32
- /**
33
- * Run a single-custodian genesis.
34
- *
35
- * Deliberately limited to the operator running the command. A multi-custodian
36
- * genesis needs each person at their own terminal with their own agent, which
37
- * is a coordination problem rather than a code one — and pretending to do it
38
- * from one shell would mean one machine briefly holding every share.
39
- */
40
- export declare function runGenesis(args: {
41
- api: Transport;
42
- modeId: string;
43
- userKey: string;
44
- email: string;
45
- identity: UsableIdentity;
46
- /** The `plt_` token setup.sh wrote. Required — see below. */
47
- token: string;
48
- displayName?: string;
49
- socketPath?: string;
50
- onStep?: (message: string) => void;
51
- }): Promise<GenesisResult>;
52
12
  /**
53
13
  * Reconstruct the seed and unlock.
54
14
  *
@@ -59,9 +19,9 @@ export declare function runGenesis(args: {
59
19
  export declare function runUnlock(args: {
60
20
  api: Transport;
61
21
  userKey: string;
62
- identity?: UsableIdentity;
63
- phrase?: string[];
64
- socketPath?: string;
22
+ phrase: string[];
23
+ ceremonyKey: string;
24
+ fingerprint: string;
65
25
  /**
66
26
  * This custodian's sealed envelope, read from the ceremony.
67
27
  *
@@ -69,11 +29,10 @@ export declare function runUnlock(args: {
69
29
  * unlocking. Opening it is a local step now, so no factor travels.
70
30
  */
71
31
  sealed?: SealedShare;
72
- /** The salt the phrase wrapping key was derived over. */
73
- phraseSalt?: string;
74
32
  onStep?: (message: string) => void;
75
33
  }): Promise<{
76
34
  fingerprint: string;
35
+ collected?: number;
36
+ required?: number;
37
+ unlocked?: boolean;
77
38
  }>;
78
- /** Resolve `--key` to a usable identity, with an actionable error if it cannot. */
79
- export declare function resolveIdentity(selector: string | undefined, socketPath?: string): Promise<UsableIdentity>;
@@ -10,3 +10,8 @@ import { type AgentIdentity } from '@forgezero/runtime/ssh-agent';
10
10
  */
11
11
  export declare function requestHeaders(apiBase: string, cookie: string | null): Record<string, string>;
12
12
  export declare function useCustodyIdentity(identity: AgentIdentity, socketPath?: string): void;
13
+ export declare function safeApiPath(value: string): string;
14
+ export declare function targetCoordinate(value: string): {
15
+ computeReference: string;
16
+ profile: string;
17
+ };
@@ -0,0 +1,22 @@
1
+ /** A short-lived browser-authorized session used only by the public CLI. */
2
+ export interface CliSession {
3
+ api: string;
4
+ /** Browser application origin that approved this session and fresh actions. */
5
+ app?: string;
6
+ realm: string;
7
+ cookie: string;
8
+ createdAtTs: number;
9
+ lastUsedAtTs: number;
10
+ user?: {
11
+ key?: string;
12
+ email?: string;
13
+ displayName?: string;
14
+ };
15
+ }
16
+ export declare function canonicalApi(value: string): string;
17
+ export declare const sessionId: (api: string, realm: string) => string;
18
+ export declare function defaultSessionPath(env?: NodeJS.ProcessEnv): string;
19
+ export declare function activeSession(path?: string): CliSession | null;
20
+ export declare function sessionFor(api: string, realm: string, path?: string): CliSession | null;
21
+ export declare function saveSession(session: CliSession, path?: string): void;
22
+ export declare function removeSession(api: string, realm: string, path?: string): void;
@@ -1,5 +1,6 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import type { DeploymentManager, DeploymentResult, GitSourceAuth } from './deployment';
3
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
3
4
  export interface RemoteDeploymentClaim {
4
5
  runKey: string;
5
6
  pipelineKey: string;
@@ -36,6 +37,7 @@ export interface DeploymentPullOptions {
36
37
  setTimer?: (callback: () => void, ms: number) => unknown;
37
38
  clearTimer?: (handle: unknown) => void;
38
39
  onEvent?: (event: string, detail?: unknown) => void;
40
+ telemetry?: AgentOperationTelemetry;
39
41
  }
40
42
  export type PullResult = {
41
43
  status: 'idle';
@@ -1,4 +1,5 @@
1
1
  import { type DrainReport, type QueueTask } from '@forgezero/runtime/queue';
2
+ import { type GitHostResolver } from './git-egress';
2
3
  import type { SoftwareRequirement } from './software';
3
4
  import { type RunResult } from './pipeline';
4
5
  import type { SecretCache } from './cache';
@@ -58,6 +59,8 @@ export interface DeploymentOptions {
58
59
  knownHostsPath?: string;
59
60
  /** Server-owned, operator-pinned host keys for a dynamically assigned source. */
60
61
  knownHostsContent?: string;
62
+ /** Test/embedding seam. Production uses the operating system resolver. */
63
+ resolveGitHost?: GitHostResolver;
61
64
  cache?: Pick<SecretCache, 'get'>;
62
65
  /** Non-secret values explicitly passed to every project phase. */
63
66
  environment?: Record<string, string>;
@@ -0,0 +1,50 @@
1
+ export declare const AGENT_EGRESS_TABLE = "forgezero_agent_egress";
2
+ export declare const SYSTEMD_RESOLVED_STUB = "/run/systemd/resolve/stub-resolv.conf";
3
+ export declare const SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
4
+ export declare const BLOCKED_IPV4: readonly ["0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "168.63.129.16/32", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4"];
5
+ export declare const BLOCKED_IPV6: readonly ["::/128", "::1/128", "::ffff:0:0/96", "64:ff9b::/96", "64:ff9b:1::/48", "100::/64", "fc00::/7", "fec0::/10", "fe80::/10", "ff00::/8", "2001::/32", "2001:2::/48", "2001:10::/28", "2001:20::/28", "2001:db8::/32", "2002::/16", "3fff::/20"];
6
+ export declare const normalizeEgressTcpPorts: (ports: readonly number[]) => readonly number[];
7
+ export interface LoopbackEgressGrant {
8
+ uid: number;
9
+ tcpPorts: readonly number[];
10
+ /** When present, reject every other public protocol/port for this UID. */
11
+ publicTcpPorts?: readonly number[];
12
+ }
13
+ /** Native cgroup filtering stays active even if an operator later reloads nftables. */
14
+ export declare function systemdAgentEgressDirectives(loopbackTcpPorts?: readonly number[]): string;
15
+ /** One atomic nftables transaction installed before systemd marks the policy ready. */
16
+ export declare function renderAgentEgressNft(uids: readonly number[], replace?: boolean, loopback?: LoopbackEgressGrant): string;
17
+ export interface EgressCommandResult {
18
+ exitCode: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ }
22
+ export type EgressCommand = (argv: readonly string[], stdin?: string) => EgressCommandResult;
23
+ export declare function resolveServiceUid(user: string, command?: EgressCommand): number;
24
+ export declare function assertResolvedStub(realpath?: (path: string) => string): void;
25
+ export declare function verifyAgentEgressPolicy(uids: readonly number[], command?: EgressCommand, loopback?: LoopbackEgressGrant): boolean;
26
+ export declare function applyAgentEgressPolicy(options: {
27
+ users: readonly string[];
28
+ loopback?: {
29
+ user: string;
30
+ tcpPorts: readonly number[];
31
+ publicTcpPorts?: readonly number[];
32
+ };
33
+ command?: EgressCommand;
34
+ realpath?: (path: string) => string;
35
+ getuid?: () => number;
36
+ }): readonly number[];
37
+ export declare function superviseAgentEgressPolicy(options: {
38
+ users: readonly string[];
39
+ loopback?: {
40
+ user: string;
41
+ tcpPorts: readonly number[];
42
+ publicTcpPorts?: readonly number[];
43
+ };
44
+ command?: EgressCommand;
45
+ realpath?: (path: string) => string;
46
+ getuid?: () => number;
47
+ intervalMs?: number;
48
+ notifyReady?: () => void;
49
+ wait?: (milliseconds: number) => Promise<void>;
50
+ }): Promise<never>;