Attribute agent workflow dispatch
This commit is contained in:
parent
28779a2866
commit
c514ab7f81
5 changed files with 503 additions and 14 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "ed1fbc0c464627dc5cd56d5b01343bd084e04c95",
|
"source_commit": "3cea82a67d5b5aca88470c0d06677b8b4237da11",
|
||||||
"release_name": "dryrun-ed1fbc0c4646",
|
"release_name": "dryrun-3cea82a67d5b",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,7 @@ enum CliSession {
|
||||||
Anonymous,
|
Anonymous,
|
||||||
HumanSession,
|
HumanSession,
|
||||||
AgentPublicKey {
|
AgentPublicKey {
|
||||||
|
agent: String,
|
||||||
public_key_fingerprint: Digest,
|
public_key_fingerprint: Digest,
|
||||||
browser_interaction_required: bool,
|
browser_interaction_required: bool,
|
||||||
},
|
},
|
||||||
|
|
@ -2285,11 +2286,13 @@ fn auth_state_value() -> Value {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
CliSession::AgentPublicKey {
|
CliSession::AgentPublicKey {
|
||||||
|
agent,
|
||||||
public_key_fingerprint,
|
public_key_fingerprint,
|
||||||
browser_interaction_required,
|
browser_interaction_required,
|
||||||
} => json!({
|
} => json!({
|
||||||
"kind": "agent_public_key",
|
"kind": "agent_public_key",
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
|
"agent": agent,
|
||||||
"source": "DISASMER_AGENT_PUBLIC_KEY",
|
"source": "DISASMER_AGENT_PUBLIC_KEY",
|
||||||
"public_key_fingerprint": public_key_fingerprint,
|
"public_key_fingerprint": public_key_fingerprint,
|
||||||
"browser_interaction_required": browser_interaction_required,
|
"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 coordinator = run_coordinator_endpoint(&plan)?;
|
||||||
let process = "vp-current".to_owned();
|
let process = "vp-current".to_owned();
|
||||||
let mut session = JsonLineSession::connect(&coordinator)?;
|
let mut session = JsonLineSession::connect(&coordinator)?;
|
||||||
let response = session.request_allow_error(json!({
|
let mut request = json!({
|
||||||
"type": "start_process",
|
"type": "start_process",
|
||||||
"tenant": tenant.clone(),
|
"tenant": tenant.clone(),
|
||||||
"project": project.clone(),
|
"project": project.clone(),
|
||||||
"process": process.clone(),
|
"process": process.clone(),
|
||||||
"restart": false,
|
"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 run_start = run_start_summary(&response);
|
||||||
let status = run_start
|
let status = run_start
|
||||||
.get("status")
|
.get("status")
|
||||||
|
|
@ -3435,6 +3440,7 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
|
||||||
"tenant": tenant,
|
"tenant": tenant,
|
||||||
"project": project,
|
"project": project,
|
||||||
"user": user,
|
"user": user,
|
||||||
|
"workflow_actor": response.get("actor").cloned().unwrap_or(Value::Null),
|
||||||
"coordinator": coordinator,
|
"coordinator": coordinator,
|
||||||
"process": process,
|
"process": process,
|
||||||
"run_start": run_start,
|
"run_start": run_start,
|
||||||
|
|
@ -3464,6 +3470,7 @@ fn run_start_summary(response: &Value) -> Value {
|
||||||
"accepted": true,
|
"accepted": true,
|
||||||
"process": response.get("process").cloned().unwrap_or(Value::Null),
|
"process": response.get("process").cloned().unwrap_or(Value::Null),
|
||||||
"coordinator_epoch": response.get("epoch").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,
|
"restart": false,
|
||||||
"single_active_process_boundary": true,
|
"single_active_process_boundary": true,
|
||||||
"next_actions": [
|
"next_actions": [
|
||||||
|
|
@ -3712,6 +3719,7 @@ fn session_from_env() -> CliSession {
|
||||||
if let Some(public_key) = std::env::var_os("DISASMER_AGENT_PUBLIC_KEY") {
|
if let Some(public_key) = std::env::var_os("DISASMER_AGENT_PUBLIC_KEY") {
|
||||||
let public_key = public_key.to_string_lossy();
|
let public_key = public_key.to_string_lossy();
|
||||||
return CliSession::AgentPublicKey {
|
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()),
|
public_key_fingerprint: Digest::sha256(public_key.as_bytes()),
|
||||||
browser_interaction_required: false,
|
browser_interaction_required: false,
|
||||||
};
|
};
|
||||||
|
|
@ -3722,6 +3730,28 @@ fn session_from_env() -> CliSession {
|
||||||
CliSession::Anonymous
|
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 {
|
fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
|
||||||
let mut capabilities = NodeCapabilities::detect_current();
|
let mut capabilities = NodeCapabilities::detect_current();
|
||||||
for cap in &args.caps {
|
for cap in &args.caps {
|
||||||
|
|
@ -3989,6 +4019,7 @@ mod tests {
|
||||||
args,
|
args,
|
||||||
PathBuf::from("/repo"),
|
PathBuf::from("/repo"),
|
||||||
CliSession::AgentPublicKey {
|
CliSession::AgentPublicKey {
|
||||||
|
agent: "agent-ci".to_owned(),
|
||||||
public_key_fingerprint: Digest::sha256("agent-key"),
|
public_key_fingerprint: Digest::sha256("agent-key"),
|
||||||
browser_interaction_required: false,
|
browser_interaction_required: false,
|
||||||
},
|
},
|
||||||
|
|
@ -4003,12 +4034,83 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan.session,
|
plan.session,
|
||||||
CliSession::AgentPublicKey {
|
CliSession::AgentPublicKey {
|
||||||
|
agent: "agent-ci".to_owned(),
|
||||||
public_key_fingerprint: Digest::sha256("agent-key"),
|
public_key_fingerprint: Digest::sha256("agent-key"),
|
||||||
browser_interaction_required: false,
|
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]
|
#[test]
|
||||||
fn run_local_flag_overrides_logged_in_hosted_selection() {
|
fn run_local_flag_overrides_logged_in_hosted_selection() {
|
||||||
let Cli {
|
let Cli {
|
||||||
|
|
|
||||||
|
|
@ -473,6 +473,40 @@ impl Coordinator {
|
||||||
Ok(record.clone())
|
Ok(record.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn authorize_agent_project_run(
|
||||||
|
&self,
|
||||||
|
tenant: &TenantId,
|
||||||
|
project: &ProjectId,
|
||||||
|
agent: &AgentId,
|
||||||
|
public_key_fingerprint: &Digest,
|
||||||
|
) -> Result<AgentPublicKeyRecord, CoordinatorError> {
|
||||||
|
let record = self
|
||||||
|
.durable
|
||||||
|
.agent_public_keys
|
||||||
|
.get(&(tenant.clone(), project.clone(), agent.clone()))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
CoordinatorError::Unauthorized(
|
||||||
|
"agent public key is not registered for this tenant/project".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if record.revoked {
|
||||||
|
return Err(CoordinatorError::Unauthorized(
|
||||||
|
"agent public key has been revoked".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if &record.public_key_fingerprint != public_key_fingerprint {
|
||||||
|
return Err(CoordinatorError::Unauthorized(
|
||||||
|
"agent public key fingerprint does not match the registered key".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !record.scopes.iter().any(|scope| scope == "project:run") {
|
||||||
|
return Err(CoordinatorError::Unauthorized(
|
||||||
|
"agent public key is not scoped for project runs".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(record.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_process(
|
pub fn start_process(
|
||||||
&mut self,
|
&mut self,
|
||||||
tenant: TenantId,
|
tenant: TenantId,
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,12 @@ pub enum CoordinatorRequest {
|
||||||
LaunchTask {
|
LaunchTask {
|
||||||
tenant: String,
|
tenant: String,
|
||||||
project: String,
|
project: String,
|
||||||
actor_user: String,
|
#[serde(default)]
|
||||||
|
actor_user: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
actor_agent: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
agent_public_key_fingerprint: Option<Digest>,
|
||||||
process: String,
|
process: String,
|
||||||
task: String,
|
task: String,
|
||||||
environment: Option<EnvironmentRequirements>,
|
environment: Option<EnvironmentRequirements>,
|
||||||
|
|
@ -193,6 +198,12 @@ pub enum CoordinatorRequest {
|
||||||
StartProcess {
|
StartProcess {
|
||||||
tenant: String,
|
tenant: String,
|
||||||
project: String,
|
project: String,
|
||||||
|
#[serde(default)]
|
||||||
|
actor_user: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
actor_agent: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
agent_public_key_fingerprint: Option<Digest>,
|
||||||
process: String,
|
process: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
restart: bool,
|
restart: bool,
|
||||||
|
|
@ -426,6 +437,17 @@ pub struct DebugAuditEvent {
|
||||||
pub used_debug_read_bytes: u64,
|
pub used_debug_read_bytes: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct WorkflowActor {
|
||||||
|
pub kind: String,
|
||||||
|
pub user: Option<UserId>,
|
||||||
|
pub agent: Option<AgentId>,
|
||||||
|
pub credential_kind: CredentialKind,
|
||||||
|
pub public_key_fingerprint: Option<Digest>,
|
||||||
|
pub authenticated_without_browser: bool,
|
||||||
|
pub scopes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct TaskAssignment {
|
pub struct TaskAssignment {
|
||||||
pub tenant: TenantId,
|
pub tenant: TenantId,
|
||||||
|
|
@ -539,6 +561,7 @@ pub enum CoordinatorResponse {
|
||||||
TaskLaunched {
|
TaskLaunched {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
task: TaskId,
|
task: TaskId,
|
||||||
|
actor: WorkflowActor,
|
||||||
placement: Placement,
|
placement: Placement,
|
||||||
assignment: TaskAssignment,
|
assignment: TaskAssignment,
|
||||||
},
|
},
|
||||||
|
|
@ -560,6 +583,7 @@ pub enum CoordinatorResponse {
|
||||||
ProcessStarted {
|
ProcessStarted {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
epoch: u64,
|
epoch: u64,
|
||||||
|
actor: WorkflowActor,
|
||||||
},
|
},
|
||||||
NodeReconnected {
|
NodeReconnected {
|
||||||
node: NodeId,
|
node: NodeId,
|
||||||
|
|
@ -1222,6 +1246,8 @@ impl CoordinatorService {
|
||||||
tenant,
|
tenant,
|
||||||
project,
|
project,
|
||||||
actor_user,
|
actor_user,
|
||||||
|
actor_agent,
|
||||||
|
agent_public_key_fingerprint,
|
||||||
process,
|
process,
|
||||||
task,
|
task,
|
||||||
environment,
|
environment,
|
||||||
|
|
@ -1240,9 +1266,13 @@ impl CoordinatorService {
|
||||||
let project = ProjectId::new(project);
|
let project = ProjectId::new(project);
|
||||||
let process = ProcessId::new(process);
|
let process = ProcessId::new(process);
|
||||||
let task = TaskId::new(task);
|
let task = TaskId::new(task);
|
||||||
let actor = UserId::new(actor_user);
|
let actor = self.workflow_actor(
|
||||||
self.coordinator
|
&tenant,
|
||||||
.upsert_user(tenant.clone(), actor, CredentialKind::BrowserSession);
|
&project,
|
||||||
|
actor_user,
|
||||||
|
actor_agent,
|
||||||
|
agent_public_key_fingerprint,
|
||||||
|
)?;
|
||||||
let active = self.coordinator.active_process(&process).ok_or_else(|| {
|
let active = self.coordinator.active_process(&process).ok_or_else(|| {
|
||||||
CoordinatorError::Unauthorized(
|
CoordinatorError::Unauthorized(
|
||||||
"task launch requires an active coordinator-side virtual process"
|
"task launch requires an active coordinator-side virtual process"
|
||||||
|
|
@ -1309,6 +1339,7 @@ impl CoordinatorService {
|
||||||
Ok(CoordinatorResponse::TaskLaunched {
|
Ok(CoordinatorResponse::TaskLaunched {
|
||||||
process,
|
process,
|
||||||
task,
|
task,
|
||||||
|
actor,
|
||||||
placement,
|
placement,
|
||||||
assignment,
|
assignment,
|
||||||
})
|
})
|
||||||
|
|
@ -1445,6 +1476,9 @@ impl CoordinatorService {
|
||||||
CoordinatorRequest::StartProcess {
|
CoordinatorRequest::StartProcess {
|
||||||
tenant,
|
tenant,
|
||||||
project,
|
project,
|
||||||
|
actor_user,
|
||||||
|
actor_agent,
|
||||||
|
agent_public_key_fingerprint,
|
||||||
process,
|
process,
|
||||||
restart,
|
restart,
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -1452,6 +1486,13 @@ impl CoordinatorService {
|
||||||
let project = ProjectId::new(project);
|
let project = ProjectId::new(project);
|
||||||
let process = ProcessId::new(process);
|
let process = ProcessId::new(process);
|
||||||
self.coordinator.ensure_tenant_active(&tenant)?;
|
self.coordinator.ensure_tenant_active(&tenant)?;
|
||||||
|
let actor = self.workflow_actor(
|
||||||
|
&tenant,
|
||||||
|
&project,
|
||||||
|
actor_user,
|
||||||
|
actor_agent,
|
||||||
|
agent_public_key_fingerprint,
|
||||||
|
)?;
|
||||||
if let Some(active) = self
|
if let Some(active) = self
|
||||||
.coordinator
|
.coordinator
|
||||||
.active_process_for_project(&tenant, &project)
|
.active_process_for_project(&tenant, &project)
|
||||||
|
|
@ -1492,6 +1533,7 @@ impl CoordinatorService {
|
||||||
Ok(CoordinatorResponse::ProcessStarted {
|
Ok(CoordinatorResponse::ProcessStarted {
|
||||||
process,
|
process,
|
||||||
epoch: self.coordinator.coordinator_epoch(),
|
epoch: self.coordinator.coordinator_epoch(),
|
||||||
|
actor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
CoordinatorRequest::ReconnectNode {
|
CoordinatorRequest::ReconnectNode {
|
||||||
|
|
@ -2245,6 +2287,55 @@ impl CoordinatorService {
|
||||||
Ok(event)
|
Ok(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn workflow_actor(
|
||||||
|
&mut self,
|
||||||
|
tenant: &TenantId,
|
||||||
|
project: &ProjectId,
|
||||||
|
actor_user: Option<String>,
|
||||||
|
actor_agent: Option<String>,
|
||||||
|
agent_public_key_fingerprint: Option<Digest>,
|
||||||
|
) -> Result<WorkflowActor, CoordinatorServiceError> {
|
||||||
|
if let Some(agent) = actor_agent {
|
||||||
|
let agent = AgentId::new(agent);
|
||||||
|
let public_key_fingerprint = agent_public_key_fingerprint.ok_or_else(|| {
|
||||||
|
CoordinatorError::Unauthorized(
|
||||||
|
"agent workflow dispatch requires public key fingerprint".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let record = self.coordinator.authorize_agent_project_run(
|
||||||
|
tenant,
|
||||||
|
project,
|
||||||
|
&agent,
|
||||||
|
&public_key_fingerprint,
|
||||||
|
)?;
|
||||||
|
return Ok(WorkflowActor {
|
||||||
|
kind: "agent".to_owned(),
|
||||||
|
user: Some(record.user),
|
||||||
|
agent: Some(agent),
|
||||||
|
credential_kind: CredentialKind::PublicKey,
|
||||||
|
public_key_fingerprint: Some(public_key_fingerprint),
|
||||||
|
authenticated_without_browser: true,
|
||||||
|
scopes: record.scopes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let actor = UserId::new(actor_user.unwrap_or_else(|| "user".to_owned()));
|
||||||
|
self.coordinator.upsert_user(
|
||||||
|
tenant.clone(),
|
||||||
|
actor.clone(),
|
||||||
|
CredentialKind::BrowserSession,
|
||||||
|
);
|
||||||
|
Ok(WorkflowActor {
|
||||||
|
kind: "user".to_owned(),
|
||||||
|
user: Some(actor),
|
||||||
|
agent: None,
|
||||||
|
credential_kind: CredentialKind::BrowserSession,
|
||||||
|
public_key_fingerprint: None,
|
||||||
|
authenticated_without_browser: false,
|
||||||
|
scopes: vec!["project:read".to_owned(), "project:run".to_owned()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn export_endpoint(
|
fn export_endpoint(
|
||||||
&self,
|
&self,
|
||||||
node: &NodeId,
|
node: &NodeId,
|
||||||
|
|
@ -2681,6 +2772,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_agent_workflow_actor(actor: &WorkflowActor, fingerprint: &Digest) {
|
||||||
|
assert_eq!(actor.kind, "agent");
|
||||||
|
assert_eq!(actor.user, Some(UserId::from("user")));
|
||||||
|
assert_eq!(actor.agent, Some(AgentId::from("agent-ci")));
|
||||||
|
assert_eq!(actor.credential_kind, CredentialKind::PublicKey);
|
||||||
|
assert_eq!(actor.public_key_fingerprint.as_ref(), Some(fingerprint));
|
||||||
|
assert!(actor.authenticated_without_browser);
|
||||||
|
assert!(actor.scopes.iter().any(|scope| scope == "project:run"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn service_creates_selects_and_lists_signed_in_user_projects() {
|
fn service_creates_selects_and_lists_signed_in_user_projects() {
|
||||||
let mut service = CoordinatorService::new(7);
|
let mut service = CoordinatorService::new(7);
|
||||||
|
|
@ -2812,6 +2913,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2926,6 +3030,160 @@ mod tests {
|
||||||
assert!(record.revoked);
|
assert!(record.revoked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn service_runs_agent_workflows_with_scoped_key_attribution() {
|
||||||
|
let mut service = CoordinatorService::new(7);
|
||||||
|
let agent_fingerprint = Digest::sha256("agent-key-v1");
|
||||||
|
|
||||||
|
service
|
||||||
|
.handle_request(CoordinatorRequest::RegisterAgentPublicKey {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
user: "user".to_owned(),
|
||||||
|
agent: "agent-ci".to_owned(),
|
||||||
|
public_key: "agent-key-v1".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bad_key = service
|
||||||
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: Some("agent-ci".to_owned()),
|
||||||
|
agent_public_key_fingerprint: Some(Digest::sha256("other-key")),
|
||||||
|
process: "vp-agent-bad-key".to_owned(),
|
||||||
|
restart: false,
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(bad_key.to_string().contains("fingerprint"));
|
||||||
|
|
||||||
|
service
|
||||||
|
.handle_request(CoordinatorRequest::AttachNode {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
node: "worker-linux".to_owned(),
|
||||||
|
public_key: "worker-linux-public-key".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
service
|
||||||
|
.handle_request(CoordinatorRequest::ReportNodeCapabilities {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
node: "worker-linux".to_owned(),
|
||||||
|
capabilities: linux_capabilities(),
|
||||||
|
cached_environment_digests: Vec::new(),
|
||||||
|
dependency_cache_digests: Vec::new(),
|
||||||
|
source_snapshots: Vec::new(),
|
||||||
|
artifact_locations: Vec::new(),
|
||||||
|
direct_connectivity: true,
|
||||||
|
online: true,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let CoordinatorResponse::ProcessStarted {
|
||||||
|
actor: start_actor, ..
|
||||||
|
} = service
|
||||||
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: Some("agent-ci".to_owned()),
|
||||||
|
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
|
||||||
|
process: "vp-agent".to_owned(),
|
||||||
|
restart: false,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected agent-authenticated process start");
|
||||||
|
};
|
||||||
|
assert_agent_workflow_actor(&start_actor, &agent_fingerprint);
|
||||||
|
|
||||||
|
let CoordinatorResponse::TaskLaunched {
|
||||||
|
actor: launch_actor,
|
||||||
|
placement,
|
||||||
|
assignment,
|
||||||
|
..
|
||||||
|
} = service
|
||||||
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: Some("agent-ci".to_owned()),
|
||||||
|
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
|
||||||
|
process: "vp-agent".to_owned(),
|
||||||
|
task: "compile-linux".to_owned(),
|
||||||
|
environment: None,
|
||||||
|
environment_digest: None,
|
||||||
|
required_capabilities: vec![Capability::Command],
|
||||||
|
dependency_cache: None,
|
||||||
|
source_snapshot: None,
|
||||||
|
required_artifacts: Vec::new(),
|
||||||
|
quota_available: true,
|
||||||
|
policy_allowed: true,
|
||||||
|
command: "cargo".to_owned(),
|
||||||
|
command_args: vec!["test".to_owned()],
|
||||||
|
artifact_path: "/vfs/artifacts/dap-output.txt".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected agent-authenticated task launch");
|
||||||
|
};
|
||||||
|
assert_agent_workflow_actor(&launch_actor, &agent_fingerprint);
|
||||||
|
assert_eq!(placement.node, NodeId::from("worker-linux"));
|
||||||
|
assert_eq!(assignment.node, NodeId::from("worker-linux"));
|
||||||
|
|
||||||
|
let quota_error = service
|
||||||
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: Some("agent-ci".to_owned()),
|
||||||
|
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
|
||||||
|
process: "vp-agent".to_owned(),
|
||||||
|
task: "compile-linux-quota".to_owned(),
|
||||||
|
environment: None,
|
||||||
|
environment_digest: None,
|
||||||
|
required_capabilities: vec![Capability::Command],
|
||||||
|
dependency_cache: None,
|
||||||
|
source_snapshot: None,
|
||||||
|
required_artifacts: Vec::new(),
|
||||||
|
quota_available: false,
|
||||||
|
policy_allowed: true,
|
||||||
|
command: "cargo".to_owned(),
|
||||||
|
command_args: vec!["test".to_owned()],
|
||||||
|
artifact_path: "/vfs/artifacts/quota.txt".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(quota_error
|
||||||
|
.to_string()
|
||||||
|
.contains("quota unavailable for placement"));
|
||||||
|
|
||||||
|
let policy_error = service
|
||||||
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: Some("agent-ci".to_owned()),
|
||||||
|
agent_public_key_fingerprint: Some(agent_fingerprint),
|
||||||
|
process: "vp-agent".to_owned(),
|
||||||
|
task: "compile-linux-policy".to_owned(),
|
||||||
|
environment: None,
|
||||||
|
environment_digest: None,
|
||||||
|
required_capabilities: vec![Capability::Command],
|
||||||
|
dependency_cache: None,
|
||||||
|
source_snapshot: None,
|
||||||
|
required_artifacts: Vec::new(),
|
||||||
|
quota_available: true,
|
||||||
|
policy_allowed: false,
|
||||||
|
command: "cargo".to_owned(),
|
||||||
|
command_args: vec!["test".to_owned()],
|
||||||
|
artifact_path: "/vfs/artifacts/policy.txt".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(policy_error.to_string().contains("policy denied placement"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn service_attaches_node_starts_process_and_records_scoped_task_event() {
|
fn service_attaches_node_starts_process_and_records_scoped_task_event() {
|
||||||
let mut service = CoordinatorService::new(7);
|
let mut service = CoordinatorService::new(7);
|
||||||
|
|
@ -2944,6 +3202,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2952,7 +3213,16 @@ mod tests {
|
||||||
started,
|
started,
|
||||||
CoordinatorResponse::ProcessStarted {
|
CoordinatorResponse::ProcessStarted {
|
||||||
process: ProcessId::from("process"),
|
process: ProcessId::from("process"),
|
||||||
epoch: 7
|
epoch: 7,
|
||||||
|
actor: WorkflowActor {
|
||||||
|
kind: "user".to_owned(),
|
||||||
|
user: Some(UserId::from("user")),
|
||||||
|
agent: None,
|
||||||
|
credential_kind: CredentialKind::BrowserSession,
|
||||||
|
public_key_fingerprint: None,
|
||||||
|
authenticated_without_browser: false,
|
||||||
|
scopes: vec!["project:read".to_owned(), "project:run".to_owned()],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -3178,6 +3448,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3294,6 +3567,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3390,6 +3666,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3467,7 +3746,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::LaunchTask {
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
actor_user: "user".to_owned(),
|
actor_user: Some("user".to_owned()),
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
task: "task".to_owned(),
|
task: "task".to_owned(),
|
||||||
environment: None,
|
environment: None,
|
||||||
|
|
@ -3599,6 +3880,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3621,7 +3905,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::LaunchTask {
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
actor_user: "user".to_owned(),
|
actor_user: Some("user".to_owned()),
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
task: task.to_owned(),
|
task: task.to_owned(),
|
||||||
environment: None,
|
environment: None,
|
||||||
|
|
@ -3687,7 +3973,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::LaunchTask {
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
actor_user: "user".to_owned(),
|
actor_user: Some("user".to_owned()),
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
task: "package-linux".to_owned(),
|
task: "package-linux".to_owned(),
|
||||||
environment: None,
|
environment: None,
|
||||||
|
|
@ -3715,6 +4003,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process-a".to_owned(),
|
process: "process-a".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3728,6 +4019,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process-a".to_owned(),
|
process: "process-a".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -3740,6 +4034,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process-b".to_owned(),
|
process: "process-b".to_owned(),
|
||||||
restart: true,
|
restart: true,
|
||||||
})
|
})
|
||||||
|
|
@ -3752,6 +4049,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process-a".to_owned(),
|
process: "process-a".to_owned(),
|
||||||
restart: true,
|
restart: true,
|
||||||
})
|
})
|
||||||
|
|
@ -3761,6 +4061,15 @@ mod tests {
|
||||||
CoordinatorResponse::ProcessStarted {
|
CoordinatorResponse::ProcessStarted {
|
||||||
process: ProcessId::from("process-a"),
|
process: ProcessId::from("process-a"),
|
||||||
epoch: 7,
|
epoch: 7,
|
||||||
|
actor: WorkflowActor {
|
||||||
|
kind: "user".to_owned(),
|
||||||
|
user: Some(UserId::from("user")),
|
||||||
|
agent: None,
|
||||||
|
credential_kind: CredentialKind::BrowserSession,
|
||||||
|
public_key_fingerprint: None,
|
||||||
|
authenticated_without_browser: false,
|
||||||
|
scopes: vec!["project:read".to_owned(), "project:run".to_owned()],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -3780,6 +4089,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -4129,6 +4441,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -4304,6 +4619,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -4556,6 +4874,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "vp-control".to_owned(),
|
process: "vp-control".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -4569,11 +4890,14 @@ mod tests {
|
||||||
task,
|
task,
|
||||||
placement,
|
placement,
|
||||||
assignment,
|
assignment,
|
||||||
|
..
|
||||||
} = service
|
} = service
|
||||||
.handle_request(CoordinatorRequest::LaunchTask {
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
actor_user: "user".to_owned(),
|
actor_user: Some("user".to_owned()),
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "vp-control".to_owned(),
|
process: "vp-control".to_owned(),
|
||||||
task: "compile-linux".to_owned(),
|
task: "compile-linux".to_owned(),
|
||||||
environment: None,
|
environment: None,
|
||||||
|
|
@ -4634,6 +4958,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "vp-control".to_owned(),
|
process: "vp-control".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
@ -4643,7 +4970,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::LaunchTask {
|
.handle_request(CoordinatorRequest::LaunchTask {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
actor_user: "user".to_owned(),
|
actor_user: Some("user".to_owned()),
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "vp-control".to_owned(),
|
process: "vp-control".to_owned(),
|
||||||
task: "compile-linux".to_owned(),
|
task: "compile-linux".to_owned(),
|
||||||
environment: None,
|
environment: None,
|
||||||
|
|
@ -4768,6 +5097,9 @@ mod tests {
|
||||||
.handle_request(CoordinatorRequest::StartProcess {
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
tenant: "tenant-b".to_owned(),
|
tenant: "tenant-b".to_owned(),
|
||||||
project: "project-b".to_owned(),
|
project: "project-b".to_owned(),
|
||||||
|
actor_user: None,
|
||||||
|
actor_agent: None,
|
||||||
|
agent_public_key_fingerprint: None,
|
||||||
process: "process".to_owned(),
|
process: "process".to_owned(),
|
||||||
restart: false,
|
restart: false,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ for (const [name, pattern] of [
|
||||||
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
||||||
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
||||||
["CLI key lifecycle coverage", /fn key_lifecycle_reports_project_scoped_agent_credentials\(\)/],
|
["CLI key lifecycle coverage", /fn key_lifecycle_reports_project_scoped_agent_credentials\(\)/],
|
||||||
|
["CLI agent workflow actor coverage", /fn run_with_agent_public_key_sends_attributable_workflow_actor\(\)/],
|
||||||
["CLI node revoke coverage", /fn node_revoke_reports_scoped_credential_revocation\(\)/],
|
["CLI node revoke coverage", /fn node_revoke_reports_scoped_credential_revocation\(\)/],
|
||||||
["CLI admin public API coverage", /fn admin_status_and_suspend_use_public_coordinator_api\(\)/],
|
["CLI admin public API coverage", /fn admin_status_and_suspend_use_public_coordinator_api\(\)/],
|
||||||
["CLI debug attach coverage", /fn debug_attach_reports_public_authorization\(\)/],
|
["CLI debug attach coverage", /fn debug_attach_reports_public_authorization\(\)/],
|
||||||
|
|
@ -141,6 +142,21 @@ expect(
|
||||||
"coordinator agent key lifecycle coverage",
|
"coordinator agent key lifecycle coverage",
|
||||||
/fn service_manages_project_scoped_agent_public_keys\(\)/
|
/fn service_manages_project_scoped_agent_public_keys\(\)/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"coordinator agent workflow dispatch coverage",
|
||||||
|
/fn service_runs_agent_workflows_with_scoped_key_attribution\(\)/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"coordinator agent workflow authorization",
|
||||||
|
/authorize_agent_project_run\([\s\S]*project:run/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"coordinator agent workflow actor fields",
|
||||||
|
/pub struct WorkflowActor[\s\S]*agent: Option<AgentId>[\s\S]*public_key_fingerprint: Option<Digest>[\s\S]*authenticated_without_browser/
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
coordinator,
|
coordinator,
|
||||||
"coordinator node revoke coverage",
|
"coordinator node revoke coverage",
|
||||||
|
|
@ -176,6 +192,11 @@ expect(
|
||||||
"CLI surfaces debug audit and quota fields",
|
"CLI surfaces debug audit and quota fields",
|
||||||
/debug_reads_quota_limited[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
/debug_reads_quota_limited[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI sends agent workflow actor fields",
|
||||||
|
/fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/
|
||||||
|
);
|
||||||
|
|
||||||
for (const [name, pattern] of [
|
for (const [name, pattern] of [
|
||||||
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue