Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0641371
Add extension manifest discovery and trust model
cxnmai Jul 13, 2026
85b5a48
Add extension panel view model seam
cxnmai Jul 13, 2026
284f72f
Add editor extension API types
cxnmai Jul 13, 2026
087fd34
Wire extension panel controller into workspace
cxnmai Jul 13, 2026
e4d2ce1
Add Extensions tool to workspace rail
cxnmai Jul 13, 2026
83f0009
Add extension panel interactions and trust prompt
cxnmai Jul 13, 2026
b0cbf98
Expose extension document snapshots
cxnmai Jul 13, 2026
711b0f7
Apply extension edits transactionally
cxnmai Jul 13, 2026
988766d
Define extension component interface
cxnmai Jul 13, 2026
8002fc3
Add Wasmtime extension runtime
cxnmai Jul 13, 2026
122f592
Reset editor lifecycle on disk refresh
cxnmai Jul 13, 2026
89d0b55
Test extension edit validation
cxnmai Jul 13, 2026
1d5d065
Fix extension API imports
cxnmai Jul 13, 2026
fe0c76a
Test extension validation and coordination
cxnmai Jul 13, 2026
8bd17e9
Render extension action panel
cxnmai Jul 13, 2026
e00568c
Box replacement documents in extension edits
cxnmai Jul 13, 2026
716d50c
Satisfy extension crate test lint
cxnmai Jul 13, 2026
c5a35e5
Enable WASI HTTP for trusted extensions
cxnmai Jul 13, 2026
72f3739
Fix extension panel integration
cxnmai Jul 13, 2026
33abc92
Document extension authoring and capabilities
cxnmai Jul 13, 2026
7c9a07d
Cache compiled extension components by digest
cxnmai Jul 13, 2026
ec41958
Add extension JSON wire types
cxnmai Jul 13, 2026
838254b
Support structural block splices
cxnmai Jul 13, 2026
d9a7e1c
Expose equation source selections
cxnmai Jul 13, 2026
ef51a74
Bridge extension editor APIs through JSON
cxnmai Jul 13, 2026
d3324d0
Test extension history and block splices
cxnmai Jul 13, 2026
68561a9
Keep extension tests independent of GPUI harness
cxnmai Jul 13, 2026
36ea98e
Test extension JSON edit conversion
cxnmai Jul 13, 2026
c333e22
Add deferred directory grants to extension API
cxnmai Jul 13, 2026
99940c0
Exercise component invocation and host callback
cxnmai Jul 13, 2026
e590de2
Verify cancellation against running component
cxnmai Jul 13, 2026
b8a4e18
Tighten component cache lock scopes
cxnmai Jul 13, 2026
73cd50b
Connect extension discovery runtime and trust
cxnmai Jul 13, 2026
ee41f08
Add bounded extension editor host bridge
cxnmai Jul 13, 2026
566d33c
Bridge extension actions to active editor
cxnmai Jul 13, 2026
3f6af76
Harden extension grants refresh and events
cxnmai Jul 13, 2026
c314a34
Scope document directory to declared actions
cxnmai Jul 13, 2026
0028a0b
Document extension directory grants
cxnmai Jul 13, 2026
996e1b4
Add all-capabilities extension scaffold
cxnmai Jul 13, 2026
396a09b
Add extension action dispatch and document edits
cxnmai Jul 13, 2026
d9df4a1
Demonstrate rich document block insertion
cxnmai Jul 13, 2026
c18d416
Exercise WASI and directory capabilities
cxnmai Jul 13, 2026
1b64127
Cover destructive extension actions
cxnmai Jul 13, 2026
77160bf
Finalize guest bindings and runtime demos
cxnmai Jul 13, 2026
f70ed31
Persist and exercise directory grants
cxnmai Jul 13, 2026
d15a492
Handle normalized selection edits and tests
cxnmai Jul 13, 2026
89a9808
Document the capability example workflow
cxnmai Jul 13, 2026
ef6856e
Explain all-capabilities extension actions
cxnmai Jul 13, 2026
19e7d4e
Link base WASI with extension HTTP support
cxnmai Jul 13, 2026
209de19
fix(extensions): allow guest DNS lookup
cxnmai Jul 13, 2026
490ed6f
Enable precise extension JSON numbers
cxnmai Jul 13, 2026
dc2ab4d
Decode asset IDs safely in extension edits
cxnmai Jul 13, 2026
bf92699
Emit string asset IDs in extension example
cxnmai Jul 13, 2026
61e9f62
Derive extension asset hashes from bytes
cxnmai Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,081 changes: 1,062 additions & 19 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions crates/flowstate-extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "flowstate-extension"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
anyhow.workspace = true
bytes = "1.12.1"
hex = "0.4.3"
parking_lot = "0.12.5"
serde = { workspace = true, features = ["derive"] }
sha2 = "0.11.0"
thiserror = "2.0.18"
toml.workspace = true
wasmtime = { version = "38", features = ["component-model", "cranelift", "runtime"] }
wasmtime-wasi = "38"
wasmtime-wasi-http = "38"
Comment on lines +9 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest version of the wasmtime crate on crates.io as of July 2026, and are there any known security advisories for wasmtime version 38?

