Attribute agent workflow dispatch

This commit is contained in:
Michel Paulissen 2026-07-03 21:56:48 +02:00
parent 28779a2866
commit c514ab7f81
5 changed files with 503 additions and 14 deletions

View file

@ -558,6 +558,7 @@ enum CliSession {
Anonymous,
HumanSession,
AgentPublicKey {
agent: String,
public_key_fingerprint: Digest,
browser_interaction_required: bool,
},
@ -2285,11 +2286,13 @@ fn auth_state_value() -> Value {
})
}
CliSession::AgentPublicKey {
agent,
public_key_fingerprint,
browser_interaction_required,
} => json!({
"kind": "agent_public_key",
"authenticated": true,
"agent": agent,
"source": "DISASMER_AGENT_PUBLIC_KEY",
"public_key_fingerprint": public_key_fingerprint,
"browser_interaction_required": browser_interaction_required,
@ -3415,13 +3418,15 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
let coordinator = run_coordinator_endpoint(&plan)?;
let process = "vp-current".to_owned();
let mut session = JsonLineSession::connect(&coordinator)?;
let response = session.request_allow_error(json!({
let mut request = json!({
"type": "start_process",
"tenant": tenant.clone(),
"project": project.clone(),
"process": process.clone(),
"restart": false,
}))?;
});
add_workflow_actor_fields(&mut request, &plan.session, &user);
let response = session.request_allow_error(request)?;
let run_start = run_start_summary(&response);
let status = run_start
.get("status")
@ -3435,6 +3440,7 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
"tenant": tenant,
"project": project,
"user": user,
"workflow_actor": response.get("actor").cloned().unwrap_or(Value::Null),
"coordinator": coordinator,
"process": process,
"run_start": run_start,
@ -3464,6 +3470,7 @@ fn run_start_summary(response: &Value) -> Value {
"accepted": true,
"process": response.get("process").cloned().unwrap_or(Value::Null),
"coordinator_epoch": response.get("epoch").cloned().unwrap_or(Value::Null),
"actor": response.get("actor").cloned().unwrap_or(Value::Null),
"restart": false,
"single_active_process_boundary": true,
"next_actions": [
@ -3712,6 +3719,7 @@ fn session_from_env() -> CliSession {
if let Some(public_key) = std::env::var_os("DISASMER_AGENT_PUBLIC_KEY") {
let public_key = public_key.to_string_lossy();
return CliSession::AgentPublicKey {
agent: std::env::var("DISASMER_AGENT_ID").unwrap_or_else(|_| "agent".to_owned()),
public_key_fingerprint: Digest::sha256(public_key.as_bytes()),
browser_interaction_required: false,
};
@ -3722,6 +3730,28 @@ fn session_from_env() -> CliSession {
CliSession::Anonymous
}
fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback_user: &str) {
let Value::Object(map) = request else {
return;
};
match session {
CliSession::AgentPublicKey {
agent,
public_key_fingerprint,
..
} => {
map.insert("actor_agent".to_owned(), json!(agent));
map.insert(
"agent_public_key_fingerprint".to_owned(),
json!(public_key_fingerprint),
);
}
CliSession::HumanSession | CliSession::Anonymous => {
map.insert("actor_user".to_owned(), json!(fallback_user));
}
}
}
fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
let mut capabilities = NodeCapabilities::detect_current();
for cap in &args.caps {
@ -3989,6 +4019,7 @@ mod tests {
args,
PathBuf::from("/repo"),
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: Digest::sha256("agent-key"),
browser_interaction_required: false,
},
@ -4003,12 +4034,83 @@ mod tests {
assert_eq!(
plan.session,
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: Digest::sha256("agent-key"),
browser_interaction_required: false,
}
);
}
#[test]
fn run_with_agent_public_key_sends_attributable_workflow_actor() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user-live".to_owned(),
coordinator: None,
},
)
.unwrap();
let fingerprint = Digest::sha256("agent-key");
let expected_fingerprint = fingerprint.to_string();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"start_process""#));
assert!(line.contains(r#""tenant":"tenant-live""#));
assert!(line.contains(r#""project":"project-live""#));
assert!(line.contains(r#""actor_agent":"agent-ci""#));
assert!(line.contains(&format!(
r#""agent_public_key_fingerprint":"{expected_fingerprint}""#
)));
assert!(!line.contains(r#""actor_user""#));
stream
.write_all(
format!(
r#"{{"type":"process_started","process":"vp-current","epoch":7,"actor":{{"kind":"agent","user":"user-live","agent":"agent-ci","credential_kind":"public_key","public_key_fingerprint":"{expected_fingerprint}","authenticated_without_browser":true,"scopes":["project:read","project:run"]}}}}"#
)
.as_bytes(),
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = run_report(
RunArgs {
entry: Some("build".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
json: false,
},
PathBuf::from("/unused"),
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: fingerprint,
browser_interaction_required: false,
},
)
.unwrap();
server.join().unwrap();
assert_eq!(report["status"], "started");
assert_eq!(report["workflow_actor"]["kind"], "agent");
assert_eq!(report["workflow_actor"]["agent"], "agent-ci");
assert_eq!(
report["workflow_actor"]["authenticated_without_browser"],
true
);
assert_eq!(report["run_start"]["actor"]["kind"], "agent");
}
#[test]
fn run_local_flag_overrides_logged_in_hosted_selection() {
let Cli {