Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/flux/a2a/trust.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,33 @@ def record_interaction(
How well the capability matched expectations (0.0–1.0).
behavior_signature : float
Metric of behavioral consistency.

Raises
------
ValueError
If *capability_match* or *behavior_signature* is NaN.
"""
# Reject NaN values to prevent trust poisoning (Issue #17)
if math.isnan(capability_match):
raise ValueError(
"capability_match must not be NaN"
)
if math.isnan(behavior_signature):
raise ValueError(
"behavior_signature must not be NaN"
)
if math.isnan(latency_ms):
raise ValueError(
"latency_ms must not be NaN"
)
# Clamp trust-related floats to [0.0, 1.0]
capability_match = max(0.0, min(1.0, capability_match))
profile = self._get_profile(agent_a, agent_b)
record = InteractionRecord(
timestamp=time.time(),
success=success,
latency_ms=latency_ms,
capability_match=max(0.0, min(1.0, capability_match)),
capability_match=capability_match,
behavior_signature=behavior_signature,
)
profile.history.append(record)
Expand Down Expand Up @@ -192,7 +212,17 @@ def compute_trust(self, agent_a: str, agent_b: str) -> float:
return max(0.0, min(1.0, composite * decay))

def check_trust(self, agent_a: str, agent_b: str, threshold: float) -> bool:
"""Check if trust from *agent_a* to *agent_b* meets *threshold*."""
"""Check if trust from *agent_a* to *agent_b* meets *threshold*.

Raises
------
ValueError
If *threshold* is NaN.
"""
if math.isnan(threshold):
raise ValueError("threshold must not be NaN")
# Clamp threshold to [0.0, 1.0]
threshold = max(0.0, min(1.0, threshold))
return self.compute_trust(agent_a, agent_b) >= threshold

def revoke_trust(self, agent_a: str, agent_b: str) -> None:
Expand Down
76 changes: 58 additions & 18 deletions src/flux/vm/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from __future__ import annotations

import math
import struct
from typing import Callable, Optional

Expand Down Expand Up @@ -141,6 +142,9 @@ def __init__(
# Call stack for ENTER/LEAVE frame tracking
self._frame_stack: list[int] = [] # stack of saved SP values

# Capability tracking for CAP_REQUIRE / CAP_GRANT / CAP_REVOKE
self.capabilities: set[int] = set()

# Create default memory regions
self.memory.create_region("stack", memory_size, "system")
self.memory.create_region("heap", memory_size, "system")
Expand All @@ -150,11 +154,31 @@ def __init__(

# ── Public API ─────────────────────────────────────────────────────────

MAX_BYTECODE_SIZE = 1_048_576 # 1 MB max bytecode size

@staticmethod
def verify_bytecode(bytecode: bytes) -> None:
"""Validate bytecode before execution.

Raises:
ValueError: If bytecode is empty, too large, or invalid.
"""
if not bytecode or len(bytecode) == 0:
raise ValueError(
"Bytecode verification failed: bytecode is empty"
)
if len(bytecode) > Interpreter.MAX_BYTECODE_SIZE:
raise ValueError(
f"Bytecode verification failed: bytecode size {len(bytecode)} "
f"exceeds maximum {Interpreter.MAX_BYTECODE_SIZE} bytes"
)

def execute(self) -> int:
"""Execute bytecode until HALT, cycle budget exceeded, or error.

Returns the total number of cycles consumed.
"""
self.verify_bytecode(self.bytecode)
self.running = True
while self.running and not self.halted and self.cycle_count < self.max_cycles:
self._step()
Expand All @@ -179,6 +203,7 @@ def reset(self) -> None:
self._box_counter = 0
self._resources.clear()
self._frame_stack.clear()
self.capabilities.clear()

def dump_state(self) -> dict:
"""Return a serializable snapshot of the full VM state."""
Expand Down Expand Up @@ -532,23 +557,15 @@ def _step(self) -> None:

# ── Comparison: ICMP (generic compare with condition) ──────────────
if opcode_byte == Op.ICMP:
# Format E: [ICMP][rd:u8][rs1:u8][rs2:u8]
# rd = destination register for result, rs1/rs2 = comparison operands
# The condition code is embedded in rd's upper bits (rd & 0xF0).
# For compatibility with test vectors that encode [ICMP][cond][rs1][rs2],
# we treat the first operand byte as both destination and condition:
# bits 7-4 = condition code (0=EQ..9=UGE)
# bits 3-0 = destination register
# If rd < 16, the full byte is the condition and destination defaults to rd.
raw = self._fetch_u8()
rs1 = self._fetch_u8()
# Format: [ICMP][cond:u8][rd:u8][rs:u8]
# cond = condition code (0=EQ..9=UGE)
# rd = destination register AND first operand
# rs = second operand register
cond = self._fetch_u8()
rd = self._fetch_u8()
rs2 = self._fetch_u8()
a = self.regs.read_gp(rs1)
a = self.regs.read_gp(rd)
b_val = self.regs.read_gp(rs2)
# Test vectors use format: [ICMP][cond][rs1][rs2], result -> R(cond)
# But expected output is R0. So: rd = R0 always, cond = raw byte.
cond = raw
rd = 0 # test vectors expect result in R0
conditions = {
0: a == b_val, # EQ
1: a != b_val, # NE
Expand Down Expand Up @@ -1315,25 +1332,48 @@ def _step(self) -> None:
# ── A2A Capability: CAP_REQUIRE ────────────────────────────────────
if opcode_byte == Op.CAP_REQUIRE:
data = self._fetch_var_data()
self._dispatch_a2a("CAP_REQUIRE", data)
# Enforce capability: check if the required cap is in the set
if len(data) >= 4:
cap_id = struct.unpack_from('<I', data, 0)[0]
if cap_id not in self.capabilities:
raise VMA2AError(
f"CAP_REQUIRE: capability {cap_id} not granted",
opcode=opcode_byte,
pc=start_pc,
)
self.regs.write_gp(0, 0) # success
else:
self.regs.write_gp(0, 1) # failure
return

# ── A2A Capability: CAP_REQUEST ────────────────────────────────────
if opcode_byte == Op.CAP_REQUEST:
data = self._fetch_var_data()
# CAP_REQUEST is a signal to an external handler; forward but
# do not auto-grant. The handler decides.
self._dispatch_a2a("CAP_REQUEST", data)
return

# ── A2A Capability: CAP_GRANT ──────────────────────────────────────
if opcode_byte == Op.CAP_GRANT:
data = self._fetch_var_data()
self._dispatch_a2a("CAP_GRANT", data)
if len(data) >= 4:
cap_id = struct.unpack_from('<I', data, 0)[0]
self.capabilities.add(cap_id)
self.regs.write_gp(0, 0) # success
else:
self.regs.write_gp(0, 1) # failure
return

# ── A2A Capability: CAP_REVOKE ─────────────────────────────────────
if opcode_byte == Op.CAP_REVOKE:
data = self._fetch_var_data()
self._dispatch_a2a("CAP_REVOKE", data)
if len(data) >= 4:
cap_id = struct.unpack_from('<I', data, 0)[0]
self.capabilities.discard(cap_id)
self.regs.write_gp(0, 0) # success
else:
self.regs.write_gp(0, 1) # failure
return

# ── A2A Synchronization: BARRIER ───────────────────────────────────
Expand Down
Loading
Loading