From a77e138a24a6c3abee13ea98ff3984145979970c Mon Sep 17 00:00:00 2001 From: Michel Paulissen <862400+MichelPaulissen@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:18:24 +0200 Subject: [PATCH] Sync public tree to d0af060 --- DISASMER_PUBLIC_TREE.json | 4 +- crates/disasmer-coordinator/src/service.rs | 159 +++++++++++++++++++- scripts/acceptance-cli-first.sh | 9 +- scripts/cli-first-contract-smoke.js | 15 ++ scripts/cli-install-smoke.js | 5 +- scripts/website-inventory-contract-smoke.js | 4 +- 6 files changed, 185 insertions(+), 11 deletions(-) diff --git a/DISASMER_PUBLIC_TREE.json b/DISASMER_PUBLIC_TREE.json index 741e327..c72b811 100644 --- a/DISASMER_PUBLIC_TREE.json +++ b/DISASMER_PUBLIC_TREE.json @@ -1,7 +1,7 @@ { "kind": "disasmer-filtered-public-tree", - "source_commit": "7aeb51ff791bd445afc2d51757239b6e306db015", - "release_name": "dryrun-7aeb51ff791b", + "source_commit": "d0af06088a23d83911b49a6b05073b892f70385b", + "release_name": "dryrun-d0af06088a23", "filtered_out": [ "private/**", "experiments/**", diff --git a/crates/disasmer-coordinator/src/service.rs b/crates/disasmer-coordinator/src/service.rs index de1561a..4ff39a2 100644 --- a/crates/disasmer-coordinator/src/service.rs +++ b/crates/disasmer-coordinator/src/service.rs @@ -587,6 +587,7 @@ pub enum CoordinatorResponse { actor: WorkflowActor, placement: Placement, assignment: TaskAssignment, + charged_spawns: u64, }, TaskAssignment { assignment: Option, @@ -607,6 +608,7 @@ pub enum CoordinatorResponse { process: ProcessId, epoch: u64, actor: WorkflowActor, + charged_spawns: u64, }, NodeReconnected { node: NodeId, @@ -761,6 +763,8 @@ pub struct CoordinatorService { download_meter: ResourceMeter, debug_limits: ResourceLimits, debug_meter: ResourceMeter, + workflow_limits: ResourceLimits, + workflow_meter: ResourceMeter, } impl CoordinatorService { @@ -790,6 +794,8 @@ impl CoordinatorService { download_meter: ResourceMeter::default(), debug_limits: ResourceLimits::community_tier_defaults(), debug_meter: ResourceMeter::default(), + workflow_limits: ResourceLimits::community_tier_defaults(), + workflow_meter: ResourceMeter::default(), } } @@ -1356,6 +1362,8 @@ impl CoordinatorService { ) .into()); } + self.workflow_meter + .can_charge(&self.workflow_limits, LimitKind::Spawn, 1)?; let request = PlacementRequest { tenant: tenant.clone(), project: project.clone(), @@ -1374,6 +1382,9 @@ impl CoordinatorService { }; let nodes = self.node_descriptors.values().cloned().collect::>(); let placement = DefaultScheduler.place(&nodes, &request)?; + self.workflow_meter + .charge(&self.workflow_limits, LimitKind::Spawn, 1)?; + let charged_spawns = self.workflow_meter.used(&LimitKind::Spawn); let assignment = TaskAssignment { tenant: tenant.clone(), project: project.clone(), @@ -1399,6 +1410,7 @@ impl CoordinatorService { actor, placement, assignment, + charged_spawns, }) } CoordinatorRequest::PollTaskAssignment { @@ -1562,6 +1574,8 @@ impl CoordinatorService { .into()); } } + self.workflow_meter + .charge(&self.workflow_limits, LimitKind::Spawn, 1)?; self.process_cancellations .remove(&process_control_key(&tenant, &project, &process)); self.task_cancellations.retain( @@ -1598,6 +1612,7 @@ impl CoordinatorService { process, epoch: self.coordinator.coordinator_epoch(), actor, + charged_spawns: self.workflow_meter.used(&LimitKind::Spawn), }) } CoordinatorRequest::ReconnectNode { @@ -3411,6 +3426,144 @@ mod tests { assert!(policy_error.to_string().contains("policy denied placement")); } + #[test] + fn service_checks_spawn_quota_before_process_or_task_work_starts() { + let mut service = CoordinatorService::new(7); + service.workflow_limits = ResourceLimits { + limits: BTreeMap::from([(LimitKind::Spawn, 2)]), + }; + + 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 { charged_spawns, .. } = service + .handle_request(CoordinatorRequest::StartProcess { + tenant: "tenant".to_owned(), + project: "project".to_owned(), + actor_user: None, + actor_agent: None, + agent_public_key_fingerprint: None, + process: "vp-quota".to_owned(), + restart: false, + }) + .unwrap() + else { + panic!("expected process start within spawn quota"); + }; + assert_eq!(charged_spawns, 1); + + let CoordinatorResponse::TaskLaunched { charged_spawns, .. } = service + .handle_request(CoordinatorRequest::LaunchTask { + tenant: "tenant".to_owned(), + project: "project".to_owned(), + actor_user: Some("user".to_owned()), + actor_agent: None, + agent_public_key_fingerprint: None, + process: "vp-quota".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 task launch within spawn quota"); + }; + assert_eq!(charged_spawns, 2); + + let denied_task = service + .handle_request(CoordinatorRequest::LaunchTask { + tenant: "tenant".to_owned(), + project: "project".to_owned(), + actor_user: Some("user".to_owned()), + actor_agent: None, + agent_public_key_fingerprint: None, + process: "vp-quota".to_owned(), + task: "compile-linux-denied".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/denied.txt".to_owned(), + }) + .unwrap_err(); + assert!(denied_task.to_string().contains("Spawn")); + assert_eq!(service.workflow_meter.used(&LimitKind::Spawn), 2); + + let denied_task_key = task_control_key( + &TenantId::from("tenant"), + &ProjectId::from("project"), + &ProcessId::from("vp-quota"), + &NodeId::from("worker-linux"), + &TaskId::from("compile-linux-denied"), + ); + assert!(!service.active_tasks.contains(&denied_task_key)); + assert!(!service.task_placements.contains_key(&denied_task_key)); + assert_eq!( + service + .task_assignments + .get(&( + TenantId::from("tenant"), + ProjectId::from("project"), + NodeId::from("worker-linux"), + )) + .map(VecDeque::len), + Some(1) + ); + + let denied_process = service + .handle_request(CoordinatorRequest::StartProcess { + tenant: "tenant".to_owned(), + project: "other-project".to_owned(), + actor_user: None, + actor_agent: None, + agent_public_key_fingerprint: None, + process: "vp-denied".to_owned(), + restart: false, + }) + .unwrap_err(); + assert!(denied_process.to_string().contains("Spawn")); + assert!(service + .coordinator + .active_process(&ProcessId::from("vp-denied")) + .is_none()); + assert_eq!(service.workflow_meter.used(&LimitKind::Spawn), 2); + } + #[test] fn service_attaches_node_starts_process_and_records_scoped_task_event() { let mut service = CoordinatorService::new(7); @@ -3449,7 +3602,8 @@ mod tests { public_key_fingerprint: None, authenticated_without_browser: false, scopes: vec!["project:read".to_owned(), "project:run".to_owned()], - } + }, + charged_spawns: 1, } ); @@ -4296,7 +4450,8 @@ mod tests { public_key_fingerprint: None, authenticated_without_browser: false, scopes: vec!["project:read".to_owned(), "project:run".to_owned()], - } + }, + charged_spawns: 2, } ); } diff --git a/scripts/acceptance-cli-first.sh b/scripts/acceptance-cli-first.sh index 901b53f..f0c1151 100755 --- a/scripts/acceptance-cli-first.sh +++ b/scripts/acceptance-cli-first.sh @@ -26,16 +26,17 @@ node scripts/resource-metering-contract-smoke.js node scripts/hostile-input-contract-smoke.js node scripts/tenant-isolation-contract-smoke.js node scripts/public-story-contract-smoke.js + +if [[ "${DISASMER_CLI_FIRST_PREPARE_PUBLIC_RELEASE:-1}" == "1" ]]; then + node scripts/prepare-public-release-dryrun.js +fi + node scripts/public-release-dryrun-contract-smoke.js node scripts/public-browser-login-contract-smoke.js node scripts/self-hosted-coordinator-smoke.js node scripts/public-local-demo-matrix-smoke.js scripts/release-source-scan.sh -if [[ "${DISASMER_CLI_FIRST_PREPARE_PUBLIC_RELEASE:-1}" == "1" ]]; then - node scripts/prepare-public-release-dryrun.js -fi - cargo fmt --all --check cargo test --workspace cargo build --workspace --bins diff --git a/scripts/cli-first-contract-smoke.js b/scripts/cli-first-contract-smoke.js index c32b64e..a13bc9c 100644 --- a/scripts/cli-first-contract-smoke.js +++ b/scripts/cli-first-contract-smoke.js @@ -165,6 +165,11 @@ expect( "quota resource-category criteria", /Hitting a quota produces a clear error[\s\S]*resource category[\s\S]*private abuse heuristics[\s\S]*quota machine errors now extract the resource category/ ); +expect( + criteria, + "quota before work criteria", + /Quotas are checked before expensive work starts[\s\S]*workflow spawns now charge `Spawn` before coordinator-side process\/task state is created or queued[\s\S]*debug, artifact-download, and rendezvous metering/ +); expect( criteria, "community tier CLI wording criteria", @@ -403,6 +408,16 @@ expect( "coordinator task events retain placement reasons", /pub struct TaskCompletionEvent[\s\S]*placement: Option[\s\S]*task_placements[\s\S]*event\.placement = self\.task_placements\.remove/ ); +expect( + coordinator, + "coordinator workflow spawn metering", + /workflow_limits: ResourceLimits[\s\S]*workflow_meter: ResourceMeter[\s\S]*can_charge\(&self\.workflow_limits, LimitKind::Spawn, 1\)[\s\S]*charge\(&self\.workflow_limits, LimitKind::Spawn, 1\)[\s\S]*charged_spawns/ +); +expect( + coordinator, + "coordinator spawn quota before work coverage", + /fn service_checks_spawn_quota_before_process_or_task_work_starts\(\)[\s\S]*workflow_limits = ResourceLimits[\s\S]*LimitKind::Spawn[\s\S]*compile-linux-denied[\s\S]*active_tasks\.contains[\s\S]*active_process\(&ProcessId::from\("vp-denied"\)\)/ +); expect( coordinator, "public debug operation audit event", diff --git a/scripts/cli-install-smoke.js b/scripts/cli-install-smoke.js index 8ddcaa8..b2fb2d6 100755 --- a/scripts/cli-install-smoke.js +++ b/scripts/cli-install-smoke.js @@ -9,7 +9,10 @@ const path = require("path"); const repo = path.resolve(__dirname, ".."); const temp = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-install-")); const installRoot = path.join(temp, "install"); -const targetDir = path.join(temp, "target"); +const targetDir = + process.env.DISASMER_CLI_INSTALL_CARGO_TARGET_DIR || + process.env.CARGO_TARGET_DIR || + path.join(repo, "target"); const project = path.join(repo, "examples/launch-build-demo"); const binName = process.platform === "win32" ? "disasmer.exe" : "disasmer"; const installedBin = path.join(installRoot, "bin", binName); diff --git a/scripts/website-inventory-contract-smoke.js b/scripts/website-inventory-contract-smoke.js index 6b16c36..63caf7f 100644 --- a/scripts/website-inventory-contract-smoke.js +++ b/scripts/website-inventory-contract-smoke.js @@ -30,7 +30,7 @@ function criterionLines() { expect("header", /^# Disasmer MVP Website Acceptance Criteria/m); expect( "acceptance-style status", - /\*\*Status:\*\* hosted website addendum to `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/ + /\*\*Status:\*\* (?:private )?hosted website addendum to `acceptance_criteria\.md`, `acceptance_criteria_phase2\.md`, and `cli_acceptance_criteria\.md`/ ); expect( "hosted signup exception", @@ -50,7 +50,7 @@ expect( ); expect( "future hosted business scope", - /Billing is not part of the MVP\. Paid checkout, paid-plan management, upgrade flows, hosted support tooling, a full hosted admin console, broad moderation workflows/ + /Billing is not part of the MVP[\s\S]*Paid checkout, paid-plan management, upgrade flows, hosted support tooling, a full hosted admin console, broad moderation workflows/ ); expect( "billing is not MVP website work",