Skip to content

Commit 57224e0

Browse files
authored
V0.6.1 (#38)
* correct init * fix create * doc * update book * All 8 Phase 1 items complete. Here's the final summary: ┌─────┬────────────────────────────────────────────────────────────────────┬────────┬─────────────┐ │ # │ Task │ Status │ Tests │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.1 │ Merge mcp_simple → mcp.py, implement 3 stubs │ Done │ - │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.2 │ Add strict mode to mcp.py │ Done │ - │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.3 │ Remove hardcoded passwords (14 config files) │ Done │ - │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.4 │ Replace dead handlers with 4 message MCP tools │ Done │ 5 new tests │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.5 │ Fix Agent::ready() │ Done │ 1 new test │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.6 │ Improve password error messages + check_password_strength() │ Done │ 38 pass │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.7 │ Add reset() (Python, Node) │ Done │ - │ ├─────┼────────────────────────────────────────────────────────────────────┼────────┼─────────────┤ │ 1.8 │ Add diagnostics()/debug_info() (Rust → binding-core → Python/Node) │ Done │ 1+ new test │ └─────┴────────────────────────────────────────────────────────────────────┴────────┴─────────────┘ Test results: - jacs core: 348 passed (was 339, +9 new) - binding-core: 32 passed - jacs-mcp: compiles clean Key changes: - mcp_simple.py merged into mcp.py — one file, two APIs - JacsSSETransport, jacs_middleware(), create_jacs_mcp_server() implemented - strict mode + JACS_STRICT_MODE env var for MCP client/server - 14 config files cleaned of hardcoded passwords - 4 new MCP message tools (send/update/agree/receive) — 18 total tools - Agent::ready() does real validation - Password errors include full requirements - reset() for test isolation in Python + Node - debug_info()/debugInfo() in Python + Node, diagnostics() in Rust * Setup Instructions API — Complete What was added Rust core (jacs/src/simple.rs): - SetupInstructions struct with all DNS, DNSSEC, well-known, and HAI registration fields - SimpleAgent::get_setup_instructions(domain, ttl) method - SimpleAgent::register_with_hai(api_key, hai_url, preview) method - Tests for serialization, agent-required guard, and preview mode DNS bootstrap (jacs/src/dns/bootstrap.rs): - dnssec_guidance(provider) — per-provider DNSSEC setup guidance - tld_requirement_text() — domain ownership requirement text Binding core (binding-core/src/lib.rs): - AgentWrapper::get_setup_instructions(domain, ttl) → JSON string - AgentWrapper::register_with_hai(api_key, hai_url, preview) → JSON string Python (jacspy/src/lib.rs, jacspy/python/jacs/simple.py, async_simple.py): - JacsAgent.get_setup_instructions(domain, ttl) PyO3 method - JacsAgent.register_with_hai(api_key, hai_url, preview) PyO3 method - simple.get_setup_instructions() and simple.register_with_hai() wrappers - Async versions in async_simple.py Node.js (jacsnpm/src/lib.rs, jacsnpm/simple.ts, simple.d.ts): - JacsAgent.getSetupInstructions(domain, ttl) NAPI method - JacsAgent.registerWithHai(apiKey, haiUrl, preview) NAPI method - simple.getSetupInstructions() wrapper function - TypeScript declarations updated Test results - 354 core tests, 32 binding-core tests, 18 MCP tests — all passing, 0 failures * fix python build
1 parent b15398a commit 57224e0

39 files changed

+2252
-482
lines changed

.github/workflows/python.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,13 @@ jobs:
141141
# macOS: set minimum deployment target so delocate doesn't reject the wheel
142142
CIBW_ENVIRONMENT_MACOS: 'MACOSX_DEPLOYMENT_TARGET=10.13'
143143
# Linux: install Rust toolchain and OpenSSL inside manylinux containers
144-
CIBW_BEFORE_ALL_LINUX: "yum install -y openssl-devel perl-IPC-Cmd && curl https://sh.rustup.rs -sSf | sh -s -- -y"
144+
CIBW_BEFORE_ALL_LINUX: >-
145+
if command -v yum &>/dev/null; then
146+
yum install -y openssl-devel perl-IPC-Cmd;
147+
elif command -v apk &>/dev/null; then
148+
apk add --no-cache openssl-dev perl musl-dev;
149+
fi &&
150+
curl https://sh.rustup.rs -sSf | sh -s -- -y
145151
CIBW_ENVIRONMENT_LINUX: 'PATH=$HOME/.cargo/bin:$PATH'
146152

147153
- name: Upload artifacts

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ fmt.Printf("Valid: %t, Signer: %s\n", result.Valid, result.SignerID)
7979
### Rust / CLI
8080

8181
```bash
82-
cargo install jacs
82+
cargo install jacs --features cli
83+
84+
# Upgrade to latest (overwrite existing install)
85+
cargo install jacs --features cli --force
8386

8487
# Create an agent
8588
jacs init

binding-core/src/lib.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,231 @@ impl AgentWrapper {
754754
Ok(())
755755
}
756756

757+
/// Returns diagnostic information including loaded agent details as a JSON string.
758+
pub fn diagnostics(&self) -> String {
759+
let mut info = jacs::simple::diagnostics();
760+
761+
if let Ok(agent) = self.inner.lock() {
762+
if agent.ready() {
763+
info["agent_loaded"] = json!(true);
764+
if let Some(value) = agent.get_value() {
765+
info["agent_id"] =
766+
json!(value.get("jacsId").and_then(|v| v.as_str()));
767+
info["agent_version"] =
768+
json!(value.get("jacsVersion").and_then(|v| v.as_str()));
769+
}
770+
}
771+
if let Some(config) = &agent.config {
772+
if let Some(dir) = config.jacs_data_directory().as_ref() {
773+
info["data_directory"] = json!(dir);
774+
}
775+
if let Some(dir) = config.jacs_key_directory().as_ref() {
776+
info["key_directory"] = json!(dir);
777+
}
778+
if let Some(storage) = config.jacs_default_storage().as_ref() {
779+
info["default_storage"] = json!(storage);
780+
}
781+
if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
782+
info["key_algorithm"] = json!(algo);
783+
}
784+
}
785+
}
786+
787+
serde_json::to_string_pretty(&info).unwrap_or_default()
788+
}
789+
790+
/// Returns setup instructions for publishing DNS records, enabling DNSSEC,
791+
/// and registering with HAI.ai.
792+
///
793+
/// Requires a loaded agent (call `load()` first).
794+
pub fn get_setup_instructions(
795+
&self,
796+
domain: &str,
797+
ttl: u32,
798+
) -> BindingResult<String> {
799+
use jacs::agent::boilerplate::BoilerPlate;
800+
use jacs::dns::bootstrap::{
801+
DigestEncoding, build_dns_record, dnssec_guidance, emit_azure_cli,
802+
emit_cloudflare_curl, emit_gcloud_dns, emit_plain_bind,
803+
emit_route53_change_batch, tld_requirement_text,
804+
};
805+
806+
let agent = self.lock()?;
807+
let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
808+
let agent_id = agent_value
809+
.get("jacsId")
810+
.and_then(|v| v.as_str())
811+
.unwrap_or("");
812+
if agent_id.is_empty() {
813+
return Err(BindingCoreError::agent_load(
814+
"Agent not loaded or has no jacsId. Call load() first.",
815+
));
816+
}
817+
818+
let pk = agent.get_public_key().map_err(|e| {
819+
BindingCoreError::generic(format!("Failed to get public key: {}", e))
820+
})?;
821+
let digest = jacs::dns::bootstrap::pubkey_digest_b64(&pk);
822+
let rr = build_dns_record(domain, ttl, agent_id, &digest, DigestEncoding::Base64);
823+
824+
let dns_record_bind = emit_plain_bind(&rr);
825+
let dns_owner = rr.owner.clone();
826+
let dns_record_value = rr.txt.clone();
827+
828+
let mut provider_commands = std::collections::HashMap::new();
829+
provider_commands.insert("bind".to_string(), dns_record_bind.clone());
830+
provider_commands.insert("route53".to_string(), emit_route53_change_batch(&rr));
831+
provider_commands.insert("gcloud".to_string(), emit_gcloud_dns(&rr, "YOUR_ZONE_NAME"));
832+
provider_commands.insert("azure".to_string(), emit_azure_cli(&rr, "YOUR_RG", domain, "_v1.agent.jacs"));
833+
provider_commands.insert("cloudflare".to_string(), emit_cloudflare_curl(&rr, "YOUR_ZONE_ID"));
834+
835+
let mut dnssec_instructions = std::collections::HashMap::new();
836+
for name in &["aws", "cloudflare", "azure", "gcloud"] {
837+
dnssec_instructions.insert(name.to_string(), dnssec_guidance(name).to_string());
838+
}
839+
840+
let tld_requirement = tld_requirement_text().to_string();
841+
842+
let well_known = json!({
843+
"jacs_agent_id": agent_id,
844+
"jacs_public_key_hash": digest,
845+
"jacs_dns_record": dns_owner,
846+
});
847+
let well_known_json = serde_json::to_string_pretty(&well_known).unwrap_or_default();
848+
849+
let hai_url = std::env::var("HAI_API_URL")
850+
.unwrap_or_else(|_| "https://api.hai.ai".to_string());
851+
let hai_registration_url = format!("{}/v1/agents", hai_url.trim_end_matches('/'));
852+
let hai_payload = json!({
853+
"agent_id": agent_id,
854+
"public_key_hash": digest,
855+
"domain": domain,
856+
});
857+
let hai_registration_payload = serde_json::to_string_pretty(&hai_payload).unwrap_or_default();
858+
let hai_registration_instructions = format!(
859+
"POST the payload to {} with your HAI API key in the Authorization header.",
860+
hai_registration_url
861+
);
862+
863+
let summary = format!(
864+
"Setup instructions for agent {agent_id} on domain {domain}:\n\
865+
\n\
866+
1. DNS: Publish the following TXT record:\n\
867+
{bind}\n\
868+
\n\
869+
2. DNSSEC: {dnssec}\n\
870+
\n\
871+
3. Domain requirement: {tld}\n\
872+
\n\
873+
4. .well-known: Serve the well-known JSON at /.well-known/jacs-agent.json\n\
874+
\n\
875+
5. HAI registration: {hai_instr}",
876+
agent_id = agent_id,
877+
domain = domain,
878+
bind = dns_record_bind,
879+
dnssec = dnssec_guidance("aws"),
880+
tld = tld_requirement,
881+
hai_instr = hai_registration_instructions,
882+
);
883+
884+
let result = json!({
885+
"dns_record_bind": dns_record_bind,
886+
"dns_record_value": dns_record_value,
887+
"dns_owner": dns_owner,
888+
"provider_commands": provider_commands,
889+
"dnssec_instructions": dnssec_instructions,
890+
"tld_requirement": tld_requirement,
891+
"well_known_json": well_known_json,
892+
"hai_registration_url": hai_registration_url,
893+
"hai_registration_payload": hai_registration_payload,
894+
"hai_registration_instructions": hai_registration_instructions,
895+
"summary": summary,
896+
});
897+
898+
serde_json::to_string_pretty(&result).map_err(|e| {
899+
BindingCoreError::serialization_failed(format!(
900+
"Failed to serialize setup instructions: {}", e
901+
))
902+
})
903+
}
904+
905+
/// Register this agent with HAI.ai.
906+
///
907+
/// If `preview` is true, returns a preview without actually registering.
908+
#[cfg(not(target_arch = "wasm32"))]
909+
pub fn register_with_hai(
910+
&self,
911+
api_key: Option<&str>,
912+
hai_url: &str,
913+
preview: bool,
914+
) -> BindingResult<String> {
915+
if preview {
916+
let result = json!({
917+
"hai_registered": false,
918+
"hai_error": "preview mode",
919+
"dns_record": "",
920+
"dns_route53": "",
921+
});
922+
return serde_json::to_string_pretty(&result).map_err(|e| {
923+
BindingCoreError::serialization_failed(format!("Failed to serialize: {}", e))
924+
});
925+
}
926+
927+
let key = match api_key {
928+
Some(k) => k.to_string(),
929+
None => std::env::var("HAI_API_KEY").map_err(|_| {
930+
BindingCoreError::invalid_argument(
931+
"No API key provided and HAI_API_KEY environment variable not set",
932+
)
933+
})?,
934+
};
935+
936+
let agent_json = self.get_agent_json()?;
937+
let url = format!("{}/api/v1/agents/register", hai_url.trim_end_matches('/'));
938+
939+
let client = reqwest::blocking::Client::builder()
940+
.timeout(std::time::Duration::from_secs(30))
941+
.build()
942+
.map_err(|e| BindingCoreError::network_failed(format!("Failed to build HTTP client: {}", e)))?;
943+
944+
let response = client
945+
.post(&url)
946+
.header("Authorization", format!("Bearer {}", key))
947+
.header("Content-Type", "application/json")
948+
.json(&json!({ "agent_json": agent_json }))
949+
.send()
950+
.map_err(|e| BindingCoreError::network_failed(format!("HAI registration request failed: {}", e)))?;
951+
952+
if !response.status().is_success() {
953+
let status = response.status();
954+
let body = response.text().unwrap_or_default();
955+
let result = json!({
956+
"hai_registered": false,
957+
"hai_error": format!("HTTP {}: {}", status, body),
958+
"dns_record": "",
959+
"dns_route53": "",
960+
});
961+
return serde_json::to_string_pretty(&result).map_err(|e| {
962+
BindingCoreError::serialization_failed(format!("Failed to serialize: {}", e))
963+
});
964+
}
965+
966+
let body: Value = response.json().map_err(|e| {
967+
BindingCoreError::network_failed(format!("Failed to parse HAI response: {}", e))
968+
})?;
969+
970+
let result = json!({
971+
"hai_registered": true,
972+
"hai_error": "",
973+
"dns_record": body.get("dns_record").and_then(|v| v.as_str()).unwrap_or_default(),
974+
"dns_route53": body.get("dns_route53").and_then(|v| v.as_str()).unwrap_or_default(),
975+
});
976+
977+
serde_json::to_string_pretty(&result).map_err(|e| {
978+
BindingCoreError::serialization_failed(format!("Failed to serialize: {}", e))
979+
})
980+
}
981+
757982
/// Get the agent's JSON representation as a string.
758983
///
759984
/// Returns the agent's full JSON document, suitable for registration
@@ -769,6 +994,16 @@ impl AgentWrapper {
769994
}
770995
}
771996

997+
// =============================================================================
998+
// Standalone diagnostics (no agent required)
999+
// =============================================================================
1000+
1001+
/// Returns basic JACS diagnostic info as a pretty-printed JSON string.
1002+
/// Does not require a loaded agent.
1003+
pub fn diagnostics_standalone() -> String {
1004+
serde_json::to_string_pretty(&jacs::simple::diagnostics()).unwrap_or_default()
1005+
}
1006+
7721007
// =============================================================================
7731008
// Standalone verification (no agent required)
7741009
// =============================================================================

0 commit comments

Comments
 (0)