@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.
@@ -1,5 +1,6 @@
1
1
  import type { NodeKeyPair } from '@forgezero/runtime/identity';
2
2
  import { type SignedNodeHttpOptions } from './signed-node-http';
3
+ import type { AgentOperationTelemetry } from './telemetry-runtime';
3
4
  interface RemoteProvisionClaimBase {
4
5
  computeKey: string;
5
6
  claimToken: string;
@@ -73,6 +74,7 @@ export interface ProvisioningPullOptions extends SignedNodeHttpOptions {
73
74
  kvm: boolean;
74
75
  helper: boolean;
75
76
  };
77
+ telemetry?: AgentOperationTelemetry;
76
78
  }
77
79
  export type ProvisionPullResult = {
78
80
  status: 'idle';
@@ -70,6 +70,7 @@ async function postSignedNode(options, path, body) {
70
70
  // src/provisioning-pull.ts
71
71
  class ProvisionClaimLostError extends Error {
72
72
  }
73
+ var remoteOutcome = (cause) => cause instanceof SignedNodeHttpError && cause.status < 500 ? "refused" : "retryable";
73
74
  var post = (options, operation, body) => postSignedNode(options, `v1/metal/computes/${operation}`, body);
74
75
  async function runClaim(options, claim) {
75
76
  const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
@@ -88,10 +89,11 @@ async function runClaim(options, claim) {
88
89
  if (stopped || renewal)
89
90
  return;
90
91
  let retry;
91
- renewal = post(options, "renew", {
92
+ const renew = () => post(options, "renew", {
92
93
  computeKey: claim.computeKey,
93
94
  claimToken: claim.claimToken
94
- }).then((response) => {
95
+ });
96
+ renewal = (options.telemetry ? options.telemetry.observe("provisioning.renew", renew, () => "success", remoteOutcome) : renew()).then((response) => {
95
97
  expires = response.claimExpiresAtTs;
96
98
  options.onEvent?.("lease-renewed", { computeKey: claim.computeKey, claimExpiresAtTs: expires });
97
99
  }).catch((cause) => {
@@ -112,7 +114,8 @@ async function runClaim(options, claim) {
112
114
  schedule();
113
115
  let result;
114
116
  try {
115
- result = await options.run(claim);
117
+ const run = () => options.run(claim);
118
+ result = options.telemetry ? await options.telemetry.observe("provisioning.apply", run) : await run();
116
119
  } finally {
117
120
  stopped = true;
118
121
  clearTimer(timer);
@@ -143,7 +146,8 @@ async function complete(options, body) {
143
146
  async function pullProvisioningOnce(options) {
144
147
  if (options.metalPreflight) {
145
148
  const report = options.metalPreflight();
146
- const accepted = await postSignedNode(options, "v1/metal/preflight", report);
149
+ const preflight = () => postSignedNode(options, "v1/metal/preflight", report);
150
+ const accepted = options.telemetry ? await options.telemetry.observe("provisioning.preflight", preflight, (value) => value.ready ? "success" : "refused", remoteOutcome) : await preflight();
147
151
  if (options.metalHostname && accepted.hostname !== options.metalHostname) {
148
152
  throw new Error(`metal agent: configured inventory hostname ${options.metalHostname} is bound as ${accepted.hostname}; refusing work.`);
149
153
  }
@@ -156,7 +160,8 @@ async function pullProvisioningOnce(options) {
156
160
  if (!accepted.ready)
157
161
  return { status: "idle" };
158
162
  }
159
- const response = await post(options, "claim", {});
163
+ const claimWork = () => post(options, "claim", {});
164
+ const response = options.telemetry ? await options.telemetry.observe("provisioning.claim", claimWork, (value) => value.claim ? "success" : "idle", remoteOutcome) : await claimWork();
160
165
  if (!response.claim)
161
166
  return { status: "idle" };
162
167
  const claim = response.claim;
@@ -167,20 +172,28 @@ async function pullProvisioningOnce(options) {
167
172
  if (cause instanceof ProvisionClaimLostError)
168
173
  throw cause;
169
174
  const reason = (cause instanceof Error ? cause.message : String(cause)).slice(0, 2000);
170
- await complete(options, {
175
+ const acknowledge2 = () => complete(options, {
171
176
  computeKey: claim.computeKey,
172
177
  claimToken: claim.claimToken,
173
178
  ok: false,
174
179
  detail: reason
175
180
  });
181
+ if (options.telemetry) {
182
+ await options.telemetry.observe("provisioning.complete", acknowledge2, () => "success", remoteOutcome);
183
+ } else
184
+ await acknowledge2();
176
185
  return { status: "failed", claim, reason };
177
186
  }
178
- await complete(options, {
187
+ const acknowledge = () => complete(options, {
179
188
  computeKey: claim.computeKey,
180
189
  claimToken: claim.claimToken,
181
190
  ok: true,
182
191
  ...result.guestAddress ? { guestAddress: result.guestAddress } : {}
183
192
  });
193
+ if (options.telemetry) {
194
+ await options.telemetry.observe("provisioning.complete", acknowledge, () => "success", remoteOutcome);
195
+ } else
196
+ await acknowledge();
184
197
  return { status: claim.action === "delete" ? "terminated" : "running", claim, result };
185
198
  }
186
199
  function startProvisioningPull(options) {
@@ -0,0 +1,20 @@
1
+ import type { AgentTelemetry, AgentTelemetryEvent, AgentTelemetryOperation, AgentTelemetryOutcome } from './telemetry';
2
+ /**
3
+ * Process-wide operation/state accounting layered over the deliberately small
4
+ * OTLP emitter. Callers still cannot attach arbitrary attributes or payloads.
5
+ */
6
+ export declare class AgentTelemetryRuntime {
7
+ private readonly telemetry;
8
+ private readonly now;
9
+ private active;
10
+ private completed;
11
+ private failed;
12
+ private draining;
13
+ constructor(telemetry: AgentTelemetry, now?: () => number);
14
+ event(event: AgentTelemetryEvent): void;
15
+ setDraining(draining: boolean): void;
16
+ observe<T>(operation: AgentTelemetryOperation, work: () => Promise<T>, outcome?: (value: T) => AgentTelemetryOutcome, errorOutcome?: (cause: unknown) => AgentTelemetryOutcome): Promise<T>;
17
+ close(): Promise<void>;
18
+ private recordState;
19
+ }
20
+ export type AgentOperationTelemetry = Pick<AgentTelemetryRuntime, 'observe'>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Dependency-free OTLP/HTTP telemetry for the machine Agent.
3
+ *
4
+ * Callers can name only reviewed operations, outcomes and lifecycle events.
5
+ * There is deliberately no field for a URL, request/response body, header,
6
+ * credential, tenant coordinate, command, error detail or arbitrary label.
7
+ */
8
+ export declare const AGENT_TELEMETRY_OPERATIONS: readonly ["agent.heartbeat", "agent.update", "attestation.refresh", "deployment.claim", "deployment.run", "deployment.renew", "deployment.complete", "migration.claim", "migration.run", "migration.renew", "migration.complete", "provisioning.preflight", "provisioning.claim", "provisioning.apply", "provisioning.renew", "provisioning.complete", "vault.sync", "agent.other"];
9
+ export type AgentTelemetryOperation = (typeof AGENT_TELEMETRY_OPERATIONS)[number];
10
+ export declare const AGENT_TELEMETRY_OUTCOMES: readonly ["success", "idle", "refused", "retryable", "failed"];
11
+ export type AgentTelemetryOutcome = (typeof AGENT_TELEMETRY_OUTCOMES)[number];
12
+ export declare const AGENT_TELEMETRY_EVENTS: readonly ["agent.started", "agent.draining", "agent.stopped", "agent.update_prepared", "agent.update_recovered", "telemetry.queue_overflow"];
13
+ export type AgentTelemetryEvent = (typeof AGENT_TELEMETRY_EVENTS)[number];
14
+ export interface AgentTelemetryConfig {
15
+ endpoint: string;
16
+ serviceName: string;
17
+ instanceId: string;
18
+ environment: 'production' | 'development';
19
+ flushIntervalMs: number;
20
+ traceSampleRatio: number;
21
+ }
22
+ export interface AgentOperationObservation {
23
+ operation: AgentTelemetryOperation;
24
+ outcome: AgentTelemetryOutcome;
25
+ startedAtMs: number;
26
+ durationMs: number;
27
+ }
28
+ export interface AgentTelemetryState {
29
+ active: number;
30
+ completed: number;
31
+ failed: number;
32
+ draining: boolean;
33
+ }
34
+ export interface AgentTelemetry {
35
+ recordOperation(observation: AgentOperationObservation): void;
36
+ recordEvent(event: AgentTelemetryEvent): void;
37
+ recordState(state: AgentTelemetryState): void;
38
+ flush(): Promise<void>;
39
+ close(): Promise<void>;
40
+ }
41
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
42
+ export declare const AGENT_OTLP_EXPORT_TIMEOUT_MS = 5000;
43
+ export declare const AGENT_OTLP_MAX_ATTEMPTS = 2;
44
+ export declare const AGENT_OTLP_MAX_RETRY_DELAY_MS = 1000;
45
+ export declare const AGENT_TELEMETRY_MAX_SPANS = 256;
46
+ export declare const AGENT_TELEMETRY_MAX_LOGS = 64;
47
+ /** Resolve once at process startup; production refuses to run without an exporter. */
48
+ export declare function resolveAgentTelemetryConfig(env?: Record<string, string | undefined>): AgentTelemetryConfig | null;
49
+ export declare function createAgentTelemetry(input: AgentTelemetryConfig | null, options?: {
50
+ fetch?: FetchLike;
51
+ warn?: (message: string) => void;
52
+ now?: () => number;
53
+ random?: () => number;
54
+ wait?: (milliseconds: number) => Promise<void>;
55
+ }): AgentTelemetry;
56
+ export {};
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.31";
2
+ export declare const VERSION = "0.1.33";
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
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
2
  "name": "@forgezero/agent",
4
- "version": "0.1.31",
3
+ "version": "0.1.33",
5
4
  "type": "module",
6
5
  "scripts": {
7
6
  "check": "tsc --noEmit",
@@ -18,19 +17,20 @@
18
17
  "@noble/post-quantum": "^0.6.1"
19
18
  },
20
19
  "dependencies": {
21
- "@forgezero/runtime": "^0.1.4",
22
- "@forgezero/vault": "^0.1.7",
23
- "@noble/curves": "^2.2.0",
24
- "@noble/hashes": "^2.2.0",
25
- "@noble/post-quantum": "^0.6.1",
26
- "@scure/bip39": "^2.2.0"
20
+ "@forgezero/runtime": "0.1.5",
21
+ "@forgezero/vault": "0.1.8",
22
+ "@noble/curves": "2.2.0",
23
+ "@noble/hashes": "2.2.0",
24
+ "@noble/post-quantum": "0.6.1",
25
+ "@scure/bip39": "2.2.0"
27
26
  },
28
27
  "bin": {
29
28
  "fz-agent": "dist/fz-agent.js",
30
29
  "fz": "dist/fz.js"
31
30
  },
32
31
  "publishConfig": {
33
- "access": "public"
32
+ "access": "public",
33
+ "provenance": true
34
34
  },
35
35
  "files": [
36
36
  "dist",
@@ -53,7 +53,7 @@
53
53
  "repository": {
54
54
  "type": "git",
55
55
  "url": "git+https://github.com/forgezero-net/packages.git",
56
- "directory": "packages/agent"
56
+ "directory": "agent"
57
57
  },
58
58
  "bugs": "https://github.com/forgezero-net/packages/issues",
59
59
  "exports": {