Skip to main content

packc/
store_client.rs

1//! Minimal store client for publishing AgenticWorker packs. Mirrors
2//! greentic-designer `store::client::publish_agentic_worker`.
3
4use anyhow::{Context, Result, anyhow};
5use sha2::{Digest, Sha256};
6
7use crate::agent_pack::{DescribeMeta, agentic_worker_describe};
8
9/// Build the `{describe, artifactSha256}` upload metadata for a pack. The
10/// describe's `manifestSha256` and the top-level `artifactSha256` are BOTH
11/// the lowercase-hex sha256 of `pack_bytes` (the final .gtpack).
12pub fn publish_metadata(
13    pack_bytes: &[u8],
14    id: &str,
15    name: &str,
16    version: &str,
17    summary: &str,
18) -> Result<serde_json::Value> {
19    let sha = hex::encode(Sha256::digest(pack_bytes));
20    let describe = agentic_worker_describe(&DescribeMeta {
21        id: id.to_string(),
22        name: name.to_string(),
23        version: version.to_string(),
24        summary: summary.to_string(),
25        manifest_sha256: sha.clone(),
26    });
27    Ok(serde_json::json!({ "describe": describe, "artifactSha256": sha }))
28}
29
30/// POST the pack to `{store}/api/v1/agentic-workers` (multipart
31/// metadata+artifact, Bearer token). `409 Conflict` (already published) is
32/// returned as a distinct `already published (409): <body>` error; non-2xx
33/// otherwise carries the status code and body.
34pub async fn publish_agentic_worker(
35    store_url: &str,
36    token: &str,
37    metadata: &serde_json::Value,
38    pack_bytes: Vec<u8>,
39) -> Result<serde_json::Value> {
40    let url = format!("{}/api/v1/agentic-workers", store_url.trim_end_matches('/'));
41    let metadata_str = serde_json::to_string(metadata).context("encode metadata")?;
42    let part = reqwest::multipart::Part::bytes(pack_bytes)
43        .file_name("artifact.gtpack")
44        .mime_str("application/zip")?;
45    let form = reqwest::multipart::Form::new()
46        .text("metadata", metadata_str)
47        .part("artifact", part);
48    let resp = reqwest::Client::new()
49        .post(&url)
50        .bearer_auth(token)
51        .multipart(form)
52        .send()
53        .await
54        .context("POST agentic-workers")?;
55    let status = resp.status();
56    let body = resp.text().await.unwrap_or_default();
57    if status == reqwest::StatusCode::CONFLICT {
58        return Err(anyhow!("already published (409): {body}"));
59    }
60    if !status.is_success() {
61        return Err(anyhow!("store returned {}: {body}", status.as_u16()));
62    }
63    Ok(serde_json::from_str(&body).unwrap_or(serde_json::json!({ "raw": body })))
64}