-
Notifications
You must be signed in to change notification settings - Fork 2
Run GitHub Issue and PTY stats Resource providers on WASIp2 #411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
schickling-assistant
merged 4 commits into
schickling-assistant/2026-09-01-wasip2-resource-executor
from
schickling-assistant/2026-09-01-wasip2-resource-providers
Sep 1, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8b51460
feat(resource): add WASIp2 providers
schickling-assistant abfc96a
fix(resource): bind provider invocation ownership
schickling-assistant f4b52d4
fix(resource): migrate providers to reviewed ABI
schickling-assistant 26e6821
fix(resource): normalize provider WIT
schickling-assistant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [package] | ||
| name = "st2-github-issue-component" | ||
| version = "0.1.0" | ||
| edition = "2024" | ||
| publish = false | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib"] | ||
|
|
||
| [dependencies] | ||
| serde = { version = "1", features = ["derive"] } | ||
| serde_json = "1" | ||
| sha2 = "0.10" | ||
| wit-bindgen = "0.57.1" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| use serde::{Deserialize, Serialize}; | ||
| use sha2::{Digest as _, Sha256}; | ||
|
|
||
| wit_bindgen::generate!({ | ||
| path: "../../wit/github-issue", | ||
| world: "github-issue-provider", | ||
| with: { | ||
| "compoundingtech:st2-github-issue/github-issue@0.1.0": generate, | ||
| }, | ||
| }); | ||
|
|
||
| use compoundingtech::st2_github_issue::github_issue; | ||
| use exports::st2::resource_provider::provider_api; | ||
|
|
||
| const SELECTOR_SCHEMA: &str = r#"{ | ||
| "type": "object", | ||
| "properties": { | ||
| "owner": { "type": "string" }, | ||
| "repo": { "type": "string" }, | ||
| "number": { "type": "integer" }, | ||
| "etag": { "type": "string" }, | ||
| "topics": { | ||
| "type": "array", | ||
| "items": { "type": "string" }, | ||
| "uniqueItems": true | ||
| } | ||
| }, | ||
| "required": ["owner", "repo", "number"], | ||
| "additionalProperties": false | ||
| }"#; | ||
|
|
||
| struct Component; | ||
|
|
||
| #[derive(Deserialize)] | ||
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | ||
| struct Selector { | ||
| owner: String, | ||
| repo: String, | ||
| number: u64, | ||
| #[serde(default)] | ||
| etag: Option<String>, | ||
| #[serde(default)] | ||
| topics: Vec<String>, | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct GitHubIssue { | ||
| number: u64, | ||
| state: String, | ||
| title: String, | ||
| updated_at: String, | ||
| html_url: String, | ||
| } | ||
|
|
||
| #[derive(Serialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| struct Carrier<'a> { | ||
| resource: &'static str, | ||
| owner: &'a str, | ||
| repo: &'a str, | ||
| number: u64, | ||
| state: &'a str, | ||
| title: &'a str, | ||
| updated_at: &'a str, | ||
| html_url: &'a str, | ||
| } | ||
|
|
||
| impl provider_api::Guest for Component { | ||
| fn describe() -> Result<provider_api::ProviderDescriptor, provider_api::DescriptorError> { | ||
| Ok(provider_api::ProviderDescriptor { | ||
| capabilities: vec![provider_api::SchedulingCapability::Demand], | ||
| selector_schema_json: SELECTOR_SCHEMA.into(), | ||
| default_selector_json: "{}".into(), | ||
| topics: vec!["issue".into()], | ||
| snapshot_media_type: "application/json".into(), | ||
| snapshot_schema_id: "st2.resource.github-issue.v1".into(), | ||
| }) | ||
| } | ||
|
|
||
| fn observe(request: provider_api::ObserveRequest) -> provider_api::ObservationResult { | ||
| observe(request).unwrap_or_else(|diagnostic| { | ||
| provider_api::ObservationResult::Failed(Some(diagnostic)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| fn observe( | ||
| request: provider_api::ObserveRequest, | ||
| ) -> Result<provider_api::ObservationResult, String> { | ||
| let selector: Selector = serde_json::from_str(&request.selector_json) | ||
| .map_err(|_| "invalid GitHub issue selector".to_owned())?; | ||
| if selector.owner.is_empty() || selector.repo.is_empty() || selector.number == 0 { | ||
| return Err("GitHub issue selector fields must be non-empty".into()); | ||
| } | ||
| let response = github_issue::get(&github_issue::IssueRequest { | ||
| owner: selector.owner.clone(), | ||
| repo: selector.repo.clone(), | ||
| number: selector.number, | ||
| etag: selector.etag, | ||
| }) | ||
| .map_err(map_source_error)?; | ||
| let (etag, body) = match response { | ||
| github_issue::IssueResponse::NotModified(_) => { | ||
| return Ok(provider_api::ObservationResult::Unchanged); | ||
| } | ||
| github_issue::IssueResponse::Ok(value) => value, | ||
| }; | ||
| let issue: GitHubIssue = serde_json::from_slice(&body) | ||
| .map_err(|_| "GitHub response was invalid".to_owned())?; | ||
| if issue.number != selector.number { | ||
| return Err("GitHub response did not match the requested issue".into()); | ||
| } | ||
| let bytes = serde_json::to_vec(&Carrier { | ||
| resource: "github-issue", | ||
| owner: &selector.owner, | ||
| repo: &selector.repo, | ||
| number: issue.number, | ||
| state: &issue.state, | ||
| title: &issue.title, | ||
| updated_at: &issue.updated_at, | ||
| html_url: &issue.html_url, | ||
| }) | ||
| .map_err(|_| "GitHub response normalization failed".to_owned())?; | ||
| let digest = Sha256::digest(&bytes); | ||
| if request.prior_digest.as_deref() == Some(digest.as_slice()) { | ||
| return Ok(provider_api::ObservationResult::Unchanged); | ||
| } | ||
| let _ = (request.uri, request.demand_watermark, selector.topics); | ||
| let facts = vec![ | ||
| provider_api::Fact { | ||
| key: "state".into(), | ||
| before: provider_api::FactValue::Omitted, | ||
| after: provider_api::FactValue::Value(issue.state), | ||
| }, | ||
| provider_api::Fact { | ||
| key: "etag".into(), | ||
| before: provider_api::FactValue::Omitted, | ||
| after: etag.map_or(provider_api::FactValue::Null, provider_api::FactValue::Value), | ||
| }, | ||
| ]; | ||
| Ok(provider_api::ObservationResult::Published( | ||
| provider_api::Publication { | ||
| schema_id: "st2.resource.github-issue.v1".into(), | ||
| media_type: "application/json".into(), | ||
| bytes, | ||
| topics: vec!["issue".into()], | ||
| facts: Some(facts), | ||
| }, | ||
| )) | ||
| } | ||
|
|
||
| fn map_source_error(error: github_issue::IssueError) -> String { | ||
| match error { | ||
| github_issue::IssueError::Denied => "GitHub issue scope denied", | ||
| github_issue::IssueError::Unavailable => "GitHub is unavailable", | ||
| github_issue::IssueError::ResourceExhausted => "GitHub response exceeded limits", | ||
| github_issue::IssueError::DeadlineExceeded => "GitHub request deadline exceeded", | ||
| } | ||
| .into() | ||
| } | ||
|
|
||
| export!(Component); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [package] | ||
| name = "st2-pty-stats-component" | ||
| version = "0.1.0" | ||
| edition = "2024" | ||
| publish = false | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib"] | ||
|
|
||
| [dependencies] | ||
| serde = { version = "1", features = ["derive"] } | ||
| serde_json = "1" | ||
| sha2 = "0.10" | ||
| wit-bindgen = "0.57.1" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| use serde::Serialize; | ||
| use sha2::{Digest as _, Sha256}; | ||
|
|
||
| wit_bindgen::generate!({ | ||
| path: "../../wit/pty-stats", | ||
| world: "pty-stats-provider", | ||
| with: { | ||
| "compoundingtech:st2-pty-stats/pty-stats@0.1.0": generate, | ||
| }, | ||
| }); | ||
|
|
||
| use compoundingtech::st2_pty_stats::pty_stats; | ||
| use exports::st2::resource_provider::provider_api; | ||
|
|
||
| const SELECTOR_SCHEMA: &str = r#"{ | ||
| "type": "object", | ||
| "properties": { | ||
| "session": { "type": "string" }, | ||
| "topics": { | ||
| "type": "array", | ||
| "items": { "type": "string" }, | ||
| "uniqueItems": true | ||
| } | ||
| }, | ||
| "additionalProperties": false | ||
| }"#; | ||
|
|
||
| struct Component; | ||
|
|
||
| #[derive(serde::Deserialize)] | ||
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | ||
| struct Selector { | ||
| #[serde(default)] | ||
| session: Option<String>, | ||
| #[serde(default)] | ||
| topics: Vec<String>, | ||
| } | ||
|
|
||
| #[derive(Serialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| struct Carrier<'a> { | ||
| resource: &'static str, | ||
| scope: Scope<'a>, | ||
| stats: &'a serde_json::Value, | ||
| } | ||
|
|
||
| #[derive(Serialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| enum Scope<'a> { | ||
| All, | ||
| Session(&'a str), | ||
| } | ||
|
|
||
| impl provider_api::Guest for Component { | ||
| fn describe() -> Result<provider_api::ProviderDescriptor, provider_api::DescriptorError> { | ||
| Ok(provider_api::ProviderDescriptor { | ||
| capabilities: vec![provider_api::SchedulingCapability::Demand], | ||
| selector_schema_json: SELECTOR_SCHEMA.into(), | ||
| default_selector_json: "{}".into(), | ||
| topics: vec!["stats".into()], | ||
| snapshot_media_type: "application/json".into(), | ||
| snapshot_schema_id: "st2.resource.pty-stats.v1".into(), | ||
| }) | ||
| } | ||
|
|
||
| fn observe(request: provider_api::ObserveRequest) -> provider_api::ObservationResult { | ||
| observe(request).unwrap_or_else(|diagnostic| { | ||
| provider_api::ObservationResult::Failed(Some(diagnostic)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| fn observe( | ||
| request: provider_api::ObserveRequest, | ||
| ) -> Result<provider_api::ObservationResult, String> { | ||
| let selector: Selector = serde_json::from_str(&request.selector_json) | ||
| .map_err(|_| "invalid PTY stats selector".to_owned())?; | ||
| if selector.session.as_deref().is_some_and(str::is_empty) { | ||
| return Err("PTY session scope must be non-empty".into()); | ||
| } | ||
| let scope = selector.session.as_ref().map_or(pty_stats::Scope::All, |session| { | ||
| pty_stats::Scope::Session(session.clone()) | ||
| }); | ||
| let outcome = pty_stats::get(&scope).map_err(map_source_error)?; | ||
| if outcome.stdout_truncated || outcome.stderr_truncated { | ||
| return Err("PTY stats output exceeded limits".into()); | ||
| } | ||
| match outcome.exit { | ||
| pty_stats::ExitStatus::Code(0) => {} | ||
| pty_stats::ExitStatus::Code(_) | pty_stats::ExitStatus::Signal(_) => { | ||
| return Ok(provider_api::ObservationResult::Failed(Some( | ||
| "pty stats exited unsuccessfully".into(), | ||
| ))); | ||
| } | ||
| } | ||
| let stats: serde_json::Value = serde_json::from_slice(&outcome.stdout) | ||
| .map_err(|_| "pty stats returned invalid JSON".to_owned())?; | ||
| let carrier_scope = selector | ||
| .session | ||
| .as_deref() | ||
| .map_or(Scope::All, Scope::Session); | ||
| let scope_fact = selector.session.clone().unwrap_or_else(|| "all".into()); | ||
| let bytes = serde_json::to_vec(&Carrier { | ||
| resource: "pty-stats", | ||
| scope: carrier_scope, | ||
| stats: &stats, | ||
| }) | ||
| .map_err(|_| "PTY stats normalization failed".to_owned())?; | ||
| let digest = Sha256::digest(&bytes); | ||
| if request.prior_digest.as_deref() == Some(digest.as_slice()) { | ||
| return Ok(provider_api::ObservationResult::Unchanged); | ||
| } | ||
| let _ = (request.uri, request.demand_watermark, selector.topics); | ||
| Ok(provider_api::ObservationResult::Published( | ||
| provider_api::Publication { | ||
| schema_id: "st2.resource.pty-stats.v1".into(), | ||
| media_type: "application/json".into(), | ||
| bytes, | ||
| topics: vec!["stats".into()], | ||
| facts: Some(vec![provider_api::Fact { | ||
| key: "scope".into(), | ||
| before: provider_api::FactValue::Omitted, | ||
| after: provider_api::FactValue::Value(scope_fact), | ||
| }]), | ||
| }, | ||
| )) | ||
| } | ||
|
|
||
| fn map_source_error(error: pty_stats::PtyStatsError) -> String { | ||
| match error { | ||
| pty_stats::PtyStatsError::Denied => "PTY stats scope denied", | ||
| pty_stats::PtyStatsError::Unavailable => "PTY stats is unavailable", | ||
| pty_stats::PtyStatsError::ResourceExhausted => "PTY stats output exceeded limits", | ||
| pty_stats::PtyStatsError::DeadlineExceeded => "PTY stats deadline exceeded", | ||
| pty_stats::PtyStatsError::Cancelled => "PTY stats was cancelled", | ||
| } | ||
| .into() | ||
| } | ||
|
|
||
| export!(Component); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a caller omits a selector and uses this advertised default,
{}is missing the schema-requiredowner,repo, andnumberfields, soobserveimmediately returnsFailed("invalid GitHub issue selector"). Make the default valid or remove the claim that this provider supplies a usable default.Useful? React with 👍 / 👎.