feat: add explicit Worker WIP mode
This commit is contained in:
Generated
+217
-3
@@ -88,6 +88,20 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -438,7 +452,16 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -447,6 +470,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -496,6 +525,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
@@ -1249,6 +1284,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -1330,10 +1374,21 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-set 0.5.3",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1418,6 +1473,17 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
|
||||
dependencies = [
|
||||
"borrow-or-share",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1460,6 +1526,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fraction"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-operation"
|
||||
version = "0.1.0"
|
||||
@@ -2278,6 +2354,32 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.22.1",
|
||||
"bytecount",
|
||||
"email_address",
|
||||
"fancy-regex 0.16.2",
|
||||
"fraction",
|
||||
"idna",
|
||||
"itoa",
|
||||
"num-cmp",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"referencing",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid-simd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kasuari"
|
||||
version = "0.4.12"
|
||||
@@ -2730,6 +2832,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
@@ -2756,6 +2872,21 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-cmp"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
@@ -2792,6 +2923,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -2925,6 +3067,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -3628,6 +3776,20 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"fluent-uri",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -4674,7 +4836,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.11.0",
|
||||
"fancy-regex",
|
||||
"fancy-regex 0.11.0",
|
||||
"filedescriptor",
|
||||
"finl_unicode",
|
||||
"fixedbitset",
|
||||
@@ -5409,6 +5571,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-simd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"uuid",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -5427,6 +5600,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vtparse"
|
||||
version = "0.6.2"
|
||||
@@ -6063,6 +6242,36 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wip-client"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73a82416411af69b7eb4ad0ed9a954df062769d3a03b3a1c59b0b4bf617f3534"
|
||||
dependencies = [
|
||||
"wip-http",
|
||||
"wip-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wip-http"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec0d32d1e56e52fb9ce925db808354a3432b3ebbd112fab8c443c9b865c58c6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"wip-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wip-protocol"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "275ffaebf544a077de7578fd3674c062a098a72c5a904cf66e934f76bd9efbab"
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
version = "0.6.5"
|
||||
@@ -6210,7 +6419,9 @@ dependencies = [
|
||||
"fs4",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"http",
|
||||
"include_dir",
|
||||
"jsonschema",
|
||||
"libc",
|
||||
"manifest",
|
||||
"mcp",
|
||||
@@ -6237,6 +6448,9 @@ dependencies = [
|
||||
"tracing",
|
||||
"tungstenite 0.28.0",
|
||||
"uuid",
|
||||
"wip-client",
|
||||
"wip-http",
|
||||
"wip-protocol",
|
||||
"workdir",
|
||||
]
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ default-members = [
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.88"
|
||||
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
@@ -124,6 +125,7 @@ decodal-language-service = "0.4.0"
|
||||
decodal-language-tools = "0.4.0"
|
||||
fs4 = "0.13"
|
||||
futures = "0.3"
|
||||
http = "1"
|
||||
libc = "0.2"
|
||||
schemars = "1.2"
|
||||
serde = "1.0"
|
||||
@@ -144,5 +146,8 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
url = "2.5"
|
||||
uuid = "1.23"
|
||||
wip-client = "0.1.0"
|
||||
wip-http = "0.1.0"
|
||||
wip-protocol = "0.1.0"
|
||||
zeroize = "1"
|
||||
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "manifest"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap = "1"
|
||||
|
||||
@@ -590,6 +590,8 @@ impl From<FeatureConfig> for FeatureConfigPartial {
|
||||
pub struct WorkerMetaConfig {
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mode: Option<crate::WorkerMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
@@ -925,6 +927,7 @@ impl WorkerMetaConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
name: upper.name.or(self.name),
|
||||
mode: upper.mode.or(self.mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1356,7 +1359,10 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
|
||||
validate_mcp_config(&cfg.mcp)?;
|
||||
|
||||
Ok(WorkerManifest {
|
||||
worker: WorkerMeta { name },
|
||||
worker: WorkerMeta {
|
||||
name,
|
||||
mode: cfg.worker.mode.unwrap_or_default(),
|
||||
},
|
||||
model: cfg.model,
|
||||
engine,
|
||||
scope: cfg.scope,
|
||||
@@ -1391,6 +1397,7 @@ mod tests {
|
||||
WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("test".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
@@ -1498,10 +1505,31 @@ mod tests {
|
||||
fn resolve_minimal_succeeds() {
|
||||
let manifest: WorkerManifest = minimal_valid().try_into().unwrap();
|
||||
assert_eq!(manifest.worker.name, "test");
|
||||
assert_eq!(manifest.worker.mode, crate::WorkerMode::Tools);
|
||||
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
|
||||
assert!(manifest.permissions.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_mode_is_explicit_opt_in_and_persists_in_snapshots() {
|
||||
let mut configured = minimal_valid();
|
||||
configured.worker.mode = Some(crate::WorkerMode::Wip);
|
||||
let manifest: WorkerManifest = configured.try_into().unwrap();
|
||||
assert_eq!(manifest.worker.mode, crate::WorkerMode::Wip);
|
||||
|
||||
let snapshot = crate::write_persisted_worker_manifest_snapshot(&manifest).unwrap();
|
||||
let restored = crate::read_persisted_worker_manifest_snapshot(snapshot.clone()).unwrap();
|
||||
assert_eq!(restored.worker.mode, crate::WorkerMode::Wip);
|
||||
|
||||
let mut legacy = snapshot;
|
||||
legacy["manifest"]["worker"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("mode");
|
||||
let restored = crate::read_persisted_worker_manifest_snapshot(legacy).unwrap();
|
||||
assert_eq!(restored.worker.mode, crate::WorkerMode::Tools);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_mcp_stdio_config_preserves_explicit_policy() {
|
||||
let mut cfg = minimal_valid();
|
||||
@@ -1784,6 +1812,7 @@ mod tests {
|
||||
let lower = WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("lower".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
model_id: Some("lower-model".into()),
|
||||
@@ -1794,6 +1823,7 @@ mod tests {
|
||||
let upper = WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("upper".into()),
|
||||
mode: None,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2248,6 +2278,7 @@ enabled = false
|
||||
.merge(WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("feature-test".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
@@ -2351,6 +2382,7 @@ enabled = true
|
||||
.merge(WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("feature-merge-test".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
@@ -2428,6 +2460,7 @@ permission = "write"
|
||||
let overlay = WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("x".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
|
||||
@@ -670,6 +670,21 @@ pub struct WorkspaceMemorySettingsSnapshot {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkerMeta {
|
||||
pub name: String,
|
||||
/// Model-facing capability surface. Existing manifests default to ordinary tools.
|
||||
#[serde(default)]
|
||||
pub mode: WorkerMode,
|
||||
}
|
||||
|
||||
/// Model-facing Worker capability surface.
|
||||
///
|
||||
/// WIP is explicit opt-in: it replaces individual model-visible tools with the
|
||||
/// discover/inspect/call Worldspace client while retaining the same host authority.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkerMode {
|
||||
#[default]
|
||||
Tools,
|
||||
Wip,
|
||||
}
|
||||
|
||||
/// When the engine may begin executing provider-confirmed tool calls.
|
||||
|
||||
@@ -626,6 +626,7 @@ fn resolve_profile_value(
|
||||
let config = WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some(worker_name),
|
||||
mode: profile.worker.and_then(|worker| worker.mode),
|
||||
},
|
||||
model: profile.model.unwrap_or_default(),
|
||||
engine: profile.engine.unwrap_or_default(),
|
||||
@@ -668,6 +669,8 @@ struct ProfileConfig {
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
worker: Option<ProfileWorkerConfig>,
|
||||
#[serde(default)]
|
||||
model: Option<ModelManifest>,
|
||||
#[serde(default)]
|
||||
engine: Option<EngineManifestConfig>,
|
||||
@@ -691,6 +694,13 @@ struct ProfileConfig {
|
||||
skills: Option<SkillsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ProfileWorkerConfig {
|
||||
#[serde(default)]
|
||||
mode: Option<crate::WorkerMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ProfileScopeConfig {
|
||||
@@ -864,8 +874,15 @@ fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), Profi
|
||||
)));
|
||||
}
|
||||
}
|
||||
if map.contains_key("worker") {
|
||||
return Err(ProfileError::InvalidProfile("field `worker` is runtime-bound and is not allowed in reusable Profiles; pass the Worker name via CLI/TUI runtime inputs".into()));
|
||||
if let Some(worker) = map.get("worker") {
|
||||
let Some(worker) = worker.as_object() else {
|
||||
return Err(ProfileError::InvalidProfile(
|
||||
"field `worker` must be an object containing only reusable `mode` policy".into(),
|
||||
));
|
||||
};
|
||||
if worker.contains_key("name") {
|
||||
return Err(ProfileError::InvalidProfile("field `worker.name` is runtime-bound and is not allowed in reusable Profiles; pass the Worker name via CLI/TUI runtime inputs".into()));
|
||||
}
|
||||
}
|
||||
if let Some(scope) = map.get("scope").and_then(|v| v.as_object()) {
|
||||
for key in ["allow", "deny"] {
|
||||
@@ -1691,6 +1708,7 @@ mod tests {
|
||||
r#"
|
||||
slug = "coder"
|
||||
scope = "workspace_read"
|
||||
worker = { mode = "wip" }
|
||||
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
@@ -1710,6 +1728,7 @@ reasoning = "high"
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved.manifest.worker.name, "runtime-worker");
|
||||
assert_eq!(resolved.manifest.worker.mode, crate::WorkerMode::Wip);
|
||||
assert_eq!(resolved.manifest.model.scheme, Some(SchemeKind::Anthropic));
|
||||
assert_eq!(
|
||||
resolved.manifest.engine.reasoning,
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "worker"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
autobins = false
|
||||
|
||||
[features]
|
||||
@@ -34,6 +35,7 @@ chrono = "0.4"
|
||||
config-source = { path = "../config-source" }
|
||||
include_dir = "0.7.4"
|
||||
fs4 = { workspace = true, features = ["sync"] }
|
||||
http = { workspace = true }
|
||||
flow = { path = "../flow" }
|
||||
fs-operation = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
@@ -46,6 +48,10 @@ arc-swap = "1.9.1"
|
||||
tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] }
|
||||
tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] }
|
||||
futures-util = { version = "0.3", features = ["sink"] }
|
||||
jsonschema = { version = "0.33.0", default-features = false }
|
||||
wip-client = { workspace = true }
|
||||
wip-http = { workspace = true }
|
||||
wip-protocol = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dotenv = "0.15.0"
|
||||
|
||||
@@ -1631,6 +1631,23 @@ where
|
||||
if let Some((resident_summary, system_prompt_override)) = feature_prompt_contribution {
|
||||
worker.install_system_prompt_contribution(resident_summary, system_prompt_override);
|
||||
}
|
||||
if worker.manifest().worker.mode == manifest::WorkerMode::Wip {
|
||||
let permissions = worker.manifest().permissions.clone();
|
||||
let security_context = format!(
|
||||
"workspace={};worker={};session={}",
|
||||
worker.workspace_id().map_or("standalone", |id| id.as_str()),
|
||||
worker.manifest().worker.name,
|
||||
worker.session_id()
|
||||
);
|
||||
crate::wip::install_wip_mode(worker.engine_mut(), permissions, security_context).map_err(
|
||||
|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("install WIP Worker surface: {error}"),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
}
|
||||
worker.attach_tracker(tracker);
|
||||
Ok(workdir_for_view)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,12 @@ where
|
||||
authority,
|
||||
} = spec;
|
||||
|
||||
// Feature-owned private Internal Workers use their explicitly supplied tool
|
||||
// composition. They are not model-facing direct SubWorkers and must not
|
||||
// inherit a parent's WIP surface (which is finalized only by the normal
|
||||
// controller path); keeping Tool mode also preserves manifest permission
|
||||
// hooks for these private jobs.
|
||||
manifest.worker.mode = manifest::WorkerMode::Tools;
|
||||
// Internal identities are run-scoped and never enter the public Runtime Worker catalog.
|
||||
manifest.worker.name = format!("internal-{}-{}", identity.kind, identity.run_id);
|
||||
// Internal jobs only receive the explicitly supplied Feature set below. A
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod shared_state;
|
||||
mod shutdown_after_idle;
|
||||
pub mod skill;
|
||||
pub mod spawn;
|
||||
pub mod wip;
|
||||
|
||||
mod internal_worker;
|
||||
mod interrupt_prep;
|
||||
|
||||
@@ -21,16 +21,7 @@ impl PermissionHook {
|
||||
}
|
||||
|
||||
fn action_for(&self, input: &ToolCallSummary) -> ToolPermissionAction {
|
||||
let target = permission_target(&input.arguments);
|
||||
self.config
|
||||
.rules
|
||||
.iter()
|
||||
.find(|rule| {
|
||||
rule.tool.eq_ignore_ascii_case(&input.tool_name)
|
||||
&& wildcard_match(&rule.pattern, &target)
|
||||
})
|
||||
.map(|rule| rule.action)
|
||||
.unwrap_or(self.config.default_action)
|
||||
permission_action_for(&self.config, &input.tool_name, &input.arguments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +30,9 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
|
||||
let Some(permissions) = self.manifest().permissions.clone() else {
|
||||
return;
|
||||
};
|
||||
self.add_pre_tool_call_hook(PermissionHook::new(permissions));
|
||||
if self.manifest().worker.mode == manifest::WorkerMode::Tools {
|
||||
self.add_pre_tool_call_hook(PermissionHook::new(permissions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +52,22 @@ impl Hook<PreToolCall> for PermissionHook {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn permission_action_for(
|
||||
config: &ToolPermissionConfig,
|
||||
tool_name: &str,
|
||||
arguments: &Value,
|
||||
) -> ToolPermissionAction {
|
||||
let target = permission_target(arguments);
|
||||
config
|
||||
.rules
|
||||
.iter()
|
||||
.find(|rule| {
|
||||
rule.tool.eq_ignore_ascii_case(tool_name) && wildcard_match(&rule.pattern, &target)
|
||||
})
|
||||
.map(|rule| rule.action)
|
||||
.unwrap_or(config.default_action)
|
||||
}
|
||||
|
||||
fn permission_denied_message(input: &ToolCallSummary) -> String {
|
||||
format!(
|
||||
"permission denied: tool `{}` arguments matched the manifest permission policy",
|
||||
|
||||
@@ -883,6 +883,7 @@ fn build_spawn_config_json(
|
||||
let config = WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some(name.to_string()),
|
||||
mode: None,
|
||||
},
|
||||
model: model.clone(),
|
||||
engine: EngineManifestConfig {
|
||||
@@ -957,6 +958,7 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi
|
||||
WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some(manifest.worker.name.clone()),
|
||||
mode: Some(manifest.worker.mode),
|
||||
},
|
||||
model: manifest.model.clone(),
|
||||
engine: EngineManifestConfig {
|
||||
@@ -1871,6 +1873,7 @@ enabled = false
|
||||
WorkerManifestConfig {
|
||||
worker: WorkerMetaConfig {
|
||||
name: Some("parent".into()),
|
||||
mode: None,
|
||||
},
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
@@ -2188,7 +2191,8 @@ max_tokens = 3333
|
||||
let delegated = tmp.path().join("delegated");
|
||||
std::fs::create_dir_all(&parent_deny).unwrap();
|
||||
std::fs::create_dir_all(&delegated).unwrap();
|
||||
let parent = parent_manifest(&parent_root, Some(&parent_deny));
|
||||
let mut parent = parent_manifest(&parent_root, Some(&parent_deny));
|
||||
parent.worker.mode = manifest::WorkerMode::Wip;
|
||||
let scope = vec![abs_rule(&delegated, Permission::Read)];
|
||||
let available = AvailableProfiles {
|
||||
registry: None,
|
||||
@@ -2206,6 +2210,7 @@ max_tokens = 3333
|
||||
);
|
||||
|
||||
assert_eq!(config.worker.name.as_deref(), Some("inherited-child"));
|
||||
assert_eq!(config.worker.mode, Some(manifest::WorkerMode::Wip));
|
||||
assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
|
||||
assert_eq!(config.engine.instruction.as_deref(), Some("default"));
|
||||
assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ It is not a dumping ground for external research, old plans, API inventories, or
|
||||
25. [`design/subjektiv-store.md`](design/subjektiv-store.md) — subject-scoped Memoryのownership、authorization、Session attribution、revision、candidate consolidation境界。
|
||||
26. [`design/subjektiv-surface.md`](design/subjektiv-surface.md) — confirmed Memoryから生成するresident surface、availability、append-only restore injection。
|
||||
27. [`development/subjektiv-product-cutover.md`](development/subjektiv-product-cutover.md) — legacy Workspace Memoryからsubjektivへのoperator cutover、explicit reset、backup/rollback runbook。
|
||||
28. [`design/worker-wip-mode.md`](design/worker-wip-mode.md) — explicit-opt-in Worker WIP mode, Worldspace projection, authority, cache, and retry contracts.
|
||||
|
||||
## What belongs here
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Worker WIP mode
|
||||
|
||||
## Status and dependencies
|
||||
|
||||
Workers support two explicit model-facing modes:
|
||||
|
||||
```toml
|
||||
[worker]
|
||||
name = "example"
|
||||
mode = "tools" # default
|
||||
```
|
||||
|
||||
```toml
|
||||
[worker]
|
||||
name = "example"
|
||||
mode = "wip"
|
||||
```
|
||||
|
||||
Profiles select the same policy without owning the runtime Worker name:
|
||||
|
||||
```toml
|
||||
worker = { mode = "wip" }
|
||||
```
|
||||
|
||||
Omission resolves to `tools`, so existing manifests, profiles, persisted snapshots, and restored Workers retain the ordinary individual-tool surface. A direct Internal SubWorker selected with `profile = "inherit"` inherits the parent mode as reusable behavior. `default` and named profile selections use that profile's own mode and do not implicitly inherit WIP.
|
||||
|
||||
The implementation uses the crates.io `wip-client`, `wip-protocol`, and `wip-http` **0.1.0** releases. It does not use Git/path dependencies or the old `wip-https` name. Those releases require Rust 1.88; the workspace declares `rust-version = "1.88"`, and the changed crates inherit it. WIP Host/interface crates are not needed for the async compatibility adapter: the published client/protocol/HTTP state machines remain authoritative while Yoi supplies an in-process async Host transport.
|
||||
|
||||
## LLM surface and route contract
|
||||
|
||||
A WIP Worker exposes exactly these generic LLM tools instead of exposing every enabled ordinary tool twice:
|
||||
|
||||
- `WipDiscover`: observe an object path and bounded descendants, beginning at `/`.
|
||||
- `WipInspect`: fetch the descriptor for an opaque interface reference returned by discovery.
|
||||
- `WipCall`: call an operation using fresh object and interface observations.
|
||||
|
||||
The compatibility projection mounts enabled ordinary tools at:
|
||||
|
||||
```text
|
||||
/tools/<exact-tool-registration-name>
|
||||
```
|
||||
|
||||
Each object has one opaque interface reference:
|
||||
|
||||
```text
|
||||
yoi.tool/<exact-tool-registration-name>/v1
|
||||
```
|
||||
|
||||
Its single operation is `call`. `WipCall.arguments` is the original ordinary tool argument object. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them.
|
||||
|
||||
The interface descriptor uses WIP `Json` for compatibility input and includes the exact original JSON Schema in descriptor documentation. Before execution, the Host validates the JSON value with that original schema and then delegates to the original async `Tool`. Constraints are therefore neither approximated nor silently dropped. A schema that cannot be compiled prevents WIP startup instead of creating a weaker projection.
|
||||
|
||||
## Native projection extension and collisions
|
||||
|
||||
`worker::wip::WipMountRegistry` is the Host-owned mount allocator. A Feature can independently construct a `WipProjection` with an async `WipOperationHandler`, then pass its aggregate registry through `install_wip_mode_with_mounts` before compatibility finalization.
|
||||
|
||||
- Routes must be canonical absolute WIP paths.
|
||||
- Object names must match the final route segment.
|
||||
- A projected object exposes exactly its declared interface.
|
||||
- Unrelated duplicate routes and conflicting interface descriptors fail startup.
|
||||
- A native projection replaces a compatibility projection only when both declare the same semantic capability (`tool:<registration-name>` for compatibility tools).
|
||||
- When native is already selected, the compatibility tool is hidden rather than exposed through a second entry.
|
||||
|
||||
The unit sample `mount_collision_and_native_replacement_are_explicit` demonstrates independent native registration, deterministic replacement, and collision rejection.
|
||||
|
||||
## Authority and execution
|
||||
|
||||
Discovery is not authority. The Worldspace contains only the tools already registered from enabled Features and the Worker's current execution context. Calling a compatibility operation:
|
||||
|
||||
1. resolves the exact mounted route;
|
||||
2. verifies object and interface membership and validators;
|
||||
3. validates the WIP descriptor call;
|
||||
4. validates the original JSON Schema;
|
||||
5. evaluates manifest tool permission rules against the **original tool name and original arguments**;
|
||||
6. invokes the original async `Tool` without blocking the Tokio runtime.
|
||||
|
||||
Filesystem scope, provider Workdir capability checks, Backend Workspace authority, Feature enablement, and input-specific restrictions stay in the original tool implementation and are checked again at invocation. `ask` permissions remain denied fail-closed because the runtime has no approval protocol. The gateway does not mint authority or expose authentication material.
|
||||
|
||||
Pre/post Engine history still records one bounded `WipCall` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification.
|
||||
|
||||
## Client lifetime, restoration, and compaction
|
||||
|
||||
The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions are keyed by the canonical endpoint and an opaque security-context identity derived by the Host from Workspace, Worker, and session identity. Authentication credentials are not included in that identity and are never emitted to descriptors, tool output, or history.
|
||||
|
||||
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `WipDiscover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
|
||||
|
||||
## Errors, cancellation, and retries
|
||||
|
||||
The adapter keeps these outcomes distinct:
|
||||
|
||||
- **rejected/not dispatched**: route, membership, validator, descriptor, schema, or permission failure; the original operation does not run;
|
||||
- **stale**: object/interface validator mismatch; refresh before reconstructing a call;
|
||||
- **cancelled**: the original provider confirms cooperative cancellation and its bounded cancelled output is retained;
|
||||
- **interrupted**: provider interruption is retained separately from cancellation;
|
||||
- **disconnected/timeout after dispatch**: `wip-client` records an unknown outcome;
|
||||
- **operation outcome unknown**: an executing provider fails without a rollback/non-execution guarantee, a response cannot be completed after dispatch, or the call future is force-closed.
|
||||
|
||||
An unknown mutating outcome must never be retried automatically. The model must inspect external state or ask for operator guidance. Retrieval failures may be explicitly refreshed.
|
||||
|
||||
## Measurement
|
||||
|
||||
The runtime tracks bounded counters for:
|
||||
|
||||
- ordinary individual-tool schema bytes hidden by WIP mode;
|
||||
- the three WIP gateway schema bytes;
|
||||
- discover, descriptor-inspection, and operation round trips;
|
||||
- successful operations.
|
||||
|
||||
These values are returned with discovery/inspection output and are intended for comparing prompt input volume, exploration round trips, and representative operation success with ordinary Tool mode. No fixed reduction percentage is promised.
|
||||
Reference in New Issue
Block a user