Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ libc = "0.2"
sha2 = "0.10"
# The hub's per-launch connect secrets (hub.rs): v4 UUIDs from the OS CSPRNG.
uuid = { workspace = true }
base64 = "0.22"
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
Expand Down
30 changes: 23 additions & 7 deletions crates/shell/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,23 +252,39 @@ pub fn register_mail_services() {
});
}

#[cfg(any(feature = "app-hub", native_mobile))]
/// The `model` service's grant: an app as itself, by its host-side
/// admission, and the system agent through the studio's image tool while
/// developer mode covers it (ADR 0006 slice 2).
pub(crate) fn model_grant(app: &str, host_dir: &std::path::Path) -> bool {
crate::host_tools::studio::system_caller_admitted(app)
|| crate::host_tools::script_apps::admitted_host(app, host_dir).is_ok()
}

/// The budget scope of a `model` call: the app's contained account, and
/// the device for the system agent's studio calls, in a build that has the
/// studio.
#[cfg(any(feature = "app-hub", native_mobile))]
pub(crate) fn model_scope(app: &str) -> Option<String> {
if crate::host_tools::studio::SUPPORTED && app == crate::host_tools::SYSTEM {
return Some("device".into());
}
crate::app_storage::lifecycle::contained_account(app)
.map(|account| crate::app_storage::account_hash(&account))
}

