Tutorials
Each executable reference has C#, Go, Rust, and TypeScript project sources. Source tabs show code from the current public source revision. Dated test records retain the source commit and application revision used by that run. The @glixo/extension-ui authoring helper is separate from the Accessible Theme UI sample.
Provider: build and test an Ollama connection
This walkthrough uses the current public reference snapshot. It builds a provider component, explains connection approval, and summarizes the installed-package fixture's coverage and limits.
1. Get the pinned source and toolchain
Clone the public source and check out the exact revision shown beside the source tabs:
git clone https://github.com/tmedanovic/glixo-community-modules.git
git -C glixo-community-modules checkout 7052b3f954418cff91439236942a6623d75e8907
Choose the project under references/ollama-provider/ and use its README.md. Its reference.json selects the language recipe in packages/extension-sdk/support-matrix.json; that matrix is the command authority, including the final wasm-tools component wit check.
| Language | Pinned toolchain | Recipe |
|---|---|---|
| C# | .NET 10 reference README; .NET SDK 10.0.301, componentize-dotnet 0.8.0-preview00011 | ollama-provider-csharp-v3 |
| Go | Go reference README; Go 1.27.1, componentize-go 0.4.3 | ollama-provider-go-v3 |
| Rust | Rust reference README; Rust/Cargo 1.96.0, wasm32-wasip2, wit-bindgen 0.62.0, wasm-tools 1.239.0 |
ollama-provider-rust-v3 |
| TypeScript | TypeScript reference README; Node.js 22.14.0, npm 10.9.2, TypeScript 6.0.3, Jco 1.35.0, componentize-js 0.23.0, esbuild 0.28.2 | ollama-provider-typescript-v3 |
Use the published CLI for a new project: glxdev new --kind llm-provider --language <csharp|go|rust|typescript>, followed by glxdev check and glxdev build. To build the reference sources directly, use the matching README; the support matrix lists the exact build commands. Each recipe inspects its output with wasm-tools component wit.
The CLI bundles these source templates; the SDK package releases are tracked separately.
2. Review the manifest and approve the connection
The v2 manifest declares the account configuration provider-account and requests one http.connection capability: endpoint ollama, methods GET and POST, and paths /api/tags, /api/show, and /api/chat. The account configuration chooses the host-approved endpoint name and may select a default model. It does not supply an arbitrary URL.
Set contextWindowTokens in that configuration to choose Ollama's context size. The examples default to 8192 tokens and reject values above 32768. Every chat request sends this value as Ollama's options.num_ctx, including requests without sampling options. Keep this value within the memory budget of the machine running Ollama; its model metadata may advertise a much larger context than that machine can hold.
Install the package through the host's supported signed-package flow, review the package identity and requested capability, grant http.connection, then separately approve the account's ollama connection for exactly those methods and paths. The host issues an invocation-bound endpoint handle. Each broker request is checked against that handle and the manifest; a guest-supplied URL or a different path cannot widen authority. Re-consent after changing the manifest scope.
3. Discover a model and make a request
Model listing uses GET /api/tags; model details use POST /api/show with { "model": "..." }; chat uses streaming POST /api/chat. Check the selected model's returned capabilities before including tools, vision, or thinking. Use Ollama's current list models, chat, and show model details contracts; /api/show is a POST.
The guest reads NDJSON incrementally, maps text, reasoning, tool calls, usage, finish and errors to provider events, and cancels/releases the broker response handle when the caller cancels or drops the stream. For tool continuation, preserve the assistant message with its tool call, execute only through the host's normal tool path, then include the matching tool result (tool_name and content) in the next chat request. A model that does not advertise tools in /api/show does not qualify tool use; do not infer tool support from a fixture.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/csharp/src/Provider.cs
public static L.ProviderDescriptor Describe(L.ProviderContext context)
{
return ResultBoundary(() =>
{
var (config, endpoint) = Resolve(context);
using (config)
{
var models = Models(endpoint);
var selected = config.RootElement.TryGetProperty("model", out var model) ? models.Where(m => m == model.GetString()) : models.Take(64);
var caps = new HashSet<string>(StringComparer.Ordinal);
foreach (var name in selected) caps.UnionWith(Capabilities(endpoint, name));
var advertised = new List<L.ProviderCapability> { new("chat", false), new("streaming", false) };
foreach (var capability in new[] { ("tools", "tools"), ("vision", "images"), ("thinking", "reasoning") })
if (caps.Contains(capability.Item1)) advertised.Add(new(capability.Item2, true));
return new L.ProviderDescriptor("community.ollama", "Ollama", "0.1.0", models, advertised);
}
});
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/go/provider/provider.go
func Describe(c ctx) types.Result[glixo_llm_types_types.ProviderDescriptor, string] {
cfg, ep, e := cfgEndpoint(c)
if e != nil {
return optErr[glixo_llm_types_types.ProviderDescriptor](e)
}
names, e := list(ep)
if e != nil {
return optErr[glixo_llm_types_types.ProviderDescriptor](e)
}
selected := names
if model, ok := cfg["model"].(string); ok {
selected = []string{}
for _, n := range names {
if n == model {
selected = append(selected, n)
}
}
} else if len(selected) > 64 {
selected = selected[:64]
}
found := map[string]bool{}
for _, n := range selected {
a, e := caps(ep, n)
if e != nil {
return optErr[glixo_llm_types_types.ProviderDescriptor](e)
}
for k := range a {
found[k] = true
}
}
cs := []glixo_llm_types_types.ProviderCapability{{Name: "chat", Optional: false}, {Name: "streaming", Optional: false}}
for _, pair := range [][2]string{{"tools", "tools"}, {"vision", "images"}, {"thinking", "reasoning"}} {
if found[pair[0]] {
cs = append(cs, glixo_llm_types_types.ProviderCapability{Name: pair[1], Optional: true})
}
}
return types.Ok[glixo_llm_types_types.ProviderDescriptor, string](glixo_llm_types_types.ProviderDescriptor{Id: "community.ollama", Name: "Ollama", Version: "0.1.0", Models: names, Capabilities: cs})
}
func ValidateConfiguration(config string, c ctx) types.Result[glixo_llm_types_types.ConfigurationReport, string] {
c.ConfigurationJson = config
cfg, ep, e := cfgEndpoint(c)
ds := []string{}
if e != nil {
ds = append(ds, e.Error())
} else if model, ok := cfg["model"].(string); ok {
ms, er := list(ep)
if er != nil {
return optErr[glixo_llm_types_types.ConfigurationReport](er)
}
found := false
for _, n := range ms {
if n == model {
found = true
}
}
if !found {
ds = append(ds, "configured model is not present in /api/tags")
}
}
return types.Ok[glixo_llm_types_types.ConfigurationReport, string](glixo_llm_types_types.ConfigurationReport{Valid: len(ds) == 0, Diagnostics: ds})
}
func ListModels(c ctx) types.Result[[]string, string] {
_, ep, e := cfgEndpoint(c)
if e != nil {
return optErr[[]string](e)
}
ms, e := list(ep)
if e != nil {
return optErr[[]string](e)
}
return types.Ok[[]string, string](ms)
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/rust/src/lib.rs
fn describe(context: glixo::llm_types::types::ProviderContext) -> Result<glixo::llm_types::types::ProviderDescriptor, String> {
let (cfg, grant) = parse_config(&context)?;
let names = models(&grant)?;
let selected: Vec<String> = cfg.get("model").and_then(Value::as_str).map(|m| names.iter().filter(|n| n.as_str() == m).cloned().collect()).unwrap_or_else(|| names.iter().take(64).cloned().collect());
let mut found = HashSet::new();
for model in selected { found.extend(capabilities(&grant, &model)?); }
let mut advertised = vec![glixo::llm_types::types::ProviderCapability { name: "chat".into(), optional: false }, glixo::llm_types::types::ProviderCapability { name: "streaming".into(), optional: false }];
for (upstream, name) in [("tools", "tools"), ("vision", "images"), ("thinking", "reasoning")] {
if found.contains(upstream) { advertised.push(glixo::llm_types::types::ProviderCapability { name: name.into(), optional: true }); }
}
Ok(glixo::llm_types::types::ProviderDescriptor { id: "community.ollama".into(), name: "Ollama".into(), version: "0.1.0".into(), models: names, capabilities: advertised })
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/typescript/src/provider.ts
function listModels(context: Context): string[] {
try { return listTags(context); }
catch (error) { throwWitError(error); }
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/csharp/src/Provider.cs
public static uint Start(L.LlmRequest request, L.ProviderContext context)
{
return ResultBoundary(() =>
{
var (config, endpoint) = Resolve(context);
using (config)
{
var caps = Capabilities(endpoint, request.model);
var bytes = SerializeJson(ChatBody(request, caps, ContextWindowTokens(config.RootElement)));
var stream = new NdjsonStream(new HostBroker(), HttpRequest(endpoint, "/api/chat", "POST", bytes));
var status = stream.Status();
if (status is < 200 or > 299) { stream.Dispose(); throw new InvalidOperationException($"ollama_http_{status}"); }
var handle = unchecked((uint)Interlocked.Increment(ref _nextHandle));
Sessions[handle] = new Session(stream, request.requestId);
return handle;
}
});
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/go/provider/provider.go
func Start(r request, c ctx) types.Result[uint32, string] {
cfg, ep, e := cfgEndpoint(c)
if e != nil {
return optErr[uint32](e)
}
ca, e := caps(ep, r.Model)
if e != nil {
return optErr[uint32](e)
}
contextWindow, e := contextWindowTokens(cfg)
if e != nil {
return optErr[uint32](e)
}
body, e := chatBody(r, ca, contextWindow)
if e != nil {
return optErr[uint32](e)
}
b, e := json.Marshal(body)
if e != nil {
return optErr[uint32](e)
}
s, e := httpbroker.Open(brokerAdapter{}, reqHTTP(ep, "/api/chat", "POST", b), 1<<20)
if e != nil {
return optErr[uint32](e)
}
st, e := s.Status()
if e != nil || st < 200 || st > 299 {
s.Close()
if e == nil {
e = fmt.Errorf("ollama_http_%d", st)
}
return optErr[uint32](e)
}
id := sessions.next.Add(1)
if id == 0 {
id = sessions.next.Add(1)
}
sessions.Lock()
sessions.items[id] = &session{stream: s, requestID: r.RequestId}
sessions.Unlock()
return types.Ok[uint32, string](id)
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/rust/src/lib.rs
fn start(req: glixo::llm_types::types::LlmRequest, context: glixo::llm_types::types::ProviderContext) -> Result<u32, String> {
let (config, grant) = parse_config(&context)?;
let context_window = context_window_tokens(&config)?;
if req.model.trim().is_empty() { return Err("model_required".into()); }
let caps = capabilities(&grant, &req.model)?;
let body = chat_body(&req, &caps, context_window)?;
let mut stream = NdjsonStream::open(HostBroker, request(&grant, "/api/chat", "POST", Some(serde_json::to_vec(&body).map_err(err_message)?)), 1024 * 1024)?;
let status = stream.status()?;
if !(200..=299).contains(&status) { stream.close(); return Err(format!("ollama_http_{status}")); }
let handle = NEXT_STREAM.fetch_add(1, Ordering::Relaxed).max(1);
STREAMS.with(|streams| { streams.borrow_mut().insert(handle, Session { stream, request_id: req.request_id, done: false, terminal_sent: false, cancelled: false, tool_index: 0, tool_calls_seen: false, pending: VecDeque::new() }); });
Ok(handle)
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/ollama-provider/typescript/src/provider.ts
function start(request: Request, context: Context): number {
let stream: NdjsonStream | undefined;
try {
const cfg = config(context);
const caps = showModel(context, request.model);
const body = buildChat(request, caps, cfg.contextWindowTokens);
stream = new NdjsonStream(broker, requestFor(endpoint(context), "/api/chat", "POST", body), 1024 * 1024);
const status = stream.status();
if (status < 200 || status > 299) throw new Error(`ollama_http_${status}`);
const id = nextHandle++;
streamSessions.set(id, { stream, requestId: request.requestId, model: request.model, terminalSent: false, cancelled: false, toolIndex: 0, toolCallsSeen: false, pending: [] });
return id;
} catch (error) { stream?.close(); throwWitError(error); }
}4. Run the available checks
All four signed packages passed offline and local Gemma tests using a source-built protocol-2 runner. The live tests completed a tool call, returned its result to the model, and received the final response in each language. Stable Glixo 0.7.8 still ships protocol 1, so these guests need a newer app release.
The local model checks used bounded context settings. They confirm the provider flow against Gemma, but do not mean protocol 2 is available in a released Glixo app.
Troubleshooting
- The host denies HTTP: compare the endpoint name, method, and exact path with both the manifest request and the separately approved account connection. Do not add a guest-controlled URL.
- A model responds without tools: inspect
/api/show; omit tool definitions unless the selected model advertisestools. - A stream is cancelled: propagate cancellation and release the response handle. Do not keep reading or retrying a cancelled stream.
- The component is rejected: verify that the artifact exports
glixo:llm-provider-compat/provider-compat@3.0.0and was built with the pinned recipe and WIT dependencies.
Service: publish a durable event
The Mail Watch reference uses scheduled Microsoft Graph delta reads, stores opaque checkpoints, and publishes a deduplicated mail.received event. Metadata access and message-body access are separate grants. Events do not start agent work or send mail by themselves.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/mail-watch/csharp/MailWatch.cs
* A bounded wake reads only Inbox metadata through the host HTTP broker.
* The handler never reads body content, sends mail, or follows a guest-selected host.
*/
public static MailServiceResult Handle(MailServiceRequest request, IBroker broker, IScopedState state)
{
_ = state;
if (request.Kind != "backgroundServices" || request.ContributionId != ContributionId)
throw new InvalidOperationException("contribution_mismatch");
var operation = request.Input.Operation;
if (operation is not ("initialize" or "wake" or "health" or "stop"))
throw new InvalidOperationException("service_operation_invalid");
var checkpoint = request.Input.Checkpoint ?? new MailCheckpoint();
if (operation is "health" or "stop") return Result(checkpoint, []);
var handles = request.Context?.ResourceHandles;
if (handles is null || !handles.TryGetValue(OAuthHandleName, out var oauthHandle) || string.IsNullOrWhiteSpace(oauthHandle))
throw new InvalidOperationException("graph_oauth_lease_missing");
var configuredPages = request.Configuration?.MaxPagesPerWake ?? MaxPagesPerWake;
if (configuredPages is < 1 or > MaxPagesPerWake) throw new InvalidOperationException("mail_watch_page_limit_invalid");
var initializing = operation == "initialize" || checkpoint.Initializing || !checkpoint.Initialized;
var cursor = checkpoint.PendingUrl ?? checkpoint.DeltaUrl ?? InitialDeltaUrl;
var deltaUrl = checkpoint.DeltaUrl;
string? pendingUrl = null;
var events = new List<MailEvent>();
for (var page = 0; page < configuredPages; page++)
{
var document = ReadGraphJson(broker, new BrokerRequest(
ValidateGraphDeltaUrl(cursor), "GET", [new Header("Accept", "application/json")], null, null,
5000, MaxPageBytes, 200, 299, null, OAuthHandleName, "Authorization", "Bearer"));
if (!document.RootElement.TryGetProperty("value", out var values) || values.ValueKind != JsonValueKind.Array)
throw new InvalidOperationException("graph_delta_value_invalid");
if (!initializing)
{
foreach (var item in values.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Object && !item.TryGetProperty("@removed", out _))
{
var emitted = ToMailEvent(item);
if (emitted is not null) events.Add(emitted);
}
}
}
if (document.RootElement.TryGetProperty("@odata.nextLink", out var next) && next.ValueKind == JsonValueKind.String)
{
cursor = ValidateGraphDeltaUrl(next.GetString()!);
pendingUrl = next.GetString();
continue;
}
if (!document.RootElement.TryGetProperty("@odata.deltaLink", out var finalDelta) || finalDelta.ValueKind != JsonValueKind.String)
throw new InvalidOperationException("graph_delta_link_missing");
deltaUrl = ValidateGraphDeltaUrl(finalDelta.GetString()!);
pendingUrl = null;
initializing = false;
break;
}
if (pendingUrl is null && initializing) throw new InvalidOperationException("graph_initial_sync_incomplete");
return Result(new MailCheckpoint
{
Initialized = !initializing,
Initializing = initializing,
DeltaUrl = deltaUrl,
PendingUrl = pendingUrl,
}, events);
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/mail-watch/go/mailwatch/mailwatch.go
// Handle performs a bounded Inbox delta read with the host's scoped HTTP broker.
// It never reads message bodies, sends mail, or selects a non-Graph origin.
func Handle(request Request, broker httpbroker.Broker, _ state.Store) (Result, error) {
if request.Kind != "backgroundServices" || request.ContributionID != ContributionID {
return Result{}, errors.New("contribution_mismatch")
}
operation := request.Input.Operation
if operation != "initialize" && operation != "wake" && operation != "health" && operation != "stop" {
return Result{}, errors.New("service_operation_invalid")
}
checkpoint := Checkpoint{}
if request.Input.Checkpoint != nil { checkpoint = *request.Input.Checkpoint }
if operation == "health" || operation == "stop" {
return healthy(checkpoint), nil
}
if request.Context == nil || request.Context.ResourceHandles["oauth"] == "" {
return Result{}, errors.New("graph_oauth_lease_missing")
}
pageLimit := maxPagesPerWake
if request.Configuration != nil {
pageLimit = request.Configuration.MaxPagesPerWake
}
if pageLimit < 1 || pageLimit > maxPagesPerWake {
return Result{}, errors.New("mail_watch_page_limit_invalid")
}
initializing := operation == "initialize" || checkpoint.Initializing || !checkpoint.Initialized
cursor := checkpoint.PendingURL
if cursor == "" { cursor = checkpoint.DeltaURL }
if cursor == "" { cursor = initialDeltaURL }
deltaURL := checkpoint.DeltaURL
pendingURL := ""
events := make([]Event, 0)
for page := 0; page < pageLimit; page++ {
validated, err := ValidateGraphDeltaURL(cursor)
if err != nil { return Result{}, err }
document, err := readGraphJSON(broker, httpbroker.Request{
URL: validated, Method: "GET", Headers: []httpbroker.Header{{Name: "Accept", Value: "application/json"}},
TimeoutMS: u32(5000), MaxResponseBytes: u32(maxPageBytes), AcceptedStatusMin: u16(200), AcceptedStatusMax: u16(299),
SecretHandle: str("oauth"), AuthHeader: str("Authorization"), AuthScheme: str("Bearer"),
})
if err != nil { return Result{}, err }
var response graphDocument
if err := json.Unmarshal(document, &response); err != nil || response.Value == nil {
return Result{}, errors.New("graph_delta_value_invalid")
}
if !initializing {
for _, raw := range response.Value {
var message graphMessage
if err := json.Unmarshal(raw, &message); err != nil || message.Removed != nil || message.ID == "" || message.ReceivedDateTime == "" { continue }
sender := message.From.EmailAddress.Address
if len(sender) > 320 { sender = sender[:320] }
payload := Payload{MessageID: message.ID, ReceivedAt: message.ReceivedDateTime, Sender: sender}
events = append(events, Event{Type: EventType, IdempotencyKey: "mail:" + stableID(message.ID), Payload: payload})
}
}
if response.NextLink != nil {
cursor = *response.NextLink
if _, err := ValidateGraphDeltaURL(cursor); err != nil { return Result{}, err }
pendingURL = *response.NextLink
continue
}
if response.DeltaLink == nil { return Result{}, errors.New("graph_delta_link_missing") }
deltaURL, err = ValidateGraphDeltaURL(*response.DeltaLink)
if err != nil { return Result{}, err }
pendingURL = ""
initializing = false
break
}
if pendingURL == "" && initializing { return Result{}, errors.New("graph_initial_sync_incomplete") }
return Result{Checkpoint: Checkpoint{Initialized: !initializing, Initializing: initializing, DeltaURL: deltaURL, PendingURL: pendingURL}, Events: events, Health: "healthy"}, nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/mail-watch/rust/src/lib.rs
/// Performs one bounded Inbox delta poll through the host-issued HTTP broker.
/// It does not read message bodies, send mail, or loop beyond its page budget.
pub fn handle_mail_watch<B: Broker>(
request: &ServiceRequest,
broker: &mut B,
_state: &mut dyn ScopedState,
) -> Result<ServiceResult, String> {
if request.kind != "backgroundServices" || request.contribution_id != CONTRIBUTION_ID {
return Err("contribution_mismatch".into());
}
let operation = request.input.operation.as_str();
if !matches!(operation, "initialize" | "wake" | "health" | "stop") {
return Err("service_operation_invalid".into());
}
let checkpoint = request.input.checkpoint.clone().unwrap_or_default();
if matches!(operation, "health" | "stop") {
return Ok(healthy(checkpoint));
}
let handles = request.context.as_ref().and_then(|context| context.resource_handles.as_ref());
let _oauth_handle = handles.and_then(|items| items.get("oauth")).filter(|value| !value.is_empty())
.ok_or_else(|| "graph_oauth_lease_missing".to_string())?;
let page_limit = request.configuration.as_ref().and_then(|config| config.max_pages_per_wake).unwrap_or(MAX_PAGES_PER_WAKE);
if !(1..=MAX_PAGES_PER_WAKE).contains(&page_limit) { return Err("mail_watch_page_limit_invalid".into()); }
let mut initializing = operation == "initialize" || checkpoint.initializing || !checkpoint.initialized;
let mut cursor = checkpoint.pending_url.clone().or(checkpoint.delta_url.clone()).unwrap_or_else(|| INITIAL_DELTA_URL.into());
let mut delta_url = checkpoint.delta_url.clone();
let mut pending_url: Option<String> = None;
let mut events = Vec::new();
for _ in 0..page_limit {
let validated = validate_graph_delta_url(&cursor)?;
let document = read_graph_json(broker, HttpRequest {
url: validated,
method: "GET".into(),
headers: vec![Header { name: "Accept".into(), value: "application/json".into() }],
body: None,
content_type: None,
timeout_ms: Some(5000),
max_response_bytes: Some(MAX_PAGE_BYTES as u32),
accepted_status_min: Some(200),
accepted_status_max: Some(299),
endpoint_handle: None,
secret_handle: Some("oauth".into()),
auth_header: Some("Authorization".into()),
auth_scheme: Some("Bearer".into()),
})?;
let values = document.get("value").and_then(Value::as_array).ok_or_else(|| "graph_delta_value_invalid".to_string())?;
if !initializing {
for item in values {
if item.get("@removed").is_some() { continue; }
if let Some(event) = to_mail_event(item) { events.push(event); }
}
}
if let Some(next) = document.get("@odata.nextLink").and_then(Value::as_str) {
validate_graph_delta_url(next)?;
cursor = next.to_string();
pending_url = Some(next.to_string());
continue;
}
let final_delta = document.get("@odata.deltaLink").and_then(Value::as_str)
.ok_or_else(|| "graph_delta_link_missing".to_string())?;
delta_url = Some(validate_graph_delta_url(final_delta)?);
pending_url = None;
initializing = false;
break;
}
if pending_url.is_none() && initializing { return Err("graph_initial_sync_incomplete".into()); }
Ok(ServiceResult {
checkpoint: Checkpoint { initialized: !initializing, initializing, delta_url, pending_url },
events,
health: "healthy".into(),
})
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/mail-watch/typescript/src/mail-watch.ts
* One bounded guest call reads Graph's Inbox delta endpoint through the host broker.
* It never requests body content, sends mail, follows a guest-selected host, or starts a loop.
*/
export function handleMailWatch(
request: MailServiceRequest,
broker: Broker,
_state: ScopedState,
): MailServiceResult {
if (request.kind !== 'backgroundServices' || request.contributionId !== CONTRIBUTION_ID) {
throw new Error('contribution_mismatch');
}
const operation = request.input?.operation;
if (!['initialize', 'wake', 'health', 'stop'].includes(operation)) throw new Error('service_operation_invalid');
const previous = request.input.checkpoint ?? {};
if (operation === 'health' || operation === 'stop') return { checkpoint: previous, events: [], health: 'healthy' };
const oauthHandle = request.context?.resourceHandles?.[SECRET_HANDLE_NAME];
if (typeof oauthHandle !== 'string' || oauthHandle.length === 0) throw new Error('graph_oauth_lease_missing');
let initializing = operation === 'initialize' || previous.initializing === true || previous.initialized !== true;
let cursor = previous.pendingUrl ?? previous.deltaUrl ?? INITIAL_DELTA_URL;
let pendingUrl: string | undefined;
let deltaUrl = previous.deltaUrl;
const events: MailEvent[] = [];
const configuredPages = request.configuration?.maxPagesPerWake ?? MAX_PAGES_PER_WAKE;
if (!Number.isInteger(configuredPages) || configuredPages < 1 || configuredPages > MAX_PAGES_PER_WAKE) {
throw new Error('mail_watch_page_limit_invalid');
}
for (let page = 0; page < configuredPages; page++) {
const graphUrl = validateGraphDeltaUrl(cursor);
const document = readGraphJson(broker, {
url: graphUrl,
method: 'GET',
headers: [{ name: 'Accept', value: 'application/json' }],
secretHandle: SECRET_HANDLE_NAME,
authHeader: 'Authorization',
authScheme: 'Bearer',
timeoutMs: 5000,
maxResponseBytes: MAX_PAGE_BYTES,
acceptedStatusMin: 200,
acceptedStatusMax: 299,
});
if (!Array.isArray(document.value)) throw new Error('graph_delta_value_invalid');
if (!initializing) {
for (const item of document.value) {
if (isRecord(item) && !('@removed' in item)) {
const event = toMailEvent(item);
if (event) events.push(event);
}
}
}
const next = document['@odata.nextLink'];
if (typeof next === 'string') {
cursor = validateGraphDeltaUrl(next);
pendingUrl = next;
continue;
}
const finalDelta = document['@odata.deltaLink'];
if (typeof finalDelta !== 'string') throw new Error('graph_delta_link_missing');
deltaUrl = validateGraphDeltaUrl(finalDelta);
pendingUrl = undefined;
initializing = false;
break;
}
if (pendingUrl === undefined && initializing) throw new Error('graph_initial_sync_incomplete');
return {
checkpoint: {
initialized: !initializing,
initializing,
...(deltaUrl ? { deltaUrl } : {}),
...(pendingUrl ? { pendingUrl } : {}),
},
events,
health: 'healthy',
};
}Qualification: All four guests pass protocol-2 fixture cases. The TypeScript package also passed a persisted Code.Server scheduler journey using deterministic Graph responses, database-backed grants, encrypted OAuth configuration, checkpoint persistence, outbox deduplication, and event delivery. C#/Go/Rust installed-host journeys and real Graph credentials/mail delivery remain untested. The service remains Preview overall.
Middleware: observe and transform with provenance
The message pipeline separates read-only observation from outgoing-input and assistant-output transforms. The runtime preserves the original transcript and records the package identity, digest, stage, time, and human-readable reason for an applied transform.
Conversation Insights
Conversation Insights is a read-only observer reference. Its implementation source does not establish permission to read message content or prove that the observer is installed in a production host.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/conversation-insights/csharp/src/GuestImpl.cs
internal static string Analyze(JsonElement input)
{
if (Number(input, "schemaVersion") != 1)
throw new InvalidOperationException("middleware_schema_unsupported");
var operation = Text(input, "operation");
if (operation == "committed")
{
if (!input.TryGetProperty("committedMessage", out var message) || message.ValueKind != JsonValueKind.Object)
throw new InvalidOperationException("committed_message_missing");
var role = Text(message, "role");
var observation = new CommittedObservation(
1, 1, role == "user" ? 1 : 0, role == "assistant" ? 1 : 0,
Count(message, "toolCallCount"), Count(message, "toolResultCount"), Count(message, "attachmentCount"));
return JsonSerializer.Serialize(new CommittedResult(observation, "insights-recorded"), GuestJsonContext.Default.CommittedResult);
}
if (operation == "completed")
{
if (!input.TryGetProperty("completion", out var completion) || completion.ValueKind != JsonValueKind.Object)
throw new InvalidOperationException("completion_missing");
var hasUsage = completion.TryGetProperty("actualUsage", out var usage) && usage.ValueKind == JsonValueKind.Object;
var actualUsage = hasUsage ? new UsageSnapshot(
NullableCount(usage, "inputTokens"), NullableCount(usage, "outputTokens"),
NullableCount(usage, "cachedTokens"), NullableCount(usage, "reasoningTokens"), NullableCount(usage, "totalTokens")) : null;
var observation = new CompletedObservation(
1, Count(completion, "latencyMilliseconds"), Count(completion, "userMessageCount"),
Count(completion, "assistantMessageCount"), Count(completion, "toolCallCount"),
Count(completion, "toolResultCount"), Count(completion, "attachmentCount"), hasUsage, actualUsage,
Count(completion, "estimatedInputCharacters"), Count(completion, "estimatedOutputCharacters"), true);
return JsonSerializer.Serialize(new CompletedResult(observation, "analysis-complete"), GuestJsonContext.Default.CompletedResult);
}
throw new InvalidOperationException("operation_unsupported");
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/conversation-insights/go/internal/middleware/guest.go
func Analyze(input object) (object, error) {
if _, ok := unsigned(input["schemaVersion"]); !ok || count(input,"schemaVersion") != 1 { return nil, errors.New("middleware_schema_unsupported") }
switch input["operation"] {
case "committed":
message := asObject(input["committedMessage"]); if message == nil { return nil, errors.New("committed_message_missing") }
role, _ := message["role"].(string); user, assistant := uint64(0), uint64(0)
if role == "user" { user = 1 }; if role == "assistant" { assistant = 1 }
return object{"observation":object{"schemaVersion":1,"messageCount":1,"userMessageCount":user,"assistantMessageCount":assistant,"toolCallCount":count(message,"toolCallCount"),"toolResultCount":count(message,"toolResultCount"),"attachmentCount":count(message,"attachmentCount")},"auditDescription":"insights-recorded"},nil
case "completed":
completion := asObject(input["completion"]); if completion == nil { return nil, errors.New("completion_missing") }
usage := asObject(completion["actualUsage"]); var actual any
if usage != nil { actual = object{"inputTokens":nullableCount(usage,"inputTokens"),"outputTokens":nullableCount(usage,"outputTokens"),"cachedTokens":nullableCount(usage,"cachedTokens"),"reasoningTokens":nullableCount(usage,"reasoningTokens"),"totalTokens":nullableCount(usage,"totalTokens")} }
return object{"observation":object{"schemaVersion":1,"latencyMilliseconds":count(completion,"latencyMilliseconds"),"userMessageCount":count(completion,"userMessageCount"),"assistantMessageCount":count(completion,"assistantMessageCount"),"toolCallCount":count(completion,"toolCallCount"),"toolResultCount":count(completion,"toolResultCount"),"attachmentCount":count(completion,"attachmentCount"),"actualUsageAvailable":usage!=nil,"actualUsage":actual,"estimatedInputCharacters":count(completion,"estimatedInputCharacters"),"estimatedOutputCharacters":count(completion,"estimatedOutputCharacters"),"textStatisticsAreEstimates":true},"auditDescription":"analysis-complete"},nil
default: return nil, errors.New("operation_unsupported")
}
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/conversation-insights/rust/src/lib.rs
pub fn analyze(input: &Value) -> Result<Value, String> {
let request = object(input).ok_or_else(|| "middleware_request_invalid".to_owned())?;
if request.get("schemaVersion").and_then(Value::as_u64) != Some(1) { return Err("middleware_schema_unsupported".into()); }
let operation = request.get("operation").and_then(Value::as_str).unwrap_or("");
if operation == "committed" {
let message = object(request.get("committedMessage").ok_or_else(|| "committed_message_missing".to_owned())?)
.ok_or_else(|| "committed_message_missing".to_owned())?;
let role = message.get("role").and_then(Value::as_str).unwrap_or("");
return Ok(json!({"observation": {
"schemaVersion":1,"messageCount":1,"userMessageCount":if role == "user" { 1 } else { 0 },
"assistantMessageCount":if role == "assistant" { 1 } else { 0 },"toolCallCount":count(Some(message),"toolCallCount"),
"toolResultCount":count(Some(message),"toolResultCount"),"attachmentCount":count(Some(message),"attachmentCount")
},"auditDescription":"insights-recorded"}));
}
if operation == "completed" {
let completion = object(request.get("completion").ok_or_else(|| "completion_missing".to_owned())?)
.ok_or_else(|| "completion_missing".to_owned())?;
let usage = completion.get("actualUsage").and_then(Value::as_object);
let actual_usage = usage.map(|u| json!({
"inputTokens":nullable_count(Some(u),"inputTokens"),"outputTokens":nullable_count(Some(u),"outputTokens"),
"cachedTokens":nullable_count(Some(u),"cachedTokens"),"reasoningTokens":nullable_count(Some(u),"reasoningTokens"),
"totalTokens":nullable_count(Some(u),"totalTokens")
})).unwrap_or(Value::Null);
return Ok(json!({"observation": {
"schemaVersion":1,"latencyMilliseconds":count(Some(completion),"latencyMilliseconds"),
"userMessageCount":count(Some(completion),"userMessageCount"),"assistantMessageCount":count(Some(completion),"assistantMessageCount"),
"toolCallCount":count(Some(completion),"toolCallCount"),"toolResultCount":count(Some(completion),"toolResultCount"),
"attachmentCount":count(Some(completion),"attachmentCount"),"actualUsageAvailable":usage.is_some(),"actualUsage":actual_usage,
"estimatedInputCharacters":count(Some(completion),"estimatedInputCharacters"),
"estimatedOutputCharacters":count(Some(completion),"estimatedOutputCharacters"),"textStatisticsAreEstimates":true
},"auditDescription":"analysis-complete"}));
}
Err("operation_unsupported".into())
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/conversation-insights/typescript/src/guest.ts
export function analyze(inputValue: unknown): JsonObject {
const input = object(inputValue);
if (input?.schemaVersion !== 1) throw new Error("middleware_schema_unsupported");
if (input.operation === "committed") {
const message = object(input.committedMessage);
if (!message) throw new Error("committed_message_missing");
const role = message.role;
return {
observation: {
schemaVersion: 1,
messageCount: 1,
userMessageCount: role === "user" ? 1 : 0,
assistantMessageCount: role === "assistant" ? 1 : 0,
toolCallCount: count(message, "toolCallCount"),
toolResultCount: count(message, "toolResultCount"),
attachmentCount: count(message, "attachmentCount")
},
auditDescription: "insights-recorded"
};
}
if (input.operation === "completed") {
const completion = object(input.completion);
if (!completion) throw new Error("completion_missing");
const usage = object(completion.actualUsage);
const actualUsage = usage ? {
inputTokens: nullableCount(usage, "inputTokens"),
outputTokens: nullableCount(usage, "outputTokens"),
cachedTokens: nullableCount(usage, "cachedTokens"),
reasoningTokens: nullableCount(usage, "reasoningTokens"),
totalTokens: nullableCount(usage, "totalTokens")
} : null;
return {
observation: {
schemaVersion: 1,
latencyMilliseconds: count(completion, "latencyMilliseconds"),
userMessageCount: count(completion, "userMessageCount"),
assistantMessageCount: count(completion, "assistantMessageCount"),
toolCallCount: count(completion, "toolCallCount"),
toolResultCount: count(completion, "toolResultCount"),
attachmentCount: count(completion, "attachmentCount"),
actualUsageAvailable: usage !== undefined,
actualUsage,
estimatedInputCharacters: count(completion, "estimatedInputCharacters"),
estimatedOutputCharacters: count(completion, "estimatedOutputCharacters"),
textStatisticsAreEstimates: true
},
auditDescription: "analysis-complete"
};
}
throw new Error("operation_unsupported");
}Prompt Redactor
Prompt Redactor demonstrates a bounded message transform. Input and output transforms require their separate grants; an enabled mandatory redaction stage must fail closed when it cannot safely transform.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/prompt-redactor/csharp/src/GuestImpl.cs
public static string PreviewText(string text, IReadOnlyList<(string Match, string Replacement)> rules)
{
ArgumentNullException.ThrowIfNull(text);
if (rules.Count > 64) throw new InvalidOperationException("redaction_rules_invalid");
foreach (var (match, replacement) in rules)
{
if (string.IsNullOrEmpty(match) || match.EnumerateRunes().Count() > 512 || replacement.EnumerateRunes().Count() > 1024)
throw new InvalidOperationException("redaction_rule_invalid");
text = text.Replace(match, replacement, StringComparison.Ordinal);
}
return text;
}
internal static RedactorResponse Redact(JsonElement input, JsonElement configuration)
{
if (Number(input, "schemaVersion") != 1 || Text(input, "operation") != "before-provider")
throw new InvalidOperationException("operation_unsupported");
if (!input.TryGetProperty("conversation", out var conversation)
|| !conversation.TryGetProperty("messages", out var messages) || messages.ValueKind != JsonValueKind.Array)
throw new InvalidOperationException("conversation_missing");
var rules = ReadRules(configuration);
var patches = new List<TextPatch>();
foreach (var message in messages.EnumerateArray())
{
if (Text(message, "role") != "user" || !message.TryGetProperty("isHostAuthority", out var authority)
|| authority.ValueKind != JsonValueKind.False || Text(message, "id") is not { } messageId
|| !message.TryGetProperty("parts", out var parts) || parts.ValueKind != JsonValueKind.Array)
continue;
foreach (var part in parts.EnumerateArray())
{
if (Text(part, "kind") != "text" || Text(part, "id") is not { } partId || Text(part, "text") is not { } original)
continue;
var changed = PreviewText(original, rules.Select(rule => (rule.Match, rule.Replacement)).ToArray());
if (!string.Equals(changed, original, StringComparison.Ordinal))
patches.Add(new TextPatch(messageId, partId, changed));
}
}
return new RedactorResponse(patches, patches.Count == 0 ? "no-change" : "redaction-applied");
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/prompt-redactor/go/internal/middleware/guest.go
func PreviewText(text string,rules []Rule)(string,error) {
for _,rule:=range rules { if rule.Match==""||utf8.RuneCountInString(rule.Match)>512||utf8.RuneCountInString(rule.Replacement)>1024 { return "",errors.New("redaction_rule_invalid") }; text=strings.ReplaceAll(text,rule.Match,rule.Replacement) }
return text,nil
}
func Redact(input,configuration object)(object,error) {
if input["schemaVersion"]!=json.Number("1") && input["schemaVersion"]!=float64(1) { return nil,errors.New("middleware_schema_unsupported") }
if input["operation"]!="before-provider" { return nil,errors.New("operation_unsupported") }
conversation:=asObject(input["conversation"]); raw,ok:=conversation["messages"].([]any); if !ok { return nil,errors.New("conversation_missing") }
rules,err:=parseRules(configuration); if err!=nil { return nil,err }
patches:=make([]object,0)
for _,value:=range raw { message:=asObject(value); if message["role"]!="user"||message["isHostAuthority"]!=false { continue }; messageID,mok:=message["id"].(string); parts,pok:=message["parts"].([]any); if !mok||!pok { continue }
for _,pv:=range parts { part:=asObject(pv); if part["kind"]!="text" { continue }; partID,iok:=part["id"].(string); text,tok:=part["text"].(string); if !iok||!tok { continue }; updated,e:=PreviewText(text,rules); if e!=nil { return nil,e }; if updated!=text { patches=append(patches,object{"messageId":messageID,"partId":partID,"text":updated}) } }
}
audit:="no-change"; if len(patches)>0 { audit="redaction-applied" }; return object{"textPatches":patches,"auditDescription":audit},nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/prompt-redactor/rust/src/lib.rs
pub fn preview_text(text: &str, rules: &[Rule]) -> Result<String, String> {
let mut output = text.to_owned();
for rule in rules {
if rule.from.is_empty() || rule.from.chars().count() > 512 || rule.to.chars().count() > 1024 {
return Err("redaction_rule_invalid".into());
}
output = output.replace(&rule.from, &rule.to);
}
Ok(output)
}
pub fn redact(input: &Value, configuration: &Value) -> Result<Value, String> {
if input.get("schemaVersion").and_then(Value::as_u64) != Some(1)
|| input.get("operation").and_then(Value::as_str) != Some("before-provider") {
return Err("operation_unsupported".into());
}
let messages = input.get("conversation").and_then(|v| v.get("messages")).and_then(Value::as_array)
.ok_or_else(|| "conversation_missing".to_owned())?;
let rules = rules(configuration)?;
let mut patches = Vec::new();
for message in messages {
if message.get("role").and_then(Value::as_str) != Some("user")
|| message.get("isHostAuthority").and_then(Value::as_bool) != Some(false) { continue; }
let (Some(message_id), Some(parts)) = (message.get("id").and_then(Value::as_str), message.get("parts").and_then(Value::as_array)) else { continue; };
for part in parts {
if part.get("kind").and_then(Value::as_str) != Some("text") { continue; }
let (Some(part_id), Some(text)) = (part.get("id").and_then(Value::as_str), part.get("text").and_then(Value::as_str)) else { continue; };
let transformed = preview_text(text, &rules)?;
if transformed != text { patches.push(json!({"messageId":message_id,"partId":part_id,"text":transformed})); }
}
}
let audit = if patches.is_empty() { "no-change" } else { "redaction-applied" };
Ok(json!({"textPatches":patches,"auditDescription":audit}))
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/prompt-redactor/typescript/src/guest.ts
export function previewText(text: string, rules: readonly Rule[]): string {
if (rules.length > 64) throw new Error("redaction_rules_invalid");
let result = text;
for (const rule of rules) {
if (!rule.match.length || [...rule.match].length > 512 || [...rule.replacement].length > 1024) throw new Error("redaction_rule_invalid");
result = result.split(rule.match).join(rule.replacement);
}
return result;
}
export function redact(inputValue: unknown, configuration: unknown): JsonObject {
const input = object(inputValue);
if (input?.schemaVersion !== 1 || input.operation !== "before-provider") throw new Error("operation_unsupported");
const conversation = object(input.conversation);
const messages = conversation?.messages;
if (!Array.isArray(messages)) throw new Error("conversation_missing");
const rules = rulesFrom(configuration);
const textPatches: Patch[] = [];
for (const messageValue of messages) {
const message = object(messageValue);
if (!message || message.role !== "user" || message.isHostAuthority !== false || typeof message.id !== "string" || !Array.isArray(message.parts)) continue;
for (const partValue of message.parts) {
const part = object(partValue);
if (!part || part.kind !== "text" || typeof part.id !== "string" || typeof part.text !== "string") continue;
const transformed = previewText(part.text, rules);
if (transformed !== part.text) textPatches.push({ messageId: message.id, partId: part.id, text: transformed });
}
}
return { textPatches, auditDescription: textPatches.length ? "redaction-applied" : "no-change" };
}Host gate: prove deterministic ordering, cancellation and size budgets, optional versus mandatory failure behavior, safe attachment and tool-call validation, account boundaries, and removal during an in-flight request. Middleware is not advertised as an available public contribution until this installed request path passes.
Sandboxed web UI: Accessible Theme
The Accessible Theme example has four language action guests and a sandboxed settings panel. It does not change Glixo Code's appearance. Windows isolation checks passed 7/7 in a source-built app; the signed package, account grant, and full action journey are still being checked. Linux custom panels are unavailable, and mobile is unsupported. See UI support and limitations. The reusable @glixo/extension-ui controls helper is Preview authoring source, not an installed theme.
Workspace references
These examples use host-issued workspace handles and bounded operations. They do not grant ambient filesystem access and do not claim access beyond the selected workspace.
Workspace Health
Workspace Health shows a bounded workspace inspection path. A truncated host result is a sample, not a complete project total.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-health/csharp/src/Handler.cs
public static string Invoke(string requestJson, IWorkspaceReader workspace)
{
using var request = JsonDocument.Parse(requestJson, new JsonDocumentOptions { MaxDepth = 32 });
var root = request.RootElement;
if (root.GetProperty("kind").GetString() != "tools" || root.GetProperty("contributionId").GetString() != "inspect")
throw new InvalidOperationException("contribution_mismatch");
var handle = WorkspaceHandle(root);
var limit = OptionalInteger(root.GetProperty("input"), "maxFiles", 100, 1, 100);
var (items, truncated) = ReadInventory(workspace.Search(handle, "", (uint)limit), limit);
var total = items.Aggregate(0L, (sum, item) => checked(sum + item.Bytes));
if (total > MaximumJsonInteger) throw new InvalidOperationException("workspace_search_result_invalid");
var extensions = new SortedDictionary<string, int>(Utf8Comparer.Instance);
foreach (var item in items)
{
var name = item.Path[(item.Path.LastIndexOf('/') + 1)..];
var dot = name.LastIndexOf('.');
var extension = dot <= 0 ? "[none]" : name[dot..].ToLowerInvariant();
extensions[extension] = extensions.GetValueOrDefault(extension) + 1;
}
return WriteJson(writer =>
{
writer.WriteStartObject();
writer.WriteNumber("sampledFiles", items.Length);
writer.WriteNumber("sampledBytes", total);
writer.WriteBoolean("truncated", truncated);
writer.WritePropertyName("eligibleFileTotals");
if (truncated) writer.WriteNullValue();
else
{
writer.WriteStartObject(); writer.WriteNumber("fileCount", items.Length);
writer.WriteNumber("bytes", total); writer.WriteEndObject();
}
writer.WriteStartObject("sampledExtensions");
foreach (var (extension, count) in extensions) writer.WriteNumber(extension, count);
writer.WriteEndObject();
writer.WriteStartArray("largestFiles");
foreach (var item in items.OrderByDescending(item => item.Bytes)
.ThenBy(item => item.Path, Utf8Comparer.Instance).Take(10))
{
writer.WriteStartObject(); writer.WriteString("path", item.Path);
writer.WriteNumber("bytes", item.Bytes); writer.WriteEndObject();
}
writer.WriteEndArray(); writer.WriteEndObject();
});
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-health/go/internal/workspacehealth/handler.go
func Handle(requestJSON string, reader workspace.Reader) (string, error) {
if len(requestJSON) > 128*1024 {
return "", errors.New("guest_envelope_invalid")
}
var envelope guest.Envelope[json.RawMessage]
if err := json.Unmarshal([]byte(requestJSON), &envelope); err != nil {
return "", errors.New("guest_envelope_invalid")
}
if envelope.Kind != "tools" || envelope.ContributionID != "inspect" {
return "", errors.New("contribution_mismatch")
}
handle, hasHandle := envelope.Context.ResourceHandles["workspace"]
if !hasHandle || strings.TrimSpace(handle) == "" || strings.TrimSpace(envelope.Context.SessionID) == "" {
return "", errors.New("workspace_handle_missing")
}
input, ok := envelope.Input.(map[string]any)
if !ok {
return "", errors.New("input_invalid")
}
limit, err := optionalInteger(input, "maxFiles", 100, 1, 100)
if err != nil {
return "", err
}
raw, err := reader.Search(handle, "", uint32(limit))
if err != nil {
return "", err
}
items, truncated, err := readInventory(raw, limit)
if err != nil {
return "", err
}
output, err := summarize(items, truncated)
if err != nil {
return "", err
}
encoded, err := json.Marshal(output)
if err != nil {
return "", errors.New("guest_result_invalid")
}
return string(encoded), nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-health/rust/src/lib.rs
pub fn handle(request_json: &str, reader: &mut impl Workspace) -> Result<String, String> {
if request_json.len() > MAX_REQUEST_BYTES {
return Err("guest_envelope_invalid".into());
}
let envelope: GuestEnvelope<Value> =
serde_json::from_str(request_json).map_err(|_| "guest_envelope_invalid")?;
if envelope.kind != "tools" || envelope.contribution_id != "inspect" {
return Err("contribution_mismatch".into());
}
if envelope.context.session_id.trim().is_empty() {
return Err("guest_envelope_invalid".into());
}
let handle = envelope
.context
.resource_handles
.get("workspace")
.filter(|value| !value.trim().is_empty())
.ok_or("workspace_handle_missing")?;
let input = envelope.input.as_object().ok_or("input_invalid")?;
let limit = optional_integer(input, "maxFiles", 100, 1, 100, "input_limit_invalid")?;
let raw = reader.search(handle, "", limit as u32)?;
let (items, truncated) = read_inventory(&raw, limit as usize)?;
let output = summarize(items, truncated)?;
serde_json::to_string(&output).map_err(|_| "guest_result_invalid".into())
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-health/typescript/src/handler.ts
export function handleWorkspaceHealth(envelope: HostEnvelope, workspace: WorkspaceReader): WorkspaceHealthResult {
if (envelope.kind !== 'tools' || envelope.contributionId !== CONTRIBUTION_ID) throw new Error('contribution_mismatch');
const handle = envelope.context?.resourceHandles?.workspace;
if (typeof handle !== 'string' || handle.length === 0) throw new Error('workspace_handle_missing');
const limit = envelope.input?.maxFiles ?? 100;
if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new Error('max_files_out_of_range');
const raw = workspace.search(handle, '', limit);
let parsed: SearchResult;
try { parsed = JSON.parse(raw) as SearchResult; } catch { throw new Error('workspace_search_result_invalid'); }
if (!parsed || !Array.isArray(parsed.items) || typeof parsed.truncated !== 'boolean') throw new Error('workspace_search_result_invalid');
const items = parsed.items.map(validateItem).sort((left, right) => compareUtf8(left.path, right.path));
const sampledBytes = items.reduce((total, item) => total + item.bytes, 0);
const sampledExtensions: Record<string, number> = {};
for (const item of items) {
const filename = item.path.slice(item.path.lastIndexOf('/') + 1);
const dot = filename.lastIndexOf('.');
const extension = dot <= 0 ? '[none]' : filename.slice(dot).toLowerCase();
sampledExtensions[extension] = (sampledExtensions[extension] ?? 0) + 1;
}
const largestFiles = [...items].sort((left, right) => right.bytes - left.bytes || compareUtf8(left.path, right.path)).slice(0, 10);
return {
sampledFiles: items.length,
sampledBytes,
truncated: parsed.truncated,
eligibleFileTotals: parsed.truncated ? null : { fileCount: items.length, bytes: sampledBytes },
sampledExtensions: Object.fromEntries(Object.entries(sampledExtensions).sort(([left], [right]) => compareUtf8(left, right))),
largestFiles,
};
}Document Index
Document Index searches selected-workspace relative paths and returns small, bounded excerpts from matching files. It is path matching, not a semantic or full-text index.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/document-index/csharp/src/Handler.cs
public static string Invoke(string requestJson, IWorkspaceReader workspace)
{
using var request = JsonDocument.Parse(requestJson, new JsonDocumentOptions { MaxDepth = 32 });
var root = request.RootElement;
if (root.GetProperty("kind").GetString() != "dataSources"
|| root.GetProperty("contributionId").GetString() != "search")
throw new InvalidOperationException("contribution_mismatch");
var handle = Inventory.WorkspaceHandle(root);
var input = root.GetProperty("input");
var query = input.GetProperty("query").GetString();
if (string.IsNullOrWhiteSpace(query) || query.Length > 128) throw new InvalidOperationException("query_invalid");
var limit = Inventory.OptionalInteger(input, "limit", 10, 1, 20);
var excerptLimit = Inventory.OptionalInteger(input, "excerptBytes", 96, 1, 96);
// Search matches paths, not contents. The host controls the workspace root and read bound.
var (matches, truncated) = Inventory.ReadInventory(workspace.Search(handle, query, (uint)limit), limit);
return Inventory.WriteJson(writer =>
{
writer.WriteStartObject(); writer.WriteString("query", query);
writer.WriteStartArray("items");
foreach (var item in matches)
{
writer.WriteStartObject(); writer.WriteString("path", item.Path); writer.WriteNumber("bytes", item.Bytes);
if (item.Bytes > 4096)
{
writer.WriteNull("excerpt"); writer.WriteBoolean("excerptTruncated", true);
}
else
{
var text = workspace.Read(handle, item.Path, 4096);
var bytes = StrictUtf8.GetBytes(text);
if (bytes.Length > 4096) throw new InvalidOperationException("workspace_read_result_invalid");
var end = Math.Min(bytes.Length, excerptLimit);
while (end > 0)
{
try { StrictUtf8.GetString(bytes, 0, end); break; }
catch (DecoderFallbackException) { end--; }
}
writer.WriteString("excerpt", StrictUtf8.GetString(bytes, 0, end));
writer.WriteBoolean("excerptTruncated", bytes.Length > excerptLimit);
}
writer.WriteEndObject();
}
writer.WriteEndArray(); writer.WriteBoolean("truncated", truncated); writer.WriteEndObject();
});
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/document-index/go/internal/documentindex/handler.go
func Handle(requestJSON string, reader workspace.Reader) (string, error) {
if len(requestJSON) > 128*1024 {
return "", errors.New("guest_envelope_invalid")
}
var envelope guest.Envelope[json.RawMessage]
if err := json.Unmarshal([]byte(requestJSON), &envelope); err != nil {
return "", errors.New("guest_envelope_invalid")
}
if envelope.Kind != "dataSources" || envelope.ContributionID != "search" {
return "", errors.New("contribution_mismatch")
}
handle, hasHandle := envelope.Context.ResourceHandles["workspace"]
if !hasHandle || strings.TrimSpace(handle) == "" || strings.TrimSpace(envelope.Context.SessionID) == "" {
return "", errors.New("workspace_handle_missing")
}
input, ok := envelope.Input.(map[string]any)
if !ok {
return "", errors.New("input_invalid")
}
query, ok := input["query"].(string)
if !ok || strings.TrimSpace(query) == "" || utf16Length(query) > 128 {
return "", errors.New("query_invalid")
}
limit, err := optionalInteger(input, "limit", 10, 1, 20, "result_limit_invalid")
if err != nil {
return "", err
}
excerptLimit, err := optionalInteger(input, "excerptBytes", maximumExcerptBytes, 1, maximumExcerptBytes, "excerpt_limit_invalid")
if err != nil {
return "", err
}
raw, err := reader.Search(handle, query, uint32(limit))
if err != nil {
return "", err
}
items, truncated, err := readInventory(raw, limit)
if err != nil {
return "", err
}
results := make([]map[string]any, 0, len(items))
for _, item := range items {
var excerpt any
excerptTruncated := false
if item.Bytes > maximumReadBytes {
excerpt = nil
excerptTruncated = true
} else {
content, err := reader.Read(handle, item.Path, maximumReadBytes)
if err != nil {
return "", err
}
if !utf8.ValidString(content) || len([]byte(content)) > maximumReadBytes {
return "", errors.New("workspace_read_result_invalid")
}
contentBytes := []byte(content)
end := min(excerptLimit, len(contentBytes))
for end > 0 && !utf8.Valid(contentBytes[:end]) {
end--
}
excerpt = string(contentBytes[:end])
excerptTruncated = len(contentBytes) > excerptLimit
}
results = append(results, map[string]any{
"path": item.Path, "bytes": item.Bytes, "excerpt": excerpt, "excerptTruncated": excerptTruncated,
})
}
encoded, err := json.Marshal(map[string]any{"query": query, "items": results, "truncated": truncated})
if err != nil {
return "", errors.New("guest_result_invalid")
}
return string(encoded), nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/document-index/rust/src/lib.rs
pub fn handle(request_json: &str, reader: &mut impl Workspace) -> Result<String, String> {
if request_json.len() > MAX_REQUEST_BYTES {
return Err("guest_envelope_invalid".into());
}
let envelope: GuestEnvelope<Value> =
serde_json::from_str(request_json).map_err(|_| "guest_envelope_invalid")?;
if envelope.kind != "dataSources" || envelope.contribution_id != "search" {
return Err("contribution_mismatch".into());
}
if envelope.context.session_id.trim().is_empty() {
return Err("guest_envelope_invalid".into());
}
let handle = envelope
.context
.resource_handles
.get("workspace")
.filter(|value| !value.trim().is_empty())
.ok_or("workspace_handle_missing")?;
let input = envelope.input.as_object().ok_or("input_invalid")?;
let query = input
.get("query")
.and_then(Value::as_str)
.filter(|query| !query.trim().is_empty() && query.encode_utf16().count() <= 128)
.ok_or("query_invalid")?;
let limit = optional_integer(input, "limit", 10, 1, 20, "result_limit_invalid")? as usize;
let excerpt_limit = optional_integer(
input,
"excerptBytes",
MAX_EXCERPT_BYTES as u64,
1,
MAX_EXCERPT_BYTES as u64,
"excerpt_limit_invalid",
)? as usize;
let raw = reader.search(handle, query, limit as u32)?;
let (items, truncated) = read_inventory(&raw, limit)?;
let mut output = Vec::with_capacity(items.len());
for item in items {
let (excerpt, excerpt_truncated) = if item.bytes > MAX_READ_BYTES {
(Value::Null, true)
} else {
let content = reader.read(handle, &item.path, MAX_READ_BYTES as u32)?;
if content.len() > MAX_READ_BYTES as usize {
return Err("workspace_read_result_invalid".into());
}
let mut end = excerpt_limit.min(content.len());
while !content.is_char_boundary(end) {
end -= 1;
}
(
Value::String(content[..end].to_owned()),
content.len() > excerpt_limit,
)
};
output.push(json!({"path":item.path,"bytes":item.bytes,"excerpt":excerpt,"excerptTruncated":excerpt_truncated}));
}
serde_json::to_string(&json!({"query":query,"items":output,"truncated":truncated}))
.map_err(|_| "guest_result_invalid".into())
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/document-index/typescript/src/handler.ts
export function handleDocumentIndex(envelope: HostEnvelope, workspace: WorkspaceReader): DocumentIndexResult {
if (envelope.kind !== 'dataSources' || envelope.contributionId !== CONTRIBUTION_ID) throw new Error('contribution_mismatch');
const handle = envelope.context?.resourceHandles?.workspace;
if (typeof handle !== 'string' || handle.length === 0) throw new Error('workspace_handle_missing');
const query = envelope.input?.query;
const limit = envelope.input?.limit ?? 10;
const excerptLimit = envelope.input?.excerptBytes ?? maxExcerptBytes;
if (typeof query !== 'string' || query.trim().length === 0 || query.length > 128) throw new Error('query_invalid');
if (!Number.isInteger(limit) || limit < 1 || limit > 20) throw new Error('result_limit_invalid');
if (!Number.isInteger(excerptLimit) || excerptLimit < 1 || excerptLimit > maxExcerptBytes) throw new Error('excerpt_limit_invalid');
const raw = workspace.search(handle, query, limit);
let parsed: SearchResult;
try { parsed = JSON.parse(raw) as SearchResult; } catch { throw new Error('workspace_search_result_invalid'); }
if (!parsed || !Array.isArray(parsed.items) || typeof parsed.truncated !== 'boolean') throw new Error('workspace_search_result_invalid');
const matches = parsed.items.map(validateItem)
.sort((left, right) => compareUtf8(left.path, right.path))
.slice(0, limit);
const items = matches.map((item): IndexedDocument => {
if (item.bytes > maxReadBytes) return { ...item, excerpt: null, excerptTruncated: true };
const contents = workspace.read(handle, item.path, maxReadBytes);
if (typeof contents !== 'string') throw new Error('workspace_read_result_invalid');
const bytes = new TextEncoder().encode(contents);
return { ...item, excerpt: utf8Prefix(contents, bytes, excerptLimit), excerptTruncated: bytes.length > excerptLimit };
});
return { query, items, truncated: parsed.truncated };
}Workspace Storage
Workspace Storage demonstrates namespaced key/value operations with a prefix-scoped grant. The guest chooses logical keys; it cannot choose an unrestricted storage prefix.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-storage/csharp/src/StorageHandler.cs
public static JsonElement Handle(string requestJson, IScopedState state)
{
using var document = JsonDocument.Parse(requestJson);
var envelope = document.RootElement;
if (envelope.ValueKind != JsonValueKind.Object
|| ReadString(envelope, "kind") != "tools"
|| ReadString(envelope, "contributionId") != ContributionId)
throw new InvalidOperationException("contribution_mismatch");
if (!envelope.TryGetProperty("input", out var input) || input.ValueKind != JsonValueKind.Object)
throw new InvalidOperationException("input_object_required");
var operation = ReadString(input, "operation") ?? throw new InvalidOperationException("storage_operation_invalid");
JsonElement response;
switch (operation)
{
case "get":
{
EnsureFields(input, "operation", "key");
var key = ReadKey(input, "key");
response = JsonSerializer.SerializeToElement(new GetResponse(operation, key, state.Get(key)), StorageJsonContext.Default.GetResponse);
break;
}
case "set":
{
EnsureFields(input, "operation", "key", "value");
var key = ReadKey(input, "key");
var value = ReadString(input, "value") ?? throw new InvalidOperationException("value_string_required");
if (Encoding.UTF8.GetByteCount(value) > MaxValueBytes) throw new InvalidOperationException("value_too_large");
state.Set(key, value);
response = JsonSerializer.SerializeToElement(new SetResponse(operation, key, true), StorageJsonContext.Default.SetResponse);
break;
}
case "list":
{
EnsureFields(input, "operation", "prefix");
if (input.TryGetProperty("prefix", out var prefixElement) && prefixElement.ValueKind != JsonValueKind.String)
throw new InvalidOperationException("key_prefix_invalid");
var prefix = ReadString(input, "prefix") ?? "";
if (prefix.Length > 0 && !ValidKey(prefix)) throw new InvalidOperationException("key_prefix_invalid");
var fullPrefix = prefix.Length == 0 ? TestPrefix : StorageKey(prefix);
var keys = state.List(fullPrefix)
.Where(key => key.StartsWith(TestPrefix, StringComparison.Ordinal))
.Select(key => key[TestPrefix.Length..])
.Where(key => ValidKey(key) && key.StartsWith(prefix, StringComparison.Ordinal))
.OrderBy(key => key, StringComparer.Ordinal)
.Take(100)
.ToArray();
response = JsonSerializer.SerializeToElement(new ListResponse(operation, prefix, keys), StorageJsonContext.Default.ListResponse);
break;
}
case "delete":
{
EnsureFields(input, "operation", "key");
var key = ReadKey(input, "key");
response = JsonSerializer.SerializeToElement(new DeleteResponse(operation, key, state.Delete(key)), StorageJsonContext.Default.DeleteResponse);
break;
}
default: throw new InvalidOperationException("storage_operation_invalid");
}
return response;
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-storage/go/storage/handler.go
func Handle(requestJSON string, store state.Store) (string, error) {
var envelope Envelope
if err := json.Unmarshal([]byte(requestJSON), &envelope); err != nil { return "", errors.New("request_json_invalid") }
if envelope.Kind != "tools" || envelope.ContributionID != ContributionID { return "", errors.New("contribution_mismatch") }
var input map[string]json.RawMessage
if err := json.Unmarshal(envelope.Input, &input); err != nil || input == nil { return "", errors.New("input_object_required") }
var operation string
if json.Unmarshal(input["operation"], &operation) != nil { return "", errors.New("storage_operation_invalid") }
readString := func(name string) (string, bool) {
var value string
if json.Unmarshal(input[name], &value) != nil { return "", false }
return value, true
}
ensureOnly := func(names ...string) bool {
allowed := map[string]bool{"operation": true}
for _, name := range names { allowed[name] = true }
for name := range input { if !allowed[name] { return false } }
return true
}
var response map[string]any
switch operation {
case "get":
if !ensureOnly("key") { return "", errors.New("storage_input_invalid") }
keyValue, ok := readString("key"); if !ok { return "", errors.New("storage_key_invalid") }
key, err := storageKey(keyValue); if err != nil { return "", err }
value, exists, err := store.Get(key); if err != nil { return "", err }
var result any; if exists { result = value }
response = map[string]any{"operation": operation, "key": keyValue, "value": result}
case "set":
if !ensureOnly("key", "value") { return "", errors.New("storage_input_invalid") }
keyValue, ok := readString("key"); if !ok { return "", errors.New("storage_key_invalid") }
value, ok := readString("value"); if !ok { return "", errors.New("value_string_required") }
if len([]byte(value)) > MaxValueBytes { return "", errors.New("value_too_large") }
key, err := storageKey(keyValue); if err != nil { return "", err }
if err := store.Set(key, value); err != nil { return "", err }
response = map[string]any{"operation": operation, "key": keyValue, "stored": true}
case "list":
if !ensureOnly("prefix") { return "", errors.New("storage_input_invalid") }
prefix, ok := readString("prefix"); if !ok { if input["prefix"] != nil { return "", errors.New("key_prefix_invalid") }; prefix = "" }
fullPrefix := TestPrefix
if prefix != "" { var err error; fullPrefix, err = storageKey(prefix); if err != nil { return "", errors.New("key_prefix_invalid") } }
all, err := store.List(fullPrefix); if err != nil { return "", err }
keys := make([]string, 0, len(all))
for _, key := range all {
if strings.HasPrefix(key, TestPrefix) { logical := strings.TrimPrefix(key, TestPrefix); if validKey(logical) && strings.HasPrefix(logical, prefix) { keys = append(keys, logical) } }
}
sort.Strings(keys); if len(keys) > 100 { keys = keys[:100] }
response = map[string]any{"operation": operation, "prefix": prefix, "keys": keys}
case "delete":
if !ensureOnly("key") { return "", errors.New("storage_input_invalid") }
keyValue, ok := readString("key"); if !ok { return "", errors.New("storage_key_invalid") }
key, err := storageKey(keyValue); if err != nil { return "", err }
deleted, err := store.Delete(key); if err != nil { return "", err }
response = map[string]any{"operation": operation, "key": keyValue, "deleted": deleted}
default:
return "", errors.New("storage_operation_invalid")
}
encoded, err := json.Marshal(response); if err != nil { return "", errors.New("response_json_invalid") }
return string(encoded), nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-storage/rust/src/lib.rs
pub fn handle_storage(envelope: &Value, state: &mut impl ScopedState) -> Result<Value, String> {
let envelope = object(envelope)?;
if field(envelope, "kind") != Some("tools") || field(envelope, "contributionId") != Some(CONTRIBUTION_ID) {
return Err("contribution_mismatch".into());
}
let input = object(envelope.get("input").ok_or_else(|| "input_object_required".to_string())?)?;
match field(input, "operation") {
Some("get") => {
ensure_fields(input, &["operation", "key"])?;
let key = storage_key(field(input, "key"))?;
Ok(json!({"operation":"get","key":field(input,"key"),"value":state.get(&key)?}))
}
Some("set") => {
ensure_fields(input, &["operation", "key", "value"])?;
let key = storage_key(field(input, "key"))?;
let value = field(input, "value").ok_or_else(|| "value_string_required".to_string())?;
if value.len() > MAX_VALUE_BYTES { return Err("value_too_large".into()); }
state.set(&key, value)?;
Ok(json!({"operation":"set","key":field(input,"key"),"stored":true}))
}
Some("list") => {
ensure_fields(input, &["operation", "prefix"])?;
if input.get("prefix").is_some_and(|value| !value.is_string()) { return Err("key_prefix_invalid".into()); }
let prefix = field(input, "prefix").unwrap_or("");
if !prefix.is_empty() && !valid_key(prefix) { return Err("key_prefix_invalid".into()); }
let full_prefix = if prefix.is_empty() { TEST_PREFIX.to_owned() } else { storage_key(Some(prefix))? };
let mut keys = state.list(&full_prefix)?.into_iter()
.filter_map(|key| key.strip_prefix(TEST_PREFIX).map(str::to_owned))
.filter(|key| valid_key(key) && key.starts_with(prefix))
.collect::<Vec<_>>();
keys.sort();
keys.truncate(100);
Ok(json!({"operation":"list","prefix":prefix,"keys":keys}))
}
Some("delete") => {
ensure_fields(input, &["operation", "key"])?;
let key = storage_key(field(input, "key"))?;
Ok(json!({"operation":"delete","key":field(input,"key"),"deleted":state.delete(&key)?}))
}
_ => Err("storage_operation_invalid".into()),
}
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workspace-storage/typescript/src/handler.ts
export function handleStorage(envelope: HostEnvelope, state: ScopedState): StorageResponse {
if (envelope.kind !== 'tools' || envelope.contributionId !== CONTRIBUTION_ID) {
throw new Error('contribution_mismatch');
}
const input = envelope.input;
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('input_object_required');
switch (input.operation) {
case 'get': {
const key = storageKey(input.key);
return { operation: 'get', key: input.key!, value: state.get(key) ?? null };
}
case 'set': {
const key = storageKey(input.key);
if (typeof input.value !== 'string') throw new Error('value_string_required');
if (new TextEncoder().encode(input.value).byteLength > maxValueBytes) throw new Error('value_too_large');
state.set(key, input.value);
return { operation: 'set', key: input.key!, stored: true };
}
case 'list': {
const prefix = input.prefix ?? '';
if (prefix && !keyPattern.test(prefix)) throw new Error('key_prefix_invalid');
const fullPrefix = prefix ? storageKey(prefix) : TEST_PREFIX;
const keys = state.list(fullPrefix)
.filter((key) => key.startsWith(TEST_PREFIX))
.map((key) => key.slice(TEST_PREFIX.length))
.filter((key) => keyPattern.test(key) && key.startsWith(prefix))
.sort((left, right) => left.localeCompare(right, 'en'))
.slice(0, 100);
return { operation: 'list', prefix, keys };
}
case 'delete': {
const key = storageKey(input.key);
return { operation: 'delete', key: input.key!, deleted: state.delete(key) };
}
default:
throw new Error('storage_operation_invalid');
}
}MCP: look up an issue through an approved endpoint
Issue Lookup calls one fixed issues.lookup tool through a user-approved Streamable HTTP MCP endpoint. Tool name, HTTP method, and /mcp path are fixed by the reference; input does not select a URL or endpoint handle.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/issue-lookup-mcp/csharp/IssueLookup.cs
public static string Handle(string requestJson, IBroker broker)
{
using var envelope = JsonDocument.Parse(requestJson);
var root = envelope.RootElement;
if (Text(root, "kind") != "tools" || Text(root, "contributionId") != "issue-lookup")
throw new InvalidOperationException("guest_envelope_invalid");
var input = GetObject(root, "input");
var issueKey = Text(input, "issueKey");
if (issueKey is null || !IssueKeyPattern.IsMatch(issueKey)) throw new InvalidOperationException("issue_key_invalid");
var configuration = GetObject(root, "configuration");
if (Text(configuration, "endpointName") != "issues") throw new InvalidOperationException("endpoint_name_must_be_issues");
var context = GetObject(root, "context");
var endpoint = ResolveEndpoint(context);
using var requestBuffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(requestBuffer))
{
writer.WriteStartObject();
writer.WriteString("jsonrpc", "2.0");
writer.WriteNumber("id", 1);
writer.WriteString("method", "tools/call");
writer.WritePropertyName("params");
writer.WriteStartObject();
writer.WriteString("name", "issues.lookup");
writer.WritePropertyName("arguments");
writer.WriteStartObject();
writer.WriteString("issueKey", issueKey);
writer.WriteEndObject();
writer.WritePropertyName("_meta");
writer.WriteStartObject();
writer.WriteString("io.modelcontextprotocol/protocolVersion", "2026-07-28");
writer.WritePropertyName("io.modelcontextprotocol/clientInfo");
writer.WriteStartObject();
writer.WriteString("name", "glixo-issue-lookup");
writer.WriteString("version", "0.1.0");
writer.WriteEndObject();
writer.WriteEndObject();
writer.WriteEndObject();
writer.WriteEndObject();
}
var body = requestBuffer.ToArray();
var request = new BrokerRequest(
endpoint.Url,
"POST",
[
new Header("Accept", "application/json"),
new Header("MCP-Protocol-Version", "2026-07-28"),
new Header("Mcp-Method", "tools/call"),
new Header("Mcp-Name", "issues.lookup")
],
body, "application/json", 15_000, MaxResponseBytes, 200, 299,
endpoint.Handle, null, null, null);
var handle = broker.Start(request);
var completed = false;
try
{
if (broker.Status(handle) != 200) throw new InvalidOperationException("mcp_http_status_invalid");
var contentType = broker.ResponseHeaders(handle).FirstOrDefault(header =>
string.Equals(header.Name, "content-type", StringComparison.OrdinalIgnoreCase))?.Value;
if (contentType is null || !string.Equals(contentType.Split(';', 2)[0].Trim(), "application/json", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("mcp_content_type_invalid");
using var response = new MemoryStream();
var emptyReads = 0;
while (true)
{
var chunk = broker.Read(handle, 16 * 1024);
if (chunk is null) break;
if (chunk.Length == 0 && ++emptyReads > 32) throw new InvalidOperationException("mcp_response_stalled");
if (chunk.Length > 0) emptyReads = 0;
if (response.Length + chunk.Length > MaxResponseBytes) throw new InvalidOperationException("mcp_response_too_large");
response.Write(chunk);
}
using var document = JsonDocument.Parse(response.ToArray());
var rpc = document.RootElement;
if (Text(rpc, "jsonrpc") != "2.0" || !rpc.TryGetProperty("id", out var id) || id.GetInt32() != 1 ||
rpc.TryGetProperty("error", out _)) throw new InvalidOperationException("mcp_call_failed");
if (!rpc.TryGetProperty("result", out var result) || !result.TryGetProperty("content", out var content) ||
content.ValueKind != JsonValueKind.Array || content.GetArrayLength() != 1)
throw new InvalidOperationException("mcp_result_content_missing");
var item = content[0];
var summary = Text(item, "text");
if (Text(item, "type") != "text" || summary is null || Encoding.UTF8.GetByteCount(summary) > 64 * 1024)
throw new InvalidOperationException("mcp_result_content_invalid");
completed = true;
using var resultBuffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(resultBuffer))
{
writer.WriteStartObject();
writer.WriteString("issueKey", issueKey);
writer.WriteString("summary", summary);
writer.WriteEndObject();
}
return Encoding.UTF8.GetString(resultBuffer.ToArray());
}
finally
{
if (!completed) broker.Cancel(handle);
broker.Drop(handle);
}
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/issue-lookup-mcp/go/internal/issuelookup/handler.go
func Lookup(requestJSON string, broker httpbroker.Broker) (object, error) {
decoder := json.NewDecoder(strings.NewReader(requestJSON)); decoder.UseNumber()
var request object
if err := decoder.Decode(&request); err != nil { return nil, errors.New("guest_envelope_invalid") }
if _, err := decoder.Token(); err != io.EOF { return nil, errors.New("guest_envelope_invalid") }
input := asObject(request["input"])
issueKey := stringValue(input["issueKey"])
if len(issueKey) < 3 || len(issueKey) > 20 { return nil, errors.New("issue_key_invalid") }
prefix, suffix, ok := strings.Cut(issueKey, "-")
if !ok || len(prefix) == 0 || len(prefix) > 10 || prefix[0] < 'A' || prefix[0] > 'Z' || len(suffix) == 0 || len(suffix) > 9 || suffix[0] == '0' {
return nil, errors.New("issue_key_invalid")
}
for _, char := range prefix { if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { return nil, errors.New("issue_key_invalid") } }
for _, char := range suffix { if char < '0' || char > '9' { return nil, errors.New("issue_key_invalid") } }
url, endpointHandle, err := endpoint(request); if err != nil { return nil, err }
body, _ := json.Marshal(object{"jsonrpc":"2.0", "id":1, "method":"tools/call", "params":object{
"name":"issues.lookup", "arguments":object{"issueKey":issueKey}, "_meta":object{
"io.modelcontextprotocol/protocolVersion":"2026-07-28", "io.modelcontextprotocol/clientInfo":object{"name":"glixo-issue-lookup", "version":"0.1.0"},
},
}})
timeout, limit := uint32(15000), uint32(maxResponseBytes)
minStatus, maxStatus := uint16(200), uint16(299)
handle, err := broker.HTTPStart(httpbroker.Request{URL:url, Method:"POST", Headers:[]httpbroker.Header{
{Name:"Accept", Value:"application/json"}, {Name:"MCP-Protocol-Version", Value:"2026-07-28"},
{Name:"Mcp-Method", Value:"tools/call"}, {Name:"Mcp-Name", Value:"issues.lookup"},
}, Body:body, ContentType:ptr("application/json"), TimeoutMS:&timeout, MaxResponseBytes:&limit,
AcceptedStatusMin:&minStatus, AcceptedStatusMax:&maxStatus, EndpointHandle:&endpointHandle})
if err != nil { return nil, errors.New("mcp_request_denied") }
complete := false
defer func() { if !complete { broker.HTTPCancel(handle) }; broker.HTTPDrop(handle) }()
status, err := broker.HTTPStatus(handle); if err != nil { return nil, errors.New("mcp_status_failed") }
if status != 200 { return nil, errors.New("mcp_http_status_invalid") }
headers, err := broker.HTTPResponseHeaders(handle); if err != nil { return nil, errors.New("mcp_headers_failed") }
contentType := ""; for _, header := range headers { if strings.EqualFold(header.Name,"content-type") { contentType=header.Value; break } }
mediaType, _, mediaErr := mime.ParseMediaType(contentType); if mediaErr != nil || !strings.EqualFold(mediaType, "application/json") { return nil, errors.New("mcp_content_type_invalid") }
var response bytes.Buffer
emptyReads := 0
for { chunk, more, readErr := broker.HTTPRead(handle, 16*1024); if readErr != nil { return nil, errors.New("http_read_failed") }; if !more { break }; if len(chunk) == 0 { emptyReads++; if emptyReads > 32 { return nil, errors.New("mcp_response_stalled") }; continue }; emptyReads = 0; if response.Len()+len(chunk)>maxResponseBytes { return nil, errors.New("mcp_response_too_large") }; _,_=response.Write(chunk) }
var rpc object; if err:=json.Unmarshal(response.Bytes(), &rpc); err!=nil { return nil, errors.New("mcp_response_invalid_json") }
_, hasError := rpc["error"]
if rpc["jsonrpc"]!="2.0" || rpc["id"]!=float64(1) || hasError { return nil, errors.New("mcp_call_failed") }
result:=asObject(rpc["result"]); content,ok:=result["content"].([]any); if !ok || len(content)!=1 { return nil, errors.New("mcp_result_content_missing") }
item:=asObject(content[0]); summary,ok:=item["text"].(string); if stringValue(item["type"])!="text" || !ok || len(summary)>64*1024 { return nil, errors.New("mcp_result_content_invalid") }
complete=true
return object{"issueKey":issueKey,"summary":summary},nil
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/issue-lookup-mcp/rust/src/lib.rs
fn call_request(url: String, endpoint_handle: String, issue_key: &str) -> Result<Value, String> {
let mut broker = guest::HostBroker;
let body = serde_json::to_vec(&json!({
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"issues.lookup","arguments":{"issueKey":issue_key},"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"glixo-issue-lookup","version":"0.1.0"}
}}
})).map_err(|_| "mcp_request_invalid")?;
let request = HttpRequest {
url,
method: "POST".into(),
headers: vec![
Header { name: "Accept".into(), value: "application/json".into() },
Header { name: "MCP-Protocol-Version".into(), value: "2026-07-28".into() },
Header { name: "Mcp-Method".into(), value: "tools/call".into() },
Header { name: "Mcp-Name".into(), value: "issues.lookup".into() },
],
body: Some(body), content_type: Some("application/json".into()), timeout_ms: Some(15_000),
max_response_bytes: Some(MAX_RESPONSE_BYTES as u32), accepted_status_min: Some(200),
accepted_status_max: Some(299), endpoint_handle: Some(endpoint_handle), secret_handle: None,
auth_header: None, auth_scheme: None,
};
let handle = broker.start(request).map_err(|_| "mcp_request_denied")?;
let mut complete = false;
let result = (|| {
if broker.status(handle).map_err(|_| "mcp_status_failed")? != 200 { return Err("mcp_http_status_invalid".into()); }
let headers = broker.response_headers(handle).map_err(|_| "mcp_headers_failed")?;
let content_type = headers.iter().find(|h| h.name.eq_ignore_ascii_case("content-type"))
.map(|h| h.value.as_str()).ok_or("mcp_content_type_invalid")?;
let media_type = content_type.split(';').next().unwrap_or("").trim();
if !media_type.eq_ignore_ascii_case("application/json") { return Err("mcp_content_type_invalid".into()); }
let mut bytes = Vec::new();
let mut empty_reads = 0;
loop {
let chunk = broker.read(handle, 16 * 1024).map_err(|_| "http_read_failed")?;
let Some(chunk) = chunk else { break; };
if chunk.is_empty() {
empty_reads += 1;
if empty_reads > 32 { return Err("mcp_response_stalled".into()); }
continue;
}
empty_reads = 0;
if bytes.len().saturating_add(chunk.len()) > MAX_RESPONSE_BYTES { return Err("mcp_response_too_large".into()); }
bytes.extend_from_slice(&chunk);
}
let response: Value = serde_json::from_slice(&bytes).map_err(|_| "mcp_response_invalid_json")?;
if text(&response, "jsonrpc") != Some("2.0") || response.get("id").and_then(Value::as_u64) != Some(1)
|| response.get("error").is_some() { return Err("mcp_call_failed".into()); }
let content = response.get("result").and_then(|r| r.get("content")).and_then(Value::as_array)
.ok_or("mcp_result_content_missing")?;
if content.len() != 1 || text(&content[0], "type") != Some("text") { return Err("mcp_result_content_invalid".into()); }
let summary = text(&content[0], "text").ok_or("mcp_result_content_invalid")?;
if summary.len() > 64 * 1024 { return Err("mcp_result_content_invalid".into()); }
complete = true;
Ok(json!({"issueKey":issue_key,"summary":summary}))
})();
if !complete { broker.cancel(handle); }
broker.drop_response(handle);
result
}
pub fn invoke(request_json: &str) -> Result<String, String> {
let envelope: Value = serde_json::from_str(request_json).map_err(|_| "guest_envelope_invalid")?;
let issue_key = text(envelope.get("input").ok_or("guest_envelope_invalid")?, "issueKey")
.filter(|key| {
let (prefix, suffix) = key.split_once('-').unwrap_or(("", ""));
!prefix.is_empty() && prefix.len() <= 10 && prefix.as_bytes()[0].is_ascii_uppercase()
&& prefix.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
&& !suffix.is_empty() && suffix.len() <= 9 && suffix.bytes().all(|b| b.is_ascii_digit()) && !suffix.starts_with('0')
}).ok_or("issue_key_invalid")?;
let (url, endpoint) = approved_endpoint(&envelope)?;
let response = call_request(url, endpoint, issue_key)?;
serde_json::to_string(&response).map_err(|_| "guest_result_invalid".into())
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/issue-lookup-mcp/typescript/src/handler.ts
export function lookupIssue(envelope: HostEnvelope, broker: HttpBroker): JsonObject {
if (envelope.kind !== "tools" || envelope.contributionId !== "issue-lookup") throw new Error("guest_envelope_invalid");
const issueKey = parseIssueKey(envelope.input);
const endpoint = resolveEndpoint(envelope);
const requestBody = encoder.encode(JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "issues.lookup",
arguments: { issueKey },
_meta: {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { name: "glixo-issue-lookup", version: "0.1.0" }
}
}
}));
const request: BrokerRequest = {
url: endpoint.baseUrl,
method: "POST",
headers: [
{ name: "Accept", value: "application/json" },
{ name: "MCP-Protocol-Version", value: "2026-07-28" },
{ name: "Mcp-Method", value: "tools/call" },
{ name: "Mcp-Name", value: "issues.lookup" }
],
body: requestBody,
contentType: "application/json",
timeoutMs: 15_000,
maxResponseBytes: MAX_RESPONSE_BYTES,
acceptedStatusMin: 200,
acceptedStatusMax: 299,
endpointHandle: endpoint.handle
};
const handle = unwrap<number>(broker.httpStart(request), "mcp_request_denied");
let completed = false;
try {
const status = unwrap<number>(broker.httpStatus(handle), "mcp_status_failed");
if (status !== 200) throw new Error("mcp_http_status_invalid");
const headers = unwrap<readonly { name: string; value: string }[]>(broker.httpResponseHeaders(handle), "mcp_headers_failed");
const contentType = headers.find((header) => header.name.toLowerCase() === "content-type")?.value;
if (!contentType || contentType.split(";", 1)[0].trim().toLowerCase() !== "application/json") throw new Error("mcp_content_type_invalid");
const output = parseMcpResponse(readBody(broker, handle), issueKey);
completed = true;
return output;
} finally {
if (!completed) broker.httpCancel(handle);
broker.httpDrop(handle);
}
}Host gate: prove the selected account's approved endpoint and connection grant, typed request validation, bounded response handling, cancellation, revoke behavior, and installed-host invocation. A language build or fixture alone does not qualify an external tracker integration.
Workflow node: count words and characters
The workflow-text-stats reference demonstrates an extension tool as a step in a Workflow Designer graph. The four language projects use the same signed text-stats tool contract; the source tabs show its input validation and counting logic.
- Create a project from the published
@glixo/glxdev0.3.3 template withglxdev new --kind reference --reference workflow-text-stats --language typescript --output ./workflow-text-stats. Replacetypescriptwithcsharp,go, orrustto use another language. Follow the project README to check and build it, then prepare the signed package through the supported package flow. The manifest declares a requiredtextinput andwordCountandcharacterCountoutputs. It requeststool.invokeonly fortext-statsand declares the tool read-only. The pinned public source contains the reference implementation shown in the tabs below. - Install the signed package for the account used by Workflow Designer, then approve its requested tool permission. Eligible installed tools appear in the Designer's extension-node palette.
- Add
text-statsto a draft workflow. SetincludeWhitespaceandminimumWordLengthin the node settings, or keep their defaults (trueand1). - Bind the required
textinput to the run input with{ "text": "$.text" }. Workflow input mappings select a scalar value from the run input or a preceding node's output. - Publish the workflow and run it with
{"text":"Glixo makes tools."}. The step returns{"wordCount":3,"characterCount":18}. Punctuation remains part of a word;minimumWordLengthaffects only the word count, andincludeWhitespaceaffects only the character count.
This example passed signed installed-host tests in all four languages with the protocol-2 source-built runner and verified embedded pin. Stable Glixo 0.7.8 ships protocol 1, so an app release carrying protocol 2 is needed before this workflow can run there. The Designer keeps each node's settings outside the workflow graph and stores secret values in the host vault. Tools that require confirmation for every call need an approval step and cannot run automatically here.
Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workflow-text-stats/csharp/src/Handler.cs
public static string Invoke(string requestJson)
{
if (Encoding.UTF8.GetByteCount(requestJson) > 128 * 1024)
throw new InvalidOperationException("guest_envelope_invalid");
try
{
using var document = JsonDocument.Parse(requestJson, new JsonDocumentOptions { MaxDepth = 24 });
var root = document.RootElement;
if (root.GetProperty("kind").GetString() != "tools" || root.GetProperty("contributionId").GetString() != "text-stats")
throw new InvalidOperationException("contribution_mismatch");
var input = root.GetProperty("input");
if (input.ValueKind != JsonValueKind.Object || !input.TryGetProperty("text", out var textElement) || textElement.ValueKind != JsonValueKind.String)
throw new InvalidOperationException("text_required");
var text = textElement.GetString() ?? throw new InvalidOperationException("text_required");
var includeWhitespace = OptionalBoolean(input, "includeWhitespace", true);
var minimumWordLength = OptionalInteger(input, "minimumWordLength", 1, 1, MaximumMinimumWordLength);
var result = Analyze(text, includeWhitespace, minimumWordLength);
var output = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(output))
{
writer.WriteStartObject();
writer.WriteNumber("wordCount", result.WordCount);
writer.WriteNumber("characterCount", result.CharacterCount);
writer.WriteEndObject();
}
return Encoding.UTF8.GetString(output.WrittenSpan);
}
catch (InvalidOperationException)
{
throw;
}
catch (JsonException)
{
throw new InvalidOperationException("guest_envelope_invalid");
}
catch (KeyNotFoundException)
{
throw new InvalidOperationException("guest_envelope_invalid");
}
}
public static StatsResult Analyze(string text, bool includeWhitespace = true, int minimumWordLength = 1)
{
if (minimumWordLength < 1 || minimumWordLength > MaximumMinimumWordLength)
throw new InvalidOperationException("minimum_word_length_out_of_range");
var wordCount = 0;
var characterCount = 0;
var currentWordLength = 0;
var scalarCount = 0;
foreach (var rune in text.EnumerateRunes())
{
scalarCount++;
if (scalarCount > MaximumTextScalars) throw new InvalidOperationException("text_too_long");
if (IsWhiteSpace(rune.Value))
{
if (includeWhitespace) characterCount++;
if (currentWordLength >= minimumWordLength) wordCount++;
currentWordLength = 0;
}
else
{
characterCount++;
currentWordLength++;
}
}
if (currentWordLength >= minimumWordLength) wordCount++;
return new StatsResult(wordCount, characterCount);
}
private static bool OptionalBoolean(JsonElement input, string name, bool fallback)
{
if (!input.TryGetProperty(name, out var value)) return fallback;
if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
throw new InvalidOperationException("include_whitespace_invalid");
return value.GetBoolean();
}
private static int OptionalInteger(JsonElement input, string name, int fallback, int minimum, int maximum)
{
if (!input.TryGetProperty(name, out var value)) return fallback;
if (!value.TryGetInt32(out var number) || number < minimum || number > maximum)
throw new InvalidOperationException("minimum_word_length_out_of_range");
return number;
}
private static bool IsWhiteSpace(int scalar) => scalar is >= 0x0009 and <= 0x000D
or 0x0020 or 0x0085 or 0x00A0 or 0x1680
or >= 0x2000 and <= 0x200A or 0x2028 or 0x2029 or 0x202F or 0x205F or 0x3000;Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workflow-text-stats/go/internal/workflowtextstats/stats.go
func Handle(requestJSON string) (string, error) {
if len(requestJSON) > 128*1024 {
return "", errors.New("guest_envelope_invalid")
}
decoder := json.NewDecoder(strings.NewReader(requestJSON))
decoder.DisallowUnknownFields()
var request envelope
if err := decoder.Decode(&request); err != nil {
return "", errors.New("guest_envelope_invalid")
}
if err := decoder.Decode(new(any)); err != io.EOF {
return "", errors.New("guest_envelope_invalid")
}
if request.Kind != "tools" || request.ContributionID != "text-stats" {
return "", errors.New("contribution_mismatch")
}
if len(request.Input.Text) == 0 || bytes.Equal(bytes.TrimSpace(request.Input.Text), []byte("null")) {
return "", errors.New("text_required")
}
var text string
if err := json.Unmarshal(request.Input.Text, &text); err != nil {
return "", errors.New("text_required")
}
includeWhitespace := true
if len(request.Input.IncludeWhitespace) != 0 {
if bytes.Equal(bytes.TrimSpace(request.Input.IncludeWhitespace), []byte("null")) || json.Unmarshal(request.Input.IncludeWhitespace, &includeWhitespace) != nil {
return "", errors.New("include_whitespace_invalid")
}
}
minimumWordLength := 1
if len(request.Input.MinimumWordLength) != 0 {
if bytes.Equal(bytes.TrimSpace(request.Input.MinimumWordLength), []byte("null")) || json.Unmarshal(request.Input.MinimumWordLength, &minimumWordLength) != nil {
return "", errors.New("minimum_word_length_out_of_range")
}
}
result, err := Analyze(text, includeWhitespace, minimumWordLength)
if err != nil {
return "", err
}
encoded, err := json.Marshal(result)
if err != nil {
return "", errors.New("guest_result_invalid")
}
return string(encoded), nil
}
func Analyze(text string, includeWhitespace bool, minimumWordLength int) (Result, error) {
if minimumWordLength < 1 || minimumWordLength > maximumMinimumWordLength {
return Result{}, errors.New("minimum_word_length_out_of_range")
}
if !utf8.ValidString(text) {
return Result{}, errors.New("text_invalid_unicode")
}
if utf8.RuneCountInString(text) > maximumTextScalars {
return Result{}, errors.New("text_too_long")
}
result := Result{}
wordLength := 0
for _, scalar := range text {
if isWhiteSpace(scalar) {
if includeWhitespace {
result.CharacterCount++
}
if wordLength >= minimumWordLength {
result.WordCount++
}
wordLength = 0
continue
}
result.CharacterCount++
wordLength++
}
if wordLength >= minimumWordLength {
result.WordCount++
}
return result, nil
}
func isWhiteSpace(scalar rune) bool {
return scalar >= 0x0009 && scalar <= 0x000D || scalar == 0x0020 || scalar == 0x0085 || scalar == 0x00A0 || scalar == 0x1680 ||
scalar >= 0x2000 && scalar <= 0x200A || scalar == 0x2028 || scalar == 0x2029 || scalar == 0x202F || scalar == 0x205F || scalar == 0x3000
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workflow-text-stats/rust/src/lib.rs
pub fn handle(request_json: &str) -> Result<String, String> {
if request_json.len() > MAX_REQUEST_BYTES {
return Err("guest_envelope_invalid".into());
}
let request: Value = serde_json::from_str(request_json).map_err(|_| "guest_envelope_invalid")?;
if request.get("kind").and_then(Value::as_str) != Some("tools")
|| request.get("contributionId").and_then(Value::as_str) != Some("text-stats")
{
return Err("contribution_mismatch".into());
}
let input = request.get("input").and_then(Value::as_object).ok_or("input_invalid")?;
let text = input.get("text").and_then(Value::as_str).ok_or("text_required")?;
let include_whitespace = input
.get("includeWhitespace")
.map(Value::as_bool)
.transpose_option("include_whitespace_invalid")?
.unwrap_or(true);
let minimum_word_length = input
.get("minimumWordLength")
.map(Value::as_u64)
.transpose_option("minimum_word_length_out_of_range")?
.unwrap_or(1);
if minimum_word_length > MAXIMUM_MINIMUM_WORD_LENGTH as u64 {
return Err("minimum_word_length_out_of_range".into());
}
let minimum_word_length = minimum_word_length as usize;
let result = analyze(text, include_whitespace, minimum_word_length)?;
serde_json::to_string(&json!({
"wordCount": result.word_count,
"characterCount": result.character_count
}))
.map_err(|_| "guest_result_invalid".into())
}
pub fn analyze(text: &str, include_whitespace: bool, minimum_word_length: usize) -> Result<StatsResult, String> {
if minimum_word_length < 1 || minimum_word_length > MAXIMUM_MINIMUM_WORD_LENGTH {
return Err("minimum_word_length_out_of_range".into());
}
let mut result = StatsResult { word_count: 0, character_count: 0 };
let mut word_length = 0;
let mut scalar_count = 0;
for scalar in text.chars() {
scalar_count += 1;
if scalar_count > MAX_TEXT_SCALARS { return Err("text_too_long".into()); }
if is_white_space(scalar) {
if include_whitespace { result.character_count += 1; }
if word_length >= minimum_word_length { result.word_count += 1; }
word_length = 0;
} else {
result.character_count += 1;
word_length += 1;
}
}
if word_length >= minimum_word_length { result.word_count += 1; }
Ok(result)
}
fn is_white_space(scalar: char) -> bool {
matches!(scalar as u32,
0x0009..=0x000D | 0x0020 | 0x0085 | 0x00A0 | 0x1680 |
0x2000..=0x200A | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000)
}Source: https://github.com/tmedanovic/glixo-community-modules@7052b3f954418cff91439236942a6623d75e8907 · references/workflow-text-stats/typescript/src/handler.ts
export function handleWorkflowTextStats(requestJson: string): TextStatsResult {
if (new TextEncoder().encode(requestJson).length > 128 * 1024) throw 'guest_envelope_invalid';
let envelope: HostEnvelope;
try { envelope = JSON.parse(requestJson) as HostEnvelope; } catch { throw 'guest_envelope_invalid'; }
if (!envelope || envelope.kind !== 'tools' || envelope.contributionId !== CONTRIBUTION_ID) throw 'contribution_mismatch';
if (!isRecord(envelope.input)) throw 'input_invalid';
const input = envelope.input;
if (typeof input.text !== 'string') throw 'text_required';
if (input.includeWhitespace !== undefined && typeof input.includeWhitespace !== 'boolean') throw 'include_whitespace_invalid';
if (Object.hasOwn(input, 'minimumWordLength') && input.minimumWordLength === null) throw 'minimum_word_length_out_of_range';
const minimumValue = input.minimumWordLength;
const minimumWordLength = minimumValue === undefined ? 1 : typeof minimumValue === 'number' ? minimumValue : Number.NaN;
if (!Number.isSafeInteger(minimumWordLength) || minimumWordLength < 1 || minimumWordLength > 128) throw 'minimum_word_length_out_of_range';
const result = analyze(input.text, input.includeWhitespace ?? true, minimumWordLength);
return result;
}
export function analyze(text: string, includeWhitespace = true, minimumWordLength = 1): TextStatsResult {
if (!Number.isSafeInteger(minimumWordLength) || minimumWordLength < 1 || minimumWordLength > 128) throw 'minimum_word_length_out_of_range';
const scalars = Array.from(text);
if (scalars.length > MAXIMUM_TEXT_SCALARS) throw 'text_too_long';
let wordCount = 0;
let characterCount = 0;
let wordLength = 0;
for (const scalar of scalars) {
if (isWhiteSpace(scalar.codePointAt(0)!)) {
if (includeWhitespace) characterCount += 1;
if (wordLength >= minimumWordLength) wordCount += 1;
wordLength = 0;
} else {
characterCount += 1;
wordLength += 1;
}
}
if (wordLength >= minimumWordLength) wordCount += 1;
return { wordCount, characterCount };
}
function isWhiteSpace(scalar: number): boolean {
return scalar >= 0x0009 && scalar <= 0x000D || scalar === 0x0020 || scalar === 0x0085 || scalar === 0x00A0 || scalar === 0x1680
|| scalar >= 0x2000 && scalar <= 0x200A || scalar === 0x2028 || scalar === 0x2029 || scalar === 0x202F || scalar === 0x205F || scalar === 0x3000;
}References and pinned sources
Implementation excerpts identify exact source commits. Source snapshots can advance while the public SDK and host qualification remain incomplete; the page preserves the identity it was generated from. See all source pins and acceptance evidence.