Render an Apple configuration profile from the server's PACC document
iOS is the one client none of the mail autoconfiguration reaches: Apple Mail reads neither Mozilla autoconfig nor Autodiscover for IMAP, so adding an account means the device probing for ports - and a probe that meets a filtered port waits out a TCP timeout and reports the account as unverifiable, which the person reads as a wrong password. A profile states the hosts and the ports instead, and carries mail, calendars and contacts in one install. It invents none of them: site.yaml names a PACC document - the standard defined to be fetched from the mail domain itself - and every host, port and collection path comes from there, so the server is described in one place and the profile follows it. The document holds no address, username or password. iOS asks for those at install, which means one profile serves everyone with an account and it can be fetched by anyone: it says no more than PACC already says in public. Identifiers are derived from the site and the payload rather than drawn at random, because iOS replaces a profile whose ids it knows and stacks a duplicate when they differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
co-authored by
Claude Opus 5
parent
43a2f3a069
commit
44c7ee6870
+183
-37
@@ -127,7 +127,12 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
continue;
|
||||
}
|
||||
let bucket = &model["buckets"][need["lands_in"].as_str().unwrap_or_default()];
|
||||
let done: Vec<&str> = need["done_when"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
let done: Vec<&str> = need["done_when"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_str)
|
||||
.collect();
|
||||
for (run, route) in all_routes(bucket, &done).into_iter().enumerate() {
|
||||
let mut states = vec![bucket["initial"].as_str().unwrap_or("open").to_string()];
|
||||
states.extend(route.iter().map(|m| m.to.clone()));
|
||||
@@ -137,7 +142,10 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
run,
|
||||
target: states.last().cloned().unwrap_or_default(),
|
||||
record: BTreeMap::from([("note".to_string(), json!("dry run"))]),
|
||||
pressed: route.iter().map(|m| format!("{} -> {}", m.from, m.label)).collect(),
|
||||
pressed: route
|
||||
.iter()
|
||||
.map(|m| format!("{} -> {}", m.from, m.label))
|
||||
.collect(),
|
||||
moved_by: route.iter().map(|m| m.by.clone()).collect(),
|
||||
touches: route.len(),
|
||||
ask_backs: Vec::new(),
|
||||
@@ -148,9 +156,16 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
writeln!(cases_file, "{}", serde_json::to_string(&case)?)?;
|
||||
}
|
||||
}
|
||||
std::fs::write(out.join("score.json"), serde_json::to_string_pretty(&scores)?)?;
|
||||
std::fs::write(
|
||||
out.join("score.json"),
|
||||
serde_json::to_string_pretty(&scores)?,
|
||||
)?;
|
||||
let relays: usize = scores.values().map(|s| s.cases).sum();
|
||||
println!("dry run: {relays} route(s) across {} need(s) written to {}", scores.len(), out.join("cases.jsonl").display());
|
||||
println!(
|
||||
"dry run: {relays} route(s) across {} need(s) written to {}",
|
||||
scores.len(),
|
||||
out.join("cases.jsonl").display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -180,7 +195,10 @@ pub fn run(argv: &[String]) -> anyhow::Result<()> {
|
||||
writeln!(cases_file, "{}", serde_json::to_string(&case)?)?;
|
||||
}
|
||||
}
|
||||
std::fs::write(out.join("score.json"), serde_json::to_string_pretty(&scores)?)?;
|
||||
std::fs::write(
|
||||
out.join("score.json"),
|
||||
serde_json::to_string_pretty(&scores)?,
|
||||
)?;
|
||||
print_scores(&scores);
|
||||
Ok(())
|
||||
}
|
||||
@@ -257,7 +275,8 @@ fn play(
|
||||
);
|
||||
let reply = ask(visitor_cmd, &prompt, &seed(i * 10 + 1))?;
|
||||
if reply["abandon"] == true {
|
||||
case.outcome = Outcome::Abandoned(reply["why"].as_str().unwrap_or_default().to_string());
|
||||
case.outcome =
|
||||
Outcome::Abandoned(reply["why"].as_str().unwrap_or_default().to_string());
|
||||
return Ok(case);
|
||||
}
|
||||
if let Some(values) = reply["values"].as_object() {
|
||||
@@ -277,9 +296,18 @@ fn play(
|
||||
// collected, could this role really do what the button claims?
|
||||
let bucket_name = need["lands_in"].as_str().unwrap_or_default();
|
||||
let bucket = &model["buckets"][bucket_name];
|
||||
let done: Vec<&str> = need["done_when"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
|
||||
let done: Vec<&str> = need["done_when"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_str)
|
||||
.collect();
|
||||
let initial = bucket["initial"].as_str().unwrap_or("open").to_string();
|
||||
let target = done.get(run % done.len().max(1)).copied().unwrap_or_default().to_string();
|
||||
let target = done
|
||||
.get(run % done.len().max(1))
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
case.target = target.clone();
|
||||
case.states.push(initial.clone());
|
||||
let Some(route) = route_to(bucket, &initial, &target) else {
|
||||
@@ -304,7 +332,11 @@ fn play(
|
||||
serde_json::to_string_pretty(&case.record)?,
|
||||
);
|
||||
let reply = ask(handler_cmd, &prompt, &seed(100 + step))?;
|
||||
let question = reply["need_to_know"].as_str().unwrap_or_default().trim().to_string();
|
||||
let question = reply["need_to_know"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
if reply["can_act"] == false && !question.is_empty() {
|
||||
let answer = ask(
|
||||
visitor_cmd,
|
||||
@@ -314,7 +346,10 @@ fn play(
|
||||
&seed(200 + step),
|
||||
)?;
|
||||
let n = case.ask_backs.len() + 1;
|
||||
case.record.insert(format!("follow_up_{n}"), json!({ "asked": question, "answered": answer["answer"] }));
|
||||
case.record.insert(
|
||||
format!("follow_up_{n}"),
|
||||
json!({ "asked": question, "answered": answer["answer"] }),
|
||||
);
|
||||
case.ask_backs.push(AskBack {
|
||||
state: from.clone(),
|
||||
button: label.clone(),
|
||||
@@ -343,21 +378,47 @@ pub(crate) struct Move {
|
||||
/// desk, and the submitter's own link. Roles overlap on one aggregate,
|
||||
/// so a route is a relay, not one desk's work.
|
||||
pub(crate) fn all_moves(bucket: &Value) -> Vec<Move> {
|
||||
let legal = |from: &str, to: &str| bucket["transitions"][from].as_array().is_some_and(|a| a.iter().any(|x| x == to));
|
||||
let legal = |from: &str, to: &str| {
|
||||
bucket["transitions"][from]
|
||||
.as_array()
|
||||
.is_some_and(|a| a.iter().any(|x| x == to))
|
||||
};
|
||||
let mut moves: Vec<Move> = Vec::new();
|
||||
for desk in bucket["desks"].as_array().into_iter().flatten() {
|
||||
for m in desk["moves"].as_array().into_iter().flatten() {
|
||||
let (Some(from), Some(label), Some(to)) = (m["from"].as_str(), m["label"].as_str(), m["to"].as_str()) else { continue };
|
||||
let (Some(from), Some(label), Some(to)) =
|
||||
(m["from"].as_str(), m["label"].as_str(), m["to"].as_str())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if legal(from, to) && !moves.iter().any(|x| x.from == from && x.to == to) {
|
||||
moves.push(Move { from: from.into(), label: label.into(), to: to.into(), by: desk["group"].as_str().unwrap_or("ungated").into() });
|
||||
moves.push(Move {
|
||||
from: from.into(),
|
||||
label: label.into(),
|
||||
to: to.into(),
|
||||
by: desk["group"].as_str().unwrap_or("ungated").into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let states: Vec<String> = bucket["transitions"].as_object().map(|t| t.keys().cloned().collect()).unwrap_or_default();
|
||||
for to in bucket["self_moves"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
let states: Vec<String> = bucket["transitions"]
|
||||
.as_object()
|
||||
.map(|t| t.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
for to in bucket["self_moves"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_str)
|
||||
{
|
||||
for from in &states {
|
||||
if legal(from, to) && !moves.iter().any(|x| &x.from == from && x.to == to) {
|
||||
moves.push(Move { from: from.clone(), label: "their own link".into(), to: to.into(), by: SUBMITTER.into() });
|
||||
moves.push(Move {
|
||||
from: from.clone(),
|
||||
label: "their own link".into(),
|
||||
to: to.into(),
|
||||
by: SUBMITTER.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +449,13 @@ fn route_to(bucket: &Value, from: &str, to: &str) -> Option<Vec<Move>> {
|
||||
/// dry run walks, so that each button of each role is pressed at least
|
||||
/// once against the real engine.
|
||||
pub(crate) fn all_routes(bucket: &Value, done: &[&str]) -> Vec<Vec<Move>> {
|
||||
fn walk(state: &str, done: &[&str], moves: &[Move], path: &mut Vec<Move>, out: &mut Vec<Vec<Move>>) {
|
||||
fn walk(
|
||||
state: &str,
|
||||
done: &[&str],
|
||||
moves: &[Move],
|
||||
path: &mut Vec<Move>,
|
||||
out: &mut Vec<Vec<Move>>,
|
||||
) {
|
||||
if done.contains(&state) {
|
||||
out.push(path.clone());
|
||||
return;
|
||||
@@ -403,7 +470,13 @@ pub(crate) fn all_routes(bucket: &Value, done: &[&str]) -> Vec<Vec<Move>> {
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
walk(bucket["initial"].as_str().unwrap_or("open"), done, &all_moves(bucket), &mut Vec::new(), &mut out);
|
||||
walk(
|
||||
bucket["initial"].as_str().unwrap_or("open"),
|
||||
done,
|
||||
&all_moves(bucket),
|
||||
&mut Vec::new(),
|
||||
&mut out,
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -424,7 +497,11 @@ pub(crate) fn resolve_choice(names: &[&str], raw: &str) -> String {
|
||||
/// be (`FORMAT=json`, honoured by engines/ollama.sh) and the first
|
||||
/// object in the reply is taken either way.
|
||||
pub(crate) fn ask(cmd: &str, prompt: &str, seed: &str) -> anyhow::Result<Value> {
|
||||
let reply = llm_with(cmd, prompt, &[("FORMAT", "json"), ("SEED", seed), ("TEMPERATURE", "0.7")])?;
|
||||
let reply = llm_with(
|
||||
cmd,
|
||||
prompt,
|
||||
&[("FORMAT", "json"), ("SEED", seed), ("TEMPERATURE", "0.7")],
|
||||
)?;
|
||||
Ok(first_object(&reply).unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
@@ -462,7 +539,11 @@ fn tally(score: &mut NeedScore, case: &Case) {
|
||||
score.buttons.entry(button.clone()).or_default().0 += 1;
|
||||
}
|
||||
for a in &case.ask_backs {
|
||||
score.buttons.entry(format!("{} -> {}", a.state, a.button)).or_default().1 += 1;
|
||||
score
|
||||
.buttons
|
||||
.entry(format!("{} -> {}", a.state, a.button))
|
||||
.or_default()
|
||||
.1 += 1;
|
||||
}
|
||||
match &case.outcome {
|
||||
Outcome::Finished(_) => score.finished += 1,
|
||||
@@ -476,7 +557,9 @@ fn tally(score: &mut NeedScore, case: &Case) {
|
||||
}
|
||||
Outcome::Abandoned(why) => {
|
||||
score.abandoned += 1;
|
||||
score.abandon_reasons.push(format!("{}: {why}", case.persona));
|
||||
score
|
||||
.abandon_reasons
|
||||
.push(format!("{}: {why}", case.persona));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,7 +584,15 @@ fn per_case(n: usize, s: &NeedScore) -> String {
|
||||
fn print_scores(scores: &BTreeMap<String, NeedScore>) {
|
||||
println!(
|
||||
"\n{:<28} {:>5} {:>9} {:>9} {:>10} {:>9} {:>8} {:>10} {:>13}",
|
||||
"need", "cases", "finished", "misroute", "abandoned", "no route", "touches", "ask-backs", "unanswerable"
|
||||
"need",
|
||||
"cases",
|
||||
"finished",
|
||||
"misroute",
|
||||
"abandoned",
|
||||
"no route",
|
||||
"touches",
|
||||
"ask-backs",
|
||||
"unanswerable"
|
||||
);
|
||||
for (need, s) in scores {
|
||||
println!(
|
||||
@@ -529,7 +620,11 @@ fn print_scores(scores: &BTreeMap<String, NeedScore>) {
|
||||
" {need} [{} / {}]{} {}",
|
||||
a.state,
|
||||
a.button,
|
||||
if a.answerable { "" } else { " (visitor did not know)" },
|
||||
if a.answerable {
|
||||
""
|
||||
} else {
|
||||
" (visitor did not know)"
|
||||
},
|
||||
a.question
|
||||
);
|
||||
}
|
||||
@@ -550,10 +645,16 @@ fn print_scores(scores: &BTreeMap<String, NeedScore>) {
|
||||
pub fn compare(argv: &[String]) -> anyhow::Result<()> {
|
||||
let (a, b) = match (argv.get(1), argv.get(2)) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
_ => return Err(anyhow!("usage: portal-trainer compare <a/score.json> <b/score.json>")),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"usage: portal-trainer compare <a/score.json> <b/score.json>"
|
||||
))
|
||||
}
|
||||
};
|
||||
let load = |p: &String| -> anyhow::Result<BTreeMap<String, NeedScore>> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(p).with_context(|| format!("reading {p}"))?)?)
|
||||
Ok(serde_json::from_str(
|
||||
&std::fs::read_to_string(p).with_context(|| format!("reading {p}"))?,
|
||||
)?)
|
||||
};
|
||||
let (a, b) = (load(a)?, load(b)?);
|
||||
println!(
|
||||
@@ -565,11 +666,27 @@ pub fn compare(argv: &[String]) -> anyhow::Result<()> {
|
||||
println!(
|
||||
"{:<28} {:>14} {:>14} {:>14} {:>16} {:>16}",
|
||||
need,
|
||||
format!("{} -> {}", pct(sa.misrouted, sa.cases), pct(sb.misrouted, sb.cases)),
|
||||
format!("{} -> {}", pct(sa.abandoned, sa.cases), pct(sb.abandoned, sb.cases)),
|
||||
format!(
|
||||
"{} -> {}",
|
||||
pct(sa.misrouted, sa.cases),
|
||||
pct(sb.misrouted, sb.cases)
|
||||
),
|
||||
format!(
|
||||
"{} -> {}",
|
||||
pct(sa.abandoned, sa.cases),
|
||||
pct(sb.abandoned, sb.cases)
|
||||
),
|
||||
format!("{} -> {}", pct(sa.stuck, sa.cases), pct(sb.stuck, sb.cases)),
|
||||
format!("{} -> {}", per_case(sa.touches, sa), per_case(sb.touches, sb)),
|
||||
format!("{} -> {}", per_case(sa.ask_backs, sa), per_case(sb.ask_backs, sb)),
|
||||
format!(
|
||||
"{} -> {}",
|
||||
per_case(sa.touches, sa),
|
||||
per_case(sb.touches, sb)
|
||||
),
|
||||
format!(
|
||||
"{} -> {}",
|
||||
per_case(sa.ask_backs, sa),
|
||||
per_case(sb.ask_backs, sb)
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -589,8 +706,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_choice_is_matched_by_its_heading_however_the_model_dresses_it() {
|
||||
let names = ["Ask about working", "Ask about working with us", "Get a price"];
|
||||
assert_eq!(resolve_choice(&names, "Ask about working with us: For carpenters who..."), "Ask about working with us");
|
||||
let names = [
|
||||
"Ask about working",
|
||||
"Ask about working with us",
|
||||
"Get a price",
|
||||
];
|
||||
assert_eq!(
|
||||
resolve_choice(&names, "Ask about working with us: For carpenters who..."),
|
||||
"Ask about working with us"
|
||||
);
|
||||
assert_eq!(resolve_choice(&names, " get a PRICE "), "Get a price");
|
||||
assert_eq!(resolve_choice(&names, "Something else"), "Something else");
|
||||
}
|
||||
@@ -620,7 +744,10 @@ mod tests {
|
||||
// Losing a quoted job is the other group's button, so the way
|
||||
// from open to that ending is a relay: two roles, one case.
|
||||
let route = route_to(&bucket(), "surveyed", "lost").unwrap();
|
||||
let relay: Vec<(&str, &str)> = route.iter().map(|m| (m.label.as_str(), m.by.as_str())).collect();
|
||||
let relay: Vec<(&str, &str)> = route
|
||||
.iter()
|
||||
.map(|m| (m.label.as_str(), m.by.as_str()))
|
||||
.collect();
|
||||
assert_eq!(relay, [("Quote sent", "pm"), ("Theirs", "other")]);
|
||||
assert_eq!(route_to(&bucket(), "open", "open").unwrap().len(), 0);
|
||||
assert!(route_to(&bucket(), "won", "open").is_none());
|
||||
@@ -630,7 +757,10 @@ mod tests {
|
||||
fn the_submitters_own_link_is_a_move_like_any_other() {
|
||||
let mut b = bucket();
|
||||
b["self_moves"] = json!(["lost"]);
|
||||
let theirs: Vec<Move> = all_moves(&b).into_iter().filter(|m| m.by == SUBMITTER).collect();
|
||||
let theirs: Vec<Move> = all_moves(&b)
|
||||
.into_iter()
|
||||
.filter(|m| m.by == SUBMITTER)
|
||||
.collect();
|
||||
// Only where the machine allows it and no desk already holds it:
|
||||
// open->lost is the project manager's and quoted->lost the other
|
||||
// group's, which leaves surveyed->lost to the link.
|
||||
@@ -641,7 +771,10 @@ mod tests {
|
||||
#[test]
|
||||
fn a_dry_run_walks_every_route_to_every_ending() {
|
||||
let routes = all_routes(&bucket(), &["won", "lost"]);
|
||||
let ends: Vec<String> = routes.iter().map(|r| format!("{}:{}", r.len(), r.last().unwrap().to)).collect();
|
||||
let ends: Vec<String> = routes
|
||||
.iter()
|
||||
.map(|r| format!("{}:{}", r.len(), r.last().unwrap().to))
|
||||
.collect();
|
||||
// open->lost; open->surveyed->quoted->won; and the relay
|
||||
// open->surveyed->quoted->lost, closed by the other group.
|
||||
assert_eq!(ends.len(), 3, "{ends:?}");
|
||||
@@ -664,15 +797,28 @@ mod tests {
|
||||
moved_by: vec!["pm".into(), "pm".into()],
|
||||
touches: 3,
|
||||
ask_backs: vec![
|
||||
AskBack { state: "open".into(), button: "Looked".into(), question: "Which floor?".into(), answerable: true },
|
||||
AskBack { state: "surveyed".into(), button: "Quote sent".into(), question: "Load-bearing?".into(), answerable: false },
|
||||
AskBack {
|
||||
state: "open".into(),
|
||||
button: "Looked".into(),
|
||||
question: "Which floor?".into(),
|
||||
answerable: true,
|
||||
},
|
||||
AskBack {
|
||||
state: "surveyed".into(),
|
||||
button: "Quote sent".into(),
|
||||
question: "Load-bearing?".into(),
|
||||
answerable: false,
|
||||
},
|
||||
],
|
||||
outcome,
|
||||
};
|
||||
tally(&mut score, &base(Outcome::Finished("won".into())));
|
||||
tally(&mut score, &base(Outcome::Stuck("no route".into())));
|
||||
tally(&mut score, &base(Outcome::Misrouted("Wrong door".into())));
|
||||
assert_eq!((score.cases, score.finished, score.stuck, score.misrouted), (3, 1, 1, 1));
|
||||
assert_eq!(
|
||||
(score.cases, score.finished, score.stuck, score.misrouted),
|
||||
(3, 1, 1, 1)
|
||||
);
|
||||
assert_eq!((score.ask_backs, score.unanswerable), (6, 3));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user