Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
a72e3e2
docs(audit): six-seat 0.01% audit + roadmap + investor one-pager
claude Jul 3, 2026
231025b
fix(chain-provider): close MKT-1 — fail-closed global uniqueness on t…
claude Jul 3, 2026
a62fc05
perf+quality: type the mint error surface (MintError) + first audit-e…
claude Jul 3, 2026
662fc3f
docs(strategy): state-of-system + master plan — 8-seat grade, archite…
claude Jul 3, 2026
422ea5d
docs(vision): North Star for Flint — cross-industry panel synthesis (…
claude Jul 3, 2026
d5c80f1
docs(vision): reconcile North Star with the Fable logic audit
claude Jul 3, 2026
1c43160
docs(vision): production North Star — converged wedge (agent accounta…
claude Jul 3, 2026
c9dc7ef
feat(carrier): peer authentication (G-CARRIER-PEER) — verified DID + …
claude Jul 3, 2026
4601c9a
feat(audit): portable mandate receipt — the standalone 'admissible re…
claude Jul 4, 2026
178d3f9
feat(audit): per-mandate receipt scoped to one capability, set-bound …
claude Jul 4, 2026
ab7e329
feat(cli): elastos verify-receipt — independent, fail-closed mandate-…
claude Jul 4, 2026
7846c9c
feat(mandate): operator mandate lifecycle — grant, durably-attested r…
claude Jul 4, 2026
42696bf
fix(mandate): fold in guardian + red-team findings — durable enforcem…
claude Jul 4, 2026
028e88a
feat(mandate): operator mandate list + one-command demo of the full loop
claude Jul 4, 2026
2e94f48
fix(mandate): fold in Sprint 4 reviews — no LIVE lie after revoke-all…
claude Jul 4, 2026
43eb867
feat(mandate): wire the act leg — POST /api/standing-grants/dispatch …
claude Jul 4, 2026
0a3a300
fix(mandate): fold in Sprint 5 reviews — replay guard, agent-key bind…
claude Jul 4, 2026
6bfdd88
feat(mandate): real executor seam behind dispatch — reconciliation is…
claude Jul 4, 2026
8dd17b6
feat(mandate): wire the first real affordance — runtime.audit_verify …
claude Jul 4, 2026
6eeff99
feat(mandate): demo performs a real agent act end-to-end (grant → per…
claude Jul 4, 2026
1ca0f47
feat(mandate): second real affordance — runtime.content_seen, a state…
claude Jul 4, 2026
7b76a7b
feat(ui): Flint mandate-card dashboard — the accountability loop, mad…
claude Jul 4, 2026
8fdc134
fix(ui): fold in dashboard review — never fabricate a receipt, honest…
claude Jul 4, 2026
f95ea43
feat(shell): Mandates opens as a native app in the ElastOS shell
claude Jul 4, 2026
89110a1
fix(shell): fold in Mandates app review — mark the preview signer, co…
claude Jul 4, 2026
c9c6700
feat(shell): mandates app reads LIVE data over the home-gateway (Spri…
claude Jul 4, 2026
7eced9b
feat(shell): in-shell revoke — the mandate kill switch (Sprint 13)
claude Jul 4, 2026
4bb1fcb
fix(shell): fold in Sprint 13 council findings — wrong-target kill sw…
claude Jul 4, 2026
b9a83ac
feat(mandates): durable mandates — registry + replay guard survive re…
claude Jul 4, 2026
31bd767
fix(mandates): fold in Sprint 14 council findings — power-loss durabi…
claude Jul 4, 2026
c5922ff
feat(mandates): grant from the shell — issue a mandate in the Mandate…
claude Jul 5, 2026
0884689
feat(mandates): runtime.notify — the first side-effecting affordance …
claude Jul 5, 2026
7927145
feat(mandates): runtime.state_put — second side-effecting affordance,…
claude Jul 5, 2026
b31a925
feat(mandates): Agent State panel — the operator watches what agents …
claude Jul 5, 2026
60b6418
harden(mandates): bounded time-windowed replay guard — pay down G-M7
claude Jul 5, 2026
c29c909
docs(mandates): consolidation ledger — the Flint mandate engine, one …
claude Jul 5, 2026
bf80226
harden(mandates): G-M4 — agent-key binding by default, surfaced, and …
claude Jul 5, 2026
4064da0
Sprint 21 (G-M7): per-mandate dispatch rate budget + grant-existence …
claude Jul 5, 2026
ed5fd9c
Sprint 22: dispatch rate is a first-class mandate property (per-grant…
claude Jul 5, 2026
d5b23d9
Sprint 22 council fold: honest 429, service-layer budget invariant, u…
claude Jul 5, 2026
9acd60a
Sprint 23 (closes G-M7): bounded grant-registry retention
claude Jul 5, 2026
5237a80
Sprint 23 council fold: honest bounds on the retention claim (doc-only)
claude Jul 5, 2026
8c05818
Sprint 24: fail-closed mandate mint — grant_durable emits CapabilityG…
claude Jul 5, 2026
78ede17
Sprint 24 guardian fold: durability-bound + reconciliation-scope word…
claude Jul 5, 2026
f2cf61a
Sprint 24 red-team fold: memory-log warn (F1) + compensating revoke o…
claude Jul 5, 2026
3e4395c
Sprint 25: runtime.state_get — the attested read side of the mandate-…
claude Jul 5, 2026
c6dcde2
Sprint 25 council fold: honest one-bit semantics (F1/F3) + require bi…
claude Jul 5, 2026
c8bbbaf
Sprint 26: agent-facing dispatch — an agent acts under its bound mand…
claude Jul 5, 2026
25f8b60
Sprint 26 council fold: precisely scope the charge-on-authorized clos…
claude Jul 5, 2026
a598c48
Sprint 27: the spend-capped runtime.pay affordance — an agent spends …
claude Jul 6, 2026
47ece5a
Sprint 27 council fold: harden the pay affordance's money invariants
claude Jul 6, 2026
2ade592
Sprint 28: durable money cap + operator provisioning — the money surv…
claude Jul 6, 2026
654c826
Sprint 28 council fold: honest failure edges on the durable money cap
claude Jul 6, 2026
258c6bc
Sprint 29: the real payment rail — two-generals-honest money over HTTP
claude Jul 6, 2026
081cac5
Sprint 29 council fold: honest failure surfaces + hardened rail edges
claude Jul 6, 2026
9be2731
Sprint 30: the payment ledger — every rail attempt custodied, indeter…
claude Jul 6, 2026
f727815
Sprint 30 council fold: the ledger joins the money-trusted core, hone…
claude Jul 6, 2026
565a510
Sprint 31: the Money panel — budgets, pending payments, reconciliatio…
claude Jul 6, 2026
6658d2a
Sprint 31 council fold: narrow the money surface, close the poisoned-…
claude Jul 6, 2026
a7f994c
Sprint 32: responsible-entity binding — the liability DID in the gran…
claude Jul 6, 2026
6d5cb62
S32 council fold: responsible-entity honesty (declared, not attested)
claude Jul 6, 2026
d60a76b
S33: harden the shell money-authorization perimeter (S31 F1)
claude Jul 6, 2026
11fa980
S33 council fold: canonical single-use key + pre-effect re-credit
claude Jul 6, 2026
095c60f
S34: the Flint↔DRM wedge — DrmMarketplaceProvider + rail_ref receipt
claude Jul 7, 2026
3dfa14f
S34 council fold: money-invariant classifier + broadcast-accepted hon…
claude Jul 7, 2026
a297bdb
S35: confirmation-aware DRM settlement + on-chain idempotency
claude Jul 7, 2026
2d7927b
S35 council fold: record-before-broadcast + confirmation fail-closed
claude Jul 7, 2026
f55b477
S36: DRM price gate — the spend cap is a literal on-chain ceiling
claude Jul 7, 2026
ef08d27
S36 council fold: quantity pin + pay-token drift + declared token
claude Jul 7, 2026
3d4bf39
S36.5 quality audit fold: money-path hygiene + open-source presentabi…
claude Jul 7, 2026
5a187a9
S37: the confirmation scheduler — pending DRM buys resolve themselves
claude Jul 7, 2026
94b6bba
S38: the Marketplace face — an agent shops under a mandate, the opera…
claude Jul 8, 2026
9a611fa
S39: runtime.market_quote — the agent shops within its mandate
claude Jul 8, 2026
fa680b7
S40: the chain-read deadline — no runtime thread parks forever on the…
claude Jul 8, 2026
7bf3dde
S41: deadline the sibling providers — one shared capsule watchdog, bo…
claude Jul 8, 2026
d7f3e65
S42: bound the access-path content sidecars — no thread parks on a hu…
claude Jul 8, 2026
4bb26d9
S43: type the pre/post-broadcast outcome — retire the string refund c…
claude Jul 8, 2026
2a35b85
S44: structured rail discriminator on the ledger record — close MKT-D…
claude Jul 9, 2026
14db6ce
S45: live testnet buy — verify-receipt CLI round-trip (gated) + live-…
claude Jul 9, 2026
133c340
Repo-wide rustfmt — make the CI fmt gate truthful (mechanical, no log…
claude Jul 9, 2026
ecae553
S46: Track B — close the trust residuals (P16 secrets strip, prepare-…
claude Jul 9, 2026
e726956
S46 council fold: strip secrets at EVERY capsule seam + structural gu…
claude Jul 9, 2026
3f6c341
S47: bounded viewer sessions — sweep + cap with deferred off-lock rea…
claude Jul 9, 2026
1ad189b
S48: the second market vertical — ERC-20 checkout + the market-provid…
claude Jul 9, 2026
6eae3b8
S48 red-team fold: erc20 mode parse fail-closed + mock-reservation bo…
claude Jul 9, 2026
0cd66ae
S48 guardian fold: zero-warning workspace + closed-world rail selecto…
claude Jul 9, 2026
538500a
S49: SPEC-mandate-v1 — the receipt + signed-intent wire format, pinne…
claude Jul 9, 2026
7ef8fbb
S49 council fold: spec honesty + verify_strict + revoke/preimage ratc…
claude Jul 9, 2026
f6a7089
S50: runtime.negotiate — the shop loop's middle leg (Track D3)
claude Jul 9, 2026
2d29c43
S50 council fold: shared price-gate conversion + bounded seller reaso…
claude Jul 9, 2026
c8dca99
S51: audit group commit — concurrent emits share fsyncs, contract unc…
claude Jul 9, 2026
71710bb
S51 council fold: poison-under-chain-lock + torn-tail quarantine + fl…
claude Jul 9, 2026
42b1ece
S52: audit-log single-opener flock + off-lock stdout echo + auditor p…
claude Jul 10, 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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ jobs:
- name: cargo check
working-directory: elastos
run: cargo check --workspace
# Type-check the network-gated live-buy test so a rename can't silently rot the operator's
# only live tool (it stays #[ignore]d + feature-off in the gate; this never runs it). S45 F3.
- name: cargo check (live-chain test)
working-directory: elastos
run: cargo check -p elastos-server --features live-chain --tests

test:
name: Test
Expand All @@ -58,6 +63,13 @@ jobs:
run: cargo test --workspace
# Network-sensitive tests are #[ignore] and won't run.
# Run them explicitly with: cargo test --workspace -- --ignored
- name: cargo test (dev-modes lane)
working-directory: elastos
# The money-path construction ratchets (S43 typed BuyError, the S46 prepare-leg
# deadline, chain-mock buys) are `#[cfg(feature = "dev-modes")]` — without this lane
# the gate never compiles them, and a ratchet outside the gate cannot ratchet
# (council S46 guardian F3). Lib-only; shares the workspace build cache.
run: cargo test -p elastos-server --lib --features dev-modes

build-release:
name: Build Release (x86_64)
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md),
- device DID, passkey principals, wallet proof bindings, and Recovery Kit
foundations without making wallet addresses the Runtime identity root
- agent capsule with signed gossip and verified-only AI responses
- Flint mandates: grant an AI agent scoped, expiring, revocable authority (a mandate, not
your keys), watch and kill it from the Mandates shell app, let it spend real money under a
durable spend cap on a payment rail (HTTPS adapter or on-chain DRM marketplace), and export
a portable signed receipt verifiable off-box with `elastos verify-receipt` — see
[docs/FLINT_MANDATE_ENGINE.md](docs/FLINT_MANDATE_ENGINE.md)

Important Browser status: the Browser ABI and hosted proof path are real, but
the final product browser is not complete. Stable arbitrary-site media, accepted
Expand Down Expand Up @@ -273,6 +278,8 @@ and release/docs slices.
| [docs/CONTENT_AVAILABILITY.md](docs/CONTENT_AVAILABILITY.md) | SmartWeb content availability, IPLD-compatible manifests, and provider boundary |
| [docs/WALLET_PROVIDER.md](docs/WALLET_PROVIDER.md) | Wallet, account, approval, proof, and signer/provider boundary |
| [docs/CHAIN_PROVIDER.md](docs/CHAIN_PROVIDER.md) | Typed blockchain provider boundary |
| [docs/FLINT_MANDATE_ENGINE.md](docs/FLINT_MANDATE_ENGINE.md) | Flint — mandates for AI agents: scoped, revocable authority, spend-capped payments, portable signed receipts |
| [docs/DRM_MARKETPLACE_RAIL.md](docs/DRM_MARKETPLACE_RAIL.md) | The DRM marketplace payment rail (on-chain settlement under a mandate) |
| [docs/BROWSER_CAPSULE.md](docs/BROWSER_CAPSULE.md) | Browser/Net/Exit/Engine ABI and current proof boundary |
| [docs/BROWSER_PROVIDER_BAKEOFF.md](docs/BROWSER_PROVIDER_BAKEOFF.md) | Browser provider comparison and acceptance gates |
| [docs/SITES.md](docs/SITES.md) | Local site hosting and public exposure |
Expand Down
115 changes: 72 additions & 43 deletions capsules/chain-provider/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,44 +394,41 @@ pub(super) fn decode_asset_created_log(entry: &Value) -> Option<(String, String,
/// (a precise canonical `decode_mint_content_id`, else the relayer-safe `mint_input_binds_content_id`
/// substring match). FAIL-CLOSED: `None` if no candidate binds the KID. Pure (no RPC) so it is
/// unit-testable; the caller scans the `AssetCreated` logs + fetches each candidate's input live.
pub(super) fn pick_asset_created_binding_kid(
pub(super) fn collect_kid_bindings(
decoded: &[(String, String, u64, u64, String)],
inputs: &std::collections::HashMap<String, String>,
want_content_id: &str,
) -> Option<(String, String)> {
// Gather every candidate whose mint calldata binds the KID, split by strength: PRECISE = the
// canonical `mint` decode yields exactly this contentId; SUBSTRING = the relayer-safe fallback
// (the 16 content-derived KID bytes appear in the calldata). A unique asset has a unique KID, so
// in normal operation exactly ONE candidate binds. The AssetCreated scan is NOT creator-constrained
// (topic[1] is null), so a hostile co-channel minter could embed the victim's KID in their OWN mint
// calldata; we require a UNIQUE binding and FAIL CLOSED on ambiguity (>1 distinct (operative,tokenId)
// binding the same KID) rather than bind the wrong tokenId and mis-charge the buyer. Preferring the
// canonical decode means a substring-only hostile candidate cannot displace the precise legit one.
// (Creator-constraining the scan would also let us RESOLVE the legit asset under such griefing — the
// follow-on documented in protocol.rs; this pass fails closed, which protects funds.)
let mut precise: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
let mut substring: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
) -> std::collections::BTreeSet<(String, String)> {
// Return the UNION of every distinct (operative, tokenId) whose mint calldata BINDS the KID —
// by EITHER the canonical `decode_mint_content_id` (PRECISE) OR the relayer-safe
// `mint_input_binds_content_id` (SUBSTRING: the 16 content-derived KID bytes appear in the
// calldata). A unique asset has a unique KID, so in normal operation exactly ONE (operative,
// tokenId) binds. The AssetCreated scan is channel-scoped but NOT creator-constrained within the
// channel (topic[1] is null), so a hostile CO-CHANNEL minter can embed the victim's public KID in
// their OWN mint calldata.
//
// MKT-1 fix: we take the UNION of both methods and DO NOT prefer PRECISE over SUBSTRING. The old
// "prefer precise" rule let a hostile CANONICAL mint (precise) silently displace a legit RELAYED
// mint (which decodes only as substring) — binding the buyer to the attacker's tokenId. Surfacing
// BOTH binders lets the caller FAIL CLOSED on any resulting ambiguity (>1 distinct pair, here or
// across windows) rather than mis-charge the buyer. This trades availability (a griefer can block a
// resolve by minting a collision) for safety (funds are never bound to the wrong token) — the
// correct default on a money path. Pure (no RPC), unit-testable; the caller unions across ALL
// windows and requires GLOBAL uniqueness.
let mut bindings: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
for (operative, token_id, _block, _log, tx_hash) in decoded {
let Some(input) = inputs.get(tx_hash) else {
continue;
};
let is_precise = decode_mint_content_id(input)
let precise = decode_mint_content_id(input)
.and_then(|cid| normalize_content_id_bytes16(&cid))
.as_deref()
== Some(want_content_id);
if is_precise {
precise.insert((operative.clone(), token_id.clone()));
} else if mint_input_binds_content_id(input, want_content_id) {
substring.insert((operative.clone(), token_id.clone()));
if precise || mint_input_binds_content_id(input, want_content_id) {
bindings.insert((operative.clone(), token_id.clone()));
}
}
// Prefer the canonical decode; fall back to the substring binder only when no precise binder exists.
// In either tier a UNIQUE binding is required — otherwise fail closed.
let chosen = if precise.is_empty() { &substring } else { &precise };
match chosen.len() {
1 => chosen.iter().next().cloned(),
_ => None, // 0 binders, or an ambiguous KID binding -> fail closed (the buy must not proceed)
}
bindings
}

/// Decode the leading `bytes16` contentId (KID) from a `mint(string,uint16,bytes,bytes)`
Expand Down Expand Up @@ -679,7 +676,7 @@ mod tests {
}

#[test]
fn pick_asset_created_binding_kid_matches_via_mint_calldata() {
fn collect_kid_bindings_binds_the_unique_asset() {
use std::collections::HashMap;
let kid = "9c2a000000000000000000000000e1a1";
let other = "00112233445566778899aabbccddeeff";
Expand All @@ -697,27 +694,25 @@ mod tests {
let mut inputs = HashMap::new();
inputs.insert("0xaa".to_string(), mint_a);
inputs.insert("0xbb".to_string(), mint_b.clone());
// Hit: the KID resolves to candidate A's (operative, tokenId), proven by the mint calldata.
assert_eq!(
pick_asset_created_binding_kid(&decoded, &inputs, kid),
Some(("0xopA".to_string(), "0x07".to_string())),
);
// Miss: an unknown KID -> None (fail closed; the buy must not proceed).
assert!(pick_asset_created_binding_kid(&decoded, &inputs, "deadbeefdeadbeefdeadbeefdeadbeef").is_none());
// The KID binds exactly candidate A's (operative, tokenId) — a unique binding.
let b = collect_kid_bindings(&decoded, &inputs, kid);
assert_eq!(b.len(), 1);
assert!(b.contains(&("0xopA".to_string(), "0x07".to_string())));
// Miss: an unknown KID -> empty (fail closed; the buy must not proceed).
assert!(collect_kid_bindings(&decoded, &inputs, "deadbeefdeadbeefdeadbeefdeadbeef").is_empty());
// Fail-closed: if the matching candidate's input is missing, it is skipped (no false bind).
let mut partial = HashMap::new();
partial.insert("0xbb".to_string(), mint_b);
assert!(pick_asset_created_binding_kid(&decoded, &partial, kid).is_none());
assert!(collect_kid_bindings(&decoded, &partial, kid).is_empty());
}

#[test]
fn pick_asset_created_binding_kid_fails_closed_on_ambiguous_binding() {
fn collect_kid_bindings_surfaces_both_binders_in_one_window_so_caller_fails_closed() {
use std::collections::HashMap;
let kid = "9c2a000000000000000000000000e1a1";
// Two AssetCreated candidates on the (creator-unconstrained) channel BOTH bind the same KID
// with DIFFERENT tokenIds — the legit asset (tokenId 7) and a hostile co-channel mint that
// re-uses the victim's KID (tokenId 0x66, newest). Binding either would mis-charge the buyer,
// so the resolver must FAIL CLOSED (None) rather than pick the newest candidate.
// Two AssetCreated candidates in ONE window BOTH bind the same KID with DIFFERENT tokenIds —
// the legit asset (tokenId 7) and a hostile co-channel mint re-using the victim's KID (0x66,
// newest). Both must SURFACE (set size 2) so the caller fails closed rather than pick newest.
let legit =
encode_mint_calldata("0x47cbeeb4", "ipfs://legit", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
Expand All @@ -731,9 +726,43 @@ mod tests {
let mut inputs = HashMap::new();
inputs.insert("0xaa".to_string(), legit);
inputs.insert("0xhh".to_string(), hostile);
assert!(
pick_asset_created_binding_kid(&decoded, &inputs, kid).is_none(),
"ambiguous KID binding must fail closed, not bind the newest (hostile) tokenId",
let b = collect_kid_bindings(&decoded, &inputs, kid);
assert_eq!(b.len(), 2, "both binders must surface so the caller fails closed");
}

// MKT-1 ratchet: the cross-window attack. A hostile mint in a NEWER window and the legit mint in
// an OLDER window each bind the victim's KID. The resolver accumulates bindings across ALL windows
// (never returns on the first) and requires GLOBAL uniqueness — so the union across windows has >1
// distinct (operative, tokenId) and the buy FAILS CLOSED instead of binding the newer (hostile)
// token. This models `resolve_token_id`'s global fold with the pure per-window primitive.
#[test]
fn kid_bindings_across_windows_fail_closed_on_cross_window_collision() {
use std::collections::BTreeSet;
use std::collections::HashMap;
let kid = "9c2a000000000000000000000000e1a1";
let legit =
encode_mint_calldata("0x47cbeeb4", "ipfs://legit", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
let hostile =
encode_mint_calldata("0x47cbeeb4", "ipfs://hostile", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
// NEWER window (scanned first): the hostile mint, tokenId 0x66.
let newer_decoded = vec![("0xopH".to_string(), "0x66".to_string(), 900u64, 0u64, "0xhh".to_string())];
let mut newer_inputs = HashMap::new();
newer_inputs.insert("0xhh".to_string(), hostile);
// OLDER window (scanned second): the legit mint, tokenId 0x07.
let older_decoded = vec![("0xopA".to_string(), "0x07".to_string(), 100u64, 0u64, "0xaa".to_string())];
let mut older_inputs = HashMap::new();
older_inputs.insert("0xaa".to_string(), legit);

// The global fold = union of per-window binding sets (what resolve_token_id accumulates).
let mut global: BTreeSet<(String, String)> = BTreeSet::new();
global.extend(collect_kid_bindings(&newer_decoded, &newer_inputs, kid));
global.extend(collect_kid_bindings(&older_decoded, &older_inputs, kid));
assert_eq!(
global.len(),
2,
"a cross-window KID collision must fail closed globally, never bind the newer (hostile) token",
);
}
}
Loading