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");
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn malformed_manifest_multibyte_diagnostic_is_bounded_and_redacted() {
|
||||
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 {
|
||||
let mut metadata = PodMetadata::new(self.manifest.pod.name.clone(), active);
|
||||
if self.manifest.profile.is_some() {
|
||||
metadata.resolved_manifest_snapshot = serde_json::to_value(&self.manifest).ok();
|
||||
}
|
||||
metadata
|
||||
pod_metadata_for_manifest(&self.manifest, active)
|
||||
}
|
||||
|
||||
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
||||
|
|
@ -4321,6 +4317,21 @@ fn request_config_from_worker_manifest(wm: &WorkerManifest) -> RequestConfig {
|
|||
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(
|
||||
pod_name: &str,
|
||||
snapshot: Option<serde_json::Value>,
|
||||
|
|
@ -5294,6 +5305,74 @@ 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)]
|
||||
|
|
|
|||
|
|
@ -22,51 +22,46 @@ LICENSE* # recommended license text
|
|||
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` 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
|
||||
schema_version = 1
|
||||
id = "example"
|
||||
name = "Example Plugin"
|
||||
id = "example.summarizer"
|
||||
name = "Example Summarizer"
|
||||
version = "0.1.0"
|
||||
description = "Demonstrates declarative hooks and an optional WASM module."
|
||||
|
||||
[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 = []
|
||||
description = "Adds a custom summary command."
|
||||
surfaces = ["hook"]
|
||||
|
||||
[[hooks]]
|
||||
id = "summarize-ticket"
|
||||
file = "hooks/summarize-ticket.toml"
|
||||
id = "summary"
|
||||
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.
|
||||
- `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.
|
||||
- `runtime.kind`: required runtime family. Initial values should be `declarative` and `wasm`.
|
||||
- `runtime.entry`: required for `wasm`, forbidden or ignored for purely declarative packages.
|
||||
- `runtime.abi`: required for `wasm` so the host can reject incompatible modules before initialization.
|
||||
- `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.
|
||||
- `surfaces`: optional declared contribution surface names.
|
||||
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it.
|
||||
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user