///|
/// Invalid task descriptions are rejected before filesystem access.
pub(all) suberror TaskContextError {
InvalidTaskSpec
}
///|
/// A validated, bounded task set. Construct with `TaskContextSpec::from_json`.
/// The serialized description is retained privately to prevent mutable JSON or
/// arrays supplied by a caller from changing a validated request.
pub struct TaskContextSpec {
max_tokens : Int32
priv source : String
}
///|
priv struct TaskRequest {
id : String
required : Array[String]
paths : Array[String]
}
///|
priv struct TaskCandidate {
path : String
mut required : Bool
mut mode : String
mut priority : Int32
}
///|
priv struct TaskDescription {
max_tokens : Int32
max_read_bytes : Int64
max_attempts : Int32
tasks : Array[TaskRequest]
candidates : Array[TaskCandidate]
}
///|
fn task_fields(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise TaskContextError {
guard value is Object(fields) else { raise InvalidTaskSpec }
for key in fields.keys() {
if !allowed.contains(key) {
raise InvalidTaskSpec
}
}
fields
}
///|
fn task_integer(
value : Json,
minimum : Int32,
maximum : Int32,
) -> Int32 raise TaskContextError {
guard value is Number(n, ..) &&
n >= minimum.to_double() &&
n <= maximum.to_double() &&
n.to_int().to_double() == n else {
raise InvalidTaskSpec
}
n.to_int()
}
///|
fn task_path(path : String) -> Bool {
!path.is_empty() &&
@utf8.encode(path).length() <= 4096 &&
!path.contains("\\") &&
!path.contains(":") &&
!path.contains("\u0000") &&
!path.split("/").any(part => part.is_empty() || part == "." || part == "..")
}
///|
fn parse_task_description(
value : Json,
) -> TaskDescription raise TaskContextError {
let fields = task_fields(value, [
"maxTokens", "tasks", "maxReadBytes", "maxAttempts",
])
let max_tokens = task_integer(
fields.get("maxTokens").unwrap_or(Json::null()),
1,
2147483647,
)
let max_read_bytes = task_integer(
fields.get("maxReadBytes").unwrap_or(Json(16777216)),
1,
16777216,
).to_int64()
let max_attempts = task_integer(
fields.get("maxAttempts").unwrap_or(Json(256)),
2,
256,
)
guard fields.get("tasks") is Some(Array(tasks)) &&
tasks.length() > 0 &&
tasks.length() <= 16 else {
raise InvalidTaskSpec
}
let ids : Set[String] = Set([])
let merged : Map[String, TaskCandidate] = Map([])
let requests : Array[TaskRequest] = []
for task in tasks {
let task = task_fields(task, ["id", "required", "optional"])
guard task.get("id") is Some(String(id)) &&
!id.is_empty() &&
@utf8.encode(id).length() <= 64 &&
!id.contains("\u0000") &&
!ids.contains(id) else {
raise InvalidTaskSpec
}
ids.add(id)
let required_paths : Array[String] = []
let all_paths : Array[String] = []
for (field, required) in [("required", true), ("optional", false)] {
guard task.get(field).unwrap_or(([] : Json)) is Array(entries) &&
entries.length() <= 128 else {
raise InvalidTaskSpec
}
for entry in entries {
let entry = task_fields(
entry,
if required {
["path", "mode"]
} else {
["path", "mode", "priority"]
},
)
guard entry.get("path") is Some(String(path)) && task_path(path) else {
raise InvalidTaskSpec
}
guard entry.get("mode") is Some(String(mode)) &&
(
mode == "full" ||
mode == "skeleton" ||
(!required && mode == "either")
) else {
raise InvalidTaskSpec
}
let priority = if required {
0
} else {
task_integer(entry.get("priority").unwrap_or(Json(0)), 0, 100)
}
if !all_paths.contains(path) {
all_paths.push(path)
}
if required && !required_paths.contains(path) {
required_paths.push(path)
}
match merged.get(path) {
None => merged[path] = { path, required, mode, priority, }
Some(previous) => {
previous.required = previous.required || required
previous.priority = if previous.priority > priority {
previous.priority
} else {
priority
}
previous.mode = if previous.mode == "full" || mode == "full" {
"full"
} else if previous.mode == "skeleton" || mode == "skeleton" {
"skeleton"
} else {
"either"
}
}
}
if merged.length() > 128 {
raise InvalidTaskSpec
}
}
}
required_paths.sort_by(@document.compare_path)
all_paths.sort_by(@document.compare_path)
requests.push({ id, required: required_paths, paths: all_paths, })
}
requests.sort_by((a, b) => @document.compare_path(a.id, b.id))
let candidates : Array[TaskCandidate] = merged.values().collect()
candidates.sort_by((a, b) => {
if a.required != b.required {
if a.required {
-1
} else {
1
}
} else if !a.required && a.priority != b.priority {
b.priority - a.priority
} else {
@document.compare_path(a.path, b.path)
}
})
{ max_tokens, max_read_bytes, max_attempts, tasks: requests, candidates, }
}
///|
/// Parse a strict task description without reading any files. Required entries
/// accept `full` or `skeleton`; optional entries also accept `either` and a
/// priority from 0 to 100 (default 0). Unknown fields are rejected.
pub fn TaskContextSpec::from_json(
value : Json,
) -> TaskContextSpec raise TaskContextError {
let parsed = parse_task_description(value)
let source = value.stringify()
if @utf8.encode(source).length() > max_context_text_bytes {
raise InvalidTaskSpec
}
{ max_tokens: parsed.max_tokens, source, }
}