Skip to content

Commit 2d68d2a

Browse files
authored
Merge branch 'develop' into feat/kill-containers-feature
2 parents 3642373 + 3231145 commit 2d68d2a

791 files changed

Lines changed: 107022 additions & 66692 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

evidence/tee/full-stack-local-2026-05-20.json

Lines changed: 591 additions & 153 deletions
Large diffs are not rendered by default.

packages/agent/scripts/tee-full-stack-local.ts

Lines changed: 692 additions & 259 deletions
Large diffs are not rendered by default.

packages/agent/scripts/tee-local-smoke.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { TeeEvidence } from "../src/services/tee-evidence.ts";
88
import {
99
HttpTeeKeyReleaseClient,
1010
LocalTeeKeyReleaseClient,
11+
wrapTeeReleaseKey,
1112
} from "../src/services/tee-key-release.ts";
1213
import { evaluateTeeEvidencePolicy } from "../src/services/tee-policy.ts";
1314

@@ -191,6 +192,7 @@ async function handleKeyReleaseRequest(
191192
keyId: string;
192193
context?: string;
193194
nonce: string;
195+
ephemeralPublicKey: string;
194196
policy: Parameters<typeof evaluateTeeEvidencePolicy>[1];
195197
evidence: TeeEvidence;
196198
};
@@ -207,11 +209,18 @@ async function handleKeyReleaseRequest(
207209
.update(payload.evidence.measurements?.agent ?? "", "utf8")
208210
.update(payload.evidence.measurements?.policy ?? "", "utf8")
209211
.digest("hex");
212+
// Wrap the released key to the agent's ephemeral public key so the client's
213+
// X25519/HKDF/AES-256-GCM unwrap succeeds (plan §3.2 step 5).
214+
const wrappedKey = wrapTeeReleaseKey({
215+
keyMaterialHex,
216+
agentEphemeralPublicKeyDerBase64: payload.ephemeralPublicKey,
217+
nonceHex: payload.nonce,
218+
});
210219
response.writeHead(200, { "content-type": "application/json" });
211220
response.end(
212221
JSON.stringify({
213222
keyId: payload.keyId,
214-
keyMaterialHex,
223+
wrappedKey,
215224
// Echo the client-issued nonce for the replay-binding check.
216225
nonce: payload.nonce,
217226
decision,

packages/agent/src/api/server.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,9 +1552,9 @@ async function handleRequest(
15521552
pathname === "/api/onboarding/status" &&
15531553
isCloudProvisioned;
15541554
const isWhatsAppWebhookEndpoint = pathname === "/api/whatsapp/webhook";
1555-
const isBlueBubblesWebhookEndpoint =
1556-
pathname ===
1557-
resolveBlueBubblesWebhookPath({
1555+
const blueBubblesWebhookPath =
1556+
typeof resolveBlueBubblesWebhookPath === "function"
1557+
? resolveBlueBubblesWebhookPath({
15581558
runtime: state.runtime
15591559
? {
15601560
getService: (type: string) =>
@@ -1563,7 +1563,10 @@ async function handleRequest(
15631563
).getService(type),
15641564
}
15651565
: undefined,
1566-
});
1566+
})
1567+
: null;
1568+
const isBlueBubblesWebhookEndpoint =
1569+
blueBubblesWebhookPath != null && pathname === blueBubblesWebhookPath;
15671570
const isAuthProtectedPath = isAuthProtectedRoute(pathname);
15681571

15691572
const canonicalizeRestartReason = (reason: string): string => {
@@ -1700,8 +1703,12 @@ async function handleRequest(
17001703
}
17011704

17021705
const localInferenceServerApi = await getLocalInferenceServerApi();
1703-
if (await localInferenceServerApi.handleLocalInferenceRoutes(req, res))
1706+
if (
1707+
typeof localInferenceServerApi.handleLocalInferenceRoutes === "function" &&
1708+
(await localInferenceServerApi.handleLocalInferenceRoutes(req, res))
1709+
) {
17041710
return;
1711+
}
17051712
if (
17061713
localInferenceServerApi.handleLocalInferenceTtsRoute &&
17071714
(await localInferenceServerApi.handleLocalInferenceTtsRoute(req, res, {
@@ -4422,6 +4429,12 @@ export async function startApiServer(opts?: {
44224429
agentId,
44234430
},
44244431
);
4432+
if (!result || typeof result !== "object") {
4433+
logger.warn(
4434+
"[x402] startup validator returned no result; skipping x402 route validation",
4435+
);
4436+
return;
4437+
}
44254438
if (!result.valid) {
44264439
throw new Error(
44274440
`x402 configuration invalid:\n${result.errors.map((e) => ` • ${e}`).join("\n")}`,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
2+
import { createTestVault, type TestVault } from "@elizaos/vault";
3+
import type { TeeBootGate } from "../services/tee-boot-gate.ts";
4+
import {
5+
clearTeeBootGateState,
6+
setTeeBootGateState,
7+
} from "../services/tee-boot-gate-state.ts";
8+
import {
9+
bridgeAgentWalletsToProcessEnv,
10+
ensureAgentWallets,
11+
revealAgentWalletPrivateKey,
12+
} from "./agent-wallets.ts";
13+
14+
const blockingGate: TeeBootGate = {
15+
policy: undefined,
16+
teeConfigured: true,
17+
required: true,
18+
productionProfile: false,
19+
secretsEnabled: false,
20+
};
21+
22+
const AGENT_ID = "tee-gate-agent";
23+
24+
describe("agent-wallets TEE boot-gate enforcement", () => {
25+
let test: TestVault;
26+
27+
beforeEach(async () => {
28+
clearTeeBootGateState();
29+
test = await createTestVault();
30+
await ensureAgentWallets(test.vault, AGENT_ID, "test");
31+
});
32+
33+
afterEach(async () => {
34+
clearTeeBootGateState();
35+
delete process.env.ELIZA_AGENT_WALLET_AS_USER;
36+
delete process.env.EVM_PRIVATE_KEY;
37+
delete process.env.SOLANA_PRIVATE_KEY;
38+
await test.dispose();
39+
});
40+
41+
describe("revealAgentWalletPrivateKey", () => {
42+
it("reveals normally when no gate is set (inert default)", async () => {
43+
const pk = await revealAgentWalletPrivateKey(
44+
test.vault,
45+
AGENT_ID,
46+
"evm",
47+
"test",
48+
);
49+
expect(typeof pk).toBe("string");
50+
expect(pk.length).toBeGreaterThan(0);
51+
});
52+
53+
it("refuses with a [TeeBootGate] error when the gate blocks", async () => {
54+
setTeeBootGateState(blockingGate);
55+
await expect(
56+
revealAgentWalletPrivateKey(test.vault, AGENT_ID, "evm", "test"),
57+
).rejects.toThrow(/\[TeeBootGate\].*reveal blocked/);
58+
});
59+
});
60+
61+
describe("bridgeAgentWalletsToProcessEnv", () => {
62+
it("bridges to process.env when opted in and no gate is set", async () => {
63+
process.env.ELIZA_AGENT_WALLET_AS_USER = "1";
64+
const descriptors = [
65+
{
66+
agentId: AGENT_ID,
67+
chain: "evm" as const,
68+
address: "0xabc",
69+
lastModified: Date.now(),
70+
},
71+
];
72+
await bridgeAgentWalletsToProcessEnv(
73+
test.vault,
74+
AGENT_ID,
75+
descriptors,
76+
"test",
77+
);
78+
expect(process.env.EVM_PRIVATE_KEY?.length ?? 0).toBeGreaterThan(0);
79+
});
80+
81+
it("skips the bridge (no env write) when the gate blocks", async () => {
82+
process.env.ELIZA_AGENT_WALLET_AS_USER = "1";
83+
setTeeBootGateState(blockingGate);
84+
const descriptors = [
85+
{
86+
agentId: AGENT_ID,
87+
chain: "evm" as const,
88+
address: "0xabc",
89+
lastModified: Date.now(),
90+
},
91+
];
92+
await bridgeAgentWalletsToProcessEnv(
93+
test.vault,
94+
AGENT_ID,
95+
descriptors,
96+
"test",
97+
);
98+
expect(process.env.EVM_PRIVATE_KEY).toBeUndefined();
99+
});
100+
});
101+
});

packages/agent/src/runtime/agent-wallets.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
* up — see `runtime/eliza.ts`.
1919
*/
2020

21+
import { logger } from "@elizaos/core";
2122
import type { WalletChain } from "@elizaos/shared";
2223
import { removeEntryMeta, setEntryMeta, type Vault } from "@elizaos/vault";
2324
import { deriveEvmAddress, generateWalletForChain } from "../api/wallet.ts";
25+
import { teeBootGateBlocksSecrets } from "../services/tee-boot-gate-state.ts";
2426

2527
const PREFIX = "agent";
2628
const SEGMENT = "wallet";
@@ -146,6 +148,15 @@ export async function revealAgentWalletPrivateKey(
146148
chain: WalletChain,
147149
caller?: string,
148150
): Promise<string> {
151+
// Fail-closed under a blocking TEE boot gate: a signing private key is a
152+
// high-value secret and must not be revealed when TEE evidence is not
153+
// trusted. Inert when no TEE policy is configured (the gate is unset/not
154+
// required), so normal/local-only boots are unaffected.
155+
if (teeBootGateBlocksSecrets()) {
156+
throw new Error(
157+
`[TeeBootGate] agent-wallet private-key reveal blocked: TEE evidence is not trusted (agentId=${agentId}, chain=${chain}).`,
158+
);
159+
}
149160
const key = walletKey(agentId, chain);
150161
const raw = await vault.reveal(key, caller);
151162
return parseStored(raw).privateKey;
@@ -342,6 +353,16 @@ export async function bridgeAgentWalletsToProcessEnv(
342353
): Promise<void> {
343354
// Default off. Skipping bridge unless explicitly opted in.
344355
if (process.env.ELIZA_AGENT_WALLET_AS_USER !== "1") return;
356+
// Fail-closed under a blocking TEE boot gate: do not write private keys into
357+
// process.env when TEE evidence is not trusted. Skip-with-warn so the boot
358+
// continues secret-less. Inert when no TEE policy is configured.
359+
if (teeBootGateBlocksSecrets()) {
360+
logger.warn(
361+
{ agentId },
362+
"[TeeBootGate] Skipping agent-wallet → process.env bridge: TEE evidence is not trusted.",
363+
);
364+
return;
365+
}
345366
for (const d of descriptors) {
346367
const envKey = CHAIN_TO_ENV_KEY[d.chain];
347368
if (process.env[envKey]?.trim()) continue; // user-set wins

packages/agent/src/runtime/eliza.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,15 @@ import {
221221
SandboxManager,
222222
type SandboxMode,
223223
} from "../services/sandbox-manager.ts";
224+
import { createDstackTeeProvider } from "../services/dstack-tee-provider.ts";
225+
import {
226+
evaluateTeeBootGate,
227+
type TeeBootGate,
228+
} from "../services/tee-boot-gate.ts";
229+
import {
230+
setTeeBootGateState,
231+
teeBootGateBlocksSecrets,
232+
} from "../services/tee-boot-gate-state.ts";
224233
import {
225234
resolveDefaultAgentWorkspaceDir,
226235
shouldBootstrapWorkspaceInitFiles,
@@ -3995,7 +4004,47 @@ export async function startEliza(
39954004
);
39964005
};
39974006

4007+
// One-time TEE boot gate (plan §4.1 / agent A4). Inert when no TEE policy is
4008+
// configured: `evaluateTeeBootGate` returns secretsEnabled:true and normal/
4009+
// local-only boots are unaffected. When ELIZA_TEE_REQUIRED (or a production
4010+
// profile) resolves a required policy and the evidence is not trusted, the
4011+
// gate fails closed and high-value capabilities (remote plugin sync; future
4012+
// model-key/signing consumers) are withheld. Boot still proceeds in a
4013+
// degraded, secret-less mode — it never silently continues with secrets.
4014+
const runTeeBootGate = async (): Promise<void> => {
4015+
let teeBootGate: TeeBootGate;
4016+
try {
4017+
teeBootGate = await evaluateTeeBootGate({
4018+
env: process.env,
4019+
evidenceProvider: createDstackTeeProvider({ env: process.env }),
4020+
});
4021+
} catch (err) {
4022+
// A TEE policy was configured but evidence could not be collected or
4023+
// evaluated. Fail closed rather than crash the boot.
4024+
teeBootGate = {
4025+
policy: undefined,
4026+
teeConfigured: true,
4027+
required: true,
4028+
productionProfile: process.env.ELIZA_TEE_PRODUCTION_PROFILE === "true",
4029+
secretsEnabled: false,
4030+
};
4031+
logger.error(
4032+
`[TeeBootGate] TEE evidence evaluation failed; secrets disabled (fail-closed): ${formatError(err)}`,
4033+
);
4034+
}
4035+
// Publish the one-time decision so secret-path modules (agent-wallet key
4036+
// reveal/bridge, remote plugin sync) can consult it via the shared
4037+
// singleton. Inert when no TEE: the gate's `required` is false.
4038+
setTeeBootGateState(teeBootGate);
4039+
};
4040+
39984041
const syncRemoteCapabilityPluginsIfAvailable = async (): Promise<void> => {
4042+
if (teeBootGateBlocksSecrets()) {
4043+
logger.warn(
4044+
"[TeeBootGate] Skipping remote capability plugin sync: TEE evidence is not trusted.",
4045+
);
4046+
return;
4047+
}
39994048
try {
40004049
const result = await bootstrapRemoteCapabilityPlugins(runtime, {
40014050
unloadMissing: true,
@@ -4136,6 +4185,7 @@ export async function startEliza(
41364185
await registerConnectorSetupService();
41374186
await registerRemoteCodingRunner();
41384187
await initializeCoreRuntime();
4188+
await runTeeBootGate();
41394189
await syncRemoteCapabilityPluginsIfAvailable();
41404190
await applyPluginRoleGatingIfAvailable();
41414191
await registerConversationProximityProvider();
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import type { TeeBootGate } from "./tee-boot-gate.ts";
3+
import {
4+
clearTeeBootGateState,
5+
getTeeBootGateState,
6+
setTeeBootGateState,
7+
teeBootGateBlocksSecrets,
8+
} from "./tee-boot-gate-state.ts";
9+
10+
const blockingGate: TeeBootGate = {
11+
policy: undefined,
12+
teeConfigured: true,
13+
required: true,
14+
productionProfile: false,
15+
secretsEnabled: false,
16+
};
17+
18+
const trustedRequiredGate: TeeBootGate = {
19+
policy: undefined,
20+
teeConfigured: true,
21+
required: true,
22+
productionProfile: false,
23+
secretsEnabled: true,
24+
};
25+
26+
const localOnlyGate: TeeBootGate = {
27+
policy: undefined,
28+
teeConfigured: false,
29+
required: false,
30+
productionProfile: false,
31+
secretsEnabled: true,
32+
};
33+
34+
describe("tee-boot-gate-state", () => {
35+
afterEach(() => {
36+
clearTeeBootGateState();
37+
});
38+
39+
it("is inert by default (no gate set)", () => {
40+
expect(getTeeBootGateState()).toBeUndefined();
41+
expect(teeBootGateBlocksSecrets()).toBe(false);
42+
});
43+
44+
it("set/get round-trips the published decision", () => {
45+
setTeeBootGateState(blockingGate);
46+
expect(getTeeBootGateState()).toBe(blockingGate);
47+
});
48+
49+
it("clear resets to the inert default", () => {
50+
setTeeBootGateState(blockingGate);
51+
clearTeeBootGateState();
52+
expect(getTeeBootGateState()).toBeUndefined();
53+
expect(teeBootGateBlocksSecrets()).toBe(false);
54+
});
55+
56+
it("blocks only when required AND secrets disabled", () => {
57+
setTeeBootGateState(blockingGate);
58+
expect(teeBootGateBlocksSecrets()).toBe(true);
59+
});
60+
61+
it("does not block when TEE is required but evidence is trusted", () => {
62+
setTeeBootGateState(trustedRequiredGate);
63+
expect(teeBootGateBlocksSecrets()).toBe(false);
64+
});
65+
66+
it("does not block for a local-only (not required) gate", () => {
67+
setTeeBootGateState(localOnlyGate);
68+
expect(teeBootGateBlocksSecrets()).toBe(false);
69+
});
70+
71+
it("does not block when secrets are disabled but policy is not required", () => {
72+
setTeeBootGateState({
73+
policy: undefined,
74+
teeConfigured: true,
75+
required: false,
76+
productionProfile: false,
77+
secretsEnabled: false,
78+
});
79+
expect(teeBootGateBlocksSecrets()).toBe(false);
80+
});
81+
});

0 commit comments

Comments
 (0)