fix: persist plugin snapshots for restore
This commit is contained in:
parent
ede7acfdf6
commit
07978d2df5
|
|
@ -1617,6 +1617,42 @@ mod tests {
|
||||||
assert_eq!(restored_snapshot.resolved[0].version, "0.1.0");
|
assert_eq!(restored_snapshot.resolved[0].version, "0.1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn currently_documented_manifest_shape_is_accepted() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let workspace = temp.path().join("workspace");
|
||||||
|
let plugins = workspace.join(".yoi/plugins");
|
||||||
|
fs::create_dir_all(&plugins).unwrap();
|
||||||
|
let manifest = r#"
|
||||||
|
schema_version = 1
|
||||||
|
id = "example.summarizer"
|
||||||
|
name = "Example Summarizer"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Adds a custom summary command."
|
||||||
|
surfaces = ["hook"]
|
||||||
|
|
||||||
|
[[hooks]]
|
||||||
|
id = "summary"
|
||||||
|
file = "hooks/summary.md"
|
||||||
|
"#;
|
||||||
|
write_stored_zip(
|
||||||
|
&plugins.join("documented.yoi-plugin"),
|
||||||
|
&[
|
||||||
|
("plugin.toml", manifest.as_bytes().to_vec(), 0),
|
||||||
|
("hooks/summary.md", b"summarize".to_vec(), 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let report = discover_plugins(&PluginDiscoveryOptions::new(&workspace));
|
||||||
|
|
||||||
|
assert_eq!(report.diagnostics, vec![]);
|
||||||
|
assert_eq!(report.packages.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
report.packages[0].identity.to_string(),
|
||||||
|
"project:example.summarizer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_manifest_multibyte_diagnostic_is_bounded_and_redacted() {
|
fn malformed_manifest_multibyte_diagnostic_is_bounded_and_redacted() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -924,11 +924,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
||||||
let mut metadata = PodMetadata::new(self.manifest.pod.name.clone(), active);
|
pod_metadata_for_manifest(&self.manifest, active)
|
||||||
if self.manifest.profile.is_some() {
|
|
||||||
metadata.resolved_manifest_snapshot = serde_json::to_value(&self.manifest).ok();
|
|
||||||
}
|
|
||||||
metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
||||||
|
|
@ -4321,6 +4317,21 @@ fn request_config_from_worker_manifest(wm: &WorkerManifest) -> RequestConfig {
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pod_metadata_for_manifest(
|
||||||
|
manifest: &PodManifest,
|
||||||
|
active: Option<PodActiveSegmentRef>,
|
||||||
|
) -> PodMetadata {
|
||||||
|
let mut metadata = PodMetadata::new(manifest.pod.name.clone(), active);
|
||||||
|
if should_persist_resolved_manifest_snapshot(manifest) {
|
||||||
|
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
||||||
|
}
|
||||||
|
metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_persist_resolved_manifest_snapshot(manifest: &PodManifest) -> bool {
|
||||||
|
manifest.profile.is_some() || manifest.plugins.has_resolved_plan()
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_manifest_from_pod_metadata_snapshot(
|
fn restore_manifest_from_pod_metadata_snapshot(
|
||||||
pod_name: &str,
|
pod_name: &str,
|
||||||
snapshot: Option<serde_json::Value>,
|
snapshot: Option<serde_json::Value>,
|
||||||
|
|
@ -5294,6 +5305,74 @@ permission = "write"
|
||||||
Permission::Write
|
Permission::Write
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plugin_resolved_manifest_snapshot_is_persisted_without_profile() {
|
||||||
|
let mut manifest = PodManifest::from_toml(
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
name = "plugin-snapshot"
|
||||||
|
|
||||||
|
[model]
|
||||||
|
scheme = "anthropic"
|
||||||
|
model_id = "claude-sonnet-4-20250514"
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
instruction = "saved"
|
||||||
|
|
||||||
|
[[scope.allow]]
|
||||||
|
target = "/snapshot/workspace"
|
||||||
|
permission = "read"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(manifest.profile.is_none());
|
||||||
|
assert!(
|
||||||
|
pod_metadata_for_manifest(&manifest, None)
|
||||||
|
.resolved_manifest_snapshot
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
manifest.plugins.resolved = vec![manifest::plugin::ResolvedPluginRecord {
|
||||||
|
identity: manifest::plugin::SourceQualifiedPluginId::new(
|
||||||
|
manifest::plugin::PluginSourceKind::Project,
|
||||||
|
"example",
|
||||||
|
),
|
||||||
|
source: manifest::plugin::PluginSourceKind::Project,
|
||||||
|
package_path: PathBuf::from("/snapshot/workspace/.yoi/plugins/example.yoi-plugin"),
|
||||||
|
package_label: "example.yoi-plugin".to_string(),
|
||||||
|
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
.to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
manifest: manifest::plugin::PluginPackageManifest {
|
||||||
|
schema_version: 1,
|
||||||
|
id: "example".to_string(),
|
||||||
|
name: "Example".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
description: None,
|
||||||
|
surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||||
|
runtime: None,
|
||||||
|
hooks: vec![],
|
||||||
|
},
|
||||||
|
enabled_surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||||
|
grants: manifest::plugin::PluginGrantConfig::default(),
|
||||||
|
config: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let metadata = pod_metadata_for_manifest(&manifest, None);
|
||||||
|
let snapshot = metadata
|
||||||
|
.resolved_manifest_snapshot
|
||||||
|
.expect("plugin-resolved manifest should be snapshotted");
|
||||||
|
let restored: PodManifest = serde_json::from_value(snapshot).unwrap();
|
||||||
|
|
||||||
|
assert!(restored.profile.is_none());
|
||||||
|
assert_eq!(restored.plugins.resolved.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
restored.plugins.resolved[0].digest,
|
||||||
|
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
assert_eq!(restored.plugins.resolved[0].version, "0.1.0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -22,51 +22,46 @@ LICENSE* # recommended license text
|
||||||
assets/** # optional non-executable data assets
|
assets/** # optional non-executable data assets
|
||||||
```
|
```
|
||||||
|
|
||||||
The package layout is intentionally data-first. Placing a package in a store must never execute `module.wasm`, register `hooks/*.toml`, or scan assets as prompts. Those steps happen only after explicit enablement and policy resolution.
|
The package layout is intentionally data-first. Placing a package in a store must never execute `module.wasm`, register hook metadata, or scan assets as prompts. Those steps happen only after explicit enablement and policy resolution.
|
||||||
|
|
||||||
## `plugin.toml`
|
## `plugin.toml`
|
||||||
|
|
||||||
`plugin.toml` is the package authority for package identity and declared needs. It is not the authority for runtime grants.
|
`plugin.toml` is the package authority for package identity and declared needs. It is not the authority for runtime grants.
|
||||||
|
|
||||||
Illustrative manifest shape:
|
Currently implemented strict `plugin.toml` shape:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
id = "example"
|
id = "example.summarizer"
|
||||||
name = "Example Plugin"
|
name = "Example Summarizer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Demonstrates declarative hooks and an optional WASM module."
|
description = "Adds a custom summary command."
|
||||||
|
surfaces = ["hook"]
|
||||||
[runtime]
|
|
||||||
kind = "wasm" # "declarative" or "wasm" for the initial plugin system
|
|
||||||
entry = "module.wasm"
|
|
||||||
abi = "yoi-plugin-wasm-1"
|
|
||||||
|
|
||||||
[package]
|
|
||||||
readme = "README.md"
|
|
||||||
license = "LICENSE"
|
|
||||||
|
|
||||||
[permissions]
|
|
||||||
tools = ["Bash"]
|
|
||||||
web = false
|
|
||||||
secrets = []
|
|
||||||
filesystem = []
|
|
||||||
|
|
||||||
[[hooks]]
|
[[hooks]]
|
||||||
id = "summarize-ticket"
|
id = "summary"
|
||||||
file = "hooks/summarize-ticket.toml"
|
file = "hooks/summary.md"
|
||||||
```
|
```
|
||||||
|
|
||||||
Fields proposed for the first implementation pass:
|
The package archive must contain both root `plugin.toml` and the referenced `hooks/summary.md` entry. Optional WASM metadata is accepted only for the declared future runtime boundary and is not executed:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[runtime]
|
||||||
|
kind = "wasm"
|
||||||
|
entry = "plugin.wasm"
|
||||||
|
abi = "yoi-plugin-wasm-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
First-pass fields accepted by the parser:
|
||||||
|
|
||||||
- `schema_version`: required integer; unsupported versions fail closed.
|
- `schema_version`: required integer; unsupported versions fail closed.
|
||||||
- `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself.
|
- `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself.
|
||||||
- `name`, `version`, `description`: human metadata used in listings and diagnostics.
|
- `name`, `version`, `description`: human metadata used in listings and diagnostics.
|
||||||
- `runtime.kind`: required runtime family. Initial values should be `declarative` and `wasm`.
|
- `surfaces`: optional declared contribution surface names.
|
||||||
- `runtime.entry`: required for `wasm`, forbidden or ignored for purely declarative packages.
|
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it.
|
||||||
- `runtime.abi`: required for `wasm` so the host can reject incompatible modules before initialization.
|
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
|
||||||
- `hooks`, `schemas`, `package.readme`, `package.license`: package-relative paths that must pass the same normalized-path validation as archive entries.
|
|
||||||
- `permissions`: requested authority. These declarations are requests only; they do not grant access.
|
Future descriptor sections such as `[package]`, `[permissions]`, richer `contributions`, or `runtime.kind = "declarative"` are aspirational and are intentionally rejected by the current strict parser until implemented safely.
|
||||||
|
|
||||||
The `source` is not read from `plugin.toml`. It is assigned by the store that discovered the package.
|
The `source` is not read from `plugin.toml`. It is assigned by the store that discovered the package.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user