Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
336534a
feat: #216 in-sandbox authorized cred-fetch harness — the sandbox age…
hanwencheng Jun 11, 2026
130fc72
feat: #216 v2-demo phase 5 runs in CI — mock-wire-demo.sh emulates th…
hanwencheng Jun 11, 2026
26dd172
fix: #216 delegated cred-fetch reads the master's vault — the agent-i…
hanwencheng Jun 11, 2026
4ae62c4
fix: stage-1 agent-create self-heals a §10.2 sandbox-paired (keyless)…
hanwencheng Jun 11, 2026
7c15dea
fix: chain helpers take chain_id from the pinned profile, not a live …
hanwencheng Jun 11, 2026
ade5a0f
feat: #216 default-key selection — off-chain cred manifest (no contra…
hanwencheng Jun 11, 2026
e24f414
feat: #216 default-key selection — harness e2e proof + docs (the no-U…
hanwencheng Jun 11, 2026
7b6cbba
docs(wiki): master recovery + guardians — on-chain M-of-N execution, …
hanwencheng Jun 11, 2026
c93661f
fix: email-init demo is idempotent — 'Already initialized' is success…
hanwencheng Jun 11, 2026
ba65f9e
feat: #109 two-tier audit — tier-A appendV2 anchor relay + SSE feed +…
hanwencheng Jun 11, 2026
b842e2e
feat: #109 anchor anti-spam gate + async HTTP-flush anchoring + smoke…
hanwencheng Jun 11, 2026
f789458
docs: #109 plan status — shipped in PR #281, deviations noted
hanwencheng Jun 11, 2026
376f23f
fix: clippy while-let in drain_sse_frames (CI runs -D warnings --all-…
hanwencheng Jun 11, 2026
dba8581
ci: #209 — config quarantine self-dissolves: steps 19+21 run as live …
hanwencheng Jun 11, 2026
fd9df6c
fix: #109 anchor gate must use the env-aware SidecarRegistry on the t…
hanwencheng Jun 11, 2026
eae393d
fix: smoke anchor confirm — cast receipt status prints 'true' on this…
hanwencheng Jun 11, 2026
8f8bb5d
docs: #109 plan — anchor loop verified live in CI (registry-gate + ca…
hanwencheng Jun 11, 2026
851d190
Merge remote-tracking branch 'origin/main' into claude/adoring-lampor…
hanwencheng Jun 11, 2026
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
15 changes: 8 additions & 7 deletions .github/workflows/harness-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ jobs:
# config bucket/role aren't provisioned yet (setup-cloud.sh --ci).
CONFIG_BUCKET=agentkeys-config-test-$ACCOUNT_ID
CONFIG_ROLE_ARN=arn:aws:iam::$ACCOUNT_ID:role/agentkeys-config-role-test
AUDIT_BUCKET=agentkeys-audit-test-$ACCOUNT_ID
AGENTKEYS_SIGNER_URL=https://signer-test.$TEST_BROKER_ZONE
# Worker URLs derived from TEST_BROKER_ZONE → byte-for-byte match
# setup-broker-host.sh --test's derive_companion() output.
Expand Down Expand Up @@ -968,20 +969,20 @@ jobs:
# every OTHER prereq still fails closed):
# - scope-not-set: setScopeWithWebauthn needs a real WebAuthn assertion
# (heima-scope-set.sh L172) a no-Touch-ID runner cannot produce.
# - config-role-missing (#201): the test config bucket+role is still an
# operator one-shot (setup-cloud.sh --ci) → step 19 (config WRITE)
# skips. config-worker-unreachable was dropped — broker-deploy §7b
# auto-issued config-test's cert, so step 21 (cross-class cap
# rejection, role-independent) runs as a live gate (#209 closed; its
# self-dissolving guard step was removed).
# - (#209/#201 config allowances RESOLVED 2026-06-11: config-worker-
# unreachable dropped when broker-deploy §7b auto-issued config-test's
# cert (#209 closed; self-dissolving guard removed); config-role-missing
# dropped too — the test config bucket+role is provisioned and step 19
# (config WRITE + cross-bucket denials) ran LIVE in PR #281's green
# runs. Steps 19+21 are both live gates now; CI fails closed on drift.)
#
# The workflow_dispatch `stage` input still selects a single phase
# (`--stage N`); push/PR (stage='' / 'all') runs the full 1-4 + 6 sequence.
env:
STAGE: ${{ inputs.stage }}
run: |
set -euo pipefail
ARGS=(--ci --allow-skip=scope-not-set,config-role-missing,classify-not-configured,classify-worker-unavailable)
ARGS=(--ci --allow-skip=scope-not-set,classify-not-configured,classify-worker-unavailable)
case "${STAGE:-}" in
1|2|3) ARGS+=(--stage "$STAGE") ;;
*) ;; # all / empty → full phases 1-6 (phase 5 = the mock-sandbox wire)
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/parent-control/app/_components/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export function AuditFeed({
}) {
const [filter, setFilter] = useState<string>('all');
const filtered = filter === 'all' ? events : events.filter((e) => e.chip === filter);
const filters: (ChipKind | 'all')[] = ['all', 'memory', 'creds', 'payment', 'audit', 'chain', 'broker'];
const filters: (ChipKind | 'all')[] = ['all', 'memory', 'creds', 'payment', 'audit', 'chain', 'broker', 'worker', 'anchor'];

if (events.length === 0) {
return (
Expand Down
2 changes: 2 additions & 0 deletions apps/parent-control/app/_components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type ChipKind =
| 'audit'
| 'broker'
| 'chain'
| 'worker'
| 'anchor'
| 'payment'
| 'revoke'
| 'scope'
Expand Down
2 changes: 2 additions & 0 deletions apps/parent-control/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const CHIP_STYLES: Record<ChipKind, string> = {
audit: 'chip',
broker: 'chip',
chain: 'chip ok',
worker: 'chip',
anchor: 'chip ok',
payment: 'chip warn',
revoke: 'chip bad',
scope: 'chip',
Expand Down
5 changes: 4 additions & 1 deletion crates/agentkeys-bundler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
//! all reads here are raw JSON). This bundler is PRIVATE: bound to loopback,
//! fed only by the broker — not a public alt-mempool.

pub mod legacy_tx;
// `legacy_tx` moved to core (#109) — both the bundler and the audit worker's
// tier-A anchor relay sign legacy EOA txs; one implementation, re-exported
// here so existing `agentkeys_bundler::legacy_tx` paths keep working.
pub use agentkeys_core::legacy_tx;
pub mod server;
31 changes: 28 additions & 3 deletions crates/agentkeys-core/examples/export_audit_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
//! ```

use agentkeys_core::audit::{
AuditEnvelope, AuditOpKind, AuditResult, CredFetchBody, CredStoreBody, DeviceAddBody,
K3EpochAdvanceBody, MemoryPutBody, PaymentDirectBody, PaymentEscrowRedeemBody, ScopeGrantBody,
SignEip191Body, SignEip712Body, ENVELOPE_VERSION,
AuditBatchFailedBody, AuditEnvelope, AuditOpKind, AuditResult, AuditRootAnchorBody,
CredFetchBody, CredStoreBody, DeviceAddBody, K3EpochAdvanceBody, MemoryPutBody,
PaymentDirectBody, PaymentEscrowRedeemBody, ScopeGrantBody, SignEip191Body, SignEip712Body,
ENVELOPE_VERSION,
};
use serde::Serialize;
use serde_json::{json, Value};
Expand Down Expand Up @@ -226,6 +227,30 @@ fn main() {
},
None,
),
vector(
AuditOpKind::AuditRootAnchor,
AuditRootAnchorBody {
merkle_root: hex0x(&[0x90; 32]),
op_kind_bitmap: hex0x(&{
let mut bm = [0u8; 32];
bm[31] = 0x03; // op_kinds 0+1 present
bm
}),
entry_count: 7,
relay_address: "0x4444444444444444444444444444444444444444".into(),
},
None,
),
vector(
AuditOpKind::AuditBatchFailed,
AuditBatchFailedBody {
merkle_root: hex0x(&[0x91; 32]),
entry_count: 3,
attempts: 3,
last_error: "eth_sendRawTransaction HTTP 503".into(),
},
None,
),
unknown_vector(250),
];

Expand Down
95 changes: 95 additions & 0 deletions crates/agentkeys-core/src/audit/bodies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,45 @@ pub struct ConfigTeardownBody {
pub actor_target: String,
}

// ── 90..99 — audit-service meta family (#109 tier-A hosted anchor) ─────
//
// The hosted relay anchors each per-operator Merkle batch by emitting an
// `AuditRootAnchor` envelope and committing ITS hash on-chain via the
// ungated `CredentialAudit.appendV2(operatorOmni, relayActorOmni, 90,
// envelopeHash)` — one tx per batch, real operator omni in the indexed
// topic, no contract change. Genuine anchors are distinguished from
// third-party spam by `tx.from == relay_address` (published at the
// worker's `GET /v1/audit/relay-info`). The master-gated `appendRootV2`
// remains the sovereign tier-B/C route.

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditRootAnchorBody {
/// 32-byte hex — Merkle root over the batch's envelope-hash leaves
/// (domain-separated scheme per `CredentialAudit.verifyEntryInRoot`).
pub merkle_root: String,
/// 32-byte hex — bit N set when the batch contains op_kind N (the
/// `appendRootV2` bitmap convention, carried in-body here).
pub op_kind_bitmap: String,
pub entry_count: u64,
/// 20-byte hex — the tier-A relay EOA that signed the anchor tx.
/// Verifiers match it against the anchor tx's `from`.
pub relay_address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditBatchFailedBody {
/// 32-byte hex — the root of the batch that failed to anchor. Its
/// entries are re-queued, so a later `AuditRootAnchor` (with a fresh
/// root superset) eventually covers them.
pub merkle_root: String,
pub entry_count: u64,
/// How many submission attempts were made before giving up.
pub attempts: u8,
/// Last submission error, truncated — diagnostic only, not consumed
/// programmatically.
pub last_error: String,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -448,6 +487,62 @@ mod tests {
}
}

/// §15.3b step-5 worker test for the audit-service meta family (#109):
/// canonical CBOR roundtrip + typed decode for the tier-A anchor and
/// the batch-failed alert shapes.
#[test]
fn audit_meta_family_cbor_roundtrip_and_typed_decode() {
use crate::audit::{envelope_for, AuditEnvelope, AuditOpKind, AuditResult, TypedAuditBody};

let anchor = AuditRootAnchorBody {
merkle_root: format!("0x{}", "aa".repeat(32)),
op_kind_bitmap: format!("0x{}", "00".repeat(31)) + "03",
entry_count: 7,
relay_address: format!("0x{}", "ee".repeat(20)),
};
let env = envelope_for(
[0x44; 32], // relay's derived actor omni
[0x22; 32], // the REAL operator whose batch was anchored
AuditOpKind::AuditRootAnchor,
anchor.clone(),
AuditResult::Success,
None,
None,
)
.unwrap();
let decoded =
AuditEnvelope::from_canonical_cbor(&env.to_canonical_cbor().unwrap()).unwrap();
assert_eq!(AuditOpKind::AuditRootAnchor.label(), "audit.root_anchor");
match decoded.typed_body().unwrap() {
TypedAuditBody::AuditRootAnchor(b) => assert_eq!(b, anchor),
other => panic!("unexpected typed body: {other:?}"),
}

let failed = AuditBatchFailedBody {
merkle_root: format!("0x{}", "bb".repeat(32)),
entry_count: 3,
attempts: 3,
last_error: "eth_sendRawTransaction HTTP 503".into(),
};
let env = envelope_for(
[0x44; 32],
[0x22; 32],
AuditOpKind::AuditBatchFailed,
failed.clone(),
AuditResult::Failure,
None,
None,
)
.unwrap();
let decoded =
AuditEnvelope::from_canonical_cbor(&env.to_canonical_cbor().unwrap()).unwrap();
assert_eq!(AuditOpKind::AuditBatchFailed.label(), "audit.batch_failed");
match decoded.typed_body().unwrap() {
TypedAuditBody::AuditBatchFailed(b) => assert_eq!(b, failed),
other => panic!("unexpected typed body: {other:?}"),
}
}

#[test]
fn payment_direct_body_uses_ref_as_field_name() {
// Sanity check: `ref` is a Rust reserved word, so the field is
Expand Down
18 changes: 13 additions & 5 deletions crates/agentkeys-core/src/audit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ use sha3::{Digest, Keccak256};
use thiserror::Error;

pub use bodies::{
ConfigGetBody, ConfigPutBody, ConfigTeardownBody, CredFetchBody, CredStoreBody,
CredTeardownBody, DeviceAddBody, DeviceRevokeBody, EmailReceiveBody, EmailSendBody,
K10RotateBody, K3EpochAdvanceBody, MemoryGetBody, MemoryPutBody, MemoryTeardownBody,
PaymentDirectBody, PaymentEscrowRedeemBody, ScopeGrantBody, ScopeRevokeBody, SignEip191Body,
SignEip712Body,
AuditBatchFailedBody, AuditRootAnchorBody, ConfigGetBody, ConfigPutBody, ConfigTeardownBody,
CredFetchBody, CredStoreBody, CredTeardownBody, DeviceAddBody, DeviceRevokeBody,
EmailReceiveBody, EmailSendBody, K10RotateBody, K3EpochAdvanceBody, MemoryGetBody,
MemoryPutBody, MemoryTeardownBody, PaymentDirectBody, PaymentEscrowRedeemBody, ScopeGrantBody,
ScopeRevokeBody, SignEip191Body, SignEip712Body,
};
pub use op_kind::AuditOpKind;

Expand Down Expand Up @@ -238,6 +238,8 @@ pub enum TypedAuditBody {
ConfigPut(ConfigPutBody),
ConfigGet(ConfigGetBody),
ConfigTeardown(ConfigTeardownBody),
AuditRootAnchor(AuditRootAnchorBody),
AuditBatchFailed(AuditBatchFailedBody),
}

impl TypedAuditBody {
Expand Down Expand Up @@ -277,6 +279,12 @@ impl TypedAuditBody {
AuditOpKind::ConfigTeardown => {
Self::ConfigTeardown(serde_json::from_value(value).ok()?)
}
AuditOpKind::AuditRootAnchor => {
Self::AuditRootAnchor(serde_json::from_value(value).ok()?)
}
AuditOpKind::AuditBatchFailed => {
Self::AuditBatchFailed(serde_json::from_value(value).ok()?)
}
})
}
}
Expand Down
15 changes: 13 additions & 2 deletions crates/agentkeys-core/src/audit/op_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
//! - 60-69 email family (EmailSend=60, EmailReceive=61; 62-69 reserved)
//! - 70-79 K3 family (K3EpochAdvance=70; 71-79 reserved)
//! - 80-89 config family (ConfigPut=80, ConfigGet=81, ConfigTeardown=82; 83-89 reserved)
//! - 90-255 reserved for future families
//! - 90-99 audit-service meta family (AuditRootAnchor=90, AuditBatchFailed=91; 92-99 reserved) — issue #109
//! - 100-255 reserved for future families

/// Canonical op_kind enum. The byte value MUST match the row in arch.md
/// §15.3a. The enum is `repr(u8)` so `as u8` gives the canonical byte.
Expand Down Expand Up @@ -47,6 +48,8 @@ pub enum AuditOpKind {
ConfigPut = 80,
ConfigGet = 81,
ConfigTeardown = 82,
AuditRootAnchor = 90,
AuditBatchFailed = 91,
}

impl AuditOpKind {
Expand Down Expand Up @@ -75,6 +78,8 @@ impl AuditOpKind {
80 => Self::ConfigPut,
81 => Self::ConfigGet,
82 => Self::ConfigTeardown,
90 => Self::AuditRootAnchor,
91 => Self::AuditBatchFailed,
_ => return None,
})
}
Expand Down Expand Up @@ -105,6 +110,8 @@ impl AuditOpKind {
Self::ConfigPut => "config.put",
Self::ConfigGet => "config.get",
Self::ConfigTeardown => "config.teardown",
Self::AuditRootAnchor => "audit.root_anchor",
Self::AuditBatchFailed => "audit.batch_failed",
}
}
}
Expand Down Expand Up @@ -140,6 +147,8 @@ mod tests {
AuditOpKind::ConfigPut,
AuditOpKind::ConfigGet,
AuditOpKind::ConfigTeardown,
AuditOpKind::AuditRootAnchor,
AuditOpKind::AuditBatchFailed,
];
for k in all {
let byte = k as u8;
Expand All @@ -156,7 +165,7 @@ mod tests {
#[test]
fn unknown_bytes_return_none() {
for byte in [
3u8, 9, 13, 19, 22, 32, 42, 53, 62, 71, 83, 89, 90, 200, 250, 255,
3u8, 9, 13, 19, 22, 32, 42, 53, 62, 71, 83, 89, 92, 99, 200, 250, 255,
] {
assert_eq!(
AuditOpKind::from_u8(byte),
Expand Down Expand Up @@ -193,6 +202,8 @@ mod tests {
AuditOpKind::ConfigPut as u8,
AuditOpKind::ConfigGet as u8,
AuditOpKind::ConfigTeardown as u8,
AuditOpKind::AuditRootAnchor as u8,
AuditOpKind::AuditBatchFailed as u8,
];
let s: HashSet<_> = all.iter().copied().collect();
assert_eq!(s.len(), all.len(), "duplicate byte assignment");
Expand Down
Loading
Loading