feat(profiles): seal builtin import provenance and expose read-only editor imports

Retain the evaluated JSON launch entrypoint, seal observed builtin source bytes and digests as verified provenance, and fail on missing/corrupt dependencies without Runtime builtin reevaluation. Cover real HTTP commits and launch resolution, including worker mode and Workspace Config feature composition.

Expose read-only builtin import completions/diagnostics in the editor and rebuild wasm32 release assets with wasm-bindgen 0.2.117. Document public paths, ordinary composition, limits, resave semantics, and the lazy-cycle incident.

Validation: worker-runtime 344 library + 34 integration tests; workspace-server 609 library + 11 binary tests plus final builtin HTTP/launch tests (2); archive tests (16); native WASM tests (2); real-WASM/web tests (37), editor component tests (10); root cargo check, cargo fmt --all -- --check and git diff --check HEAD pass.
This commit is contained in:
2026-10-07 03:38:49 +09:00
parent 4343be86c1
commit f086cf3375
12 changed files with 986 additions and 29 deletions
+133 -22
View File
@@ -98,31 +98,47 @@ pub fn complete_current(
let snapshot = session
.as_ref()
.ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?;
let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?;
let result = session_environment(snapshot.clone())
.complete_config(&entrypoint, &source, utf8_byte_offset, explicit)
.map_err(|error| JsValue::from_str(&format!("{error:?}")))?;
let result = result
.map(|result| {
Ok::<WasmCompletionResult, JsValue>(WasmCompletionResult {
from: utf8_to_utf16_offset(&source, result.from)?,
items: result
.items
.into_iter()
.map(|item| WasmCompletionItem {
label: item.label,
kind: format!("{:?}", item.kind).to_lowercase(),
detail: item.detail,
priority: item.priority,
})
.collect(),
})
})
.transpose()?;
encode(result)
encode(complete_snapshot(
snapshot,
&entrypoint,
&source,
utf16_offset,
explicit,
)?)
})
}
fn complete_snapshot(
snapshot: &ConfigTreeSnapshot,
entrypoint: &VirtualPath,
source: &str,
utf16_offset: usize,
explicit: bool,
) -> Result<Option<WasmCompletionResult>, JsValue> {
let utf8_byte_offset = utf16_to_utf8_offset(source, utf16_offset)?;
let result = session_environment(snapshot.clone())
.complete_config(entrypoint, source, utf8_byte_offset, explicit)
.map_err(|error| JsValue::from_str(&format!("{error:?}")))?;
let result = result
.map(|result| {
Ok::<WasmCompletionResult, JsValue>(WasmCompletionResult {
from: utf8_to_utf16_offset(source, result.from)?,
items: result
.items
.into_iter()
.map(|item| WasmCompletionItem {
label: item.label,
kind: format!("{:?}", item.kind).to_lowercase(),
detail: item.detail,
priority: item.priority,
})
.collect(),
})
})
.transpose()?;
Ok(result)
}
#[wasm_bindgen]
pub fn evaluate_snapshot(snapshot: JsValue, contract: JsValue) -> Result<JsValue, JsValue> {
let snapshot: ConfigTreeSnapshot = decode(snapshot)?;
@@ -207,3 +223,98 @@ fn js_error(error: impl std::fmt::Display) -> JsValue {
#[allow(dead_code)]
fn _assert_serializable(_: EvaluationResult) {}
#[cfg(test)]
mod tests {
use super::*;
use config_source::{ConfigContentType, ConfigEntry};
fn snapshot() -> ConfigTreeSnapshot {
ConfigTreeSnapshot::from_entries(
7,
[
ConfigEntry::new(
VirtualPath::parse("main.dcdl").unwrap(),
ConfigContentType::Decodal,
"{}",
)
.unwrap(),
// A same-suffix workspace file must not be an unknown builtin fallback.
ConfigEntry::new(
VirtualPath::parse("profiles/missing.dcdl").unwrap(),
ConfigContentType::Decodal,
"{}",
)
.unwrap(),
],
)
.unwrap()
}
#[test]
fn wasm_completion_boundary_preserves_read_only_builtin_detail() {
let snapshot = snapshot();
let source = "let 名 = 1; import \"$builtin/profiles/comp\"";
let cursor = source.encode_utf16().count() - 1;
let result = complete_snapshot(
&snapshot,
&VirtualPath::parse("main.dcdl").unwrap(),
source,
cursor,
true,
)
.unwrap()
.unwrap();
assert_eq!(result.from, "let 名 = 1; import \"".encode_utf16().count());
let item = result
.items
.iter()
.find(|item| item.label == "$builtin/profiles/companion.dcdl")
.unwrap();
assert_eq!(item.kind, "file");
assert_eq!(
item.detail.as_deref(),
Some("read-only builtin Decodal source")
);
assert_eq!(snapshot.entries.len(), 2);
assert!(
snapshot
.entries
.keys()
.all(|path| !path.as_str().starts_with("$builtin/"))
);
}
#[test]
fn wasm_analysis_boundary_reports_unknown_builtin_without_workspace_fallback() {
let snapshot = snapshot();
let path = VirtualPath::parse("main.dcdl").unwrap();
let diagnostics = session_environment(snapshot.clone())
.analyze(&path, Some("import \"$builtin/profiles/missing.dcdl\""));
assert!(!diagnostics.is_empty());
let diagnostic = diagnostics
.iter()
.find(|diagnostic| {
diagnostic
.message
.contains("unknown or non-public read-only builtin source")
})
.unwrap();
assert_eq!(diagnostic.path, path);
assert_eq!(diagnostic.revision, 7);
assert_eq!(diagnostic.tree_digest, snapshot.digest);
assert!(
diagnostic
.message
.contains("$builtin/profiles/missing.dcdl")
);
assert!(
session_environment(snapshot)
.analyze(
&VirtualPath::parse("main.dcdl").unwrap(),
Some("import \"./profiles/missing.dcdl\"")
)
.is_empty()
);
}
}
+205 -3
View File
@@ -134,15 +134,31 @@ impl ProfileSourceArchive {
selector: String,
value: serde_json::Value,
) -> Result<Self, ProfileArchiveError> {
Self::build_evaluated_profile_with_builtin_sources(id, selector, value, BTreeMap::new())
}
/// Seal the observed builtin import closure as provenance alongside the evaluated JSON.
/// Builtin sources are verified, but never evaluated to resolve this entrypoint.
pub fn build_evaluated_profile_with_builtin_sources(
id: String,
selector: String,
value: serde_json::Value,
sources: BTreeMap<String, String>,
) -> Result<Self, ProfileArchiveError> {
for key in sources.keys() {
validate_builtin_source_key(key)?;
}
let path = "profiles/evaluated.json".to_string();
let source = serde_json::to_string(&value)
.map_err(|err| ProfileArchiveError::Json(err.to_string()))?;
let mut sources = sources;
sources.insert(path.clone(), source);
Self::build_with_source_format(
ProfileSourceArchiveInput {
id,
entrypoints: BTreeMap::from([(selector, path.clone())]),
entrypoints: BTreeMap::from([(selector, path)]),
imports: BTreeMap::new(),
sources: BTreeMap::from([(path, source)]),
sources,
},
"json",
"application/json",
@@ -171,6 +187,12 @@ impl ProfileSourceArchive {
if total_source_bytes > MAX_TOTAL_BYTES {
return Err(ProfileArchiveError::LimitExceeded("total source bytes"));
}
let (kind, content_type) = if path.starts_with("$builtin/") {
validate_builtin_source_key(path)?;
("decodal", "text/x-decodal")
} else {
(kind, content_type)
};
source_meta.push(ProfileSourceArchiveSource {
path: path.clone(),
source_key: path.clone(),
@@ -310,7 +332,18 @@ impl VerifiedProfileSourceArchive {
let mut manifest_paths = BTreeSet::new();
for source in &manifest.sources {
validate_archive_path(&source.path)?;
if !source.source_key.is_empty() && source.source_key != source.path {
if source.path.starts_with("$builtin/") {
validate_builtin_source_key(&source.path)?;
if source.kind != "decodal" {
return Err(ProfileArchiveError::UnsupportedSource {
path: source.path.clone(),
kind: source.kind.clone(),
});
}
}
if (source.path.starts_with("$builtin/") || !source.source_key.is_empty())
&& source.source_key != source.path
{
return Err(ProfileArchiveError::ImportMapMismatch {
specifier: source.path.clone(),
expected: source.path.clone(),
@@ -662,6 +695,26 @@ fn append_bytes<W: Write>(
.map_err(|err| ProfileArchiveError::Io(err.to_string()))
}
fn validate_builtin_source_key(key: &str) -> Result<(), ProfileArchiveError> {
validate_archive_path(key)?;
let Some(relative) = key.strip_prefix("$builtin/profiles/") else {
return Err(ProfileArchiveError::UnsafePath(key.to_string()));
};
if !relative.ends_with(".dcdl")
|| relative.split('/').any(|part| {
part.is_empty()
|| part == "."
|| part == ".."
|| !part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte))
})
{
return Err(ProfileArchiveError::UnsafePath(key.to_string()));
}
Ok(())
}
fn validate_archive_path(path: &str) -> Result<(), ProfileArchiveError> {
if path.is_empty() || path == MANIFEST_PATH {
return if path == MANIFEST_PATH {
@@ -799,6 +852,155 @@ mod tests {
archive.content = content;
}
fn builtin_provenance_archive() -> ProfileSourceArchive {
ProfileSourceArchive::build_evaluated_profile_with_builtin_sources(
"builtin-provenance".into(),
"project:evaluated".into(),
evaluated_profile(),
BTreeMap::from([
(
"$builtin/profiles/companion.dcdl".into(),
"import \"./base.dcdl\"".into(),
),
// Intentionally invalid Decodal: provenance must not be reevaluated.
(
"$builtin/profiles/base.dcdl".into(),
"not valid Decodal !".into(),
),
]),
)
.unwrap()
}
#[test]
fn evaluated_json_seals_builtin_closure_without_runtime_evaluation() {
let archive = builtin_provenance_archive();
let verified = archive.verify().unwrap();
assert_eq!(
archive,
builtin_provenance_archive(),
"archive must be deterministic"
);
assert_eq!(archive.reference.source_graph.source_count, 3);
assert!(verified.manifest.imports.is_empty());
assert_eq!(
verified.manifest.entrypoints["project:evaluated"],
"profiles/evaluated.json"
);
for source in &verified.manifest.sources {
assert_eq!(source.source_key, source.path);
assert_eq!(
source.digest,
sha256_hex(verified.sources[&source.path].as_bytes())
);
if source.path.starts_with("$builtin/") {
assert_eq!(source.kind, "decodal");
assert_eq!(source.content_type, "text/x-decodal");
} else {
assert_eq!(source.kind, "json");
}
}
let root = tempfile::tempdir().unwrap();
let resolved = verified
.resolve_profile("project:evaluated", root.path(), "sealed-worker")
.unwrap();
assert_eq!(resolved.worker.mode, manifest::WorkerMode::Wip);
assert!(resolved.feature.task.enabled);
assert!(matches!(
verified.resolve_profile("builtin:companion", root.path(), "sealed-worker"),
Err(ProfileArchiveError::MissingEntrypoint(_))
));
}
#[test]
fn evaluated_json_rejects_missing_or_corrupt_builtin_dependency() {
for missing in [true, false] {
let mut archive = builtin_provenance_archive();
let mut verified = archive.verify().unwrap();
let path = "$builtin/profiles/base.dcdl";
if missing {
verified.sources.remove(path);
} else {
verified
.sources
.insert(path.into(), "corrupt dependency".into());
}
repack_archive(&mut archive, &verified.manifest, &verified.sources);
match archive.verify().unwrap_err() {
ProfileArchiveError::MissingSource(actual) if missing => assert_eq!(actual, path),
ProfileArchiveError::SourceDigestMismatch { path: actual, .. } if !missing => {
assert_eq!(actual, path)
}
other => panic!("unexpected error: {other}"),
}
}
}
#[test]
fn evaluated_json_builtin_provenance_rejects_unsafe_keys_and_reuses_limits() {
for key in [
"profiles/companion.dcdl",
"builtin:profiles/companion.dcdl",
"$builtin/prompts/companion.dcdl",
"$builtin/profiles/../escape.dcdl",
"$builtin/profiles/./base.dcdl",
"$builtin/profiles//base.dcdl",
"$builtin/profiles/base\\other.dcdl",
"$builtin/profiles/base\0.dcdl",
"/$builtin/profiles/base.dcdl",
"$builtin/profiles/base.json",
"profiles/evaluated.json",
] {
assert!(
matches!(
ProfileSourceArchive::build_evaluated_profile_with_builtin_sources(
"unsafe".into(),
"project:evaluated".into(),
evaluated_profile(),
BTreeMap::from([(key.into(), "{}".into())]),
),
Err(ProfileArchiveError::UnsafePath(_))
),
"{key:?}"
);
}
assert!(matches!(
ProfileSourceArchive::build_evaluated_profile_with_builtin_sources(
"oversized".into(),
"project:evaluated".into(),
evaluated_profile(),
BTreeMap::from([(
"$builtin/profiles/base.dcdl".into(),
"x".repeat(MAX_SOURCE_BYTES as usize + 1)
)]),
),
Err(ProfileArchiveError::LimitExceeded("source bytes"))
));
}
#[test]
fn evaluated_json_builtin_provenance_requires_canonical_key_and_source_format() {
for key in ["", "profiles/base.dcdl", "$builtin/profiles/other.dcdl"] {
let mut archive = builtin_provenance_archive();
let mut verified = archive.verify().unwrap();
verified.manifest.sources[0].source_key = key.into();
repack_archive(&mut archive, &verified.manifest, &verified.sources);
assert!(matches!(
archive.verify(),
Err(ProfileArchiveError::ImportMapMismatch { .. })
));
}
let mut archive = builtin_provenance_archive();
let mut verified = archive.verify().unwrap();
verified.manifest.sources[0].kind = "json".into();
verified.manifest.sources[0].content_type = "application/json".into();
repack_archive(&mut archive, &verified.manifest, &verified.sources);
assert!(matches!(
archive.verify(),
Err(ProfileArchiveError::UnsupportedSource { .. })
));
}
#[test]
fn evaluated_json_reuses_source_size_limit() {
assert!(matches!(
@@ -464,10 +464,20 @@ fn build_virtual_profile_archive(
entry: &VirtualProfileEntry,
state: &WorkspaceConfigState,
) -> Result<ProfileSourceArchive> {
ProfileSourceArchive::build_evaluated_profile(
let builtin_sources = config_source::SnapshotEnvironment::new(state.snapshot.clone())
.builtin_import_sources(&state.contract)
.map_err(|diagnostics| {
profile_validation_error(
"profile_builtin_snapshot_invalid",
&serde_json::to_string(&diagnostics)
.unwrap_or_else(|_| "builtin import snapshot failed".to_string()),
)
})?;
ProfileSourceArchive::build_evaluated_profile_with_builtin_sources(
format!("workspace-config-profile-r{}", state.snapshot.revision),
selector.to_string(),
entry.profile.clone(),
builtin_sources,
)
.map_err(|error| profile_validation_error("profile_value_archive_invalid", &error.to_string()))
}
@@ -44,6 +44,287 @@ fn source(value: &str) -> String {
)
}
#[tokio::test]
async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let token = seed_test_api_token(api.store.as_ref(), TEST_WORKSPACE_ID);
let app = build_router(api.clone());
let tree = format!("/api/w/{TEST_WORKSPACE_ID}/config/source-tree");
let commit = format!("{tree}/commit");
let initial = get_json_authenticated(app.clone(), &tree, &token).await;
let saved = request_json_authenticated(
app.clone(),
"POST",
&commit,
Some(
serde_json::to_value(request(
&api,
&source(r#"import "$builtin/profiles/companion.dcdl""#),
vec![config_source::ConfigTreeChange::Create {
// An ordinary Workspace file at the matching suffix must not rescue
// an unknown builtin import.
path: config_source::VirtualPath::parse("profiles/private.dcdl").unwrap(),
content_type: config_source::ConfigContentType::Decodal,
content: PROFILE.into(),
}],
))
.unwrap(),
),
&token,
StatusCode::CREATED,
)
.await;
assert_eq!(
saved["snapshot"]["revision"].as_u64().unwrap(),
initial["snapshot"]["revision"].as_u64().unwrap() + 1
);
let listed = get_json_authenticated(app.clone(), &tree, &token).await;
assert_eq!(listed, saved);
let entries = listed["snapshot"]["entries"].as_object().unwrap();
assert!(entries.contains_key("profiles/private.dcdl"));
assert!(
entries
.keys()
.all(|key| key != "$builtin" && !key.starts_with("$builtin/"))
);
let shadow = get_json_authenticated(
app.clone(),
&format!("{tree}/entries/profiles%2Fprivate.dcdl"),
&token,
)
.await;
assert_eq!(shadow, entries["profiles/private.dcdl"]);
assert!(
shadow["content"]
.as_str()
.unwrap()
.contains("slug = \"alpha\"")
);
let active = api
.config_store
.load_workspace_config(TEST_WORKSPACE_ID)
.unwrap()
.unwrap();
let main_digest = entries["main.dcdl"]["content_digest"].as_str().unwrap();
let base = json!({
"base_revision": listed["snapshot"]["revision"],
"base_digest": listed["snapshot"]["digest"],
"entrypoints": listed["contract"]["entrypoints"],
});
let reserved = "$builtin/profiles/companion.dcdl";
for change in [
json!({ "kind": "create", "path": reserved, "content_type": "decodal", "content": PROFILE }),
json!({ "kind": "create", "path": "$builtin", "content_type": "text", "content": "shadow" }),
json!({ "kind": "update", "path": reserved, "expected_digest": "not-a-workspace-entry", "content": PROFILE }),
json!({ "kind": "delete", "path": reserved, "expected_digest": "not-a-workspace-entry" }),
json!({ "kind": "rename", "from": "profiles/private.dcdl", "to": reserved, "expected_digest": shadow["content_digest"] }),
json!({ "kind": "rename", "from": reserved, "to": "profiles/copy.dcdl", "expected_digest": "not-a-workspace-entry" }),
] {
let mut body = base.clone();
body["changes"] = json!([change]);
let error = request_json_authenticated(
app.clone(),
"POST",
&commit,
Some(body),
&token,
StatusCode::BAD_REQUEST,
)
.await;
assert!(
error["message"]
.as_str()
.unwrap()
.contains("builtin namespace is read-only"),
"{error}"
);
assert_eq!(
get_json_authenticated(app.clone(), &tree, &token).await,
listed
);
}
let mut body = base;
body["changes"] = json!([{
"kind": "update", "path": "main.dcdl", "expected_digest": main_digest,
"content": source(r#"import "$builtin/profiles/private.dcdl""#),
}]);
let error = request_json_authenticated(
app.clone(),
"POST",
&commit,
Some(body),
&token,
StatusCode::BAD_REQUEST,
)
.await;
let message = error["message"].as_str().unwrap();
assert!(
message.contains(
"unknown or non-public read-only builtin source: $builtin/profiles/private.dcdl"
),
"{error}"
);
assert!(message.contains("\"kind\":\"import\""), "{error}");
assert_eq!(get_json_authenticated(app, &tree, &token).await, listed);
assert_eq!(
api.config_store
.load_workspace_config(TEST_WORKSPACE_ID)
.unwrap()
.unwrap(),
active
);
}
#[tokio::test]
async fn value_profiles_builtin_companion_composition_seals_observed_sources() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let recipe = r#"(import "$builtin/profiles/companion.dcdl") // {
worker = { mode = "wip"; };
feature = {
task = { enabled = true; };
ticket = { enabled = false; };
workspace_config = { enabled = true; };
};
}"#;
let state = commit_workspace_config_tree(
&api,
TEST_WORKSPACE_ID,
&request(
&api,
&source(r#"import "./recipes/custom.dcdl""#),
vec![
config_source::ConfigTreeChange::Create {
path: config_source::VirtualPath::parse("recipes/custom.dcdl").unwrap(),
content_type: config_source::ConfigContentType::Decodal,
content: recipe.into(),
},
// Builtin relative imports must not be shadowed by Workspace files.
config_source::ConfigTreeChange::Create {
path: config_source::VirtualPath::parse("profiles/base.dcdl").unwrap(),
content_type: config_source::ConfigContentType::Decodal,
content: "{ slug = \"workspace-shadow\"; }".into(),
},
],
),
)
.unwrap();
let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap();
let bundle = build_virtual_profile_config_bundle(
&projection,
&state,
TEST_WORKSPACE_ID,
"created",
"project:alpha",
)
.unwrap()
.unwrap();
assert_eq!(bundle.metadata.digest, bundle.computed_digest());
let archive = bundle.profile_source_archive.as_ref().unwrap();
let verified = archive.verify().unwrap();
let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone());
let builtin_sources = environment.builtin_import_sources(&state.contract).unwrap();
assert_eq!(
builtin_sources
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
vec![
"$builtin/profiles/base.dcdl",
"$builtin/profiles/companion.dcdl",
]
);
assert_eq!(
archive.reference.source_graph.source_count,
builtin_sources.len() + 1
);
assert_eq!(archive.reference.source_graph.import_count, 0);
assert_eq!(
verified.manifest().entrypoints["project:alpha"],
"profiles/evaluated.json"
);
for (key, bytes) in &builtin_sources {
let source = verified
.manifest()
.sources
.iter()
.find(|source| &source.source_key == key)
.unwrap();
assert_eq!(source.path, *key);
assert_eq!(source.kind, "decodal");
assert_eq!(
source.digest,
worker_runtime::profile_archive::sha256_hex(bytes.as_bytes())
);
assert_eq!(source.size_bytes, bytes.len() as u64);
}
assert!(
verified
.manifest()
.sources
.iter()
.any(|source| source.path == "profiles/evaluated.json" && source.kind == "json")
);
// Resolve using the Runtime archive consumer, without access to the config tree.
let resolved = verified
.resolve_profile(
"project:alpha",
Path::new("/runtime-worker"),
"builtin-value-worker",
)
.unwrap();
assert_eq!(resolved.worker.mode, manifest::WorkerMode::Wip);
assert!(resolved.feature.task.enabled);
assert!(!resolved.feature.ticket.enabled);
assert!(resolved.feature.workspace_config.enabled);
let evaluation = environment.evaluate_contract(&state.contract).unwrap();
assert_eq!(
resolved.model.ref_.as_deref(),
evaluation.projections[0]
.data_json
.pointer("/profile/entries/0/profile/model/ref")
.and_then(serde_json::Value::as_str)
);
let persisted = manifest::write_persisted_worker_manifest_snapshot(&resolved).unwrap();
let old_bundle_digest = bundle.metadata.digest.clone();
let updated = commit_workspace_config_tree(
&api,
TEST_WORKSPACE_ID,
&request(&api, &source(PROFILE), vec![]),
)
.unwrap();
let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &updated).unwrap();
let updated_bundle = build_virtual_profile_config_bundle(
&projection,
&updated,
TEST_WORKSPACE_ID,
"created",
"project:alpha",
)
.unwrap()
.unwrap();
assert_ne!(old_bundle_digest, updated_bundle.metadata.digest);
assert_eq!(
updated_bundle
.profile_source_archive
.as_ref()
.unwrap()
.reference
.source_graph
.source_count,
1
);
// Editing config changes future snapshots, not a sealed bundle or persisted Worker.
assert_eq!(bundle.metadata.digest, old_bundle_digest);
assert_eq!(
serde_json::to_value(manifest::read_persisted_worker_manifest_snapshot(persisted).unwrap())
.unwrap(),
serde_json::to_value(resolved).unwrap()
);
}
#[tokio::test]
async fn value_profiles_save_project_and_runtime_consume_the_same_revision() {
let dir = tempfile::tempdir().unwrap();