Update public browser login session flow
This commit is contained in:
parent
afd7456423
commit
cec080aa98
4 changed files with 388 additions and 29 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"kind": "disasmer-filtered-public-tree",
|
||||
"source_commit": "09993cb75187f1e8798d6429946085e167059e9f",
|
||||
"release_name": "dryrun-09993cb75187",
|
||||
"source_commit": "f3285b63dd9e8a320e8ca1d1dd188aa18fa9a59c",
|
||||
"release_name": "dryrun-f3285b63dd9e",
|
||||
"filtered_out": [
|
||||
"private/**",
|
||||
"experiments/**",
|
||||
|
|
|
|||
|
|
@ -617,6 +617,8 @@ struct LoginCompletionBoundaryEvidence {
|
|||
cli_contacted_coordinator: bool,
|
||||
coordinator_address: String,
|
||||
scoped_cli_session_received: bool,
|
||||
local_cli_session_file_written: bool,
|
||||
provider_tokens_persisted_locally: bool,
|
||||
provider_tokens_exposed_to_cli: bool,
|
||||
provider_tokens_sent_to_nodes: bool,
|
||||
coordinator_session_requests: u64,
|
||||
|
|
@ -724,6 +726,21 @@ struct ProjectConfig {
|
|||
coordinator: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct StoredCliSession {
|
||||
kind: String,
|
||||
coordinator: String,
|
||||
tenant: String,
|
||||
project: String,
|
||||
user: String,
|
||||
cli_session_credential_kind: String,
|
||||
token_expiry_posture: String,
|
||||
expires_at: Option<String>,
|
||||
provider_tokens_exposed_to_cli: bool,
|
||||
provider_tokens_sent_to_nodes: bool,
|
||||
created_at_unix_seconds: u64,
|
||||
}
|
||||
|
||||
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||
let config = read_project_config(&cwd)?;
|
||||
let coordinator = args.scope.coordinator.or_else(|| {
|
||||
|
|
@ -737,7 +754,7 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
|||
"cwd": cwd,
|
||||
"coordinator": coordinator,
|
||||
"coordinator_reachability": coordinator_reachability,
|
||||
"auth": auth_state_value(),
|
||||
"auth": auth_state_value(&cwd)?,
|
||||
"project": config,
|
||||
"dependencies": {
|
||||
"cargo": command_available("cargo"),
|
||||
|
|
@ -826,13 +843,61 @@ fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
|
|||
|
||||
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
||||
let config = read_project_config(&cwd)?;
|
||||
let stored_session = read_cli_session(&cwd)?;
|
||||
let active_coordinator = args
|
||||
.scope
|
||||
.coordinator
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
config
|
||||
.as_ref()
|
||||
.and_then(|config| config.coordinator.clone())
|
||||
})
|
||||
.or_else(|| {
|
||||
stored_session
|
||||
.as_ref()
|
||||
.map(|session| session.coordinator.clone())
|
||||
})
|
||||
.unwrap_or_else(default_operator_endpoint);
|
||||
let tenant = effective_scope_value(
|
||||
&args.scope.tenant,
|
||||
config
|
||||
.as_ref()
|
||||
.map(|config| config.tenant.as_str())
|
||||
.or_else(|| {
|
||||
stored_session
|
||||
.as_ref()
|
||||
.map(|session| session.tenant.as_str())
|
||||
}),
|
||||
"tenant",
|
||||
);
|
||||
let project = effective_scope_value(
|
||||
&args.scope.project,
|
||||
config
|
||||
.as_ref()
|
||||
.map(|config| config.project.as_str())
|
||||
.or_else(|| {
|
||||
stored_session
|
||||
.as_ref()
|
||||
.map(|session| session.project.as_str())
|
||||
}),
|
||||
"project",
|
||||
);
|
||||
let principal = effective_scope_value(
|
||||
&args.scope.user,
|
||||
config
|
||||
.as_ref()
|
||||
.map(|config| config.user.as_str())
|
||||
.or_else(|| stored_session.as_ref().map(|session| session.user.as_str())),
|
||||
"user",
|
||||
);
|
||||
Ok(json!({
|
||||
"command": "auth status",
|
||||
"active_coordinator": args.scope.coordinator.or_else(|| config.as_ref().and_then(|config| config.coordinator.clone())).unwrap_or_else(default_operator_endpoint),
|
||||
"principal": args.scope.user,
|
||||
"tenant": args.scope.tenant,
|
||||
"project": args.scope.project,
|
||||
"session": auth_state_value(),
|
||||
"active_coordinator": active_coordinator,
|
||||
"principal": principal,
|
||||
"tenant": tenant,
|
||||
"project": project,
|
||||
"session": auth_state_value(&cwd)?,
|
||||
"project_config": config,
|
||||
}))
|
||||
}
|
||||
|
|
@ -2739,7 +2804,10 @@ fn run_cli() -> Result<()> {
|
|||
}
|
||||
Commands::Run(args) => {
|
||||
let json_output = args.json;
|
||||
let report = run_report(args, std::env::current_dir()?, session_from_env())?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let session_project = args.project.clone().unwrap_or_else(|| cwd.clone());
|
||||
let session = session_from_sources(&session_project)?;
|
||||
let report = run_report(args, cwd, session)?;
|
||||
emit_report(&report, json_output)?;
|
||||
}
|
||||
Commands::Node {
|
||||
|
|
@ -2912,6 +2980,29 @@ fn write_project_config(project: &Path, config: &ProjectConfig) -> Result<()> {
|
|||
.with_context(|| format!("failed to write {}", file.display()))
|
||||
}
|
||||
|
||||
fn read_cli_session(project: &Path) -> Result<Option<StoredCliSession>> {
|
||||
let file = session_config_file(project);
|
||||
if !file.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let bytes =
|
||||
std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
|
||||
let session = serde_json::from_slice(&bytes)
|
||||
.with_context(|| format!("failed to parse {}", file.display()))?;
|
||||
Ok(Some(session))
|
||||
}
|
||||
|
||||
fn write_cli_session(project: &Path, session: &StoredCliSession) -> Result<PathBuf> {
|
||||
let file = session_config_file(project);
|
||||
if let Some(parent) = file.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&file, serde_json::to_vec_pretty(session)?)
|
||||
.with_context(|| format!("failed to write {}", file.display()))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn effective_project_scope(scope: &CliScopeArgs, config: Option<&ProjectConfig>) -> CliScopeArgs {
|
||||
CliScopeArgs {
|
||||
coordinator: scope
|
||||
|
|
@ -2949,30 +3040,48 @@ fn effective_scope_value(
|
|||
}
|
||||
}
|
||||
|
||||
fn auth_state_value() -> Value {
|
||||
fn auth_state_value(cwd: &Path) -> Result<Value> {
|
||||
match session_from_env() {
|
||||
CliSession::Anonymous => json!({
|
||||
CliSession::Anonymous => {
|
||||
if let Some(session) = read_cli_session(cwd)? {
|
||||
return Ok(json!({
|
||||
"kind": session.kind,
|
||||
"authenticated": true,
|
||||
"source": "session_file",
|
||||
"coordinator": session.coordinator,
|
||||
"tenant": session.tenant,
|
||||
"project": session.project,
|
||||
"principal": session.user,
|
||||
"cli_session_credential_kind": session.cli_session_credential_kind,
|
||||
"provider_tokens_exposed_to_cli": session.provider_tokens_exposed_to_cli,
|
||||
"provider_tokens_exposed_to_nodes": session.provider_tokens_sent_to_nodes,
|
||||
"expires_at": session.expires_at,
|
||||
"token_expiry_posture": session.token_expiry_posture,
|
||||
}));
|
||||
}
|
||||
Ok(json!({
|
||||
"kind": "anonymous",
|
||||
"authenticated": false,
|
||||
"source": "environment",
|
||||
"token_expiry_posture": "no_session",
|
||||
}),
|
||||
}))
|
||||
}
|
||||
CliSession::HumanSession => {
|
||||
let expires_at = std::env::var("DISASMER_TOKEN_EXPIRES_AT").ok();
|
||||
json!({
|
||||
Ok(json!({
|
||||
"kind": "human",
|
||||
"authenticated": true,
|
||||
"source": "DISASMER_TOKEN",
|
||||
"provider_tokens_exposed_to_nodes": false,
|
||||
"expires_at": expires_at,
|
||||
"token_expiry_posture": if expires_at.is_some() { "expires_at" } else { "unknown_env_token" },
|
||||
})
|
||||
}))
|
||||
}
|
||||
CliSession::AgentPublicKey {
|
||||
agent,
|
||||
public_key_fingerprint,
|
||||
browser_interaction_required,
|
||||
} => json!({
|
||||
} => Ok(json!({
|
||||
"kind": "agent_public_key",
|
||||
"authenticated": true,
|
||||
"agent": agent,
|
||||
|
|
@ -2980,7 +3089,7 @@ fn auth_state_value() -> Value {
|
|||
"public_key_fingerprint": public_key_fingerprint,
|
||||
"browser_interaction_required": browser_interaction_required,
|
||||
"token_expiry_posture": "not_applicable_public_key",
|
||||
}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3682,6 +3791,20 @@ fn execute_browser_login_completion_for_plan(
|
|||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let provider_tokens_exposed_to_cli = contains_provider_token_field(&coordinator_response);
|
||||
let local_cli_session_file_written = if scoped_cli_session_received {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let stored_session = stored_cli_session_from_login_response(
|
||||
&args,
|
||||
&coordinator,
|
||||
&coordinator_response,
|
||||
provider_tokens_exposed_to_cli,
|
||||
provider_tokens_sent_to_nodes,
|
||||
);
|
||||
write_cli_session(&cwd, &stored_session)?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(LoginCompletionReport {
|
||||
plan,
|
||||
|
|
@ -3689,6 +3812,8 @@ fn execute_browser_login_completion_for_plan(
|
|||
cli_contacted_coordinator: true,
|
||||
coordinator_address: coordinator,
|
||||
scoped_cli_session_received,
|
||||
local_cli_session_file_written,
|
||||
provider_tokens_persisted_locally: false,
|
||||
provider_tokens_exposed_to_cli,
|
||||
provider_tokens_sent_to_nodes,
|
||||
coordinator_session_requests: session.requests(),
|
||||
|
|
@ -3697,6 +3822,54 @@ fn execute_browser_login_completion_for_plan(
|
|||
})
|
||||
}
|
||||
|
||||
fn stored_cli_session_from_login_response(
|
||||
args: &LoginArgs,
|
||||
coordinator: &str,
|
||||
coordinator_response: &Value,
|
||||
provider_tokens_exposed_to_cli: bool,
|
||||
provider_tokens_sent_to_nodes: bool,
|
||||
) -> StoredCliSession {
|
||||
let session = coordinator_response.get("session").unwrap_or(&Value::Null);
|
||||
let expires_at = session
|
||||
.get("expires_at")
|
||||
.or_else(|| session.get("token_expires_at"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned);
|
||||
StoredCliSession {
|
||||
kind: "human".to_owned(),
|
||||
coordinator: coordinator.to_owned(),
|
||||
tenant: session
|
||||
.get("tenant")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(&args.tenant)
|
||||
.to_owned(),
|
||||
project: session
|
||||
.get("project")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(&args.project)
|
||||
.to_owned(),
|
||||
user: session
|
||||
.get("user")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(&args.user)
|
||||
.to_owned(),
|
||||
cli_session_credential_kind: session
|
||||
.get("cli_session_credential_kind")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("CliDeviceSession")
|
||||
.to_owned(),
|
||||
token_expiry_posture: if expires_at.is_some() {
|
||||
"expires_at".to_owned()
|
||||
} else {
|
||||
"unknown_coordinator_session".to_owned()
|
||||
},
|
||||
expires_at,
|
||||
provider_tokens_exposed_to_cli,
|
||||
provider_tokens_sent_to_nodes,
|
||||
created_at_unix_seconds: unix_timestamp_seconds(),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_interactive_browser_login(mut args: LoginArgs) -> Result<LoginCompletionReport> {
|
||||
args.browser = true;
|
||||
let callback_path = browser_callback_url();
|
||||
|
|
@ -3868,6 +4041,13 @@ fn browser_callback_timeout() -> Duration {
|
|||
Duration::from_secs(seconds)
|
||||
}
|
||||
|
||||
fn unix_timestamp_seconds() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn read_browser_callback_request(stream: &TcpStream) -> Result<BrowserCallback> {
|
||||
let mut reader = BufReader::new(
|
||||
stream
|
||||
|
|
@ -4724,6 +4904,17 @@ fn session_from_env() -> CliSession {
|
|||
CliSession::Anonymous
|
||||
}
|
||||
|
||||
fn session_from_sources(project: &Path) -> Result<CliSession> {
|
||||
let session = session_from_env();
|
||||
if session.is_authenticated() {
|
||||
return Ok(session);
|
||||
}
|
||||
if read_cli_session(project)?.is_some() {
|
||||
return Ok(CliSession::HumanSession);
|
||||
}
|
||||
Ok(CliSession::Anonymous)
|
||||
}
|
||||
|
||||
fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback_user: &str) {
|
||||
let Value::Object(map) = request else {
|
||||
return;
|
||||
|
|
@ -5586,6 +5777,60 @@ mod tests {
|
|||
})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_browser_login_session_omits_provider_token_values() {
|
||||
let Cli {
|
||||
command: Commands::Login(args),
|
||||
} = parse(&[
|
||||
"disasmer",
|
||||
"login",
|
||||
"--browser",
|
||||
"--coordinator",
|
||||
"https://coord.example.test",
|
||||
"--tenant",
|
||||
"tenant-cli",
|
||||
"--project-id",
|
||||
"project-cli",
|
||||
"--user",
|
||||
"user-cli",
|
||||
])
|
||||
else {
|
||||
panic!("wrong command");
|
||||
};
|
||||
let stored = stored_cli_session_from_login_response(
|
||||
&args,
|
||||
"https://coord.example.test",
|
||||
&json!({
|
||||
"session": {
|
||||
"tenant": "tenant-live",
|
||||
"project": "project-live",
|
||||
"user": "user-live",
|
||||
"cli_session_credential_kind": "CliDeviceSession",
|
||||
"expires_at": "2026-07-04T00:00:00Z",
|
||||
"access_token": "provider-secret",
|
||||
"id_token": "provider-id-token",
|
||||
"provider_tokens_sent_to_nodes": false
|
||||
}
|
||||
}),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let serialized = serde_json::to_string(&stored).unwrap();
|
||||
|
||||
assert_eq!(stored.kind, "human");
|
||||
assert_eq!(stored.coordinator, "https://coord.example.test");
|
||||
assert_eq!(stored.tenant, "tenant-live");
|
||||
assert_eq!(stored.project, "project-live");
|
||||
assert_eq!(stored.user, "user-live");
|
||||
assert_eq!(stored.token_expiry_posture, "expires_at");
|
||||
assert!(stored.provider_tokens_exposed_to_cli);
|
||||
assert!(!stored.provider_tokens_sent_to_nodes);
|
||||
assert!(!serialized.contains("provider-secret"));
|
||||
assert!(!serialized.contains("provider-id-token"));
|
||||
assert!(!serialized.contains("access_token"));
|
||||
assert!(!serialized.contains("id_token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_enroll_uses_public_key_without_browser_each_run() {
|
||||
let Cli {
|
||||
|
|
@ -5977,6 +6222,57 @@ mod tests {
|
|||
assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_status_reads_stored_cli_session_without_provider_tokens() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
write_cli_session(
|
||||
temp.path(),
|
||||
&StoredCliSession {
|
||||
kind: "human".to_owned(),
|
||||
coordinator: "https://coord.example.test".to_owned(),
|
||||
tenant: "tenant-session".to_owned(),
|
||||
project: "project-session".to_owned(),
|
||||
user: "user-session".to_owned(),
|
||||
cli_session_credential_kind: "CliDeviceSession".to_owned(),
|
||||
token_expiry_posture: "expires_at".to_owned(),
|
||||
expires_at: Some("2026-07-04T00:00:00Z".to_owned()),
|
||||
provider_tokens_exposed_to_cli: false,
|
||||
provider_tokens_sent_to_nodes: false,
|
||||
created_at_unix_seconds: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report = auth_status_report(
|
||||
AuthStatusArgs {
|
||||
scope: CliScopeArgs {
|
||||
coordinator: None,
|
||||
tenant: "tenant".to_owned(),
|
||||
project: "project".to_owned(),
|
||||
user: "user".to_owned(),
|
||||
json: false,
|
||||
},
|
||||
},
|
||||
temp.path().to_path_buf(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(report["active_coordinator"], "https://coord.example.test");
|
||||
assert_eq!(report["principal"], "user-session");
|
||||
assert_eq!(report["tenant"], "tenant-session");
|
||||
assert_eq!(report["project"], "project-session");
|
||||
assert_eq!(report["session"]["kind"], "human");
|
||||
assert_eq!(report["session"]["source"], "session_file");
|
||||
assert_eq!(report["session"]["authenticated"], true);
|
||||
assert_eq!(
|
||||
report["session"]["cli_session_credential_kind"],
|
||||
"CliDeviceSession"
|
||||
);
|
||||
assert_eq!(report["session"]["token_expiry_posture"], "expires_at");
|
||||
assert_eq!(report["session"]["provider_tokens_exposed_to_cli"], false);
|
||||
assert_eq!(report["session"]["provider_tokens_exposed_to_nodes"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_first_mvp_command_surface_parses() {
|
||||
for args in [
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ const path = require("path");
|
|||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow");
|
||||
const project = path.join(tmp, "project");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
fs.mkdirSync(tmp, { recursive: true });
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
|
||||
function writeOpener() {
|
||||
const opener = path.join(tmp, "browser-opener.js");
|
||||
|
|
@ -86,6 +89,7 @@ async function startCoordinator() {
|
|||
user: request.user,
|
||||
browser_credential_kind: "BrowserSession",
|
||||
cli_session_credential_kind: "CliDeviceSession",
|
||||
expires_at: "2026-07-04T00:00:00Z",
|
||||
provider_tokens_sent_to_nodes: false,
|
||||
flow: {
|
||||
authorization_url: "http://127.0.0.1:1/application/o/authorize/",
|
||||
|
|
@ -115,8 +119,19 @@ async function startCoordinator() {
|
|||
|
||||
function runDisasmer(args, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn("cargo", args, {
|
||||
cwd: repo,
|
||||
const child = cp.spawn("cargo", [
|
||||
"run",
|
||||
"-q",
|
||||
"--manifest-path",
|
||||
path.join(repo, "Cargo.toml"),
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
...args,
|
||||
], {
|
||||
cwd: project,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
|
@ -157,13 +172,6 @@ function runDisasmer(args, env) {
|
|||
try {
|
||||
const stdout = await runDisasmer(
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"-p",
|
||||
"disasmer-cli",
|
||||
"--bin",
|
||||
"disasmer",
|
||||
"--",
|
||||
"login",
|
||||
"--browser",
|
||||
"--json",
|
||||
|
|
@ -190,10 +198,45 @@ function runDisasmer(args, env) {
|
|||
assert.strictEqual(report.plan.coordinator, coordinator.url);
|
||||
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
|
||||
assert.strictEqual(report.boundary.scoped_cli_session_received, true);
|
||||
assert.strictEqual(report.boundary.local_cli_session_file_written, true);
|
||||
assert.strictEqual(report.boundary.provider_tokens_persisted_locally, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false);
|
||||
assert.strictEqual(report.boundary.coordinator_session_requests, 1);
|
||||
assert.strictEqual(coordinator.requests.length, 1);
|
||||
|
||||
const sessionFile = path.join(project, ".disasmer", "session.json");
|
||||
assert(fs.existsSync(sessionFile), "browser login should write a local CLI session file");
|
||||
const sessionText = fs.readFileSync(sessionFile, "utf8");
|
||||
const session = JSON.parse(sessionText);
|
||||
assert.strictEqual(session.kind, "human");
|
||||
assert.strictEqual(session.coordinator, coordinator.url);
|
||||
assert.strictEqual(session.tenant, "tenant-smoke");
|
||||
assert.strictEqual(session.project, "project-smoke");
|
||||
assert.strictEqual(session.user, "user-smoke");
|
||||
assert.strictEqual(session.cli_session_credential_kind, "CliDeviceSession");
|
||||
assert.strictEqual(session.token_expiry_posture, "expires_at");
|
||||
assert.strictEqual(session.expires_at, "2026-07-04T00:00:00Z");
|
||||
assert.strictEqual(session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(session.provider_tokens_sent_to_nodes, false);
|
||||
assert.doesNotMatch(
|
||||
sessionText,
|
||||
/access_token|refresh_token|id_token|provider-secret|browser-smoke-code|Bearer/
|
||||
);
|
||||
|
||||
const authStatus = JSON.parse(
|
||||
await runDisasmer(["auth", "status", "--json"], {
|
||||
...process.env,
|
||||
})
|
||||
);
|
||||
assert.strictEqual(authStatus.active_coordinator, coordinator.url);
|
||||
assert.strictEqual(authStatus.principal, "user-smoke");
|
||||
assert.strictEqual(authStatus.tenant, "tenant-smoke");
|
||||
assert.strictEqual(authStatus.project, "project-smoke");
|
||||
assert.strictEqual(authStatus.session.kind, "human");
|
||||
assert.strictEqual(authStatus.session.source, "session_file");
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_nodes, false);
|
||||
} finally {
|
||||
if (coordinator.requests.length === 0) {
|
||||
await coordinator.close().catch(() => {});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const coordinator = read("crates/disasmer-coordinator/src/service.rs");
|
|||
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
||||
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
||||
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
||||
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
||||
|
||||
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
|
||||
expect(
|
||||
|
|
@ -143,6 +144,12 @@ for (const [name, pattern] of [
|
|||
["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/],
|
||||
["login non-interactive flag", /struct LoginArgs[\s\S]*non_interactive: bool/],
|
||||
["run non-interactive flag", /struct RunArgs[\s\S]*non_interactive: bool/],
|
||||
["stored CLI session model", /struct StoredCliSession[\s\S]*cli_session_credential_kind[\s\S]*provider_tokens_exposed_to_cli[\s\S]*provider_tokens_sent_to_nodes/],
|
||||
["read CLI session helper", /fn read_cli_session\(project: &Path\) -> Result<Option<StoredCliSession>>/],
|
||||
["write CLI session helper", /fn write_cli_session\(project: &Path, session: &StoredCliSession\) -> Result<PathBuf>/],
|
||||
["session source fallback", /fn session_from_sources\(project: &Path\) -> Result<CliSession>[\s\S]*read_cli_session\(project\)\?\.is_some\(\)/],
|
||||
["browser login writes local CLI session", /local_cli_session_file_written[\s\S]*write_cli_session\(&cwd, &stored_session\)\?/],
|
||||
["browser login does not persist provider tokens", /provider_tokens_persisted_locally:\s*false/],
|
||||
["non-interactive auth report", /fn non_interactive_auth_machine_error[\s\S]*browser_opened[\s\S]*false/],
|
||||
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
||||
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
||||
|
|
@ -171,6 +178,8 @@ for (const [name, pattern] of [
|
|||
["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/],
|
||||
["CLI non-interactive run auth coverage", /fn non_interactive_run_without_session_requires_explicit_auth_or_local\(\)/],
|
||||
["CLI non-interactive browser login coverage", /fn browser_login_non_interactive_fails_before_opening_browser\(\)/],
|
||||
["CLI stored browser session coverage", /fn stored_browser_login_session_omits_provider_token_values\(\)/],
|
||||
["CLI auth status session-file coverage", /fn auth_status_reads_stored_cli_session_without_provider_tokens\(\)/],
|
||||
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
||||
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
||||
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
||||
|
|
@ -204,6 +213,17 @@ for (const [name, pattern] of [
|
|||
expect(cli, name, pattern);
|
||||
}
|
||||
|
||||
expect(
|
||||
browserLoginFlowSmoke,
|
||||
"browser login smoke checks session file",
|
||||
/provider_tokens_persisted_locally[\s\S]*path\.join\(project, "\.disasmer", "session\.json"\)[\s\S]*\["auth", "status", "--json"\]/
|
||||
);
|
||||
expect(
|
||||
browserLoginFlowSmoke,
|
||||
"browser login smoke rejects provider token persistence",
|
||||
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
|
||||
);
|
||||
|
||||
expect(
|
||||
coordinator,
|
||||
"coordinator whole-process cancellation coverage",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue