netget-tool 0.3.4

Network protocol stubbing toolkit: ~280 protocol servers and clients whose responses come from an LLM, a script, a static rule or a person.
Documentation
use crate::protocol::log_template::LogTemplate;
use crate::{
    llm::actions::{
        client_trait::{Client, ClientActionResult},
        protocol_trait::Protocol,
        ActionDefinition, Parameter, ParameterDefinition,
    },
    protocol::{ConnectContext, EventType},
    state::AppState,
};
use anyhow::{bail, Result};
use serde_json::{json, Value};
use std::sync::LazyLock;
#[derive(Default)]
pub struct NostrClientProtocol;
impl NostrClientProtocol {
    pub fn new() -> Self {
        Self
    }
}
fn p(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
    Parameter {
        name: name.into(),
        type_hint: kind.into(),
        description: description.into(),
        required,
    }
}
fn action(
    name: &str,
    description: &str,
    parameters: Vec<Parameter>,
    example: Value,
) -> ActionDefinition {
    ActionDefinition {
        name: name.into(),
        description: description.into(),
        parameters,
        example,
        log_template: Some(LogTemplate::new().with_info(format!("Nostr {name} queued"))),
    }
}
fn actions() -> Vec<ActionDefinition> {
    vec![
        action("nostr_publish", "Sign and publish a native NIP-01 event with this client's private startup identity. Only the public signed event goes on the wire; a later correlated OK reports acceptance or refusal.", vec![p("kind", "integer", "NIP-01 kind0..65535; no local kind/archive policy", true), p("content", "string", "Unmodified UTF8 content, at most64KiB", true), p("tags", "array", "At most2000 nonempty string arrays,32 items each; default empty", false), p("created_at", "integer", "Nonnegative Unix seconds; default current time", false)], json!({"type":"nostr_publish","kind":1,"content":"hello from NetGet","tags":[["t","film"]]})),
        action("nostr_subscribe", "Send REQ with1..10 selected NIP-01 filters. Reusing an id replaces its filters; EOSE keeps the live subscription open. Limit0 requests only future events.", vec![p("subscription_id", "string", "Nonempty id, at most64 Unicode characters", true), p("filters", "array", "OR between filters, AND within each: ids/authors full lowercase hex, kinds, since/until, limit0..500, or #single-letter tag lists. No extension search/count/auth.", true)], json!({"type":"nostr_subscribe","subscription_id":"film","filters":[{"kinds":[1],"#t":["film"],"limit":0}]})),
        action("nostr_close", "Send CLOSE and forget the selected subscription locally. NIP-01 provides no close acknowledgement; already-selected relay frames can still arrive and are ignored.", vec![p("subscription_id", "string", "An open subscription id", true)], json!({"type":"nostr_close","subscription_id":"film"})),
        action("nostr_relay_info", "Fetch selected optional NIP-11 metadata from the same relay URL. Advertised capabilities and limits are informational; no automatic AUTH, COUNT or payment.", vec![], json!({"type":"nostr_relay_info"})),
        action("disconnect", "Cancel pending I/O and stop this WebSocket client", vec![], json!({"type":"disconnect"})),
    ]
}
fn event(id: &str, description: &str, parameters: Vec<Parameter>) -> EventType {
    EventType::new(id, description, json!({"type":"nostr_subscribe","subscription_id":"film","filters":[{"kinds":[1],"limit":0}]})).with_parameters(parameters).with_actions(actions())
}
pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event(
        "nostr_connected",
        "Verified WebSocket upgrade; this client's public signing identity",
        vec![
            p("relay_url", "string", "WS or WSS endpoint", true),
            p(
                "pubkey",
                "string",
                "Public signing key only; relay identity is separate NIP-11 self",
                true,
            ),
        ],
    )
});
pub static EVENT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event("nostr_received_event", "A complete event with validated content hash and BIP-340 signature",vec![p("subscription_id","string","Current local subscription id",true),p("event","object","Full public native event: id,pubkey,created_at,kind,tags,content,sig",true),p("stored_phase","boolean","True before EOSE, false afterwards; does not imply local storage",true),p("matches_current_filters","boolean","Informational current-filter match; already-selected frames can reflect earlier filters after replacement",true)])
});
pub static RESULT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event(
        "nostr_publish_result",
        "Correlated native OK, or a bounded wait that expired without an OK",
        vec![
            p("id", "string", "Published event id", true),
            p(
                "status",
                "string",
                "ok or timeout; timeout does not prove relay rejection",
                true,
            ),
            p(
                "accepted",
                "boolean|null",
                "Native OK boolean, null on timeout",
                true,
            ),
            p(
                "message",
                "string",
                "Native relay message, fixed deadline text on timeout",
                true,
            ),
            p(
                "reason_prefix",
                "string|null",
                "Native machine-readable reason prefix, including unknown extension prefixes",
                true,
            ),
        ],
    )
});
pub static SUBSCRIPTION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event(
        "nostr_subscription",
        "EOSE, native CLOSED, or local CLOSE receipt",
        vec![
            p(
                "subscription_id",
                "string",
                "Affected subscription identifier",
                true,
            ),
            p(
                "status",
                "string",
                "eose, closed, or local_close; EOSE remains live",
                true,
            ),
            p(
                "message",
                "string",
                "Native CLOSED reason; empty on EOSE/local close",
                true,
            ),
            p(
                "reason_prefix",
                "string|null",
                "Native CLOSED reason prefix",
                true,
            ),
        ],
    )
});
pub static NOTICE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event(
        "nostr_notice",
        "Native NOTICE or explicit unsupported extension receipt",
        vec![
            p(
                "message",
                "string",
                "Native bounded NOTICE, or fixed unsupported-extension text",
                true,
            ),
            p(
                "message_type",
                "string",
                "NOTICE, AUTH, COUNT, or unknown; no challenge echoed",
                true,
            ),
            p(
                "supported",
                "boolean",
                "False for extension messages this client cannot perform",
                true,
            ),
        ],
    )
});
pub static INFO_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event("nostr_relay_information", "Selected optional NIP-11 metadata without fabricated capabilities",vec![p("information","object","Optional name/description/contact/images/keys/software/version/supported_nips and selected typed limitation fields. Unknown fields ignored, absent fields remain absent; no negotiated policy claim.",true)])
});
pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    event(
        "nostr_request_error",
        "Local selected-action or NIP-11 transport/schema failure; no raw payload diagnostic",
        vec![
            p(
                "category",
                "string",
                "action, relay_info, or relay_schema",
                true,
            ),
            p(
                "error",
                "string",
                "Bounded fixed validation/transport diagnostic",
                true,
            ),
        ],
    )
});
impl Protocol for NostrClientProtocol {
    fn protocol_name(&self) -> &'static str {
        "Nostr"
    }
    fn stack_name(&self) -> &'static str {
        "ETH>IP>TCP>HTTP>WS>NOSTR"
    }
    fn keywords(&self) -> Vec<&'static str> {
        vec!["nostr", "nip-01"]
    }
    fn description(&self) -> &'static str {
        "Selected signed Nostr publishing and subscriptions"
    }
    fn group_name(&self) -> &'static str {
        "Core"
    }
    fn example_prompt(&self) -> &'static str {
        "Subscribe to live film notes at ws://localhost:7777"
    }
    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
        actions()
    }
    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
        vec![]
    }
    fn get_event_types(&self) -> Vec<EventType> {
        vec![
            CONNECTED_EVENT.clone(),
            EVENT_EVENT.clone(),
            RESULT_EVENT.clone(),
            SUBSCRIPTION_EVENT.clone(),
            NOTICE_EVENT.clone(),
            INFO_EVENT.clone(),
            ERROR_EVENT.clone(),
        ]
    }
    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
        vec![
            ParameterDefinition{name:"secret_key".into(),type_hint:"string".into(),description:"Optional64 hex secp256k1 private signing key; generated identity by default. Never offered to handlers, events or incidental diagnostics. No key helper/file.".into(),required:false,example:json!("<64 hex characters>"),default:None},
            ParameterDefinition{name:"request_timeout_secs".into(),type_hint:"integer".into(),description:"Whole connect/upgrade, individual write/NIP-11 and publish-OK deadline1..30 seconds; no idle subscription deadline".into(),required:false,example:json!(10),default:Some(json!(super::DEFAULT_TIMEOUT_SECS))},
        ]
    }
    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
        use crate::protocol::metadata::*;
        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).implementation("Native NIP-01 EVENT/REQ/CLOSE and validated EVENT/OK/EOSE/CLOSED/NOTICE; selected NIP-11 metadata").llm_control("Typed public event content and filters, common memory/event handlers/injection, private signing outside model").e2e_testing("tests/client/nostr: independent nak relay/CLI, existing independent relay-server SDK evidence, NetGet pair and mocked model; bounded schemas, queues, correlations and owned cancellation").notes("No event archive or domain database. Native WS/WSS verifies certificates; browser WS only. Messages128KiB, content64KiB, tags2000x32, filter lists500, filters10, subscriptions20, pending publishes16, queues8, handler followups4; JSON depth8/nodes25000/retained2MiB. One owned NIP-11 request. Publish deadline does not infer rejection. EOSE remains live; CLOSE has no acknowledgement. Already-selected frames after close are ignored; after replacement current-filter match is informational. NIP-11 fields optional, unknown extensions ignored. No AUTH/COUNT, relay pool, private-key actions, custom trust/client certificates, URL credentials/query, proxies/redirects, automatic reconnect/replay/retry, local replacement/expiration/storage policy, encrypted messages or conformance claim. Existing relay NIP-42/45 refusals preserved.").max_inbound_bytes(crate::server::nostr::wire::MAX_MESSAGE_BYTES).build()
    }
    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
        let llm = json!({"type":"open_client","base_stack":"nostr","protocol":"nostr","remote_addr":"ws://127.0.0.1:7777","instruction":"Subscribe to live film notes and explain relay notices"});
        let mut fixed = llm.clone();
        fixed["event_handlers"] = json!([{"event_pattern":"nostr_connected","handler":{"type":"static","actions":[{"type":"nostr_subscribe","subscription_id":"film","filters":[{"kinds":[1],"#t":["film"],"limit":0}]}]}},{"event_pattern":"*","handler":{"type":"static","actions":[]}}]);
        let mut script = fixed.clone();
        script["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":"import json,sys\njson.dump({'actions':[{'type':'nostr_subscribe','subscription_id':'film','filters':[{'kinds':[1],'#t':['film'],'limit':0}]}]},sys.stdout)"});
        crate::llm::actions::StartupExamples::new(llm, script, fixed)
    }
}
impl Client for NostrClientProtocol {
    fn connect(
        &self,
        ctx: ConnectContext,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
    {
        Box::pin(super::connect(ctx))
    }
    fn execute_action(&self, value: Value) -> Result<ClientActionResult> {
        if !super::api::within_budget(&value) {
            crate::utils::json_budget::drop_iteratively(value);
            bail!("Nostr action depth/node/retained-content limit");
        }
        match super::api::action(&value)? {
            super::api::Action::Disconnect => Ok(ClientActionResult::Disconnect),
            _ => Ok(ClientActionResult::Custom {
                name: "nostr".into(),
                data: value,
            }),
        }
    }
}