Sync public tree to 4a860d7

This commit is contained in:
Michel Paulissen 2026-07-04 13:26:01 +02:00
parent 1dad0df5c4
commit 4f5df7da20
4 changed files with 236 additions and 5 deletions

View file

@ -1,7 +1,7 @@
{ {
"kind": "disasmer-filtered-public-tree", "kind": "disasmer-filtered-public-tree",
"source_commit": "13dc821c5a4ff2b093b777374ebf510197ab5500", "source_commit": "4a860d72efe34b376c4b08762555bee3fe3105f9",
"release_name": "dryrun-13dc821c5a4f", "release_name": "dryrun-4a860d72efe3",
"filtered_out": [ "filtered_out": [
"private/**", "private/**",
"experiments/**", "experiments/**",

View file

@ -1,4 +1,4 @@
use std::collections::BTreeSet; use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -10,8 +10,9 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use disasmer_core::{ use disasmer_core::{
diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata, diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata,
Capability, CliLoginFlow, Digest, EnvironmentReference, LimitKind, NodeCapabilities, Capability, CliLoginFlow, Digest, EnvironmentBackend, EnvironmentReference, LimitKind,
ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind, SourceProviderManifest, NodeCapabilities, ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind,
SourceProviderManifest,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -678,6 +679,7 @@ struct NodeAttachPlan {
node: String, node: String,
coordinator: Option<String>, coordinator: Option<String>,
capabilities: NodeCapabilities, capabilities: NodeCapabilities,
detection: NodeAttachDetectionEvidence,
grant_disclosures: Vec<CapabilityGrantDisclosure>, grant_disclosures: Vec<CapabilityGrantDisclosure>,
enrollment: Option<NodeEnrollmentPlan>, enrollment: Option<NodeEnrollmentPlan>,
} }
@ -711,6 +713,32 @@ struct CapabilityGrantDisclosure {
coordinator_policy_limited: bool, coordinator_policy_limited: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct NodeAttachDetectionEvidence {
auto_detected: bool,
os: disasmer_core::Os,
arch: String,
command_backend: String,
command_backend_available: bool,
container_backend: Option<String>,
container_backend_reported: bool,
container_backend_available: bool,
source_provider_backends: Vec<SourceProviderBackendStatus>,
manual_capability_overrides_allowed: bool,
manual_capability_overrides: Vec<String>,
recognized_capability_overrides: Vec<Capability>,
unrecognized_capability_overrides: Vec<String>,
os_arch_capabilities_require_manual_flags: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct SourceProviderBackendStatus {
provider: String,
detected: bool,
available: bool,
reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct NodeEnrollmentPlan { struct NodeEnrollmentPlan {
grant: String, grant: String,
@ -2794,6 +2822,12 @@ fn human_report(value: &Value) -> String {
} }
} }
} }
if let Some(detection) = value
.pointer("/plan/detection")
.or_else(|| value.get("detection"))
{
push_node_attach_detection(&mut lines, detection);
}
if let Some(disclosures) = value if let Some(disclosures) = value
.get("grant_disclosures") .get("grant_disclosures")
.and_then(Value::as_array) .and_then(Value::as_array)
@ -2825,6 +2859,64 @@ fn human_report(value: &Value) -> String {
lines.join("\n") lines.join("\n")
} }
fn push_node_attach_detection(lines: &mut Vec<String>, detection: &Value) {
push_nested_string_field(lines, detection, "os", "node os");
push_nested_string_field(lines, detection, "arch", "node arch");
push_nested_string_field(lines, detection, "command_backend", "command backend");
if let Some(backend) = detection.get("container_backend").and_then(Value::as_str) {
let available = detection
.get("container_backend_available")
.and_then(Value::as_bool)
.unwrap_or(false);
lines.push(format!(
"container backend: {backend} ({})",
if available { "available" } else { "reported" }
));
} else if detection
.get("container_backend_reported")
.and_then(Value::as_bool)
.unwrap_or(false)
{
lines.push("container backend: reported".to_owned());
}
if let Some(providers) = detection
.get("source_provider_backends")
.and_then(Value::as_array)
{
let entries = providers
.iter()
.filter_map(|provider| {
let name = provider.get("provider").and_then(Value::as_str)?;
let state = if provider
.get("available")
.and_then(Value::as_bool)
.unwrap_or(false)
{
"available"
} else {
"detected"
};
Some(format!("{name}={state}"))
})
.collect::<Vec<_>>();
if !entries.is_empty() {
lines.push(format!("source providers: {}", entries.join(", ")));
}
}
if let Some(overrides) = detection
.get("manual_capability_overrides")
.and_then(Value::as_array)
{
let overrides = overrides
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !overrides.is_empty() {
lines.push(format!("capability overrides: {}", overrides.join(", ")));
}
}
}
fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) { fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
if let Some(text) = value.get(key).and_then(Value::as_str) { if let Some(text) = value.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {text}")); lines.push(format!("{label}: {text}"));
@ -5748,11 +5840,18 @@ fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback
fn attach_plan(args: AttachArgs) -> NodeAttachPlan { fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
let mut capabilities = NodeCapabilities::detect_current(); let mut capabilities = NodeCapabilities::detect_current();
let mut recognized_capability_overrides = Vec::new();
let mut unrecognized_capability_overrides = Vec::new();
for cap in &args.caps { for cap in &args.caps {
if let Some(parsed) = parse_capability(&cap) { if let Some(parsed) = parse_capability(&cap) {
recognized_capability_overrides.push(parsed.clone());
capabilities.capabilities.insert(parsed); capabilities.capabilities.insert(parsed);
} else {
unrecognized_capability_overrides.push(cap.clone());
} }
} }
recognized_capability_overrides.sort();
recognized_capability_overrides.dedup();
let node = args.node.unwrap_or_else(default_node_id); let node = args.node.unwrap_or_else(default_node_id);
let public_key = args let public_key = args
.public_key .public_key
@ -5762,17 +5861,94 @@ fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
public_key_fingerprint: Digest::sha256(public_key), public_key_fingerprint: Digest::sha256(public_key),
exchanges_short_lived_grant_for_long_lived_node_identity: true, exchanges_short_lived_grant_for_long_lived_node_identity: true,
}); });
let detection = node_attach_detection_evidence(
&capabilities,
args.caps,
recognized_capability_overrides,
unrecognized_capability_overrides,
);
let grant_disclosures = capability_grant_disclosures(&capabilities); let grant_disclosures = capability_grant_disclosures(&capabilities);
NodeAttachPlan { NodeAttachPlan {
node, node,
coordinator: args.coordinator, coordinator: args.coordinator,
capabilities, capabilities,
detection,
grant_disclosures, grant_disclosures,
enrollment, enrollment,
} }
} }
fn node_attach_detection_evidence(
capabilities: &NodeCapabilities,
manual_capability_overrides: Vec<String>,
recognized_capability_overrides: Vec<Capability>,
unrecognized_capability_overrides: Vec<String>,
) -> NodeAttachDetectionEvidence {
let command_backend_available = capabilities.capabilities.contains(&Capability::Command);
let container_backend_reported = capabilities
.environment_backends
.contains(&EnvironmentBackend::Container);
let container_backend = container_backend_reported.then(|| "rootless-podman".to_owned());
let container_backend_available = container_backend_reported
&& capabilities
.capabilities
.contains(&Capability::RootlessPodman)
&& command_available("podman");
NodeAttachDetectionEvidence {
auto_detected: true,
os: capabilities.os.clone(),
arch: capabilities.arch.clone(),
command_backend: if command_backend_available {
"native-command".to_owned()
} else {
"unavailable".to_owned()
},
command_backend_available,
container_backend,
container_backend_reported,
container_backend_available,
source_provider_backends: source_provider_backend_statuses(capabilities),
manual_capability_overrides_allowed: true,
manual_capability_overrides,
recognized_capability_overrides,
unrecognized_capability_overrides,
os_arch_capabilities_require_manual_flags: false,
}
}
fn source_provider_backend_statuses(
capabilities: &NodeCapabilities,
) -> Vec<SourceProviderBackendStatus> {
let mut statuses = BTreeMap::new();
for provider in &capabilities.source_providers {
let available = match provider.as_str() {
"filesystem" => capabilities
.capabilities
.contains(&Capability::SourceFilesystem),
"git" => command_available("git"),
_ => true,
};
statuses.insert(
provider.clone(),
SourceProviderBackendStatus {
provider: provider.clone(),
detected: true,
available,
reason: if available {
"detected by local node capability probe".to_owned()
} else {
format!(
"source provider `{provider}` was detected but its local helper is missing"
)
},
},
);
}
statuses.into_values().collect()
}
fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> { fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> {
let mut disclosures = Vec::new(); let mut disclosures = Vec::new();
let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| { let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| {
@ -6225,6 +6401,27 @@ mod tests {
.capabilities .capabilities
.contains(&Capability::QuicDirect)); .contains(&Capability::QuicDirect));
assert!(!plan.capabilities.arch.is_empty()); assert!(!plan.capabilities.arch.is_empty());
assert!(plan.detection.auto_detected);
assert_eq!(plan.detection.os, plan.capabilities.os);
assert_eq!(plan.detection.arch, plan.capabilities.arch);
assert_eq!(plan.detection.command_backend, "native-command");
assert!(plan.detection.command_backend_available);
assert!(plan.detection.manual_capability_overrides_allowed);
assert_eq!(
plan.detection.manual_capability_overrides,
vec!["quic-direct".to_owned()]
);
assert!(plan
.detection
.recognized_capability_overrides
.contains(&Capability::QuicDirect));
assert!(plan.detection.unrecognized_capability_overrides.is_empty());
assert!(!plan.detection.os_arch_capabilities_require_manual_flags);
assert!(plan
.detection
.source_provider_backends
.iter()
.any(|provider| provider.provider == "filesystem" && provider.detected));
} }
#[test] #[test]

View file

@ -37,6 +37,7 @@ const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js"); const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js"); const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js"); const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
const nodeAttachSmoke = read("scripts/node-attach-smoke.js");
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m); expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
expect( expect(
@ -250,6 +251,7 @@ for (const [name, pattern] of [
["CLI locality failure classifier", /fn classify_cli_error_message\(message: &str\)[\s\S]*message_mentions_locality_failure\(&message\)[\s\S]*return "connectivity"/], ["CLI locality failure classifier", /fn classify_cli_error_message\(message: &str\)[\s\S]*message_mentions_locality_failure\(&message\)[\s\S]*return "connectivity"/],
["CLI locality failure report helper", /fn task_locality_failure_from_reason\(reason: &Value\) -> Value[\s\S]*coordinator_bulk_relay_used[\s\S]*safe_next_actions/], ["CLI locality failure report helper", /fn task_locality_failure_from_reason\(reason: &Value\) -> Value[\s\S]*coordinator_bulk_relay_used[\s\S]*safe_next_actions/],
["CLI locality failure human output", /fn push_task_locality_failures\(lines: &mut Vec<String>, tasks: &\[Value\]\)[\s\S]*locality \{task_name\}/], ["CLI locality failure human output", /fn push_task_locality_failures\(lines: &mut Vec<String>, tasks: &\[Value\]\)[\s\S]*locality \{task_name\}/],
["node attach auto-detection coverage", /fn node_attach_detects_and_accepts_capability_overrides\(\)[\s\S]*detection\.auto_detected[\s\S]*command_backend[\s\S]*source_provider_backends/],
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/], ["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/], ["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/],
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/], ["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
@ -280,6 +282,11 @@ expect(
"browser login smoke rejects provider token persistence", "browser login smoke rejects provider token persistence",
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/ /assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
); );
expect(
nodeAttachSmoke,
"node attach smoke verifies auto-detection evidence",
/plan\.detection\.auto_detected[\s\S]*plan\.detection\.command_backend[\s\S]*source_provider_backends/
);
expect( expect(
coordinator, coordinator,
@ -356,6 +363,16 @@ expect(
"CLI sends agent workflow actor fields", "CLI sends agent workflow actor fields",
/fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/ /fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/
); );
expect(
cli,
"CLI exposes node attach detection evidence",
/struct NodeAttachDetectionEvidence[\s\S]*command_backend[\s\S]*container_backend[\s\S]*source_provider_backends[\s\S]*manual_capability_overrides[\s\S]*os_arch_capabilities_require_manual_flags/
);
expect(
cli,
"CLI renders node attach detection evidence",
/fn push_node_attach_detection[\s\S]*command backend[\s\S]*container backend[\s\S]*source providers[\s\S]*capability overrides/
);
expect( expect(
cli, cli,
"CLI exposes node attach grant disclosures", "CLI exposes node attach grant disclosures",

View file

@ -212,6 +212,23 @@ function runAttachedNodeWork(addr) {
assert.ok(report.plan.capabilities.arch.length > 0); assert.ok(report.plan.capabilities.arch.length > 0);
assert.ok(report.plan.capabilities.source_providers.includes("filesystem")); assert.ok(report.plan.capabilities.source_providers.includes("filesystem"));
assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect")); assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect"));
assert.strictEqual(report.plan.detection.auto_detected, true);
assert.strictEqual(report.plan.detection.arch, report.plan.capabilities.arch);
assert.deepStrictEqual(report.plan.detection.manual_capability_overrides, ["quic-direct"]);
assert(
report.plan.detection.recognized_capability_overrides.includes("QuicDirect")
);
assert.strictEqual(
report.plan.detection.os_arch_capabilities_require_manual_flags,
false
);
assert.strictEqual(report.plan.detection.command_backend, "native-command");
assert.strictEqual(report.plan.detection.command_backend_available, true);
assert(
report.plan.detection.source_provider_backends.some(
(provider) => provider.provider === "filesystem" && provider.detected
)
);
assert.strictEqual(report.boundary.cli_contacted_coordinator, true); assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
assert.strictEqual(report.boundary.used_enrollment_exchange, true); assert.strictEqual(report.boundary.used_enrollment_exchange, true);
assert.strictEqual(report.boundary.coordinator_session_requests, 3); assert.strictEqual(report.boundary.coordinator_session_requests, 3);