💡 Result:

As of July 13, 2026, the latest version of the wasmtime crate available on crates.io is 45.0.1 [1][2]. Regarding wasmtime version 38, there are known security advisories associated with this release series: 1. GHSA-4h67-722j-5pmc (CVE-2025-62711): Versions 38.0.0, 38.0.1, and 38.0.2 contained a bug in the implementation of component-model related host-to-wasm trampolines. This vulnerability could allow a carefully crafted component to crash the host with a segfault or assertion failure [3][4]. This was patched in version 38.0.3 [3][4]. 2. GHSA-hc7m-r6v8-hg9q (RUSTSEC-2025-0118): This advisory identified an unsound API interaction where WebAssembly shared linear memory could be improperly accessed, potentially leading to data races. Versions up to and including 38.0.3 were affected [5][6]. This was addressed in version 38.0.4 [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## files\n'
git ls-files crates/flowstate-extension/Cargo.toml crates/flowstate-extension

printf '\n## Cargo.toml\n'
cat -n crates/flowstate-extension/Cargo.toml

printf '\n## trust.rs locations\n'
rg -n "Sha256::digest|Digest::digest|use sha2|sha2" crates/flowstate-extension -S

Repository: realartists-gitmo/flowstate

Length of output: 1691


🌐 Web query:

Does sha2 0.11.0 still expose the Digesttrait andSha256::digest API used in sha2 0.10.x? Also, what is the current latest 38.x patch release for wasmtime on crates.io?

💡 Result:

Yes, the sha2 crate in version 0.11.0 continues to expose the Digest trait and the Sha256::digest API, maintaining compatibility with the one-shot API patterns used in version 0.10.x [1][2][3]. Regarding the wasmtime crate, the latest 38.x patch release available on crates.io is 38.0.4 [4][5].

Citations:


Bump the wasmtime family to 38.0.4+
wasmtime 38.0.0-38.0.3 have known advisories; keep wasmtime, wasmtime-wasi, and wasmtime-wasi-http on at least 38.0.4 (or a newer major). The sha2 0.11.0 Digest::digest call in trust.rs is still valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flowstate-extension/Cargo.toml` around lines 9 - 18, Update the
wasmtime, wasmtime-wasi, and wasmtime-wasi-http dependency entries to version
38.0.4 or newer, keeping all three family members aligned or moving them to a
newer major together. Leave the existing sha2 dependency and trust.rs
Digest::digest usage unchanged.


[lints]
workspace = true

[dev-dependencies]
tempfile.workspace = true
56 changes: 56 additions & 0 deletions crates/flowstate-extension/src/discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

use crate::ExtensionManifest;

#[derive(Clone, Debug)]
pub struct InstalledExtension {
pub manifest: ExtensionManifest,
pub root: PathBuf,
pub component_path: PathBuf,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiscoveryIssue {
pub path: PathBuf,
pub message: String,
}

#[derive(Clone, Debug, Default)]
pub struct DiscoveryResult {
pub extensions: Vec<InstalledExtension>,
pub issues: Vec<DiscoveryIssue>,
}

pub fn discover_extensions(root: &Path) -> DiscoveryResult {
let mut result = DiscoveryResult::default();
let Ok(entries) = fs::read_dir(root) else { return result };
let mut manifests: Vec<_> = entries.flatten().map(|entry| entry.path().join("extension.toml")).filter(|path| path.is_file()).collect();
manifests.sort();
let mut ids = HashSet::new();
for path in manifests {
match load_extension(&path) {
Ok(extension) if ids.insert(extension.manifest.id.clone()) => result.extensions.push(extension),
Ok(extension) => result.issues.push(DiscoveryIssue { path, message: format!("duplicate extension id: {}", extension.manifest.id) }),
Err(message) => result.issues.push(DiscoveryIssue { path, message }),
}
}
result
}

fn load_extension(path: &Path) -> Result<InstalledExtension, String> {
let source = fs::read_to_string(path).map_err(|error| error.to_string())?;
let manifest = ExtensionManifest::parse(&source).map_err(|error| error.to_string())?;
let root = path.parent().ok_or("manifest has no parent directory")?.to_path_buf();
let component_path = root.join(&manifest.component);
if !component_path.is_file() {
return Err(format!("component does not exist: {}", component_path.display()));
}
let canonical_root = root.canonicalize().map_err(|error| error.to_string())?;
let canonical_component = component_path.canonicalize().map_err(|error| error.to_string())?;
if !canonical_component.starts_with(&canonical_root) {
return Err("component resolves outside the extension directory".to_owned());
}
Ok(InstalledExtension { manifest, root, component_path: canonical_component })
}
9 changes: 9 additions & 0 deletions crates/flowstate-extension/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mod discovery;
mod manifest;
mod runtime;
mod trust;

pub use discovery::{DiscoveryIssue, DiscoveryResult, InstalledExtension, discover_extensions};
pub use manifest::{ActionManifest, ExtensionManifest, ManifestError};
pub use runtime::{CancellationHandle, DirectoryAccess, DirectoryGrant, DirectoryGrantResponse, ExtensionHost, HostError, Invocation, InvocationOutput, Runtime, RuntimeConfig, RuntimeError};
pub use trust::{ComponentDigest, TrustDecision, TrustStore};
86 changes: 86 additions & 0 deletions crates/flowstate-extension/src/manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::collections::HashSet;
use std::path::{Component, Path};

use serde::Deserialize;
use thiserror::Error;

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ExtensionManifest {
pub manifest_version: u32,
pub id: String,
pub name: String,
pub version: String,
pub component: String,
#[serde(default)]
pub actions: Vec<ActionManifest>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ActionManifest {
pub id: String,
pub label: String,
#[serde(default)]
pub requires_document: bool,
}

#[derive(Debug, Error, Eq, PartialEq)]
pub enum ManifestError {
#[error("manifest TOML is invalid: {0}")]
Toml(String),
#[error("unsupported manifest version {0}")]
UnsupportedVersion(u32),
#[error("{field} must be a dot-separated identifier")]
InvalidId { field: &'static str },
#[error("{field} must not be empty")]
Empty { field: &'static str },
#[error("component must be a relative path without parent traversal")]
InvalidComponentPath,
#[error("duplicate action id: {0}")]
DuplicateAction(String),
#[error("at least one action is required")]
NoActions,
}

impl ExtensionManifest {
pub fn parse(source: &str) -> Result<Self, ManifestError> {
let manifest: Self = toml::from_str(source).map_err(|error| ManifestError::Toml(error.to_string()))?;
manifest.validate()?;
Ok(manifest)
}

pub fn validate(&self) -> Result<(), ManifestError> {
if self.manifest_version != 1 {
return Err(ManifestError::UnsupportedVersion(self.manifest_version));
}
validate_id(&self.id, "id")?;
require_text(&self.name, "name")?;
require_text(&self.version, "version")?;
let path = Path::new(&self.component);
if path.is_absolute() || path.as_os_str().is_empty() || path.components().any(|part| matches!(part, Component::ParentDir | Component::RootDir | Component::Prefix(_))) {
return Err(ManifestError::InvalidComponentPath);
}
if self.actions.is_empty() {
return Err(ManifestError::NoActions);
}
let mut ids = HashSet::new();
for action in &self.actions {
validate_id(&action.id, "action id")?;
require_text(&action.label, "action label")?;
if !ids.insert(&action.id) {
return Err(ManifestError::DuplicateAction(action.id.clone()));
}
}
Ok(())
}
}

fn require_text(value: &str, field: &'static str) -> Result<(), ManifestError> {
if value.trim().is_empty() { Err(ManifestError::Empty { field }) } else { Ok(()) }
}

fn validate_id(value: &str, field: &'static str) -> Result<(), ManifestError> {
let valid = !value.is_empty() && value.split('.').all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')));
if valid { Ok(()) } else { Err(ManifestError::InvalidId { field }) }
}
Loading
Loading