- 8732
async fn intent_policy(State(state): State<AppState>) -> Json<serde_json::Value> { - 8733
let config = state.core.config(); - 8734
Json(serde_json::json!({ - 8735
"intent": { - 8736
"enabled": config.intent.enabled, - 8737
"accept_confidence": config.intent.accept_confidence, - 8738
"provisional_confidence": config.intent.provisional_confidence, - 8739
"slice_capabilities": config.intent.slice_capabilities, - 8740
"posture": config.intent.posture, - 8741
"escalate": config.intent.escalate, - 8742
"max_classify_usd": config.intent.max_classify_usd, - 8743
"classify_timeout_secs": config.intent.classify_timeout_secs, - 8744
"autonomy": config.intent.autonomy, - 8745
"evidence_max_age_secs": config.intent.evidence_max_age_secs, - 8746
}, - 8747
"commitment": { - 8748
"enabled": config.commitment.enabled, - 8749
"lifetime_budget_usd": config.commitment.lifetime_budget_usd, - 8750
"stall_limit": config.commitment.stall_limit, - 8751
"review_every_hours": config.commitment.review_every_hours, - 8752
"default_ttl_days": config.commitment.default_ttl_days, - 8753
}, - 8754
})) - 8755
} - 8756
- 8757
#[derive(serde::Deserialize)] - 8758
struct CommitmentQuery { - 8759
/// Include closed commitments. - 8760
#[serde(default)] - 8761
all: bool, - 8762
#[serde(default)] - 8763
agent: Option<String>, - 8764
} - 8765
- 8766
/// The portfolio, in the order the scheduler would work it. - 8767
async fn list_commitments( - 8768
State(state): State<AppState>, - 8769
axum::extract::Query(q): axum::extract::Query<CommitmentQuery>, - 8770
) -> axum::response::Response { - 8771
use axum::response::IntoResponse; - 8772
let core = scoped_core!(&state, None, q.agent.as_deref()); - 8773
let ledger = vak_commit::CommitmentLedger::new(&core.sessions_home()); - 8774
let commitments = if q.all { ledger.all() } else { ledger.open() }; - 8775
let ranked = vak_commit::rank(&commitments, &vak_commit::SchedulerContext::default()); - 8776
Json(serde_json::json!({ - 8777
"commitments": commitments, - 8778
// Priorities ride alongside rather than being baked into the rows: - 8779
// the ordering is a scheduling opinion, and a UI should be able to - 8780
// show why as well as what. - 8781
"priorities": ranked, - 8782
})) - 8783
.into_response() - 8784
} - 8785
- 8786
async fn get_commitment( - 8787
State(state): State<AppState>, - 8788
axum::extract::Path(id): axum::extract::Path<String>, - 8789
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 8790
) -> axum::response::Response { - 8791
let core = scoped_core!(&state, None, q.agent.as_deref()); - 8792
let ledger = vak_commit::CommitmentLedger::new(&core.sessions_home()); - 8793
match ledger.get(&id) { - 8794
Ok(Some(commitment)) => Json(serde_json::json!({ - 8795
"commitment": commitment, - 8796
"events": ledger.events_for(&id), - 8797
})) - 8798
.into_response(), - 8799
Ok(None) => ( - 8800
StatusCode::NOT_FOUND, - 8801
Json(serde_json::json!({ "error": "no such commitment" })), - 8802
) - 8803
.into_response(), - 8804
Err(error) => ( - 8805
StatusCode::INTERNAL_SERVER_ERROR, - 8806
Json(serde_json::json!({ "error": error.to_string() })), - 8807
) - 8808
.into_response(), - 8809
} - 8810
} - 8811
- 8812
#[derive(serde::Deserialize)] - 8813
struct CloseCommitmentBody { - 8814
verdict: String, - 8815
#[serde(default)] - 8816
note: String, - 8817
#[serde(default)] - 8818
agent: Option<String>, - 8819
} - 8820
- 8821
async fn close_commitment( - 8822
State(state): State<AppState>, - 8823
axum::extract::Path(id): axum::extract::Path<String>, - 8824
Json(body): Json<CloseCommitmentBody>, - 8825
) -> axum::response::Response { - 8826
let core = scoped_core!(&state, None, body.agent.as_deref()); - 8827
let ledger = vak_commit::CommitmentLedger::new(&core.sessions_home()); - 8828
let Ok(Some(commitment)) = ledger.get(&id) else { - 8829
return ( - 8830
StatusCode::NOT_FOUND, - 8831
Json(serde_json::json!({ "error": "no such commitment" })), - 8832
) - 8833
.into_response(); - 8834
}; - 8835
let verdict = match body.verdict.as_str() { - 8836
"fulfilled" => vak_commit::Verdict::Fulfilled, - 8837
"partial" => vak_commit::Verdict::Partial, - 8838
"failed" => vak_commit::Verdict::Failed, - 8839
"abandoned" => vak_commit::Verdict::Abandoned, - 8840
"expired" => vak_commit::Verdict::Expired, - 8841
"unknown" => vak_commit::Verdict::Unknown, - 8842
other => { - 8843
return ( - 8844
StatusCode::BAD_REQUEST, - 8845
Json(serde_json::json!({ "error": format!("unknown verdict '{other}'") })), - 8846
) - 8847
.into_response(); - 8848
} - 8849
}; - 8850
let strength = commitment.achieved_strength(); - 8851
match ledger.append(&vak_commit::Event::new( - 8852
&commitment.commitment_id, - 8853
vak_commit::EventKind::Closed { - 8854
verdict, - 8855
strength, - 8856
evidence: Vec::new(), - 8857
note: body.note, - 8858
}, - 8859
)) { - 8860
Ok(()) => { - 8861
Json(serde_json::json!({ "ok": true, "verdict": verdict.as_str() })).into_response() - 8862
} - 8863
// A refused closure is a 409, not a 500: the request was well-formed - 8864
// and the server is fine — the evidence simply does not support the - 8865
// claim. The message says which evidence was missing. - 8866
Err(error) => ( - 8867
StatusCode::CONFLICT, - 8868
Json(serde_json::json!({ "error": error.to_string() })), - 8869
) - 8870
.into_response(), - 8871
} - 8872
} - 8873
- 8874
// ---- Inbox (durable attention layer, docs/design/29-personal-os.md P6) ------ - 8875
- 8876
const DEFAULT_INBOX_LIMIT: usize = 200; - 8877
- 8878
#[derive(serde::Deserialize)] - 8879
struct InboxQuery { - 8880
#[serde(default)] - 8881
limit: Option<usize>, - 8882
/// Only entries without an ack tombstone. - 8883
#[serde(default)] - 8884
unread: bool, - 8885
} - 8886
- 8887
/// Newest-first inbox entries plus the live unread total. The count always - 8888
/// reflects the full unfiltered set; `limit` bounds the returned window only. - 8889
async fn inbox_list( - 8890
State(state): State<AppState>, - 8891
axum::extract::Query(q): axum::extract::Query<InboxQuery>, - 8892
) -> Json<serde_json::Value> { - 8893
let home = state.core.shared_data_home(); - 8894
let unread_count = vak_core::inbox::unread_count(&home); - 8895
let limit = q - 8896
.limit - 8897
.unwrap_or(DEFAULT_INBOX_LIMIT) - 8898
.clamp(1, vak_core::inbox::MAX_SCAN); - 8899
let entries = if q.unread { - 8900
vak_core::inbox::unread(&home) - 8901
} else { - 8902
vak_core::inbox::list(&home, limit) - 8903
} - 8904
.into_iter() - 8905
.take(limit) - 8906
.collect::<Vec<_>>(); - 8907
let entries = entries - 8908
.into_iter() - 8909
.map(|entry| { - 8910
let mut value = serde_json::to_value(&entry).unwrap_or_else(|_| serde_json::json!({})); - 8911
if let Some(session_id) = entry.session_id.as_deref() { - 8912
let available = state.get(session_id).is_some() - 8913
|| open_historical_session(&state, session_id).is_some(); - 8914
value["origin_state"] = serde_json::json!(if available { - 8915
"available" - 8916
} else { - 8917
"unavailable" - 8918
}); - 8919
} - 8920
value - 8921
}) - 8922
.collect::<Vec<_>>(); - 8923
Json(serde_json::json!({ "entries": entries, "unread_count": unread_count })) - 8924
} - 8925
- 8926
async fn inbox_unread_count(State(state): State<AppState>) -> Json<serde_json::Value> { - 8927
Json(serde_json::json!({ - 8928
"count": vak_core::inbox::unread_count(&state.core.shared_data_home()) - 8929
})) - 8930
} - 8931
- 8932
/// Idempotent read-state: a tombstone append via `inbox::ack`. An unknown id - 8933
/// is a 404; re-acking reports `{acked:false}` instead of writing twice. - 8934
async fn inbox_ack( - 8935
State(state): State<AppState>, - 8936
Path(id): Path<String>, - 8937
) -> axum::response::Response { - 8938
use axum::response::IntoResponse; - 8939
let home = state.core.shared_data_home(); - 8940
if !vak_core::inbox::list(&home, vak_core::inbox::MAX_SCAN) - 8941
.iter() - 8942
.any(|e| e.id == id) - 8943
{ - 8944
return ( - 8945
StatusCode::NOT_FOUND, - 8946
Json(serde_json::json!({ "error": format!("unknown inbox entry '{id}'") })), - 8947
) - 8948
.into_response(); - 8949
} - 8950
match vak_core::inbox::ack(&home, &id) { - 8951
Ok(acked) => Json(serde_json::json!({ "acked": acked })).into_response(), - 8952
Err(e) => ( - 8953
StatusCode::INTERNAL_SERVER_ERROR, - 8954
Json(serde_json::json!({ "error": e.to_string() })), - 8955
) - 8956
.into_response(), - 8957
} - 8958
} - 8959
- 8960
async fn git_output(cwd: &std::path::Path, args: &[&str]) -> Option<String> { - 8961
let out = tokio::process::Command::new("git") - 8962
.args(args) - 8963
.current_dir(cwd) - 8964
.output() - 8965
.await - 8966
.ok()?; - 8967
if !out.status.success() { - 8968
return None; - 8969
} - 8970
Some(String::from_utf8_lossy(&out.stdout).into_owned()) - 8971
} - 8972
- 8973
/// Workspace diff for the review pane. Untracked files appear in `status` - 8974
/// as `??` lines; patches are split per-file client-side. - 8975
async fn session_diff( - 8976
State(state): State<AppState>, - 8977
Path(id): Path<String>, - 8978
) -> axum::response::Response { - 8979
use axum::response::IntoResponse; - 8980
let Some(handle) = state.get(&id) else { - 8981
return ( - 8982
StatusCode::NOT_FOUND, - 8983
Json(serde_json::json!({ "error": "unknown session" })), - 8984
) - 8985
.into_response(); - 8986
}; - 8987
let cwd = handle.cwd.clone(); - 8988
let Some(status) = git_output(&cwd, &["status", "--porcelain"]).await else { - 8989
return ( - 8990
StatusCode::UNPROCESSABLE_ENTITY, - 8991
Json(serde_json::json!({ "error": "not a git repository" })), - 8992
) - 8993
.into_response(); - 8994
}; - 8995
// `--no-color` is a `diff` option, not a global git flag — placed - 8996
// before the subcommand (as this read for a long time) git rejects it - 8997
// outright ("unknown option: --no-color", exit 129), and `git_output` - 8998
// turns that failure into a silently empty string via - 8999
// `unwrap_or_default()`. Every consumer of this endpoint — this - 9000
// console's Worktree Diff tab, the desktop DiffPane, `openFileSmart`'s - 9001
// diff-vs-editor routing — has been reading an empty diff regardless - 9002
// of what actually changed. - 9003
let diff = git_output(&cwd, &["diff", "--no-color", "--unified=3"]) - 9004
.await - 9005
.unwrap_or_default(); - 9006
let staged = git_output(&cwd, &["diff", "--no-color", "--cached", "--unified=3"]) - 9007
.await - 9008
.unwrap_or_default(); - 9009
Json(serde_json::json!({ - 9010
"root": cwd, - 9011
"diff": diff, - 9012
"staged_diff": staged, - 9013
"status": status, - 9014
})) - 9015
.into_response() - 9016
} - 9017
- 9018
// ---- checkpoints (time travel) ---------------------------------------------- - 9019
- 9020
async fn list_checkpoints( - 9021
State(state): State<AppState>, - 9022
Path(id): Path<String>, - 9023
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9024
) -> axum::response::Response { - 9025
// A registered session's own Core is reused when it's still open. Once - 9026
// it's closed, `session_id` alone can no longer identify its owning - 9027
// Agent, so an explicit `?agent=` is the only way to keep finding its - 9028
// checkpoints instead of silently falling back to the default Agent. - 9029
let core = scoped_core!(&state, Some(&id), q.agent.as_deref()); - 9030
// A session with no snapshots yet has no directory; that's an empty - 9031
// list, not an error. - 9032
let list = match vak_core::checkpoints::list(&core.sessions_home(), &id) { - 9033
Ok(list) if !list.is_empty() => list, - 9034
_ => match vak_core::checkpoints::list(&core.shared_data_home(), &id) { - 9035
Ok(list) => list, - 9036
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(), - 9037
Err(e) => { - 9038
return ( - 9039
StatusCode::INTERNAL_SERVER_ERROR, - 9040
Json(serde_json::json!({ "error": e.to_string() })), - 9041
) - 9042
.into_response(); - 9043
} - 9044
}, - 9045
}; - 9046
let checkpoints: Vec<serde_json::Value> = list - 9047
.iter() - 9048
.map(|cp| { - 9049
serde_json::json!({ - 9050
"seq": cp.seq, - 9051
"label": cp.label, - 9052
"created_at": cp.created_at.to_rfc3339(), - 9053
"files": cp.files.len(), - 9054
}) - 9055
}) - 9056
.collect(); - 9057
Json(serde_json::json!({ "checkpoints": checkpoints })).into_response() - 9058
} - 9059
- 9060
async fn restore_checkpoint( - 9061
State(state): State<AppState>, - 9062
Path((id, seq)): Path<(String, u32)>, - 9063
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9064
) -> axum::response::Response { - 9065
// A live run must never have its workspace mutated underneath it. Reuse - 9066
// this lookup for the cwd fallback below instead of re-fetching it. - 9067
let handle = state.get(&id); - 9068
if let Some(handle) = &handle - 9069
&& handle - 9070
.session - 9071
.lock() - 9072
.unwrap_or_else(std::sync::PoisonError::into_inner) - 9073
.is_none() - 9074
{ - 9075
return ( - 9076
StatusCode::CONFLICT, - 9077
Json(serde_json::json!({ "error": "a run is active on this session" })), - 9078
) - 9079
.into_response(); - 9080
} - 9081
// See `list_checkpoints`: a closed session needs an explicit `?agent=` - 9082
// to keep resolving its real owning Agent rather than the default one. - 9083
let core = scoped_core!(&state, Some(&id), q.agent.as_deref()); - 9084
// Best-of-N children captured inside their worktrees; attached handles - 9085
// know that cwd. Everything else restores into the workspace root. - 9086
let cwd = handle - 9087
.map(|h| h.cwd.clone()) - 9088
.unwrap_or_else(|| core.cwd().clone()); - 9089
// The blob store the manifest's hashes resolve against lives under - 9090
// whichever home the manifest itself was found in. - 9091
let (cp, checkpoints_home) = match vak_core::checkpoints::load(&core.sessions_home(), &id, seq) - 9092
{ - 9093
Ok(cp) => (cp, core.sessions_home()), - 9094
Err(_) => match vak_core::checkpoints::load(&core.shared_data_home(), &id, seq) { - 9095
Ok(cp) => (cp, core.shared_data_home()), - 9096
Err(_) => { - 9097
return ( - 9098
StatusCode::NOT_FOUND, - 9099
Json(serde_json::json!({ "error": format!("checkpoint {seq} not found") })), - 9100
) - 9101
.into_response(); - 9102
} - 9103
}, - 9104
}; - 9105
match tokio::task::spawn_blocking(move || { - 9106
vak_core::checkpoints::restore(&cwd, &checkpoints_home, &cp) - 9107
}) - 9108
.await - 9109
{ - 9110
Ok(Ok((restored, deleted))) => Json(serde_json::json!({ - 9111
"restored": restored, - 9112
"deleted": deleted, - 9113
"seq": seq, - 9114
})) - 9115
.into_response(), - 9116
Ok(Err(e)) => ( - 9117
StatusCode::INTERNAL_SERVER_ERROR, - 9118
Json(serde_json::json!({ "error": e.to_string() })), - 9119
) - 9120
.into_response(), - 9121
Err(e) => ( - 9122
StatusCode::INTERNAL_SERVER_ERROR, - 9123
Json(serde_json::json!({ "error": e.to_string() })), - 9124
) - 9125
.into_response(), - 9126
} - 9127
} - 9128
- 9129
// ---- archive (sidebar visibility; ledgers stay untouched) -------------------- - 9130
- 9131
fn archive_path(core: &Core) -> PathBuf { - 9132
core.shared_data_home().join("archive.json") - 9133
} - 9134
- 9135
fn read_archive(core: &Core) -> HashMap<String, bool> { - 9136
std::fs::read_to_string(archive_path(core)) - 9137
.ok() - 9138
.and_then(|text| serde_json::from_str(&text).ok()) - 9139
.unwrap_or_default() - 9140
} - 9141
- 9142
fn write_archive(core: &Core, map: &HashMap<String, bool>) { - 9143
if let Some(parent) = archive_path(core).parent() { - 9144
let _ = std::fs::create_dir_all(parent); - 9145
} - 9146
let tmp = archive_path(core).with_extension("json.tmp"); - 9147
if std::fs::write(&tmp, serde_json::to_string(map).unwrap_or_default()).is_ok() { - 9148
let _ = std::fs::rename(&tmp, archive_path(core)); - 9149
} - 9150
} - 9151
- 9152
fn find_session_in_cwd(core: &Core, id: &str) -> bool { - 9153
let direct = vak_session::SessionPath::sessions_dir(&core.sessions_home(), core.cwd()) - 9154
.join(format!("{id}.jsonl")); - 9155
if direct.is_file() { - 9156
return true; - 9157
} - 9158
let shared = core.shared_data_home(); - 9159
if let Ok(agents) = std::fs::read_dir(shared.join("agents")) { - 9160
for agent in agents.flatten() { - 9161
let candidate = vak_session::SessionPath::sessions_dir(&agent.path(), core.cwd()) - 9162
.join(format!("{id}.jsonl")); - 9163
if candidate.is_file() { - 9164
return true; - 9165
} - 9166
} - 9167
} - 9168
false - 9169
} - 9170
- 9171
#[derive(serde::Deserialize)] - 9172
struct ArchiveBody { - 9173
archived: bool, - 9174
} - 9175
- 9176
async fn set_archived( - 9177
State(state): State<AppState>, - 9178
Path(id): Path<String>, - 9179
Json(body): Json<ArchiveBody>, - 9180
) -> axum::response::Response { - 9181
if !find_session_in_cwd(&state.core, &id) { - 9182
return ( - 9183
StatusCode::NOT_FOUND, - 9184
Json(serde_json::json!({ "error": "unknown session" })), - 9185
) - 9186
.into_response(); - 9187
} - 9188
let mut map = read_archive(&state.core); - 9189
map.insert(id, body.archived); - 9190
write_archive(&state.core, &map); - 9191
Json(serde_json::json!({ "archived": body.archived })).into_response() - 9192
} - 9193
- 9194
async fn delete_session( - 9195
State(state): State<AppState>, - 9196
Path(id): Path<String>, - 9197
) -> axum::response::Response { - 9198
if !find_session_in_cwd(&state.core, &id) { - 9199
return ( - 9200
StatusCode::NOT_FOUND, - 9201
Json(serde_json::json!({ "error": "unknown session" })), - 9202
) - 9203
.into_response(); - 9204
} - 9205
if state.get(&id).is_some_and(|handle| { - 9206
handle - 9207
.session - 9208
.lock() - 9209
.unwrap_or_else(std::sync::PoisonError::into_inner) - 9210
.is_none() - 9211
}) { - 9212
return ( - 9213
StatusCode::CONFLICT, - 9214
Json(serde_json::json!({ "error": "cannot delete a running task" })), - 9215
) - 9216
.into_response(); - 9217
} - 9218
if !read_archive(&state.core).get(&id).copied().unwrap_or(false) { - 9219
return ( - 9220
StatusCode::BAD_REQUEST, - 9221
Json(serde_json::json!({ "error": "only archived tasks can be deleted" })), - 9222
) - 9223
.into_response(); - 9224
} - 9225
if let Err(error) = vak_core::trash::set( - 9226
&state.core.shared_data_home(), - 9227
std::slice::from_ref(&id), - 9228
true, - 9229
) { - 9230
return trash_write_failed(&error); - 9231
} - 9232
state.forget_session(&id); - 9233
// Drop the session's cached FinOps spend gate along with it (docs/design/42-managed-work-contracts.md// Phase D) — otherwise a long-running server accumulates one entry per - 9234
// session ever seen, forever. - 9235
state.core.forget_spend_gate(&id); - 9236
Json(serde_json::json!({ "trashed": id })).into_response() - 9237
} - 9238
- 9239
fn trash_write_failed(error: &std::io::Error) -> axum::response::Response { - 9240
( - 9241
StatusCode::INTERNAL_SERVER_ERROR, - 9242
Json(serde_json::json!({ "error": format!("could not update the trash: {error}") })), - 9243
) - 9244
.into_response() - 9245
} - 9246
- 9247
/// Takes a session back out of the trash. It returns archived, where it was - 9248
/// when it was trashed. - 9249
async fn restore_session( - 9250
State(state): State<AppState>, - 9251
Path(id): Path<String>, - 9252
) -> axum::response::Response { - 9253
let home = state.core.shared_data_home(); - 9254
if !find_session_in_cwd(&state.core, &id) || !vak_core::trash::is_trashed(&home, &id) { - 9255
return ( - 9256
StatusCode::NOT_FOUND, - 9257
Json(serde_json::json!({ "error": "no such session in the trash" })), - 9258
) - 9259
.into_response(); - 9260
} - 9261
if let Err(error) = vak_core::trash::set(&home, std::slice::from_ref(&id), false) { - 9262
return trash_write_failed(&error); - 9263
} - 9264
Json(serde_json::json!({ "restored": id })).into_response() - 9265
} - 9266
- 9267
async fn delete_all_archived(State(state): State<AppState>) -> axum::response::Response { - 9268
let archive = read_archive(&state.core); - 9269
let local_archived: Vec<String> = archive - 9270
.into_iter() - 9271
.filter(|(id, archived)| *archived && find_session_in_cwd(&state.core, id)) - 9272
.map(|(id, _)| id) - 9273
.collect(); - 9274
let running_archived = local_archived.iter().any(|id| { - 9275
state.get(id).is_some_and(|handle| { - 9276
handle - 9277
.session - 9278
.lock() - 9279
.unwrap_or_else(std::sync::PoisonError::into_inner) - 9280
.is_none() - 9281
}) - 9282
}); - 9283
if running_archived { - 9284
return ( - 9285
StatusCode::CONFLICT, - 9286
Json(serde_json::json!({ "error": "stop running archived tasks before deleting all" })), - 9287
) - 9288
.into_response(); - 9289
} - 9290
let home = state.core.shared_data_home(); - 9291
let already = vak_core::trash::trashed(&home); - 9292
let newly: Vec<String> = local_archived - 9293
.into_iter() - 9294
.filter(|id| !already.contains(id)) - 9295
.collect(); - 9296
if let Err(error) = vak_core::trash::set(&home, &newly, true) { - 9297
return trash_write_failed(&error); - 9298
} - 9299
for id in &newly { - 9300
state.forget_session(id); - 9301
state.core.forget_spend_gate(id); - 9302
} - 9303
Json(serde_json::json!({ "trashed": newly.len() })).into_response() - 9304
} - 9305
- 9306
async fn list_skills( - 9307
State(state): State<AppState>, - 9308
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9309
) -> axum::response::Response { - 9310
use axum::response::IntoResponse; - 9311
let core = scoped_core!(&state, None, q.agent.as_deref()); - 9312
// `path` and `scope` tell the reader WHERE a skill came from. Discovery - 9313
// reads two roots (`<cwd>/.vak/skills` then the Shared - 9314
// `~/vak-home/.vak/skills` root), and a workspace skill is a very different - 9315
// trust proposition from a Shared skill - 9316
// one -- the admin console groups by this. - 9317
let workspace_root = core.cwd().join(".vak/skills"); - 9318
let shared_root = vak_config::paths::default_workspace().join(".vak/skills"); - 9319
let skills: Vec<serde_json::Value> = core - 9320
.skills_with_shadowed() - 9321
.iter() - 9322
.map(|s| { - 9323
let scope = if s.path.starts_with(&shared_root) { - 9324
"user" - 9325
} else if s.path.starts_with(&workspace_root) { - 9326
"workspace" - 9327
} else { - 9328
"user" - 9329
}; - 9330
serde_json::json!({ - 9331
"name": s.name, - 9332
"description": s.description, - 9333
"path": s.path.display().to_string(), - 9334
"scope": scope, - 9335
"provenance": s.provenance, - 9336
"shadowed": s.shadowed, - 9337
}) - 9338
}) - 9339
.collect(); - 9340
Json(serde_json::json!({ "skills": skills })).into_response() - 9341
} - 9342
- 9343
async fn list_commands( - 9344
State(state): State<AppState>, - 9345
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9346
) -> axum::response::Response { - 9347
use axum::response::IntoResponse; - 9348
let core = scoped_core!(&state, None, q.agent.as_deref()); - 9349
Json( - 9350
serde_json::json!({ "commands": core.custom_commands().into_iter().map(|command| serde_json::json!({ - 9351
"name": command.name, "description": command.description, "source": command.source, - 9352
})).collect::<Vec<_>>() }), - 9353
) - 9354
.into_response() - 9355
} - 9356
- 9357
#[derive(Debug, serde::Deserialize)] - 9358
struct PluginMutation { - 9359
path: Option<PathBuf>, - 9360
#[serde(default)] - 9361
scope: Option<InstallScope>, - 9362
#[serde(default)] - 9363
allow_unlicensed: bool, - 9364
#[serde(default)] - 9365
agent: Option<String>, - 9366
} - 9367
- 9368
#[derive(Debug, serde::Deserialize)] - 9369
struct PluginSourceMutation { - 9370
path: PathBuf, - 9371
#[serde(default)] - 9372
scope: Option<InstallScope>, - 9373
#[serde(default)] - 9374
label: String, - 9375
#[serde(default = "default_marketplace_trust")] - 9376
trust: MarketplaceTrust, - 9377
key_id: Option<String>, - 9378
public_key: Option<String>, - 9379
signature: Option<String>, - 9380
#[serde(default)] - 9381
agent: Option<String>, - 9382
} - 9383
- 9384
fn default_marketplace_trust() -> MarketplaceTrust { - 9385
MarketplaceTrust::ManualReview - 9386
} - 9387
- 9388
fn plugin_store(core: &vak_core::Core, scope: InstallScope) -> PluginStore { - 9389
let root = match scope { - 9390
InstallScope::User => vak_config::paths::default_workspace().join(".vak"), - 9391
InstallScope::Workspace => core.cwd().join(".vak"), - 9392
}; - 9393
PluginStore::new(root) - 9394
} - 9395
- 9396
#[derive(Debug, serde::Deserialize)] - 9397
struct PluginScopeQuery { - 9398
scope: Option<InstallScope>, - 9399
#[serde(default)] - 9400
agent: Option<String>, - 9401
} - 9402
- 9403
async fn list_retired_plugins( - 9404
State(state): State<AppState>, - 9405
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9406
) -> axum::response::Response { - 9407
use axum::response::IntoResponse; - 9408
let core = scoped_core!(&state, None, q.agent.as_deref()); - 9409
let mut retired = Vec::new(); - 9410
for scope in [InstallScope::User, InstallScope::Workspace] { - 9411
if let Ok(flagged) = plugin_store(&core, scope).retired_plugins() { - 9412
for (name, tools) in flagged { - 9413
retired.push(serde_json::json!({ - 9414
"name": name, - 9415
"retired_tools": tools, - 9416
"repair": "run `vak setup seed` to auto-remove, or `vak plugins remove <name>`", - 9417
})); - 9418
} - 9419
} - 9420
} - 9421
Json(serde_json::json!({ "retired": retired })).into_response() - 9422
} - 9423
- 9424
async fn remove_retired_plugins( - 9425
State(state): State<AppState>, - 9426
axum::extract::Query(q): axum::extract::Query<AgentScopeQuery>, - 9427
) -> axum::response::Response { - 9428
let core = scoped_core!(&state, None, q.agent.as_deref()); - 9429
let mut removed = Vec::new(); - 9430
let mut errors = Vec::new(); - 9431
for scope in [InstallScope::User, InstallScope::Workspace] { - 9432
let store = plugin_store(&core, scope); - 9433
if let Ok(flagged) = store.retired_plugins() { - 9434
for (name, _) in &flagged { - 9435
match store.remove(name) { - 9436
Ok(_) => { - 9437
removed.push(name.clone()); - 9438
let config = match scope { - 9439
InstallScope::User => vak_config::global_path(), - 9440
InstallScope::Workspace => Some(vak_config::project_path(core.cwd())), - 9441
}; - 9442
if let Some(config) = config.filter(|path| path.is_file()) - 9443
&& let Err(e) = vak_config::prune_plugins_network_allow(&config, name) - 9444
{ - 9445
errors.push(format!("{name}: {e}")); - 9446
} - 9447
} - 9448
Err(e) => errors.push(format!("{name}: {e}")), - 9449
} - 9450
} - 9451
} - 9452
} - 9453
if errors.is_empty() { - 9454
( - 9455
StatusCode::OK, - 9456
Json(serde_json::json!({ "removed": removed })), - 9457
) - 9458
.into_response() - 9459
} else { - 9460
( - 9461
StatusCode::PARTIAL_CONTENT, - 9462
Json(serde_json::json!({ "removed": removed, "errors": errors })), - 9463
) - 9464
.into_response() - 9465
} - 9466
} - 9467
- 9468
#[derive(Debug, serde::Deserialize)] - 9469
struct PluginCatalogQuery { - 9470
scope: Option<InstallScope>, - 9471
q: Option<String>, - 9472
#[serde(default)] - 9473
agent: Option<String>, - 9474
} - 9475
- 9476
fn requested_plugin_scopes(scope: Option<InstallScope>) -> Vec<InstallScope> { - 9477
scope.map_or_else( - 9478
|| vec![InstallScope::User, InstallScope::Workspace], - 9479
|scope| vec![scope], - 9480
) - 9481
} - 9482
- 9483
// Presentations are process-default-workspace scoped today, unlike the - 9484
// plugin store itself; out of scope for this per-Agent isolation pass - 9485
// (not one of the audited endpoints) and left untouched deliberately. - 9486
fn presentation_store(state: &AppState) -> vak_store::presentation::PresentationStore { - 9487
vak_store::presentation::PresentationStore::new( - 9488
state.core.sessions_home().join("presentations.json"), - 9489
) - 9490
} - 9491
- 9492
#[derive(Debug, serde::Deserialize)] - 9493
struct PresentationPackBody { - 9494
records: Vec<vak_presentation::StoredPresentation>, - 9495
} - 9496
- 9497
#[derive(Debug, serde::Serialize, serde::Deserialize)] - 9498
struct PresentationExport { - 9499
schema_version: u16, - 9500
definitions: Vec<vak_presentation::StoredPresentation>, - 9501
activations: Vec<vak_presentation::PresentationActivation>, - 9502
} - 9503
- 9504
async fn list_presentations(State(state): State<AppState>) -> axum::response::Response { - 9505
let store = presentation_store(&state); - 9506
match store.load().and_then(|mut library| { - 9507
let before = library.definitions().count(); - 9508
let mut changed = false; - 9509
for seed in vak_presentation::seeds::built_in_seed_pack() { - 9510
if let Some(existing) = library.get(&seed.spec.id, seed.spec.revision) - 9511
&& existing.digest != seed.digest - 9512
{ - 9513
changed = true; - 9514
} - 9515
library.register(seed).map_err(|error| { - 9516
vak_store::presentation::PresentationStoreError::Invalid(error.to_string()) - 9517
})?; - 9518
} - 9519
if changed || library.definitions().count() != before { - 9520
store.save(&library)?; - 9521
} - 9522
Ok(effective_presentation_library( - 9523
&library, - 9524
&state.active_core().cwd().to_string_lossy(), - 9525
)) - 9526
}) { - 9527
Ok(library) => Json(serde_json::json!({ - 9528
"definitions": library.definitions().collect::<Vec<_>>(), - 9529
"activations": library.activations(), - 9530
})) - 9531
.into_response(), - 9532
Err(error) => ( - 9533
StatusCode::INTERNAL_SERVER_ERROR, - 9534
Json(serde_json::json!({ "error": error.to_string() })), - 9535
) - 9536
.into_response(), - 9537
} - 9538
} - 9539
- 9540
async fn export_presentations(State(state): State<AppState>) -> axum::response::Response { - 9541
match presentation_store(&state).load() { - 9542
Ok(library) => Json(PresentationExport { - 9543
schema_version: 1, - 9544
definitions: library.definitions().cloned().collect(), - 9545
activations: library.activations().to_vec(), - 9546
}) - 9547
.into_response(), - 9548
Err(error) => ( - 9549
StatusCode::INTERNAL_SERVER_ERROR, - 9550
Json(serde_json::json!({ "error": error.to_string() })), - 9551
) - 9552
.into_response(), - 9553
} - 9554
} - 9555
- 9556
async fn import_presentations( - 9557
State(state): State<AppState>, - 9558
Json(pack): Json<PresentationExport>, - 9559
) -> axum::response::Response { - 9560
if pack.schema_version != 1 { - 9561
return ( - 9562
StatusCode::BAD_REQUEST, - 9563
Json(serde_json::json!({ "error": "unsupported presentation pack schema" })), - 9564
) - 9565
.into_response(); - 9566
} - 9567
let store = presentation_store(&state); - 9568
let result = store.load().and_then(|mut library| { - 9569
for mut definition in pack.definitions { - 9570
// Pack import is always a preview operation. Never trust an - 9571
// enabled bit or foreign owner from an external serialized - 9572
// projection. Imported definitions belong to this user's - 9573
// library, so they can actually be activated after review. - 9574
definition.enabled = false; - 9575
definition.origin.scope = vak_presentation::LibraryScope::User; - 9576
definition.origin.owner = "user".into(); - 9577
definition.origin.plugin_id = None; - 9578
library.register(definition).map_err(|error| { - 9579
vak_store::presentation::PresentationStoreError::Invalid(error.to_string()) - 9580
})?; - 9581
} - 9582
store.save(&library) - 9583
}); - 9584
match result { - 9585
Ok(()) => Json(serde_json::json!({ "imported": true })).into_response(), - 9586
Err(error) => ( - 9587
StatusCode::BAD_REQUEST, - 9588
Json(serde_json::json!({ "error": error.to_string() })), - 9589
) - 9590
.into_response(), - 9591
} - 9592
} - 9593
- 9594
async fn get_presentation_spec( - 9595
State(state): State<AppState>, - 9596
Path((id, revision)): Path<(String, u64)>, - 9597
) -> axum::response::Response { - 9598
match presentation_store(&state).load() { - 9599
Ok(library) => match library.get(&id, revision) { - 9600
Some(definition) => Json(definition).into_response(), - 9601
None => StatusCode::NOT_FOUND.into_response(), - 9602
}, - 9603
Err(error) => ( - 9604
StatusCode::INTERNAL_SERVER_ERROR, - 9605
Json(serde_json::json!({ "error": error.to_string() })), - 9606
) - 9607
.into_response(), - 9608
} - 9609
} - 9610
- 9611
async fn register_presentations( - 9612
State(state): State<AppState>, - 9613
Json(body): Json<PresentationPackBody>, - 9614
) -> axum::response::Response { - 9615
match presentation_store(&state).register_pack(body.records) { - 9616
Ok(count) => Json(serde_json::json!({ "registered": count })).into_response(), - 9617
Err(error) => ( - 9618
StatusCode::BAD_REQUEST, - 9619
Json(serde_json::json!({ "error": error.to_string() })), - 9620
) - 9621
.into_response(), - 9622
} - 9623
} - 9624
- 9625
#[derive(Debug, serde::Deserialize)] - 9626
struct PresentationScopeBody { - 9627
scope: vak_presentation::LibraryScope, - 9628
owner: String, - 9629
} - 9630
- 9631
#[derive(Debug, serde::Deserialize)] - 9632
struct PresentationRevisionBody { - 9633
request: vak_presentation::PresentationRevisionRequest, - 9634
proposed: vak_presentation::PresentationSpec, - 9635
origin: vak_presentation::PresentationOrigin, - 9636
#[serde(default)] - 9637
chain_id: Option<String>, - 9638
} - 9639
- 9640
async fn propose_presentation_revision( - 9641
State(state): State<AppState>, - 9642
Json(body): Json<PresentationRevisionBody>, - 9643
) -> axum::response::Response { - 9644
let store = presentation_store(&state); - 9645
let result = store.load().and_then(|mut library| { - 9646
let revision = library - 9647
.register_revision(body.request, body.proposed, body.origin) - 9648
.map_err(|error| { - 9649
vak_store::presentation::PresentationStoreError::Invalid(error.to_string()) - 9650
})?; - 9651
store.save(&library)?; - 9652
Ok(revision) - 9653
}); - 9654
match result { - 9655
Ok(revision) => Json(revision).into_response(), - 9656
Err(error) => ( - 9657
StatusCode::BAD_REQUEST, - 9658
Json(serde_json::json!({ "error": error.to_string() })), - 9659
) - 9660
.into_response(), - 9661
} - 9662
} - 9663
- 9664
async fn propose_session_presentation_revision( - 9665
State(state): State<AppState>, - 9666
Path(id): Path<String>, - 9667
Json(body): Json<PresentationRevisionBody>, - 9668
) -> axum::response::Response { - 9669
let Some(handle) = state.get(&id) else { - 9670
return StatusCode::NOT_FOUND.into_response(); - 9671
}; - 9672
let chain_id = body - 9673
.chain_id - 9674
.clone() - 9675
.unwrap_or_else(|| body.request.base_id.clone()); - 9676
if chain_id.trim().is_empty() || chain_id.len() > 256 { - 9677
return StatusCode::BAD_REQUEST.into_response(); - 9678
} - 9679
let (attempts, rejected) = { - 9680
let activities = handle - 9681
.activity_buffer - 9682
.lock() - 9683
.unwrap_or_else(std::sync::PoisonError::into_inner); - 9684
activities - 9685
.iter() - 9686
.fold((0_u8, 0_u8), |(attempts, rejected), activity| { - 9687
if activity.data.get("chain_id") != Some(&chain_id) { - 9688
return (attempts, rejected); - 9689
} - 9690
match activity.kind { - 9691
vak_session::ActivityKind::PresentationProposal => ( - 9692
attempts.saturating_add(1), - 9693
rejected.saturating_add(u8::from( - 9694
activity.status == vak_session::ActivityStatus::Failed, - 9695
)), - 9696
), - 9697
vak_session::ActivityKind::PresentationFeedback - 9698
if activity.status == vak_session::ActivityStatus::Denied => - 9699
{ - 9700
(attempts, rejected.saturating_add(1)) - 9701
} - 9702
_ => (attempts, rejected), - 9703
} - 9704
}) - 9705
}; - 9706
if attempts >= 2 || rejected >= 2 || body.request.attempt == 0 || body.request.attempt > 2 { - 9707
return ( - 9708
StatusCode::TOO_MANY_REQUESTS, - 9709
Json(serde_json::json!({ "error": "presentation proposal chain exhausted" })), - 9710
) - 9711
.into_response(); - 9712
} - 9713
let store = presentation_store(&state); - 9714
let result = store.load().and_then(|mut library| { - 9715
let revision = library - 9716
.register_revision(body.request, body.proposed, body.origin) - 9717
.map_err(|error| { - 9718
vak_store::presentation::PresentationStoreError::Invalid(error.to_string()) - 9719
})?; - 9720
store.save(&library)?; - 9721
Ok(revision) - 9722
}); - 9723
match result { - 9724
Ok(revision) => { - 9725
let mut data = std::collections::BTreeMap::new(); - 9726
data.insert("spec_id".into(), revision.proposed.id.clone()); - 9727
data.insert("revision".into(), revision.proposed.revision.to_string()); - 9728
data.insert("digest".into(), revision.digest.clone()); - 9729
data.insert("chain_id".into(), chain_id.clone()); - 9730
let activity = vak_session::ActivityRecord { - 9731
activity_id: format!("presentation-proposal-{}", uuid::Uuid::now_v7()),
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.