1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! The embedder-supplied seam for `submilli:skills`: a program reads the skills
//! the harness offers, their instructions and the files bundled with them.
//!
//! Where skills come from is the harness's choice. Everything that crosses
//! [`SkillProvider`] is owned, serializable data, so a harness may implement it
//! in process or forward it over the wire.
use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
use crate::stdlib::shared::truncated;
pub const SKILLS_MODULE_NAME: &str = "submilli:skills";
/// One skill, as a listing shows it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillInfo {
pub name: String,
/// What the skill is for. The engine reduces it to one bounded line before a
/// program sees it in a listing.
pub description: Option<String>,
}
/// A loaded skill.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
pub description: Option<String>,
/// The skill's instructions.
pub content: String,
}
/// Why a skill or one of its files could not be read. Every message is safe to
/// show the program.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkillError {
/// No [`SkillProvider`] is wired into the runtime.
NotConfigured,
/// The harness has no skill of this name.
NotFound { name: String },
/// The skill has no file at this path.
FileNotFound { name: String, path: String },
/// The name is not one skill name. The engine refuses these before asking
/// the provider.
InvalidName { name: String },
/// The path is not a relative path inside the skill. The engine refuses
/// these before asking the provider; a provider may refuse more.
InvalidPath { path: String },
/// The provider answered `load(name)` with a skill of another name. The
/// engine refuses it: the policy decided on `name`.
WrongSkill { name: String },
/// Reading failed for another reason. `message` is a fixed classification
/// chosen by the provider.
Failed { message: String },
}
impl std::fmt::Display for SkillError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotConfigured => write!(
f,
"no skill provider is configured: this harness does not offer skills"
),
Self::NotFound { name } => write!(
f,
"no skill named `{}`; call `list()` for the skills you may load",
truncated(name)
),
Self::FileNotFound { name, path } => write!(
f,
"skill `{}` has no file `{}`",
truncated(name),
truncated(path)
),
Self::InvalidName { name } => write!(
f,
"`{}` is not a skill name: use a name as `list()` returns it — one segment of \
at most 4096 bytes, not empty, `.` or `..`, and without `/`, `\\` or NUL",
truncated(name)
),
Self::InvalidPath { path } => write!(
f,
"`{}` is not a path inside a skill: use a relative path of at most 4096 bytes \
with `/` separators, no empty, `.` or `..` segment, and no `\\`, `:` or NUL",
truncated(path)
),
Self::WrongSkill { name } => write!(
f,
"the harness answered `{}` with a different skill",
truncated(name)
),
Self::Failed { message } => write!(f, "{message}"),
}
}
}
impl std::error::Error for SkillError {}
/// The harness side of `submilli:skills`.
pub trait SkillProvider: Send + Sync {
/// Every skill the harness offers, each under a name [`Self::load`] accepts.
/// The engine removes the ones the policy denies to the caller, and any whose
/// name it would refuse, before the program sees the list.
fn list<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<Vec<SkillInfo>, SkillError>> + Send + 'a>>;
/// The skill named `name`, whose returned `name` must be exactly `name`.
///
/// `name` comes from the program and the policy decided on it as written, so
/// match it byte for byte: no case folding, Unicode normalization or path
/// canonicalization, or a policy that denies `secrets` would let `Secrets`
/// through. The engine passes only one segment of at most 4096 bytes that is
/// not empty, `.` or `..` and has no `/`, `\` or NUL; it may contain `:`, so
/// never use it as a path.
fn load<'a>(
&'a self,
name: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Skill, SkillError>> + Send + 'a>>;
/// The text of the file at `path` inside skill `name`. `name` is checked and
/// must be matched as for [`Self::load`]. `path` is relative, uses `/`
/// separators, has no empty, `.` or `..` segment and no `\`, `:` or NUL, and
/// is at most 4096 bytes. The engine does not decode it: a provider that
/// percent-decodes must check the decoded path again.
fn read_file<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, SkillError>> + Send + 'a>>;
}