#[cfg(any(feature = "app-hub", native_mobile))]
pub fn register_host_services() {
register_mail_services();
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
// Capability declarations describe intended use. Admission still binds
// every call to a verified app and the current host profile.
crate::ai_host::set_model_grants(|app, host_dir| {
crate::host_tools::script_apps::admitted_host(app, host_dir).is_ok()
});
crate::ai_host::set_model_grants(model_grant);
crate::ai_host::contained::set_caller_admitted(|app, host_dir| {
crate::host_tools::script_apps::admitted_host(app, host_dir).is_ok()
});
crate::ai_host::set_model_scope(|app, _| {
crate::app_storage::lifecycle::contained_account(app)
.map(|account| crate::app_storage::account_hash(&account))
});
crate::ai_host::set_model_scope(|app, _| model_scope(app));
crate::platform_services::register();
crate::device_calendar::register(|app| {
crate::app_storage::lifecycle::contained_account(app)
Expand Down
6 changes: 6 additions & 0 deletions crates/shell/src/host_tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub mod schema;
pub mod studio;
#[cfg(all(unix, any(feature = "app-hub", native_mobile)))]
pub mod studio_bundles;
#[cfg(all(unix, any(feature = "app-hub", native_mobile)))]
pub mod studio_image;
#[cfg(any(feature = "app-hub", native_mobile))]
pub(crate) mod admission;
#[cfg(feature = "toolbox-peers")]
Expand Down Expand Up @@ -127,6 +129,10 @@ pub fn pump() {
// Components' calls to their apps' host services (ADR 0014).
#[cfg(wasm_functions)]
crate::wasm_service::pump_host_calls();
// The studio's image generations (ADR 0006 slice 2): `model.image`
// calls under the calling agent's identity.
#[cfg(all(unix, any(feature = "app-hub", native_mobile)))]
studio_image::pump();
for _ in 0..8 {
let events = std::mem::take(&mut *INBOX.lock().unwrap_or_else(|e| e.into_inner()));
if events.is_empty() {
Expand Down
3 changes: 2 additions & 1 deletion crates/shell/src/host_tools/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,8 @@ impl Relay {
_ if super::studio::is_tool(&tool) => (
if call.caller_kind == CallerKind::System { Caller::SystemAgent } else { Caller::OwnAgent { client: call.client.clone() } },
super::studio::SUPPORTED && calling == owner && env.grants_all(&calling)
&& (call.caller_kind == CallerKind::AppPeer || calling == SYSTEM),
&& (call.caller_kind == CallerKind::AppPeer || calling == SYSTEM)
&& super::studio::granted_to(&calling, &tool),
),
// The shell's own: only the app's own agent, on its own peer;
// `dev.run` only while developer mode covers the app.
Expand Down
64 changes: 58 additions & 6 deletions crates/shell/src/host_tools/studio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub const APP_TOOLS: &[&str] = &[
"studio.close",
"studio.install",
"studio.uninstall",
"studio.image",
];
/// A development build only (`cfg(dev_mode)`: a debug build, or the
/// `dev-mode` feature): a release build has no studio, whatever it is
Expand All @@ -36,10 +37,33 @@ pub fn declarations(app: &str) -> Vec<Value> {
}
let mut out = vec![declaration(app)];
if cfg!(all(unix, any(feature = "app-hub", native_mobile))) {
out.extend(APP_TOOLS.iter().map(|name| app_declaration(app, name)));
out.extend(
APP_TOOLS
.iter()
.filter(|name| app == SYSTEM || !SYSTEM_ONLY.contains(*name))
.map(|name| tool_declaration(app, name)),
);
}
out
}
/// The tools of [`APP_TOOLS`] the system agent alone gets: `studio.image`
/// spends the person's image budget, so an app's agent is not offered it
/// in this slice (ADR 0006).
const SYSTEM_ONLY: &[&str] = &["studio.image"];
/// Whether `tool` is a studio tool that `app`'s agent may be granted at
/// all: every one for the system agent, all but [`SYSTEM_ONLY`] for an
/// app's own agent. The relay's grant and the executor both ask.
pub fn granted_to(app: &str, tool: &str) -> bool {
is_tool(tool) && (app == SYSTEM || !SYSTEM_ONLY.contains(&tool))
}
/// The declaration of one of [`APP_TOOLS`].
fn tool_declaration(app: &str, name: &str) -> Value {
#[cfg(all(unix, any(feature = "app-hub", native_mobile)))]
if name == super::studio_image::TOOL {
return super::studio_image::declaration(app);
}
app_declaration(app, name)
}
fn app_declaration(app: &str, name: &str) -> Value {
let (description,properties,required)=match name {
"studio.bundle_check" => ("Stage and validate an offline main.splash developer bundle from your conversation workspace. Computes and stamps the digest only on a private copy; never modifies source, signs a publisher release, or installs.",json!({"bundle_path":{"type":"string","minLength":1,"maxLength":1024}}),vec!["bundle_path"]),
Expand All @@ -54,6 +78,16 @@ fn app_declaration(app: &str, name: &str) -> Value {
"input_schema":{"type":"object","properties":properties,"required":required,"additionalProperties":false},
"output_schema":{"type":"object"},"risk":if name=="studio.install" || name=="studio.uninstall" || name=="studio.input" || name=="studio.open" {"act"} else {"read"},"background":false,"shareable":false})
}
/// Whether the system agent may use the `model` service as itself: only
/// while developer mode covers it, for the studio's image tool
/// (`apps.rs` consults this when it registers the model service).
pub fn system_caller_admitted(app: &str) -> bool {
system_caller_admitted_given(app, crate::dev_mode::grants_all(SYSTEM))
}
/// [`system_caller_admitted`] with the live grant answered by the caller.
pub fn system_caller_admitted_given(app: &str, system_granted_all: bool) -> bool {
SUPPORTED && app == SYSTEM && system_granted_all
}
/// Whether this build has the studio at all: a development build on a unix
/// target. Release builds answer no tool, module, launcher row or fixture.
pub const SUPPORTED: bool = cfg!(all(unix, dev_mode));
Expand Down Expand Up @@ -113,7 +147,7 @@ fn workspace(call: &HostToolCall) -> Result<PathBuf, String> {
.ok_or_else(|| "the broker has not confirmed this peer workspace".into())
}

fn app_available(call: &HostToolCall) -> Result<(), String> {
pub(super) fn app_available(call: &HostToolCall) -> Result<(), String> {
let account = call
.account
.as_deref()
Expand All @@ -129,7 +163,7 @@ fn app_available(call: &HostToolCall) -> Result<(), String> {
Ok(())
}

fn authorized(app: &str, tag: &crate::dev_mode::DevTag) -> bool {
pub(super) fn authorized(app: &str, tag: &crate::dev_mode::DevTag) -> bool {
crate::dev_mode::tag_valid(tag) && crate::dev_mode::grants_all(app)
}

Expand Down Expand Up @@ -179,10 +213,17 @@ impl StudioExecutor {
));
return;
}
if app != SYSTEM && SYSTEM_ONLY.contains(&call.name.as_str()) {
reply.finish(ToolOutcome::error(
"not_granted",
"studio.image is the system agent's tool",
));
return;
}
let decl = if call.name == RENDER {
declaration(&app)
} else {
app_declaration(&app, &call.name)
tool_declaration(&app, &call.name)
};
if let Err(why) = super::schema::check(&decl["input_schema"], &call.args) {
reply.finish(ToolOutcome::error("invalid_args", why));
Expand Down Expand Up @@ -218,7 +259,9 @@ impl StudioExecutor {
let running = self.running.clone();
std::thread::spawn(move || {
#[cfg(all(unix, any(feature = "app-hub", native_mobile)))]
let outcome = if call.name != RENDER {
let outcome = if call.name == super::studio_image::TOOL {
super::studio_image::execute(&call, &root, &app, &tag, &flag, &reply)
} else if call.name != RENDER {
super::studio_bundles::execute(&call, &root, &tag, &flag, &reply)
} else {
render_call(&call, &root, &app, &tag, &flag, &reply)
Expand Down Expand Up @@ -455,7 +498,9 @@ pub fn test_flow(spec_path: &str) -> Result<(), String> {
};
seen.insert(name.clone());
let tool = request["name"].as_str().unwrap_or("");
if !is_tool(tool) {
// A fixture drives the free tools only: `studio.image`
// spends the person's image budget.
if !is_tool(tool) || SYSTEM_ONLY.contains(&tool) {
continue;
}
let mut call = match HostToolCall::parse(
Expand Down Expand Up @@ -703,6 +748,13 @@ pub(super) mod scoped {
pub fn output_json(&self) -> Result<Output, String> {
self.output_extension("json")
}
/// An image file, in the formats the `model` service returns.
pub fn output_image(&self, format: &str) -> Result<Output, String> {
if !matches!(format, "png" | "jpg") {
return Err(format!("unexpected image format {format}"));
}
self.output_extension(format)
}
fn output_extension(&self, extension: &str) -> Result<Output, String> {
let name = format!(".studio-{}.{}", uuid::Uuid::new_v4(), extension);
let fd = openat(
Expand Down
Loading
Loading