Chainlink's Compute Runtime Environment (CRE) provides exactly the primitives SentinelCRE needs:
CRE runs on a Decentralized Oracle Network (DON) with Byzantine Fault Tolerant consensus. When SentinelCRE calls two AI models, the DON nodes independently execute the calls and must reach consensus on the results. This means:
- No single node can fake an AI verdict
- The system is resilient to node failures
- Verdicts are cryptographically signed by the DON
CRE's HTTPClient with ConsensusAggregationByFields is purpose-built for our use case:
httpClient.sendRequest(
runtime,
evaluateWithAI,
ConsensusAggregationByFields<AIVerdict>({
verdict: identical, // All DON nodes must get same verdict
confidence: median, // Median across nodes
reason: identical, // Consistent reasoning
}),
)CRE's EVMClient provides native contract interaction:
callContract()— Read agent policies from SentinelGuardianwriteReport()— Submit verdicts as signed reports
CRE's ConfidentialHTTPClient ensures API keys and guardrail thresholds are not visible to individual DON nodes. This is critical because:
- API keys for Claude and other models stay secret
- Policy thresholds (value limits, rate limits) stay hidden
- AI agents cannot learn their own constraints by observing DON traffic
Periodic health checks run via CronCapability:
- Every 5 minutes, read agent count + guardian responsiveness
headerByNumber()confirms chain liveness (block number + timestamp)filterLogs()queries recentActionDeniedevents for incident monitoring- Proactive defense, not just reactive
The 3rd trigger type — fires when SentinelGuardian emits critical on-chain events:
CircuitBreakerTriggered— agent frozen by automatic circuit breakerActionDenied— a verdict denied an agent's proposed action- Uses
headerByNumber()for block context andfilterLogs()for cross-agent threat summary - Near-real-time reaction — no polling delay
Queries on-chain event logs within a block range:
- Used in both cron health check and log trigger handler
- Counts recent denials for threat context
- Enables cross-agent incident correlation
Fetches block header (number + timestamp) to confirm chain is producing blocks:
- Used in health check for liveness confirmation
- Used in log trigger handler for event timestamp context
sentinel-workflow/main.ts
├── initWorkflow(config)
│ ├── handler(HTTPCapability.trigger(), onActionProposal)
│ ├── handler(CronCapability.trigger(), onHealthCheck)
│ └── handler(EVMClient.logTrigger(), onChainEvent) ← 3rd trigger
│
├── onActionProposal(runtime, payload)
│ ├── Parse HTTP payload → ActionProposal
│ ├── EVMClient.callContract() → Read agent policy
│ ├── Behavioral risk scoring (7 dimensions)
│ ├── HTTPClient.sendRequest() → Multi-AI consensus (Claude + GPT-4)
│ ├── encodeAbiParameters() → Build verdict report
│ └── EVMClient.writeReport() → Submit on-chain
│
├── onHealthCheck(runtime, payload)
│ ├── EVMClient.callContract() → Read agent count + guardian ping
│ ├── EVMClient.headerByNumber() → Chain liveness (block + timestamp)
│ └── EVMClient.filterLogs() → Recent denial incident count
│
└── onChainEvent(runtime, payload) ← Log Trigger
├── Decode event signature + agentId from log topics
├── EVMClient.headerByNumber() → Block timestamp context
└── EVMClient.filterLogs() → Cross-agent threat summary
const configSchema = z.object({
schedule: z.string(),
guardianContractAddress: z.string(),
aiEndpoint1: z.string(),
aiEndpoint2: z.string(),
// ...
})// Correct — CRE WASM compatible
const result = evmClient.callContract(runtime, { ... }).result()
// Wrong — breaks WASM compilation
const result = await evmClient.callContract(runtime, { ... })runtime.log('[SentinelCRE] ...') // Logging (not console.log)
runtime.now() // Timestamp (not Date.now())
runtime.getSecret('API_KEY') // Secrets (not process.env)The full verdict pipeline in sentinel-workflow/main.ts executes in 7 steps. Here's exactly how each CRE capability is used:
const httpCapability = new HTTPCapability()
handler(httpCapability.trigger({ authorizedKeys: [] }), onActionProposal)The workflow registers an HTTP trigger that receives ActionProposal payloads. The authorizedKeys field can restrict which signers can submit proposals in production.
const chainSelector = getNetwork({
chainFamily: 'evm',
chainSelectorName: config.evmChainSelectorName,
isTestnet: true,
})
const evmClient = new EVMClient(chainSelector)
const policyCallData = encodeFunctionData({
abi: GUARDIAN_ABI,
functionName: 'getAgentPolicy',
args: [proposal.agentId as `0x${string}`],
})
const policyResult = evmClient
.callContract(runtime, {
call: encodeCallMsg({
from: zeroAddress,
to: config.guardianContractAddress as Address,
data: policyCallData,
}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
})
.result()LAST_FINALIZED_BLOCK_NUMBER ensures all DON nodes read from the same finalized block — preventing consensus failures from block propagation delays.
const behavioralResult = analyzeAll(proposal, behaviorCtx, now.getTime(), ANOMALY_THRESHOLD)The behavioral engine (pure TypeScript, CRE WASM-compatible) runs 7 anomaly dimensions against the agent's behavioral context. The result is injected into the AI evaluation prompt so Layer 3 can factor behavioral intelligence into its verdict.
if (config.enableConfidentialCompute) {
const confClient = new ConfidentialHTTPClient()
aiVerdict = confClient
.sendRequest(
runtime,
(sendRequester: ConfidentialHTTPSendRequester) =>
evaluateWithConfidentialHttp(sendRequester, config, proposal, policyContext, behavioralResult),
ConsensusAggregationByFields<AIVerdict>({
verdict: identical, // All DON nodes must agree on APPROVED/DENIED
confidence: median, // Median confidence across nodes
reason: identical, // Consistent reasoning required
}),
)()
.result()
} else {
const httpClient = new HTTPClient()
aiVerdict = httpClient
.sendRequest(
runtime,
(sendRequester: HTTPSendRequester) =>
evaluateWithStandardHttp(sendRequester, config, proposal, policyContext, behavioralResult),
ConsensusAggregationByFields<AIVerdict>({
verdict: identical,
confidence: median,
reason: identical,
}),
)()
.result()
}Both paths use the same consensus aggregation strategy. ConsensusAggregationByFields ensures DON nodes compare the AI verdict field-by-field:
verdict: identical— all nodes must get the same APPROVED/DENIED resultconfidence: median— median smooths out minor floating-point differencesreason: identical— ensures consistent reasoning across nodes
Inside evaluateWithConfidentialHttp(), each AI model uses its own Vault DON secret:
// Model 1: Claude — Anthropic API key
const confRequest1 = {
vaultDonSecrets: [{ key: 'ANTHROPIC_API_KEY', namespace: 'sentinel' }],
request: {
url: config.aiEndpoint1,
method: 'POST',
bodyString: claudeBody,
multiHeaders: {
'x-api-key': { values: ['{{ANTHROPIC_API_KEY}}'] },
'anthropic-version': { values: ['2023-06-01'] },
},
},
}
// Model 2: GPT-4 — OpenAI API key
const confRequest2 = {
vaultDonSecrets: [{ key: 'OPENAI_API_KEY', namespace: 'sentinel' }],
request: {
url: config.aiEndpoint2,
method: 'POST',
bodyString: gptBody,
multiHeaders: {
Authorization: { values: ['Bearer {{OPENAI_API_KEY}}'] },
},
},
}{{ANTHROPIC_API_KEY}} and {{OPENAI_API_KEY}} are resolved inside the TEE from Vault DON. Node operators never see the decrypted keys, the evaluation prompts, or the AI models' responses. Using two independent model providers ensures genuine consensus diversity.
const reportBytes = encodeAbiParameters(
parseAbiParameters(
'bytes32 agentId, bool approved, string reason, address target, bytes4 funcSig, uint256 value, uint256 mintAmount',
),
[
proposal.agentId as `0x${string}`,
approved,
aiVerdict.reason,
proposal.targetContract as Address,
proposal.functionSignature as `0x${string}`,
BigInt(proposal.value),
BigInt(proposal.mintAmount),
],
)The verdict is ABI-encoded so SentinelGuardian.processVerdict(bytes reportData) can abi.decode it on-chain and run PolicyLib.checkAll() against the decoded parameters.
evmClient
.writeReport(runtime, {
to: config.guardianContractAddress as Address,
data: writeCallData,
})
.result()writeReport() submits a signed transaction as the CRE workflow's authorized address (must have WORKFLOW_ROLE on SentinelGuardian).
When DON nodes disagree on the AI verdict (e.g., non-deterministic AI output despite temperature: 0), the consensus aggregation fails. SentinelCRE handles this as a fail-safe denial:
identicalaggregation onverdictfield — If any DON node gets a different AI response,ConsensusAggregationByFieldsfails to reach consensus- CRE runtime treats consensus failure as an error — The
.result()call throws - All errors in the pipeline default to DENIED — The try/catch wrapping returns a denial response
- This is intentional — A consensus failure means we cannot be certain the AI approved the action, so the fail-safe default is denial
This means SentinelCRE never approves an action unless ALL DON nodes independently agree that BOTH AI models approved it.
const cronCapability = new CronCapability()
handler(cronCapability.trigger({ schedule: config.schedule }), onHealthCheck)The health check runs on a configurable cron schedule (default: */5 * * * *, every 5 minutes). It performs:
callContract()— reads agent count from AgentRegistry and pings SentinelGuardianheaderByNumber()— fetches the latest block header to confirm chain livenessfilterLogs()— queries recentActionDeniedevents (last 50 blocks) for incident monitoring
Returns: { status, registeredAgents, guardianReachable, latestBlock, blockTimestamp, recentDenials }.
const evmClient = new EVMClient(chainSelector)
const logTrigger = evmClient.logTrigger({
addresses: [config.guardianContractAddress],
topics: [
{ values: [EVENT_CIRCUIT_BREAKER, EVENT_ACTION_DENIED] },
{ values: [] }, { values: [] }, { values: [] },
],
})
handler(logTrigger, onChainEvent)The 3rd trigger type — fires in near-real-time when SentinelGuardian emits CircuitBreakerTriggered or ActionDenied events. The handler:
- Decodes the event signature and agentId from log topics
- Calls
headerByNumber()to get block timestamp for context - Calls
filterLogs()to query recent denials across all agents (last 100 blocks) - Returns a threat summary report
This enables event-driven monitoring without polling — the DON reacts to on-chain events as they happen.