Public dry run dryrun-1714a9eedd5b

Source commit: 1714a9eedd5b1e30c6c9aceb7a999f2f695ba83c

Public tree identity: sha256:e5daffad23fa7c7f1822adccd3c3b4ee0bc7f1a45e0d321eb9a6fb8282b269db
This commit is contained in:
Disasmer release dry run 2026-07-03 17:31:04 +02:00
commit 20c72e6066
102 changed files with 32054 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target/
/.disasmer/
**/.disasmer/
/vscode-extension/node_modules/
/private/*/Cargo.lock
/private/*/target/

2597
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

36
Cargo.toml Normal file
View file

@ -0,0 +1,36 @@
[workspace]
resolver = "2"
members = [
"crates/disasmer-cli",
"crates/disasmer-coordinator",
"crates/disasmer-core",
"crates/disasmer-dap",
"crates/disasmer-macros",
"crates/disasmer-node",
"crates/disasmer-sdk",
"examples/launch-build-demo",
]
[workspace.package]
edition = "2021"
license = "Apache-2.0 OR MIT"
repository = "https://example.invalid/disasmer"
[workspace.dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
futures-executor = "0.3"
hex = "0.4"
proc-macro2 = "1.0"
postgres = { version = "0.19", features = ["with-serde_json-1"] }
quinn = { version = "0.11.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
quote = "1.0"
rcgen = "0.14"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
syn = { version = "2.0", features = ["full"] }
tempfile = "3.10"
thiserror = "1.0"
tokio = { version = "1.52", features = ["io-util", "macros", "rt-multi-thread"] }
wasmtime = { version = "=43.0.2", default-features = false, features = ["async", "cranelift", "debug", "runtime", "std", "wat"] }

20
DISASMER_PUBLIC_TREE.json Normal file
View file

@ -0,0 +1,20 @@
{
"kind": "disasmer-filtered-public-tree",
"source_commit": "1714a9eedd5b1e30c6c9aceb7a999f2f695ba83c",
"release_name": "dryrun-1714a9eedd5b",
"filtered_out": [
"private/**",
"experiments/**",
".git",
"target",
"/*.md",
"**/.disasmer/**",
".forgejo/**"
],
"public_export": {
"host_neutral": true,
"include_forgejo_workflows": false
},
"forgejo_host": "git.michelpaulissen.com",
"default_operator_endpoint": "https://disasmer.michelpaulissen.com:9443"
}

View file

@ -0,0 +1,20 @@
[package]
name = "disasmer-cli"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[[bin]]
name = "disasmer"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
disasmer-core = { path = "../disasmer-core" }
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
tempfile.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
[package]
name = "disasmer-coordinator"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
disasmer-core = { path = "../disasmer-core" }
postgres.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true

View file

@ -0,0 +1,709 @@
use std::collections::{BTreeMap, BTreeSet};
use disasmer_core::{
Actor, AuthContext, Authorization, CredentialKind, Digest, EnrollmentError, EnrollmentGrant,
NodeCredential, NodeId, ProcessId, ProjectId, SourceProviderKind, TenantId, UserId,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod postgres_store;
pub mod service;
pub use postgres_store::{
PostgresDurableStore, PostgresStoreError, PostgresTable, POSTGRES_DURABLE_TABLES,
};
pub use service::{
CoordinatorRequest, CoordinatorResponse, CoordinatorService, CoordinatorServiceError,
SourcePreparationDisposition, SourcePreparationStatus, TaskAssignment, TaskCompletionEvent,
TaskTerminalState,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TenantRecord {
pub id: TenantId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserRecord {
pub id: UserId,
pub tenant: TenantId,
pub credential_kind: CredentialKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectRecord {
pub id: ProjectId,
pub tenant: TenantId,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeIdentityRecord {
pub id: NodeId,
pub tenant: TenantId,
pub project: ProjectId,
pub public_key: String,
pub enrollment_scope: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CredentialRecord {
pub subject: String,
pub tenant: TenantId,
pub project: Option<ProjectId>,
pub kind: CredentialKind,
pub public_key_fingerprint: Option<Digest>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceProviderConfigRecord {
pub tenant: TenantId,
pub project: ProjectId,
pub provider: SourceProviderKind,
pub manifest_digest: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServicePolicyRecord {
pub tenant: TenantId,
pub name: String,
pub digest: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectPermissionRecord {
pub tenant: TenantId,
pub project: ProjectId,
pub user: UserId,
pub can_debug: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DurableState {
pub tenants: BTreeMap<TenantId, TenantRecord>,
pub users: BTreeMap<UserId, UserRecord>,
pub projects: BTreeMap<ProjectId, ProjectRecord>,
pub node_identities: BTreeMap<NodeId, NodeIdentityRecord>,
pub credentials: BTreeMap<String, CredentialRecord>,
pub source_provider_configs:
BTreeMap<(TenantId, ProjectId, String), SourceProviderConfigRecord>,
pub service_policy_records: BTreeMap<(TenantId, String), ServicePolicyRecord>,
pub project_permissions: BTreeMap<(TenantId, ProjectId, UserId), ProjectPermissionRecord>,
}
pub trait DurableStore {
fn load(&self) -> DurableState;
fn save(&mut self, state: DurableState);
}
pub trait FallibleDurableStore {
type Error;
fn load_state(&mut self) -> Result<DurableState, Self::Error>;
fn save_state(&mut self, state: &DurableState) -> Result<(), Self::Error>;
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryDurableStore {
state: DurableState,
}
impl DurableStore for InMemoryDurableStore {
fn load(&self) -> DurableState {
self.state.clone()
}
fn save(&mut self, state: DurableState) {
self.state = state;
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActiveProcess {
pub id: ProcessId,
pub tenant: TenantId,
pub project: ProjectId,
pub connected_nodes: BTreeSet<NodeId>,
pub coordinator_epoch: u64,
}
#[derive(Clone, Debug)]
pub struct Coordinator {
durable: DurableState,
active_processes: BTreeMap<ProcessId, ActiveProcess>,
coordinator_epoch: u64,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CoordinatorError {
#[error("node identity is not enrolled")]
UnknownNode,
#[error("node enrollment failed: {0:?}")]
Enrollment(EnrollmentError),
#[error("stale virtual process state from coordinator epoch {stale_epoch}; current epoch is {current_epoch}")]
StaleProcessEpoch {
stale_epoch: u64,
current_epoch: u64,
},
#[error("unauthorized coordinator action: {0}")]
Unauthorized(String),
}
impl Coordinator {
pub fn boot(store: &impl DurableStore, coordinator_epoch: u64) -> Self {
Self {
durable: store.load(),
active_processes: BTreeMap::new(),
coordinator_epoch,
}
}
pub fn try_boot<S: FallibleDurableStore>(
store: &mut S,
coordinator_epoch: u64,
) -> Result<Self, S::Error> {
Ok(Self {
durable: store.load_state()?,
active_processes: BTreeMap::new(),
coordinator_epoch,
})
}
pub fn persist(&self, store: &mut impl DurableStore) {
store.save(self.durable.clone());
}
pub fn try_persist<S: FallibleDurableStore>(&self, store: &mut S) -> Result<(), S::Error> {
store.save_state(&self.durable)
}
pub fn coordinator_epoch(&self) -> u64 {
self.coordinator_epoch
}
pub fn upsert_tenant(&mut self, id: TenantId) {
self.durable.tenants.insert(id.clone(), TenantRecord { id });
}
pub fn upsert_user(&mut self, tenant: TenantId, id: UserId, credential_kind: CredentialKind) {
self.durable.users.insert(
id.clone(),
UserRecord {
id,
tenant,
credential_kind,
},
);
}
pub fn upsert_project(&mut self, tenant: TenantId, id: ProjectId, name: impl Into<String>) {
self.durable.projects.insert(
id.clone(),
ProjectRecord {
id,
tenant,
name: name.into(),
},
);
}
pub fn enroll_node(
&mut self,
tenant: TenantId,
project: ProjectId,
node: NodeId,
public_key: impl Into<String>,
enrollment_scope: impl Into<String>,
) {
self.durable.node_identities.insert(
node.clone(),
NodeIdentityRecord {
id: node,
tenant,
project,
public_key: public_key.into(),
enrollment_scope: enrollment_scope.into(),
},
);
}
pub fn create_node_enrollment_grant(
&self,
tenant: TenantId,
project: ProjectId,
grant_id: impl Into<String>,
scope: impl Into<String>,
expires_at_epoch_seconds: u64,
) -> EnrollmentGrant {
EnrollmentGrant {
tenant,
project,
grant_id: grant_id.into(),
scope: scope.into(),
expires_at_epoch_seconds,
consumed: false,
}
}
pub fn exchange_node_enrollment_grant(
&mut self,
grant: &mut EnrollmentGrant,
node: NodeId,
public_key: &str,
requested_scope: &str,
now_epoch_seconds: u64,
) -> Result<NodeCredential, CoordinatorError> {
let credential = grant
.exchange_for_node_identity(
node.clone(),
public_key,
requested_scope,
now_epoch_seconds,
)
.map_err(CoordinatorError::Enrollment)?;
self.enroll_node(
credential.tenant.clone(),
credential.project.clone(),
node.clone(),
public_key,
credential.scope.clone(),
);
self.durable.credentials.insert(
format!("node:{node}"),
CredentialRecord {
subject: format!("node:{node}"),
tenant: credential.tenant.clone(),
project: Some(credential.project.clone()),
kind: credential.credential_kind.clone(),
public_key_fingerprint: Some(credential.public_key_fingerprint.clone()),
},
);
Ok(credential)
}
pub fn upsert_source_provider_config(
&mut self,
tenant: TenantId,
project: ProjectId,
provider: SourceProviderKind,
manifest_digest: Digest,
) {
let provider_key = format!("{provider:?}");
self.durable.source_provider_configs.insert(
(tenant.clone(), project.clone(), provider_key),
SourceProviderConfigRecord {
tenant,
project,
provider,
manifest_digest,
},
);
}
pub fn upsert_service_policy_record(
&mut self,
tenant: TenantId,
name: impl Into<String>,
digest: Digest,
) {
let name = name.into();
self.durable.service_policy_records.insert(
(tenant.clone(), name.clone()),
ServicePolicyRecord {
tenant,
name,
digest,
},
);
}
pub fn grant_project_debug(&mut self, tenant: TenantId, project: ProjectId, user: UserId) {
self.durable.project_permissions.insert(
(tenant.clone(), project.clone(), user.clone()),
ProjectPermissionRecord {
tenant,
project,
user,
can_debug: true,
},
);
}
pub fn start_process(
&mut self,
tenant: TenantId,
project: ProjectId,
id: ProcessId,
) -> ActiveProcess {
let process = ActiveProcess {
id: id.clone(),
tenant,
project,
connected_nodes: BTreeSet::new(),
coordinator_epoch: self.coordinator_epoch,
};
self.active_processes.insert(id, process.clone());
process
}
pub fn authorize_node_for_process(
&self,
node: &NodeId,
tenant: &TenantId,
project: &ProjectId,
process: &ProcessId,
) -> Result<(), CoordinatorError> {
let identity = self
.durable
.node_identities
.get(node)
.ok_or(CoordinatorError::UnknownNode)?;
if &identity.tenant != tenant || &identity.project != project {
return Err(CoordinatorError::Unauthorized(
"node identity is outside the requested tenant/project scope".to_owned(),
));
}
let Some(active) = self.active_processes.get(process) else {
return Err(CoordinatorError::Unauthorized(
"virtual process is not active in coordinator memory".to_owned(),
));
};
if &active.tenant != tenant || &active.project != project {
return Err(CoordinatorError::Unauthorized(
"node cannot claim tasks or publish artifacts outside its process scope".to_owned(),
));
}
Ok(())
}
pub fn reconnect_node(
&mut self,
node: &NodeId,
process: Option<(&ProcessId, u64)>,
) -> Result<(), CoordinatorError> {
if !self.durable.node_identities.contains_key(node) {
return Err(CoordinatorError::UnknownNode);
}
if let Some((process_id, stale_epoch)) = process {
if stale_epoch != self.coordinator_epoch {
return Err(CoordinatorError::StaleProcessEpoch {
stale_epoch,
current_epoch: self.coordinator_epoch,
});
}
if let Some(active) = self.active_processes.get_mut(process_id) {
active.connected_nodes.insert(node.clone());
}
}
Ok(())
}
pub fn list_projects(&self, context: &AuthContext) -> Vec<ProjectRecord> {
self.durable
.projects
.values()
.filter(|project| project.tenant == context.tenant)
.cloned()
.collect()
}
pub fn authorize_debug_attach(
&self,
context: &AuthContext,
process: &ProcessId,
) -> Authorization {
let Some(active) = self.active_processes.get(process) else {
return Authorization::deny("virtual process is not active");
};
if active.tenant != context.tenant || active.project != context.project {
return Authorization::deny("tenant or project mismatch");
}
let Actor::User(user) = &context.actor else {
return Authorization::deny("debug attach requires a user identity");
};
let permission = self.durable.project_permissions.get(&(
active.tenant.clone(),
active.project.clone(),
user.clone(),
));
if !permission.is_some_and(|permission| permission.can_debug) {
return Authorization::deny("debug attach requires explicit project permission");
}
Authorization::allow("debug attach authorized for project")
}
pub fn project(&self, id: &ProjectId) -> Option<&ProjectRecord> {
self.durable.projects.get(id)
}
pub fn active_process(&self, id: &ProcessId) -> Option<&ActiveProcess> {
self.active_processes.get(id)
}
pub fn active_process_count(&self) -> usize {
self.active_processes.len()
}
pub fn node_identity(&self, id: &NodeId) -> Option<&NodeIdentityRecord> {
self.durable.node_identities.get(id)
}
pub fn source_provider_config(
&self,
tenant: &TenantId,
project: &ProjectId,
provider: &str,
) -> Option<&SourceProviderConfigRecord> {
self.durable.source_provider_configs.get(&(
tenant.clone(),
project.clone(),
provider.to_owned(),
))
}
pub fn service_policy_record(
&self,
tenant: &TenantId,
name: &str,
) -> Option<&ServicePolicyRecord> {
self.durable
.service_policy_records
.get(&(tenant.clone(), name.to_owned()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coordinator_restart_preserves_project_but_not_live_processes() {
let mut store = InMemoryDurableStore::default();
let mut first = Coordinator::boot(&store, 1);
first.upsert_tenant(TenantId::from("tenant"));
first.upsert_user(
TenantId::from("tenant"),
UserId::from("user"),
CredentialKind::CliDeviceSession,
);
first.upsert_project(TenantId::from("tenant"), ProjectId::from("project"), "demo");
first.upsert_source_provider_config(
TenantId::from("tenant"),
ProjectId::from("project"),
SourceProviderKind::Git,
Digest::sha256("git-manifest"),
);
first.upsert_service_policy_record(
TenantId::from("tenant"),
"community tier",
Digest::sha256("policy"),
);
let mut grant = first.create_node_enrollment_grant(
TenantId::from("tenant"),
ProjectId::from("project"),
"grant",
"node:attach",
100,
);
first
.exchange_node_enrollment_grant(
&mut grant,
NodeId::from("node"),
"public-key",
"node:attach",
99,
)
.unwrap();
first.start_process(
TenantId::from("tenant"),
ProjectId::from("project"),
ProcessId::from("process"),
);
first.persist(&mut store);
let mut restarted = Coordinator::boot(&store, 2);
assert!(restarted
.durable
.tenants
.contains_key(&TenantId::from("tenant")));
assert!(restarted.durable.users.contains_key(&UserId::from("user")));
assert!(restarted.project(&ProjectId::from("project")).is_some());
assert!(restarted.node_identity(&NodeId::from("node")).is_some());
assert_eq!(
restarted
.durable
.credentials
.get("node:node")
.map(|credential| &credential.kind),
Some(&CredentialKind::NodeCredential)
);
assert!(restarted
.source_provider_config(
&TenantId::from("tenant"),
&ProjectId::from("project"),
"Git"
)
.is_some());
assert!(restarted
.service_policy_record(&TenantId::from("tenant"), "community tier")
.is_some());
assert_eq!(restarted.active_process_count(), 0);
let process = ProcessId::from("process-rerun");
let rerun = restarted.start_process(
TenantId::from("tenant"),
ProjectId::from("project"),
process.clone(),
);
assert_eq!(rerun.coordinator_epoch, 2);
restarted
.reconnect_node(&NodeId::from("node"), Some((&process, 2)))
.unwrap();
assert!(restarted
.active_process(&process)
.unwrap()
.connected_nodes
.contains(&NodeId::from("node")));
}
#[test]
fn node_reconnect_rejects_stale_process_epoch_after_restart() {
let mut store = InMemoryDurableStore::default();
let mut first = Coordinator::boot(&store, 1);
first.upsert_tenant(TenantId::from("tenant"));
first.upsert_project(TenantId::from("tenant"), ProjectId::from("project"), "demo");
first.enroll_node(
TenantId::from("tenant"),
ProjectId::from("project"),
NodeId::from("node"),
"public-key",
"node",
);
first.persist(&mut store);
let mut restarted = Coordinator::boot(&store, 2);
restarted
.reconnect_node(&NodeId::from("node"), None)
.unwrap();
let error = restarted
.reconnect_node(
&NodeId::from("node"),
Some((&ProcessId::from("process"), 1)),
)
.unwrap_err();
assert!(matches!(error, CoordinatorError::StaleProcessEpoch { .. }));
}
#[test]
fn node_enrollment_grant_becomes_persistent_node_identity() {
let store = InMemoryDurableStore::default();
let mut coordinator = Coordinator::boot(&store, 1);
let mut grant = coordinator.create_node_enrollment_grant(
TenantId::from("tenant"),
ProjectId::from("project"),
"grant",
"node:attach",
100,
);
let credential = coordinator
.exchange_node_enrollment_grant(
&mut grant,
NodeId::from("node"),
"public-key",
"node:attach",
99,
)
.unwrap();
assert_eq!(credential.credential_kind, CredentialKind::NodeCredential);
assert!(coordinator.node_identity(&NodeId::from("node")).is_some());
}
#[test]
fn project_listing_is_filtered_by_tenant() {
let store = InMemoryDurableStore::default();
let mut coordinator = Coordinator::boot(&store, 1);
coordinator.upsert_project(
TenantId::from("tenant-a"),
ProjectId::from("project-a"),
"a",
);
coordinator.upsert_project(
TenantId::from("tenant-b"),
ProjectId::from("project-b"),
"b",
);
let projects = coordinator.list_projects(&AuthContext {
tenant: TenantId::from("tenant-a"),
project: ProjectId::from("project-a"),
actor: Actor::User(UserId::from("user-a")),
});
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].id, ProjectId::from("project-a"));
}
#[test]
fn node_cannot_claim_process_outside_authorized_scope() {
let store = InMemoryDurableStore::default();
let mut coordinator = Coordinator::boot(&store, 1);
coordinator.enroll_node(
TenantId::from("tenant-a"),
ProjectId::from("project-a"),
NodeId::from("node-a"),
"public-key",
"node",
);
coordinator.start_process(
TenantId::from("tenant-b"),
ProjectId::from("project-b"),
ProcessId::from("process-b"),
);
let error = coordinator
.authorize_node_for_process(
&NodeId::from("node-a"),
&TenantId::from("tenant-b"),
&ProjectId::from("project-b"),
&ProcessId::from("process-b"),
)
.unwrap_err();
assert!(matches!(error, CoordinatorError::Unauthorized(_)));
}
#[test]
fn debug_attach_requires_explicit_project_permission() {
let store = InMemoryDurableStore::default();
let mut coordinator = Coordinator::boot(&store, 1);
coordinator.start_process(
TenantId::from("tenant"),
ProjectId::from("project"),
ProcessId::from("process"),
);
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let denied = coordinator.authorize_debug_attach(&context, &ProcessId::from("process"));
coordinator.grant_project_debug(
TenantId::from("tenant"),
ProjectId::from("project"),
UserId::from("user"),
);
let allowed = coordinator.authorize_debug_attach(&context, &ProcessId::from("process"));
assert!(!denied.allowed);
assert!(denied.reason.contains("explicit project permission"));
assert!(allowed.allowed);
}
}

View file

@ -0,0 +1,22 @@
use std::io::Write;
use disasmer_coordinator::{service::bind_listener, CoordinatorService};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut listen = "127.0.0.1:0".to_owned();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--listen" {
listen = args.next().ok_or("--listen requires an address")?;
}
}
let (listener, addr) = bind_listener(&listen)?;
println!("{}", json!({ "listen": addr.to_string() }));
std::io::stdout().flush()?;
let service = CoordinatorService::new(1);
service.serve_tcp(listener)?;
Ok(())
}

View file

@ -0,0 +1,459 @@
use postgres::{Client, NoTls};
use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;
use crate::{
CredentialRecord, DurableState, FallibleDurableStore, NodeIdentityRecord,
ProjectPermissionRecord, ProjectRecord, ServicePolicyRecord, SourceProviderConfigRecord,
TenantRecord, UserRecord,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PostgresTable {
pub name: &'static str,
pub durable_record: &'static str,
pub restart_surviving: bool,
}
pub const POSTGRES_DURABLE_TABLES: &[PostgresTable] = &[
PostgresTable {
name: "disasmer_tenants",
durable_record: "tenants",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_users",
durable_record: "users",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_projects",
durable_record: "projects",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_node_identities",
durable_record: "node identities",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_credentials",
durable_record: "credentials",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_source_provider_configs",
durable_record: "source-provider configuration",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_service_policy_records",
durable_record: "durable service policy records",
restart_surviving: true,
},
PostgresTable {
name: "disasmer_project_permissions",
durable_record: "explicit project permissions",
restart_surviving: true,
},
];
#[derive(Debug, Error)]
pub enum PostgresStoreError {
#[error("postgres durable store error: {0}")]
Postgres(#[from] postgres::Error),
#[error("durable state serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
pub struct PostgresDurableStore {
client: Client,
}
impl PostgresDurableStore {
pub fn connect(connection_string: &str) -> Result<Self, PostgresStoreError> {
let mut store = Self {
client: Client::connect(connection_string, NoTls)?,
};
store.migrate()?;
Ok(store)
}
pub fn from_client(client: Client) -> Result<Self, PostgresStoreError> {
let mut store = Self { client };
store.migrate()?;
Ok(store)
}
pub fn schema_sql() -> &'static str {
POSTGRES_SCHEMA_SQL
}
pub fn durable_tables() -> &'static [PostgresTable] {
POSTGRES_DURABLE_TABLES
}
pub fn migrate(&mut self) -> Result<(), PostgresStoreError> {
self.client.batch_execute(Self::schema_sql())?;
Ok(())
}
fn query_records<T: DeserializeOwned>(
&mut self,
sql: &str,
) -> Result<Vec<T>, PostgresStoreError> {
self.client
.query(sql, &[])?
.into_iter()
.map(|row| {
let value: serde_json::Value = row.get("record");
Ok(serde_json::from_value(value)?)
})
.collect()
}
fn record_value(record: &impl Serialize) -> Result<serde_json::Value, PostgresStoreError> {
Ok(serde_json::to_value(record)?)
}
}
impl FallibleDurableStore for PostgresDurableStore {
type Error = PostgresStoreError;
fn load_state(&mut self) -> Result<DurableState, Self::Error> {
let mut state = DurableState::default();
for record in self.query_records::<TenantRecord>(
"SELECT record FROM disasmer_tenants ORDER BY tenant_id",
)? {
state.tenants.insert(record.id.clone(), record);
}
for record in
self.query_records::<UserRecord>("SELECT record FROM disasmer_users ORDER BY user_id")?
{
state.users.insert(record.id.clone(), record);
}
for record in self.query_records::<ProjectRecord>(
"SELECT record FROM disasmer_projects ORDER BY project_id",
)? {
state.projects.insert(record.id.clone(), record);
}
for record in self.query_records::<NodeIdentityRecord>(
"SELECT record FROM disasmer_node_identities ORDER BY node_id",
)? {
state.node_identities.insert(record.id.clone(), record);
}
for record in self.query_records::<CredentialRecord>(
"SELECT record FROM disasmer_credentials ORDER BY subject",
)? {
state.credentials.insert(record.subject.clone(), record);
}
for record in self.query_records::<SourceProviderConfigRecord>(
"SELECT record FROM disasmer_source_provider_configs ORDER BY tenant_id, project_id, provider_key",
)? {
let provider_key = format!("{:?}", record.provider);
state.source_provider_configs.insert(
(record.tenant.clone(), record.project.clone(), provider_key),
record,
);
}
for record in self.query_records::<ServicePolicyRecord>(
"SELECT record FROM disasmer_service_policy_records ORDER BY tenant_id, name",
)? {
state
.service_policy_records
.insert((record.tenant.clone(), record.name.clone()), record);
}
for record in self.query_records::<ProjectPermissionRecord>(
"SELECT record FROM disasmer_project_permissions ORDER BY tenant_id, project_id, user_id",
)? {
state.project_permissions.insert(
(
record.tenant.clone(),
record.project.clone(),
record.user.clone(),
),
record,
);
}
Ok(state)
}
fn save_state(&mut self, state: &DurableState) -> Result<(), Self::Error> {
let mut tx = self.client.transaction()?;
tx.batch_execute(
"
DELETE FROM disasmer_project_permissions;
DELETE FROM disasmer_service_policy_records;
DELETE FROM disasmer_source_provider_configs;
DELETE FROM disasmer_credentials;
DELETE FROM disasmer_node_identities;
DELETE FROM disasmer_projects;
DELETE FROM disasmer_users;
DELETE FROM disasmer_tenants;
",
)?;
for record in state.tenants.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_tenants (tenant_id, record) VALUES ($1, $2)",
&[&record.id.as_str(), &value],
)?;
}
for record in state.users.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_users (user_id, tenant_id, record) VALUES ($1, $2, $3)",
&[&record.id.as_str(), &record.tenant.as_str(), &value],
)?;
}
for record in state.projects.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_projects (project_id, tenant_id, record) VALUES ($1, $2, $3)",
&[&record.id.as_str(), &record.tenant.as_str(), &value],
)?;
}
for record in state.node_identities.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_node_identities (node_id, tenant_id, project_id, record) VALUES ($1, $2, $3, $4)",
&[
&record.id.as_str(),
&record.tenant.as_str(),
&record.project.as_str(),
&value,
],
)?;
}
for record in state.credentials.values() {
let value = Self::record_value(record)?;
let project_id = record.project.as_ref().map(|project| project.as_str());
tx.execute(
"INSERT INTO disasmer_credentials (subject, tenant_id, project_id, record) VALUES ($1, $2, $3, $4)",
&[&record.subject.as_str(), &record.tenant.as_str(), &project_id, &value],
)?;
}
for ((_, _, provider_key), record) in &state.source_provider_configs {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_source_provider_configs (tenant_id, project_id, provider_key, record) VALUES ($1, $2, $3, $4)",
&[
&record.tenant.as_str(),
&record.project.as_str(),
&provider_key.as_str(),
&value,
],
)?;
}
for record in state.service_policy_records.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_service_policy_records (tenant_id, name, record) VALUES ($1, $2, $3)",
&[&record.tenant.as_str(), &record.name.as_str(), &value],
)?;
}
for record in state.project_permissions.values() {
let value = Self::record_value(record)?;
tx.execute(
"INSERT INTO disasmer_project_permissions (tenant_id, project_id, user_id, record) VALUES ($1, $2, $3, $4)",
&[
&record.tenant.as_str(),
&record.project.as_str(),
&record.user.as_str(),
&value,
],
)?;
}
tx.commit()?;
Ok(())
}
}
const POSTGRES_SCHEMA_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS disasmer_tenants (
tenant_id TEXT PRIMARY KEY,
record JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS disasmer_users (
user_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
record JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS disasmer_projects (
project_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
record JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS disasmer_node_identities (
node_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
project_id TEXT NOT NULL REFERENCES disasmer_projects(project_id) ON DELETE CASCADE,
record JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS disasmer_credentials (
subject TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
project_id TEXT REFERENCES disasmer_projects(project_id) ON DELETE CASCADE,
record JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS disasmer_source_provider_configs (
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
project_id TEXT NOT NULL REFERENCES disasmer_projects(project_id) ON DELETE CASCADE,
provider_key TEXT NOT NULL,
record JSONB NOT NULL,
PRIMARY KEY (tenant_id, project_id, provider_key)
);
CREATE TABLE IF NOT EXISTS disasmer_service_policy_records (
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
name TEXT NOT NULL,
record JSONB NOT NULL,
PRIMARY KEY (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS disasmer_project_permissions (
tenant_id TEXT NOT NULL REFERENCES disasmer_tenants(tenant_id) ON DELETE CASCADE,
project_id TEXT NOT NULL REFERENCES disasmer_projects(project_id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES disasmer_users(user_id) ON DELETE CASCADE,
record JSONB NOT NULL,
PRIMARY KEY (tenant_id, project_id, user_id)
);
"#;
#[cfg(test)]
mod tests {
use disasmer_core::{
CredentialKind, Digest, NodeId, ProjectId, SourceProviderKind, TenantId, UserId,
};
use super::*;
use crate::{Coordinator, DurableStore, FallibleDurableStore, InMemoryDurableStore};
#[test]
fn postgres_schema_contains_only_restart_surviving_durable_tables() {
let names = PostgresDurableStore::durable_tables()
.iter()
.map(|table| table.name)
.collect::<Vec<_>>();
assert_eq!(names.len(), 8);
assert!(names.contains(&"disasmer_tenants"));
assert!(names.contains(&"disasmer_users"));
assert!(names.contains(&"disasmer_projects"));
assert!(names.contains(&"disasmer_node_identities"));
assert!(names.contains(&"disasmer_credentials"));
assert!(names.contains(&"disasmer_source_provider_configs"));
assert!(names.contains(&"disasmer_service_policy_records"));
assert!(names.contains(&"disasmer_project_permissions"));
assert!(PostgresDurableStore::durable_tables()
.iter()
.all(|table| table.restart_surviving));
for runtime_only in [
"active_process",
"virtual_thread",
"scheduler_state",
"debug_epoch",
"vfs_manifest",
"transient_artifact_location",
] {
assert!(
!PostgresDurableStore::schema_sql().contains(runtime_only),
"{runtime_only} must remain outside Postgres durable state"
);
}
}
#[test]
fn fallible_store_boot_uses_durable_state_and_still_drops_live_processes() {
#[derive(Default)]
struct FallibleMemoryStore {
inner: InMemoryDurableStore,
}
impl FallibleDurableStore for FallibleMemoryStore {
type Error = std::convert::Infallible;
fn load_state(&mut self) -> Result<DurableState, Self::Error> {
Ok(self.inner.load())
}
fn save_state(&mut self, state: &DurableState) -> Result<(), Self::Error> {
self.inner.save(state.clone());
Ok(())
}
}
let mut store = FallibleMemoryStore::default();
let mut first = Coordinator::try_boot(&mut store, 1).unwrap();
first.upsert_tenant(TenantId::from("tenant"));
first.upsert_user(
TenantId::from("tenant"),
UserId::from("user"),
CredentialKind::CliDeviceSession,
);
first.upsert_project(TenantId::from("tenant"), ProjectId::from("project"), "demo");
first.enroll_node(
TenantId::from("tenant"),
ProjectId::from("project"),
NodeId::from("node"),
"public-key",
"node:attach",
);
first.upsert_source_provider_config(
TenantId::from("tenant"),
ProjectId::from("project"),
SourceProviderKind::Git,
Digest::sha256("git-manifest"),
);
first.start_process(
TenantId::from("tenant"),
ProjectId::from("project"),
disasmer_core::ProcessId::from("process"),
);
first.try_persist(&mut store).unwrap();
let restarted = Coordinator::try_boot(&mut store, 2).unwrap();
assert!(restarted.project(&ProjectId::from("project")).is_some());
assert!(restarted.node_identity(&NodeId::from("node")).is_some());
assert_eq!(restarted.active_process_count(), 0);
}
#[test]
fn postgres_round_trip_runs_when_dsn_is_configured() {
let Ok(dsn) = std::env::var("DISASMER_TEST_POSTGRES") else {
return;
};
let mut store = PostgresDurableStore::connect(&dsn).unwrap();
let mut state = DurableState::default();
state.tenants.insert(
TenantId::from("tenant"),
TenantRecord {
id: TenantId::from("tenant"),
},
);
state.projects.insert(
ProjectId::from("project"),
ProjectRecord {
id: ProjectId::from("project"),
tenant: TenantId::from("tenant"),
name: "demo".to_owned(),
},
);
store.save_state(&state).unwrap();
let loaded = store.load_state().unwrap();
assert!(loaded.projects.contains_key(&ProjectId::from("project")));
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
[package]
name = "disasmer-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
hex.workspace = true
sha2.workspace = true
thiserror.workspace = true
[dev-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,939 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
auth::same_tenant_project, Actor, ArtifactId, AuthContext, Digest, LimitError, LimitKind,
NodeId, ProcessId, ProjectId, ResourceLimits, ResourceMeter, Scope, TaskId, TenantId,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum StorageLocation {
RetainedNode(NodeId),
ExplicitStore(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RetentionPolicy {
pub best_effort_node_retention: bool,
pub max_download_bytes: u64,
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
best_effort_node_retention: true,
max_download_bytes: 256 * 1024 * 1024,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DownloadPolicy {
pub max_bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DownloadAction {
pub artifact: ArtifactId,
pub source: StorageLocation,
pub scoped_token_subject: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DownloadLink {
pub artifact: ArtifactId,
pub source: StorageLocation,
pub url_path: String,
pub scoped_token_digest: Digest,
pub expires_at_epoch_seconds: u64,
pub tenant: TenantId,
pub project: ProjectId,
pub process: ProcessId,
pub actor: Actor,
pub max_bytes: u64,
pub policy_context_digest: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuedDownloadLink {
pub link: DownloadLink,
pub revoked: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactDownloadStream {
pub link: DownloadLink,
pub streamed_bytes: u64,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum DownloadError {
#[error("artifact does not exist")]
NotFound,
#[error("artifact is unavailable from current retention or explicit storage")]
Unavailable,
#[error("artifact download direct connectivity unavailable: {0}")]
DirectConnectivityUnavailable(String),
#[error("artifact download denied: {0}")]
Unauthorized(String),
#[error("artifact size {size} exceeds download limit {limit}")]
LimitExceeded { size: u64, limit: u64 },
#[error("download link token is invalid for this scoped artifact link")]
InvalidToken,
#[error("download link has expired")]
Expired,
#[error("download link has been revoked")]
Revoked,
#[error("download usage limit failed: {0}")]
Usage(String),
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("artifact is unavailable because node-local unsynced bytes were lost")]
pub struct ArtifactUnavailable;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactMetadata {
pub id: ArtifactId,
pub tenant: TenantId,
pub project: ProjectId,
pub process: ProcessId,
pub producer_task: TaskId,
pub producer_node: NodeId,
pub digest: Digest,
pub size: u64,
pub flushed_epoch: u64,
pub retaining_nodes: BTreeSet<NodeId>,
pub explicit_locations: Vec<String>,
pub coordinator_has_large_bytes: bool,
}
#[derive(Clone, Debug, Default)]
pub struct ArtifactRegistry {
artifacts: BTreeMap<ArtifactId, ArtifactMetadata>,
issued_download_links: BTreeMap<Digest, IssuedDownloadLink>,
next_epoch: u64,
}
impl ArtifactRegistry {
pub fn flush_metadata(
&mut self,
id: ArtifactId,
tenant: TenantId,
project: ProjectId,
process: ProcessId,
producer_task: TaskId,
retaining_node: NodeId,
digest: Digest,
size: u64,
) -> ArtifactMetadata {
self.next_epoch += 1;
let metadata = ArtifactMetadata {
id: id.clone(),
tenant,
project,
process,
producer_task,
producer_node: retaining_node.clone(),
digest,
size,
flushed_epoch: self.next_epoch,
retaining_nodes: BTreeSet::from([retaining_node]),
explicit_locations: Vec::new(),
coordinator_has_large_bytes: false,
};
self.artifacts.insert(id, metadata.clone());
metadata
}
pub fn sync_to_explicit_store(
&mut self,
artifact: &ArtifactId,
location: impl Into<String>,
) -> Result<(), ArtifactUnavailable> {
let metadata = self
.artifacts
.get_mut(artifact)
.ok_or(ArtifactUnavailable)?;
metadata.explicit_locations.push(location.into());
Ok(())
}
pub fn garbage_collect_node(&mut self, node: &NodeId) {
for metadata in self.artifacts.values_mut() {
metadata.retaining_nodes.remove(node);
}
}
pub fn metadata(&self, artifact: &ArtifactId) -> Option<&ArtifactMetadata> {
self.artifacts.get(artifact)
}
pub fn download_action(
&self,
context: &AuthContext,
artifact: &ArtifactId,
policy: &DownloadPolicy,
) -> Result<DownloadAction, DownloadError> {
let metadata = self
.artifacts
.get(artifact)
.ok_or(DownloadError::NotFound)?;
let scope = Scope {
tenant: metadata.tenant.clone(),
project: metadata.project.clone(),
process: Some(metadata.process.clone()),
task: Some(metadata.producer_task.clone()),
node: None,
artifact: Some(metadata.id.clone()),
};
let authz = same_tenant_project(context, &scope);
if !authz.allowed {
return Err(DownloadError::Unauthorized(authz.reason));
}
if metadata.size > policy.max_bytes {
return Err(DownloadError::LimitExceeded {
size: metadata.size,
limit: policy.max_bytes,
});
}
let source = metadata
.retaining_nodes
.iter()
.next()
.cloned()
.map(StorageLocation::RetainedNode)
.or_else(|| {
metadata
.explicit_locations
.first()
.cloned()
.map(StorageLocation::ExplicitStore)
})
.ok_or(DownloadError::Unavailable)?;
Ok(DownloadAction {
artifact: artifact.clone(),
source,
scoped_token_subject: format!(
"{}/{}/{}/{}",
metadata.tenant, metadata.project, metadata.process, metadata.id
),
})
}
pub fn downloadable_size(
&self,
context: &AuthContext,
artifact: &ArtifactId,
policy: &DownloadPolicy,
) -> Result<u64, DownloadError> {
self.download_action(context, artifact, policy)?;
let metadata = self
.artifacts
.get(artifact)
.ok_or(DownloadError::NotFound)?;
Ok(metadata.size)
}
pub fn create_download_link(
&mut self,
context: &AuthContext,
artifact: &ArtifactId,
policy: &DownloadPolicy,
token_nonce: &str,
now_epoch_seconds: u64,
ttl_seconds: u64,
) -> Result<DownloadLink, DownloadError> {
let action = self.download_action(context, artifact, policy)?;
let metadata = self
.artifacts
.get(artifact)
.ok_or(DownloadError::NotFound)?;
let expires_at_epoch_seconds = now_epoch_seconds.saturating_add(ttl_seconds);
let policy_context_digest =
download_policy_context_digest(metadata, &action.source, policy);
let scoped_token_digest = Digest::from_parts([
b"artifact-download-token:v2".as_slice(),
action.scoped_token_subject.as_bytes(),
actor_subject(&context.actor).as_bytes(),
token_nonce.as_bytes(),
metadata.digest.as_str().as_bytes(),
metadata.size.to_string().as_bytes(),
policy_context_digest.as_str().as_bytes(),
expires_at_epoch_seconds.to_string().as_bytes(),
]);
let link = DownloadLink {
artifact: artifact.clone(),
source: action.source,
url_path: format!(
"/artifacts/{}/{}/{}/{}",
metadata.tenant, metadata.project, metadata.process, metadata.id
),
scoped_token_digest,
expires_at_epoch_seconds,
tenant: metadata.tenant.clone(),
project: metadata.project.clone(),
process: metadata.process.clone(),
actor: context.actor.clone(),
max_bytes: policy.max_bytes,
policy_context_digest,
};
self.issued_download_links.insert(
link.scoped_token_digest.clone(),
IssuedDownloadLink {
link: link.clone(),
revoked: false,
},
);
Ok(link)
}
pub fn revoke_download_link(
&mut self,
context: &AuthContext,
artifact: &ArtifactId,
presented_token_digest: &Digest,
) -> Result<DownloadLink, DownloadError> {
let issued = self
.issued_download_links
.get(presented_token_digest)
.ok_or(DownloadError::InvalidToken)?;
if issued.link.artifact != *artifact || issued.link.actor != context.actor {
return Err(DownloadError::InvalidToken);
}
self.download_action(
context,
artifact,
&DownloadPolicy {
max_bytes: issued.link.max_bytes,
},
)?;
let issued = self
.issued_download_links
.get_mut(presented_token_digest)
.ok_or(DownloadError::InvalidToken)?;
issued.revoked = true;
Ok(issued.link.clone())
}
pub fn open_download_stream(
&self,
context: &AuthContext,
artifact: &ArtifactId,
policy: &DownloadPolicy,
presented_token_digest: &Digest,
now_epoch_seconds: u64,
limits: &ResourceLimits,
meter: &mut ResourceMeter,
) -> Result<ArtifactDownloadStream, DownloadError> {
let issued = self
.issued_download_links
.get(presented_token_digest)
.ok_or(DownloadError::InvalidToken)?;
if issued.link.artifact != *artifact
|| issued.link.max_bytes != policy.max_bytes
|| issued.link.actor != context.actor
{
return Err(DownloadError::InvalidToken);
}
if issued.revoked {
return Err(DownloadError::Revoked);
}
if now_epoch_seconds > issued.link.expires_at_epoch_seconds {
return Err(DownloadError::Expired);
}
let action = self.download_action(context, artifact, policy)?;
if action.source != issued.link.source {
return Err(DownloadError::Unavailable);
}
let metadata = self
.artifacts
.get(artifact)
.ok_or(DownloadError::NotFound)?;
if download_policy_context_digest(metadata, &action.source, policy)
!= issued.link.policy_context_digest
{
return Err(DownloadError::InvalidToken);
}
meter
.charge(limits, LimitKind::ArtifactDownloadBytes, 0)
.map_err(download_usage_error)?;
Ok(ArtifactDownloadStream {
link: issued.link.clone(),
streamed_bytes: 0,
})
}
pub fn stream_download_chunk(
&self,
stream: &mut ArtifactDownloadStream,
limits: &ResourceLimits,
meter: &mut ResourceMeter,
bytes: u64,
) -> Result<(), DownloadError> {
let metadata = self
.artifacts
.get(&stream.link.artifact)
.ok_or(DownloadError::NotFound)?;
if !source_is_available(metadata, &stream.link.source) {
return Err(DownloadError::Unavailable);
}
stream.stream_chunk(limits, meter, bytes)
}
}
impl ArtifactDownloadStream {
pub fn stream_chunk(
&mut self,
limits: &ResourceLimits,
meter: &mut ResourceMeter,
bytes: u64,
) -> Result<(), DownloadError> {
if self.streamed_bytes.saturating_add(bytes) > self.link.max_bytes {
return Err(DownloadError::LimitExceeded {
size: self.streamed_bytes.saturating_add(bytes),
limit: self.link.max_bytes,
});
}
meter
.charge(limits, LimitKind::ArtifactDownloadBytes, bytes)
.map_err(download_usage_error)?;
self.streamed_bytes += bytes;
Ok(())
}
}
fn download_usage_error(error: LimitError) -> DownloadError {
DownloadError::Usage(error.to_string())
}
fn download_policy_context_digest(
metadata: &ArtifactMetadata,
source: &StorageLocation,
policy: &DownloadPolicy,
) -> Digest {
Digest::from_parts([
b"artifact-download-policy-context:v1".as_slice(),
metadata.tenant.as_str().as_bytes(),
metadata.project.as_str().as_bytes(),
metadata.process.as_str().as_bytes(),
metadata.id.as_str().as_bytes(),
metadata.digest.as_str().as_bytes(),
metadata.size.to_string().as_bytes(),
storage_location_key(source).as_bytes(),
policy.max_bytes.to_string().as_bytes(),
])
}
fn actor_subject(actor: &Actor) -> String {
match actor {
Actor::User(id) => format!("user:{id}"),
Actor::Agent(id) => format!("agent:{id}"),
Actor::Node(id) => format!("node:{id}"),
Actor::Task(id) => format!("task:{id}"),
}
}
fn storage_location_key(source: &StorageLocation) -> String {
match source {
StorageLocation::RetainedNode(node) => format!("retained-node:{node}"),
StorageLocation::ExplicitStore(location) => format!("explicit-store:{location}"),
}
}
fn source_is_available(metadata: &ArtifactMetadata, source: &StorageLocation) -> bool {
match source {
StorageLocation::RetainedNode(node) => metadata.retaining_nodes.contains(node),
StorageLocation::ExplicitStore(location) => metadata.explicit_locations.contains(location),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crate::{Actor, LimitKind, ResourceLimits, ResourceMeter, UserId};
use super::*;
fn registry_with_artifact() -> ArtifactRegistry {
let mut registry = ArtifactRegistry::default();
registry.flush_metadata(
ArtifactId::from("artifact"),
TenantId::from("tenant"),
ProjectId::from("project"),
ProcessId::from("process"),
TaskId::from("task"),
NodeId::from("node"),
Digest::sha256("bytes"),
32,
);
registry
}
#[test]
fn flush_publishes_metadata_without_coordinator_bytes() {
let registry = registry_with_artifact();
let metadata = registry.metadata(&ArtifactId::from("artifact")).unwrap();
assert!(!metadata.coordinator_has_large_bytes);
assert_eq!(metadata.id, ArtifactId::from("artifact"));
assert_eq!(metadata.tenant, TenantId::from("tenant"));
assert_eq!(metadata.project, ProjectId::from("project"));
assert_eq!(metadata.process, ProcessId::from("process"));
assert_eq!(metadata.producer_task, TaskId::from("task"));
assert_eq!(metadata.producer_node, NodeId::from("node"));
assert_eq!(metadata.digest, Digest::sha256("bytes"));
assert_eq!(metadata.size, 32);
assert_eq!(metadata.flushed_epoch, 1);
assert!(metadata.retaining_nodes.contains(&NodeId::from("node")));
assert!(metadata.explicit_locations.is_empty());
}
#[test]
fn unsynced_node_loss_surfaces_as_unavailable() {
let mut registry = registry_with_artifact();
registry.garbage_collect_node(&NodeId::from("node"));
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let error = registry
.download_action(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
)
.unwrap_err();
assert_eq!(error, DownloadError::Unavailable);
}
#[test]
fn explicit_user_storage_location_survives_node_retention_loss() {
let mut registry = registry_with_artifact();
registry
.sync_to_explicit_store(&ArtifactId::from("artifact"), "s3://bucket/app")
.unwrap();
registry.garbage_collect_node(&NodeId::from("node"));
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let action = registry
.download_action(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
)
.unwrap();
assert_eq!(
action.source,
StorageLocation::ExplicitStore("s3://bucket/app".to_owned())
);
assert!(
!registry
.metadata(&ArtifactId::from("artifact"))
.unwrap()
.coordinator_has_large_bytes
);
}
#[test]
fn default_retention_policy_is_best_effort_node_retention() {
let policy = RetentionPolicy::default();
assert!(policy.best_effort_node_retention);
assert_eq!(policy.max_download_bytes, 256 * 1024 * 1024);
}
#[test]
fn cross_tenant_download_is_denied_even_with_known_artifact_id() {
let registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("other"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let error = registry
.download_action(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
)
.unwrap_err();
assert!(matches!(error, DownloadError::Unauthorized(_)));
}
#[test]
fn cross_project_download_is_denied_even_with_known_artifact_id() {
let registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("other-project"),
actor: Actor::User(UserId::from("user")),
};
let error = registry
.download_action(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
)
.unwrap_err();
assert!(matches!(error, DownloadError::Unauthorized(_)));
}
#[test]
fn download_link_is_not_created_when_artifact_is_unavailable_or_too_large() {
let mut registry = registry_with_artifact();
registry.garbage_collect_node(&NodeId::from("node"));
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
assert_eq!(
registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
"nonce",
10,
60,
)
.unwrap_err(),
DownloadError::Unavailable
);
let mut registry = registry_with_artifact();
assert!(matches!(
registry.create_download_link(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 1 },
"nonce",
10,
60,
),
Err(DownloadError::LimitExceeded { .. })
));
}
#[test]
fn download_link_is_authenticated_scoped_and_not_guessable() {
let mut registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let link = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
"nonce-a",
10,
60,
)
.unwrap();
let other = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
"nonce-b",
10,
60,
)
.unwrap();
assert_eq!(link.tenant, TenantId::from("tenant"));
assert_eq!(link.project, ProjectId::from("project"));
assert_eq!(link.process, ProcessId::from("process"));
assert_eq!(link.actor, Actor::User(UserId::from("user")));
assert_eq!(link.max_bytes, 100);
assert!(link.policy_context_digest.is_valid_sha256());
assert_eq!(link.expires_at_epoch_seconds, 70);
assert!(link
.url_path
.contains("/artifacts/tenant/project/process/artifact"));
assert_ne!(link.scoped_token_digest, other.scoped_token_digest);
assert!(matches!(link.source, StorageLocation::RetainedNode(_)));
}
#[test]
fn download_link_is_bound_to_actor_and_policy_context() {
let mut registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let other_actor = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("other-user")),
};
let policy = DownloadPolicy { max_bytes: 100 };
let link = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&policy,
"nonce",
10,
60,
)
.unwrap();
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 32)]),
};
let mut meter = ResourceMeter::default();
assert_eq!(
registry
.open_download_stream(
&other_actor,
&ArtifactId::from("artifact"),
&policy,
&link.scoped_token_digest,
11,
&limits,
&mut meter,
)
.unwrap_err(),
DownloadError::InvalidToken
);
assert_eq!(
registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 99 },
&link.scoped_token_digest,
11,
&limits,
&mut meter,
)
.unwrap_err(),
DownloadError::InvalidToken
);
assert_eq!(
registry
.revoke_download_link(
&other_actor,
&ArtifactId::from("artifact"),
&link.scoped_token_digest,
)
.unwrap_err(),
DownloadError::InvalidToken
);
}
#[test]
fn download_link_expires_and_can_be_revoked() {
let mut registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let policy = DownloadPolicy { max_bytes: 100 };
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 32)]),
};
let mut meter = ResourceMeter::default();
let expired = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&policy,
"expired",
10,
5,
)
.unwrap();
let error = registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&policy,
&expired.scoped_token_digest,
16,
&limits,
&mut meter,
)
.unwrap_err();
assert_eq!(error, DownloadError::Expired);
let active = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&policy,
"active",
20,
60,
)
.unwrap();
let revoked = registry
.revoke_download_link(
&context,
&ArtifactId::from("artifact"),
&active.scoped_token_digest,
)
.unwrap();
assert_eq!(revoked.scoped_token_digest, active.scoped_token_digest);
let error = registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&policy,
&active.scoped_token_digest,
21,
&limits,
&mut meter,
)
.unwrap_err();
assert_eq!(error, DownloadError::Revoked);
}
#[test]
fn download_stream_accounts_usage_before_and_during_streaming() {
let mut registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let policy = DownloadPolicy { max_bytes: 100 };
let link = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&policy,
"nonce",
10,
60,
)
.unwrap();
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 32)]),
};
let mut meter = ResourceMeter::default();
let mut stream = registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&policy,
&link.scoped_token_digest,
11,
&limits,
&mut meter,
)
.unwrap();
stream.stream_chunk(&limits, &mut meter, 16).unwrap();
stream.stream_chunk(&limits, &mut meter, 16).unwrap();
assert!(matches!(
stream.stream_chunk(&limits, &mut meter, 1),
Err(DownloadError::Usage(_))
));
assert_eq!(meter.used(&LimitKind::ArtifactDownloadBytes), 32);
}
#[test]
fn download_stream_fails_honestly_when_source_disappears_mid_stream() {
let mut registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let policy = DownloadPolicy { max_bytes: 100 };
let link = registry
.create_download_link(
&context,
&ArtifactId::from("artifact"),
&policy,
"nonce",
10,
60,
)
.unwrap();
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 32)]),
};
let mut meter = ResourceMeter::default();
let mut stream = registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&policy,
&link.scoped_token_digest,
11,
&limits,
&mut meter,
)
.unwrap();
registry
.stream_download_chunk(&mut stream, &limits, &mut meter, 16)
.unwrap();
registry.garbage_collect_node(&NodeId::from("node"));
assert_eq!(
registry
.stream_download_chunk(&mut stream, &limits, &mut meter, 1)
.unwrap_err(),
DownloadError::Unavailable
);
assert_eq!(stream.streamed_bytes, 16);
}
#[test]
fn guessed_download_token_is_rejected() {
let registry = registry_with_artifact();
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 32)]),
};
let mut meter = ResourceMeter::default();
let error = registry
.open_download_stream(
&context,
&ArtifactId::from("artifact"),
&DownloadPolicy { max_bytes: 100 },
&Digest::sha256("guessed"),
11,
&limits,
&mut meter,
)
.unwrap_err();
assert_eq!(error, DownloadError::InvalidToken);
}
}

View file

@ -0,0 +1,369 @@
use serde::{Deserialize, Serialize};
use crate::{AgentId, ArtifactId, Digest, NodeId, ProcessId, ProjectId, TaskId, TenantId, UserId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Actor {
User(UserId),
Agent(AgentId),
Node(NodeId),
Task(TaskId),
}
impl Actor {
pub fn kind(&self) -> IdentityKind {
match self {
Self::User(_) => IdentityKind::User,
Self::Agent(_) => IdentityKind::Agent,
Self::Node(_) => IdentityKind::Node,
Self::Task(_) => IdentityKind::Task,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdentityKind {
User,
Agent,
Node,
Project,
Task,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CredentialKind {
BrowserSession,
CliDeviceSession,
PublicKey,
NodeCredential,
TaskCredential,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthContext {
pub tenant: TenantId,
pub project: ProjectId,
pub actor: Actor,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Action {
CreateProject,
AttachNode,
CreateNodeEnrollmentGrant,
ExchangeNodeEnrollmentGrant,
LoginBrowser,
LoginCli,
EnrollAgent,
List,
Inspect,
Mutate,
ClaimTask,
DebugAttach,
DebugRead,
DownloadArtifact,
PublishArtifact,
RunNativeCommand,
RunContainer,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrowserLoginFlow {
pub authorization_url: String,
pub callback_path: String,
pub state: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CliLoginFlow {
pub verification_url: String,
pub user_code: String,
pub device_code: String,
pub expires_in_seconds: u64,
pub yields_long_lived_secret_directly: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicKeyIdentity {
pub subject: Actor,
pub public_key: String,
pub fingerprint: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnrollmentGrant {
pub tenant: TenantId,
pub project: ProjectId,
pub grant_id: String,
pub scope: String,
pub expires_at_epoch_seconds: u64,
pub consumed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeCredential {
pub node: NodeId,
pub tenant: TenantId,
pub project: ProjectId,
pub public_key_fingerprint: Digest,
pub scope: String,
pub capability_policy_digest: Digest,
pub credential_kind: CredentialKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EnrollmentError {
Expired,
AlreadyConsumed,
WrongScope,
}
impl EnrollmentGrant {
pub fn exchange_for_node_identity(
&mut self,
node: NodeId,
public_key: &str,
requested_scope: &str,
now_epoch_seconds: u64,
) -> Result<NodeCredential, EnrollmentError> {
if self.consumed {
return Err(EnrollmentError::AlreadyConsumed);
}
if now_epoch_seconds > self.expires_at_epoch_seconds {
return Err(EnrollmentError::Expired);
}
if requested_scope != self.scope {
return Err(EnrollmentError::WrongScope);
}
self.consumed = true;
let capability_policy_digest =
node_capability_policy_digest(&self.tenant, &self.project, &self.scope);
Ok(NodeCredential {
node,
tenant: self.tenant.clone(),
project: self.project.clone(),
public_key_fingerprint: Digest::sha256(public_key),
scope: self.scope.clone(),
capability_policy_digest,
credential_kind: CredentialKind::NodeCredential,
})
}
}
pub fn node_capability_policy_digest(
tenant: &TenantId,
project: &ProjectId,
scope: &str,
) -> Digest {
Digest::from_parts([
b"node-capability-policy:v1".as_slice(),
tenant.as_str().as_bytes(),
project.as_str().as_bytes(),
scope.as_bytes(),
])
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Scope {
pub tenant: TenantId,
pub project: ProjectId,
pub process: Option<ProcessId>,
pub task: Option<TaskId>,
pub node: Option<NodeId>,
pub artifact: Option<ArtifactId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Authorization {
pub allowed: bool,
pub reason: String,
}
impl Authorization {
pub fn allow(reason: impl Into<String>) -> Self {
Self {
allowed: true,
reason: reason.into(),
}
}
pub fn deny(reason: impl Into<String>) -> Self {
Self {
allowed: false,
reason: reason.into(),
}
}
}
pub fn same_tenant_project(context: &AuthContext, scope: &Scope) -> Authorization {
if context.tenant != scope.tenant {
return Authorization::deny("tenant mismatch");
}
if context.project != scope.project {
return Authorization::deny("project mismatch");
}
Authorization::allow("same tenant and project")
}
pub fn task_credentials_do_not_contain_user_session(
task: &Actor,
credentials: &[CredentialKind],
) -> Authorization {
if !matches!(task, Actor::Task(_)) {
return Authorization::deny("credential check requires task actor");
}
if credentials.iter().any(|credential| {
matches!(
credential,
CredentialKind::BrowserSession | CredentialKind::CliDeviceSession
)
}) {
return Authorization::deny(
"user OAuth/session tokens must not be passed to nodes as task credentials",
);
}
Authorization::allow("task credentials are scoped runtime credentials")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tenant_project_scope_denies_cross_tenant_access() {
let context = AuthContext {
tenant: TenantId::from("tenant-a"),
project: ProjectId::from("project-a"),
actor: Actor::User(UserId::from("user-a")),
};
let scope = Scope {
tenant: TenantId::from("tenant-b"),
project: ProjectId::from("project-a"),
process: None,
task: None,
node: None,
artifact: None,
};
assert!(!same_tenant_project(&context, &scope).allowed);
}
#[test]
fn node_enrollment_exchanges_short_lived_grant_once() {
let mut grant = EnrollmentGrant {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
grant_id: "grant".to_owned(),
scope: "node:attach".to_owned(),
expires_at_epoch_seconds: 100,
consumed: false,
};
let credential = grant
.exchange_for_node_identity(NodeId::from("node"), "public-key", "node:attach", 99)
.unwrap();
assert_eq!(credential.credential_kind, CredentialKind::NodeCredential);
assert_eq!(credential.tenant, TenantId::from("tenant"));
assert_eq!(credential.project, ProjectId::from("project"));
assert_eq!(credential.node, NodeId::from("node"));
assert_eq!(credential.scope, "node:attach");
assert_eq!(
credential.capability_policy_digest,
node_capability_policy_digest(
&TenantId::from("tenant"),
&ProjectId::from("project"),
"node:attach"
)
);
assert_eq!(
grant.exchange_for_node_identity(
NodeId::from("node2"),
"public-key",
"node:attach",
99
),
Err(EnrollmentError::AlreadyConsumed)
);
}
#[test]
fn node_capability_policy_digest_is_scoped() {
let base = node_capability_policy_digest(
&TenantId::from("tenant"),
&ProjectId::from("project"),
"node:attach",
);
let other_project = node_capability_policy_digest(
&TenantId::from("tenant"),
&ProjectId::from("other"),
"node:attach",
);
let other_scope = node_capability_policy_digest(
&TenantId::from("tenant"),
&ProjectId::from("project"),
"node:limited",
);
assert!(base.is_valid_sha256());
assert_ne!(base, other_project);
assert_ne!(base, other_scope);
}
#[test]
fn task_credentials_reject_user_session_tokens() {
for credential in [
CredentialKind::BrowserSession,
CredentialKind::CliDeviceSession,
] {
let authz = task_credentials_do_not_contain_user_session(
&Actor::Task(TaskId::from("task")),
&[CredentialKind::TaskCredential, credential],
);
assert!(!authz.allowed);
assert!(authz.reason.contains("must not be passed"));
}
let scoped = task_credentials_do_not_contain_user_session(
&Actor::Task(TaskId::from("task")),
&[
CredentialKind::TaskCredential,
CredentialKind::NodeCredential,
],
);
assert!(scoped.allowed);
}
#[test]
fn identities_remain_distinct_for_authorization() {
assert_eq!(Actor::User(UserId::from("user")).kind(), IdentityKind::User);
assert_eq!(
Actor::Agent(AgentId::from("agent")).kind(),
IdentityKind::Agent
);
assert_eq!(Actor::Node(NodeId::from("node")).kind(), IdentityKind::Node);
assert_eq!(Actor::Task(TaskId::from("task")).kind(), IdentityKind::Task);
let scope = Scope {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: Some(ProcessId::from("process")),
task: Some(TaskId::from("task")),
node: Some(NodeId::from("node")),
artifact: Some(ArtifactId::from("artifact")),
};
assert_eq!(scope.process, Some(ProcessId::from("process")));
assert_eq!(scope.artifact, Some(ArtifactId::from("artifact")));
assert_ne!(
CredentialKind::BrowserSession,
CredentialKind::CliDeviceSession
);
assert_ne!(CredentialKind::PublicKey, CredentialKind::NodeCredential);
assert_ne!(
CredentialKind::NodeCredential,
CredentialKind::TaskCredential
);
}
}

View file

@ -0,0 +1,117 @@
use serde::{Deserialize, Serialize};
use crate::{Digest, EnvironmentResource};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelectedInput {
pub path: String,
pub digest: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleIdentityInputs {
pub wasm_code: Digest,
pub task_abi: Digest,
pub environments: Vec<EnvironmentResource>,
pub source_provider_manifest: Digest,
pub selected_inputs: Vec<SelectedInput>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleMetadata {
pub identity: Digest,
pub environments: Vec<EnvironmentResource>,
pub selected_inputs: Vec<SelectedInput>,
pub embeds_full_container_images: bool,
}
impl BundleIdentityInputs {
pub fn identity(&self) -> Digest {
let mut parts = vec![
b"bundle:v1".to_vec(),
self.wasm_code.as_str().as_bytes().to_vec(),
self.task_abi.as_str().as_bytes().to_vec(),
self.source_provider_manifest.as_str().as_bytes().to_vec(),
];
let mut environments = self.environments.clone();
environments.sort_by(|left, right| left.name.cmp(&right.name));
for environment in environments {
parts.push(environment.name.as_bytes().to_vec());
parts.push(format!("{:?}", environment.kind).into_bytes());
parts.push(environment.digest.as_str().as_bytes().to_vec());
}
let mut inputs = self.selected_inputs.clone();
inputs.sort_by(|left, right| left.path.cmp(&right.path));
for input in inputs {
parts.push(input.path.into_bytes());
parts.push(input.digest.as_str().as_bytes().to_vec());
}
Digest::from_parts(parts)
}
pub fn inspectable_metadata(&self) -> BundleMetadata {
BundleMetadata {
identity: self.identity(),
environments: self.environments.clone(),
selected_inputs: self.selected_inputs.clone(),
embeds_full_container_images: false,
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{EnvironmentKind, EnvironmentRequirements};
use super::*;
fn env(digest: &str) -> EnvironmentResource {
EnvironmentResource {
name: "linux".to_owned(),
kind: EnvironmentKind::Containerfile,
recipe_path: PathBuf::from("envs/linux/Containerfile"),
context_path: PathBuf::from("envs/linux"),
digest: Digest::sha256(digest),
requirements: EnvironmentRequirements::linux_container(),
}
}
#[test]
fn bundle_identity_changes_when_environment_recipe_changes() {
let base = BundleIdentityInputs {
wasm_code: Digest::sha256("wasm"),
task_abi: Digest::sha256("abi"),
environments: vec![env("recipe-a")],
source_provider_manifest: Digest::sha256("source"),
selected_inputs: vec![],
};
let mut changed = base.clone();
changed.environments = vec![env("recipe-b")];
assert_ne!(base.identity(), changed.identity());
}
#[test]
fn bundle_metadata_is_inspectable_and_does_not_vendor_images_by_default() {
let inputs = BundleIdentityInputs {
wasm_code: Digest::sha256("wasm"),
task_abi: Digest::sha256("abi"),
environments: vec![env("recipe")],
source_provider_manifest: Digest::sha256("source"),
selected_inputs: vec![SelectedInput {
path: "inputs/config.json".to_owned(),
digest: Digest::sha256("config"),
}],
};
let metadata = inputs.inspectable_metadata();
assert_eq!(metadata.environments.len(), 1);
assert!(!metadata.embeds_full_container_images);
}
}

View file

@ -0,0 +1,189 @@
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Capability {
Command,
Containers,
RootlessPodman,
SourceFilesystem,
SourceGit,
HostFilesystem,
Network,
Secrets,
InboundPorts,
ArbitrarySyscalls,
VfsArtifacts,
Wasmtime,
WindowsCommandDev,
QuicDirect,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EnvironmentBackend {
Container,
NixFlake,
WindowsCommandDev,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Os {
Linux,
Windows,
Macos,
Other(String),
}
impl Os {
pub fn current() -> Self {
match std::env::consts::OS {
"linux" => Self::Linux,
"windows" => Self::Windows,
"macos" => Self::Macos,
other => Self::Other(other.to_owned()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeCapabilities {
pub os: Os,
pub arch: String,
pub capabilities: BTreeSet<Capability>,
pub environment_backends: BTreeSet<EnvironmentBackend>,
pub source_providers: BTreeSet<String>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CapabilityReportError {
#[error("node architecture `{0}` is invalid")]
InvalidArchitecture(String),
#[error("node OS label `{0}` is invalid")]
InvalidOsLabel(String),
#[error("source provider id `{0}` is invalid")]
InvalidSourceProvider(String),
}
impl NodeCapabilities {
pub fn detect_current() -> Self {
let os = Os::current();
let mut capabilities = BTreeSet::from([
Capability::Command,
Capability::SourceFilesystem,
Capability::VfsArtifacts,
Capability::Wasmtime,
]);
let mut environment_backends = BTreeSet::new();
match os {
Os::Linux => {
capabilities.insert(Capability::Containers);
capabilities.insert(Capability::RootlessPodman);
environment_backends.insert(EnvironmentBackend::Container);
}
Os::Windows => {
capabilities.insert(Capability::WindowsCommandDev);
environment_backends.insert(EnvironmentBackend::WindowsCommandDev);
}
Os::Macos | Os::Other(_) => {}
}
Self {
os,
arch: std::env::consts::ARCH.to_owned(),
capabilities,
environment_backends,
source_providers: BTreeSet::from(["filesystem".to_owned(), "git".to_owned()]),
}
}
pub fn with_capability(mut self, capability: Capability) -> Self {
self.capabilities.insert(capability);
self
}
pub fn has_all(&self, required: &BTreeSet<Capability>) -> bool {
required
.iter()
.all(|capability| self.capabilities.contains(capability))
}
pub fn validate_public_report(&self) -> Result<(), CapabilityReportError> {
if !valid_capability_label(&self.arch) {
return Err(CapabilityReportError::InvalidArchitecture(
self.arch.clone(),
));
}
if let Os::Other(label) = &self.os {
if !valid_capability_label(label) {
return Err(CapabilityReportError::InvalidOsLabel(label.clone()));
}
}
for provider in &self.source_providers {
if !valid_source_provider_id(provider) {
return Err(CapabilityReportError::InvalidSourceProvider(
provider.clone(),
));
}
}
Ok(())
}
}
fn valid_capability_label(label: &str) -> bool {
!label.is_empty()
&& label.len() <= 64
&& label.bytes().all(
|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.'),
)
}
fn valid_source_provider_id(provider: &str) -> bool {
!provider.is_empty()
&& provider.len() <= 64
&& provider
.bytes()
.all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
}
#[cfg(test)]
mod tests {
use super::*;
fn capabilities() -> NodeCapabilities {
NodeCapabilities {
os: Os::Linux,
arch: "x86_64".to_owned(),
capabilities: BTreeSet::from([Capability::Command]),
environment_backends: BTreeSet::new(),
source_providers: BTreeSet::from(["filesystem".to_owned(), "git".to_owned()]),
}
}
#[test]
fn capability_reports_validate_hostile_strings() {
assert!(capabilities().validate_public_report().is_ok());
let mut invalid_arch = capabilities();
invalid_arch.arch = "x86_64\nmalicious".to_owned();
assert_eq!(
invalid_arch.validate_public_report(),
Err(CapabilityReportError::InvalidArchitecture(
"x86_64\nmalicious".to_owned()
))
);
let mut invalid_provider = capabilities();
invalid_provider
.source_providers
.insert("../checkout".to_owned());
assert_eq!(
invalid_provider.validate_public_report(),
Err(CapabilityReportError::InvalidSourceProvider(
"../checkout".to_owned()
))
);
}
}

View file

@ -0,0 +1,282 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{Digest, EnvironmentResource, TaskId, VfsManifest};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckpointBoundary {
pub task_entrypoint: String,
pub serialized_args: Digest,
pub environment_digest: Digest,
pub vfs_epoch: u64,
pub task_abi: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskCheckpoint {
pub task: TaskId,
pub boundary: CheckpointBoundary,
pub vfs_manifest: VfsManifest,
pub depends_on_live_stack: bool,
pub depends_on_live_socket: bool,
pub depends_on_ephemeral_artifact_durability: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestartRequest {
pub task: TaskId,
pub entrypoint: String,
pub serialized_args: Digest,
pub environment: EnvironmentResource,
pub task_abi: Digest,
pub source_edited: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RestartDecision {
RestartTask {
task: TaskId,
from_vfs_epoch: u64,
discard_unflushed_changes: bool,
},
RestartWholeVirtualProcess {
message: String,
},
}
#[derive(Clone, Debug, Default)]
pub struct RestartPolicy;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum CompatibilityFailure {
#[error("task entrypoint changed")]
Entrypoint,
#[error("serialized task arguments changed")]
Args,
#[error("environment digest changed")]
Environment,
#[error("task ABI changed")]
TaskAbi,
#[error("checkpoint depends on unsupported live stack migration")]
LiveStack,
#[error("checkpoint depends on unsupported live socket checkpointing")]
LiveSocket,
#[error("checkpoint incorrectly treats ephemeral artifacts as durable")]
EphemeralArtifactDurability,
}
impl RestartPolicy {
pub fn decide(&self, checkpoint: &TaskCheckpoint, request: &RestartRequest) -> RestartDecision {
match compatibility_failure(checkpoint, request) {
None => RestartDecision::RestartTask {
task: checkpoint.task.clone(),
from_vfs_epoch: checkpoint.boundary.vfs_epoch,
discard_unflushed_changes: true,
},
Some(failure) => RestartDecision::RestartWholeVirtualProcess {
message: format!(
"cannot restart selected task `{}` from checkpoint: {failure}; restart the whole virtual process",
checkpoint.task
),
},
}
}
}
fn compatibility_failure(
checkpoint: &TaskCheckpoint,
request: &RestartRequest,
) -> Option<CompatibilityFailure> {
if checkpoint.depends_on_live_stack {
return Some(CompatibilityFailure::LiveStack);
}
if checkpoint.depends_on_live_socket {
return Some(CompatibilityFailure::LiveSocket);
}
if checkpoint.depends_on_ephemeral_artifact_durability {
return Some(CompatibilityFailure::EphemeralArtifactDurability);
}
if checkpoint.boundary.task_entrypoint != request.entrypoint {
return Some(CompatibilityFailure::Entrypoint);
}
if checkpoint.boundary.serialized_args != request.serialized_args {
return Some(CompatibilityFailure::Args);
}
if checkpoint.boundary.environment_digest != request.environment.digest {
return Some(CompatibilityFailure::Environment);
}
if checkpoint.boundary.task_abi != request.task_abi {
return Some(CompatibilityFailure::TaskAbi);
}
None
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{EnvironmentKind, EnvironmentRequirements, NodeId, VfsOverlay, VfsPath};
use super::*;
fn env(digest_input: &str) -> EnvironmentResource {
EnvironmentResource {
name: "linux".to_owned(),
kind: EnvironmentKind::Containerfile,
recipe_path: PathBuf::from("envs/linux/Containerfile"),
context_path: PathBuf::from("envs/linux"),
digest: Digest::sha256(digest_input),
requirements: EnvironmentRequirements::linux_container(),
}
}
fn checkpoint() -> (TaskCheckpoint, EnvironmentResource) {
let environment = env("env");
let mut overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node"));
overlay.write(
VfsPath::new("/vfs/artifacts/app").unwrap(),
Digest::sha256("app"),
3,
);
let manifest = overlay.flush();
(
TaskCheckpoint {
task: TaskId::from("task"),
boundary: CheckpointBoundary {
task_entrypoint: "compile_linux".to_owned(),
serialized_args: Digest::sha256("args"),
environment_digest: environment.digest.clone(),
vfs_epoch: manifest.epoch,
task_abi: Digest::sha256("abi"),
},
vfs_manifest: manifest,
depends_on_live_stack: false,
depends_on_live_socket: false,
depends_on_ephemeral_artifact_durability: false,
},
environment,
)
}
fn restart_request(environment: EnvironmentResource) -> RestartRequest {
RestartRequest {
task: TaskId::from("task"),
entrypoint: "compile_linux".to_owned(),
serialized_args: Digest::sha256("args"),
environment,
task_abi: Digest::sha256("abi"),
source_edited: true,
}
}
fn assert_whole_process_restart(decision: RestartDecision, expected_reason: &str) {
match decision {
RestartDecision::RestartWholeVirtualProcess { message } => {
assert!(
message.contains(expected_reason),
"restart message `{message}` did not include `{expected_reason}`"
);
assert!(
message.contains("restart the whole virtual process"),
"restart message `{message}` did not direct a whole-process restart"
);
}
RestartDecision::RestartTask { .. } => {
panic!("incompatible checkpoint unexpectedly restarted selected task")
}
}
}
#[test]
fn compatible_restart_uses_task_boundary_and_discards_unflushed_changes() {
let (checkpoint, environment) = checkpoint();
let request = restart_request(environment);
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_eq!(
decision,
RestartDecision::RestartTask {
task: TaskId::from("task"),
from_vfs_epoch: 1,
discard_unflushed_changes: true
}
);
}
#[test]
fn incompatible_environment_requires_whole_process_restart() {
let (checkpoint, _) = checkpoint();
let request = restart_request(env("changed-env"));
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "environment digest changed");
}
#[test]
fn incompatible_entrypoint_requires_whole_process_restart() {
let (checkpoint, environment) = checkpoint();
let mut request = restart_request(environment);
request.entrypoint = "package_linux".to_owned();
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "task entrypoint changed");
}
#[test]
fn incompatible_serialized_args_require_whole_process_restart() {
let (checkpoint, environment) = checkpoint();
let mut request = restart_request(environment);
request.serialized_args = Digest::sha256("changed-args");
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "serialized task arguments changed");
}
#[test]
fn incompatible_task_abi_requires_whole_process_restart() {
let (checkpoint, environment) = checkpoint();
let mut request = restart_request(environment);
request.task_abi = Digest::sha256("changed-abi");
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "task ABI changed");
}
#[test]
fn restart_never_claims_live_stack_migration() {
let (mut checkpoint, environment) = checkpoint();
checkpoint.depends_on_live_stack = true;
let request = restart_request(environment);
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "live stack");
}
#[test]
fn restart_never_claims_live_socket_checkpointing() {
let (mut checkpoint, environment) = checkpoint();
checkpoint.depends_on_live_socket = true;
let request = restart_request(environment);
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "live socket");
}
#[test]
fn restart_never_depends_on_ephemeral_artifact_durability() {
let (mut checkpoint, environment) = checkpoint();
checkpoint.depends_on_ephemeral_artifact_durability = true;
let request = restart_request(environment);
let decision = RestartPolicy.decide(&checkpoint, &request);
assert_whole_process_restart(decision, "ephemeral artifacts");
}
}

View file

@ -0,0 +1,276 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{ProcessId, TaskId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebugParticipantKind {
WasmTask,
ControlledNativeCommand,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebugRuntimeState {
Running,
Frozen,
Completed,
Failed(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebugParticipant {
pub task: TaskId,
pub name: String,
pub kind: DebugParticipantKind,
pub can_freeze: bool,
pub state: DebugRuntimeState,
pub stack_frames: Vec<String>,
pub local_values: Vec<(String, String)>,
pub task_args: Vec<(String, String)>,
pub handles: Vec<(String, String)>,
pub command_status: Option<String>,
pub recent_output: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebugStopReason {
Breakpoint { task: TaskId, line: u32 },
PauseRequest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadInspection {
pub task: TaskId,
pub name: String,
pub stack_frames: Vec<String>,
pub local_values: Vec<(String, String)>,
pub task_args: Vec<(String, String)>,
pub handles: Vec<(String, String)>,
pub command_status: Option<String>,
pub recent_output: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebugEpoch {
pub process: ProcessId,
pub epoch: u64,
pub reason: DebugStopReason,
participants: BTreeMap<TaskId, DebugParticipant>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum DebugEpochError {
#[error("participant `{task}` cannot freeze, so all-stop failed")]
CannotFreeze { task: TaskId },
#[error("participant `{0}` is not part of this debug epoch")]
UnknownParticipant(TaskId),
}
impl DebugEpoch {
pub fn all_stop(
process: ProcessId,
epoch: u64,
reason: DebugStopReason,
participants: Vec<DebugParticipant>,
) -> Result<Self, DebugEpochError> {
for participant in &participants {
if matches!(
participant.kind,
DebugParticipantKind::WasmTask | DebugParticipantKind::ControlledNativeCommand
) && !participant.can_freeze
{
return Err(DebugEpochError::CannotFreeze {
task: participant.task.clone(),
});
}
}
let participants = participants
.into_iter()
.map(|mut participant| {
if matches!(participant.state, DebugRuntimeState::Running) {
participant.state = DebugRuntimeState::Frozen;
}
(participant.task.clone(), participant)
})
.collect();
Ok(Self {
process,
epoch,
reason,
participants,
})
}
pub fn pause(
process: ProcessId,
epoch: u64,
participants: Vec<DebugParticipant>,
) -> Result<Self, DebugEpochError> {
Self::all_stop(process, epoch, DebugStopReason::PauseRequest, participants)
}
pub fn continue_all(&mut self) {
for participant in self.participants.values_mut() {
if participant.state == DebugRuntimeState::Frozen {
participant.state = DebugRuntimeState::Running;
}
}
}
pub fn inspection(&self, task: &TaskId) -> Result<ThreadInspection, DebugEpochError> {
let participant = self
.participants
.get(task)
.ok_or_else(|| DebugEpochError::UnknownParticipant(task.clone()))?;
Ok(ThreadInspection {
task: participant.task.clone(),
name: participant.name.clone(),
stack_frames: participant.stack_frames.clone(),
local_values: participant.local_values.clone(),
task_args: participant.task_args.clone(),
handles: participant.handles.clone(),
command_status: participant.command_status.clone(),
recent_output: participant.recent_output.clone(),
})
}
pub fn participant_state(&self, task: &TaskId) -> Option<&DebugRuntimeState> {
self.participants
.get(task)
.map(|participant| &participant.state)
}
pub fn thread_names(&self) -> Vec<String> {
self.participants
.values()
.map(|participant| participant.name.clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn participant(task: &str, kind: DebugParticipantKind, can_freeze: bool) -> DebugParticipant {
DebugParticipant {
task: TaskId::from(task),
name: task.to_owned(),
kind,
can_freeze,
state: DebugRuntimeState::Running,
stack_frames: vec![format!("{task}::run")],
local_values: vec![("wasm_local_0".to_owned(), "I32(41)".to_owned())],
task_args: vec![("target".to_owned(), "linux".to_owned())],
handles: vec![("artifact".to_owned(), "artifact-1".to_owned())],
command_status: Some("running".to_owned()),
recent_output: vec!["building".to_owned()],
}
}
#[test]
fn breakpoint_creates_all_stop_debug_epoch_for_wasm_and_command_tasks() {
let epoch = DebugEpoch::all_stop(
ProcessId::from("process"),
1,
DebugStopReason::Breakpoint {
task: TaskId::from("compile-linux"),
line: 42,
},
vec![
participant("main", DebugParticipantKind::WasmTask, true),
participant(
"compile-linux",
DebugParticipantKind::ControlledNativeCommand,
true,
),
],
)
.unwrap();
assert_eq!(
epoch.participant_state(&TaskId::from("main")),
Some(&DebugRuntimeState::Frozen)
);
assert_eq!(
epoch.participant_state(&TaskId::from("compile-linux")),
Some(&DebugRuntimeState::Frozen)
);
}
#[test]
fn debug_epoch_reports_freeze_failure_instead_of_claiming_all_stop() {
let error = DebugEpoch::pause(
ProcessId::from("process"),
1,
vec![participant(
"compile-linux",
DebugParticipantKind::ControlledNativeCommand,
false,
)],
)
.unwrap_err();
assert!(matches!(error, DebugEpochError::CannotFreeze { .. }));
}
#[test]
fn continue_resumes_every_frozen_participant() {
let mut epoch = DebugEpoch::pause(
ProcessId::from("process"),
1,
vec![
participant("main", DebugParticipantKind::WasmTask, true),
participant("task", DebugParticipantKind::WasmTask, true),
],
)
.unwrap();
epoch.continue_all();
assert_eq!(
epoch.participant_state(&TaskId::from("main")),
Some(&DebugRuntimeState::Running)
);
assert_eq!(
epoch.participant_state(&TaskId::from("task")),
Some(&DebugRuntimeState::Running)
);
}
#[test]
fn inspection_exposes_stack_args_handles_command_status_and_output() {
let epoch = DebugEpoch::pause(
ProcessId::from("process"),
1,
vec![participant(
"compile-linux",
DebugParticipantKind::ControlledNativeCommand,
true,
)],
)
.unwrap();
let inspection = epoch.inspection(&TaskId::from("compile-linux")).unwrap();
assert_eq!(inspection.stack_frames, vec!["compile-linux::run"]);
assert_eq!(
inspection.local_values[0],
("wasm_local_0".to_owned(), "I32(41)".to_owned())
);
assert_eq!(
inspection.task_args[0],
("target".to_owned(), "linux".to_owned())
);
assert_eq!(
inspection.handles[0],
("artifact".to_owned(), "artifact-1".to_owned())
);
assert_eq!(inspection.command_status, Some("running".to_owned()));
assert_eq!(inspection.recent_output, vec!["building"]);
}
}

View file

@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest as ShaDigest, Sha256};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Digest(String);
impl Digest {
pub fn sha256(bytes: impl AsRef<[u8]>) -> Self {
let mut hasher = Sha256::new();
hasher.update(bytes.as_ref());
Self(format!("sha256:{}", hex::encode(hasher.finalize())))
}
pub fn from_parts(parts: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self {
let mut hasher = Sha256::new();
for part in parts {
let part = part.as_ref();
hasher.update((part.len() as u64).to_be_bytes());
hasher.update(part);
}
Self(format!("sha256:{}", hex::encode(hasher.finalize())))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_valid_sha256(&self) -> bool {
let Some(hex) = self.0.strip_prefix("sha256:") else {
return false;
};
hex.len() == 64
&& hex
.bytes()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
}
}
impl std::fmt::Display for Digest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_validates_strict_sha256_syntax() {
assert!(Digest::sha256("bytes").is_valid_sha256());
assert!(!Digest("sha1:abc".to_owned()).is_valid_sha256());
assert!(!Digest("sha256:ABCDEF".to_owned()).is_valid_sha256());
assert!(!Digest("sha256:not-hex".to_owned()).is_valid_sha256());
}
}

View file

@ -0,0 +1,288 @@
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{Capability, Digest, Os};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EnvironmentKind {
Containerfile,
Dockerfile,
NixFlake,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentRequirements {
pub os: Option<Os>,
pub arch: Option<String>,
pub capabilities: BTreeSet<Capability>,
}
impl EnvironmentRequirements {
pub fn linux_container() -> Self {
Self {
os: Some(Os::Linux),
arch: None,
capabilities: BTreeSet::from([Capability::Containers, Capability::RootlessPodman]),
}
}
pub fn windows_command_dev() -> Self {
Self {
os: Some(Os::Windows),
arch: None,
capabilities: BTreeSet::from([Capability::WindowsCommandDev]),
}
}
pub fn unconstrained() -> Self {
Self {
os: None,
arch: None,
capabilities: BTreeSet::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentResource {
pub name: String,
pub kind: EnvironmentKind,
pub recipe_path: PathBuf,
pub context_path: PathBuf,
pub digest: Digest,
pub requirements: EnvironmentRequirements,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentReference {
pub name: String,
pub byte_offset: usize,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentDiagnostic {
pub reference: EnvironmentReference,
pub message: String,
}
#[derive(Debug, Error)]
pub enum EnvironmentError {
#[error("failed to read environment resources under {path}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
pub fn discover_environments(
project_root: &Path,
) -> Result<Vec<EnvironmentResource>, EnvironmentError> {
let envs_dir = project_root.join("envs");
if !envs_dir.exists() {
return Ok(Vec::new());
}
let mut resources = Vec::new();
let entries = fs::read_dir(&envs_dir).map_err(|source| EnvironmentError::Read {
path: envs_dir.clone(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| EnvironmentError::Read {
path: envs_dir.clone(),
source,
})?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if let Some(resource) = discover_one(project_root, name, &path)? {
resources.push(resource);
}
}
resources.sort_by(|left, right| left.name.cmp(&right.name));
Ok(resources)
}
pub fn diagnose_environment_references(
source: &str,
environments: &[EnvironmentResource],
) -> Vec<EnvironmentDiagnostic> {
let known = environments
.iter()
.map(|environment| environment.name.as_str())
.collect::<BTreeSet<_>>();
find_env_macro_references(source)
.into_iter()
.filter(|reference| !known.contains(reference.name.as_str()))
.map(|reference| EnvironmentDiagnostic {
message: format!(
"missing Disasmer environment `{}`; expected envs/{}/Containerfile or envs/{}/Dockerfile",
reference.name, reference.name, reference.name
),
reference,
})
.collect()
}
fn discover_one(
project_root: &Path,
name: &str,
env_dir: &Path,
) -> Result<Option<EnvironmentResource>, EnvironmentError> {
let candidates = [
("Containerfile", EnvironmentKind::Containerfile),
("Dockerfile", EnvironmentKind::Dockerfile),
("flake.nix", EnvironmentKind::NixFlake),
];
for (file_name, kind) in candidates {
let recipe_path = env_dir.join(file_name);
if !recipe_path.exists() {
continue;
}
let recipe_bytes = fs::read(&recipe_path).map_err(|source| EnvironmentError::Read {
path: recipe_path.clone(),
source,
})?;
let relative_recipe = recipe_path
.strip_prefix(project_root)
.unwrap_or(&recipe_path)
.to_string_lossy();
let digest = Digest::from_parts([
b"environment:v1".as_slice(),
name.as_bytes(),
format!("{kind:?}").as_bytes(),
relative_recipe.as_bytes(),
recipe_bytes.as_slice(),
]);
let requirements = match kind {
EnvironmentKind::Containerfile | EnvironmentKind::Dockerfile
if name.eq_ignore_ascii_case("windows") =>
{
EnvironmentRequirements::windows_command_dev()
}
EnvironmentKind::Containerfile | EnvironmentKind::Dockerfile => {
EnvironmentRequirements::linux_container()
}
EnvironmentKind::NixFlake => EnvironmentRequirements::unconstrained(),
};
return Ok(Some(EnvironmentResource {
name: name.to_owned(),
kind,
recipe_path,
context_path: env_dir.to_path_buf(),
digest,
requirements,
}));
}
Ok(None)
}
fn find_env_macro_references(source: &str) -> Vec<EnvironmentReference> {
let mut references = Vec::new();
let mut cursor = 0;
while let Some(index) = source[cursor..].find("env!(") {
let start = cursor + index;
let mut pos = start + "env!(".len();
while source[pos..].starts_with(char::is_whitespace) {
pos += source[pos..]
.chars()
.next()
.map(char::len_utf8)
.unwrap_or(1);
}
if !source[pos..].starts_with('"') {
cursor = pos;
continue;
}
pos += 1;
let name_start = pos;
while pos < source.len() && !source[pos..].starts_with('"') {
pos += source[pos..]
.chars()
.next()
.map(char::len_utf8)
.unwrap_or(1);
}
if pos < source.len() {
references.push(EnvironmentReference {
name: source[name_start..pos].to_owned(),
byte_offset: start,
});
}
cursor = pos.saturating_add(1);
}
references
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
#[test]
fn discovers_containerfile_environments_by_logical_name() {
let temp = tempfile::tempdir().unwrap();
let linux = temp.path().join("envs/linux");
fs::create_dir_all(&linux).unwrap();
fs::write(linux.join("Containerfile"), "FROM alpine\n").unwrap();
let envs = discover_environments(temp.path()).unwrap();
assert_eq!(envs.len(), 1);
assert_eq!(envs[0].name, "linux");
assert_eq!(envs[0].kind, EnvironmentKind::Containerfile);
assert!(!envs[0].digest.as_str().is_empty());
}
#[test]
fn missing_env_macro_reference_reports_clear_diagnostic() {
let source = r#"fn main() { let _ = env!("windows"); }"#;
let diagnostics = diagnose_environment_references(source, &[]);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0]
.message
.contains("envs/windows/Containerfile"));
}
#[test]
fn windows_environment_name_uses_windows_development_requirements() {
let temp = tempfile::tempdir().unwrap();
let windows = temp.path().join("envs/windows");
fs::create_dir_all(&windows).unwrap();
fs::write(
windows.join("Dockerfile"),
"# user-attached windows dev contract\n",
)
.unwrap();
let envs = discover_environments(temp.path()).unwrap();
assert_eq!(envs[0].name, "windows");
assert_eq!(envs[0].requirements.os, Some(Os::Windows));
assert!(envs[0]
.requirements
.capabilities
.contains(&Capability::WindowsCommandDev));
}
}

View file

@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use crate::{Capability, EnvironmentResource};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum GuestRuntimeKind {
Wasmtime,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CommandBackendKind {
LinuxRootlessPodman,
WindowsCommandDev,
StubbedWindowsSandbox,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandInvocation {
pub program: String,
pub args: Vec<String>,
pub env: Option<EnvironmentResource>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandPlan {
pub guest_runtime: GuestRuntimeKind,
pub backend: CommandBackendKind,
pub required_capability: Capability,
pub user_attached_development_execution: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeCommandPolicy {
pub hosted_control_plane: bool,
pub node_has_command_capability: bool,
}
impl NativeCommandPolicy {
pub fn authorize(&self) -> Result<(), String> {
if self.hosted_control_plane {
return Err("hosted coordinator control plane cannot run native commands".to_owned());
}
if !self.node_has_command_capability {
return Err("selected node or task lacks native command capability".to_owned());
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskBoundaryValue {
SmallJson(serde_json::Value),
SourceSnapshot(crate::Digest),
Blob(crate::Digest),
Artifact(crate::ArtifactId),
VfsManifest(crate::Digest),
}
impl TaskBoundaryValue {
pub fn reject_host_only(type_name: &str) -> Result<Self, String> {
Err(format!(
"task boundary value `{type_name}` is host-only; use small serialized data or handles"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hosted_control_plane_cannot_authorize_native_command() {
let policy = NativeCommandPolicy {
hosted_control_plane: true,
node_has_command_capability: true,
};
assert!(policy
.authorize()
.unwrap_err()
.contains("hosted coordinator"));
}
#[test]
fn raw_pointer_style_task_argument_is_rejected() {
let error = TaskBoundaryValue::reject_host_only("*const u8").unwrap_err();
assert!(error.contains("host-only"));
}
}

View file

@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
macro_rules! id_type {
($name:ident) => {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
let value = value.into();
assert!(
!value.trim().is_empty(),
concat!(stringify!($name), " cannot be empty")
);
Self(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
};
}
id_type!(AgentId);
id_type!(ArtifactId);
id_type!(NodeId);
id_type!(ProcessId);
id_type!(ProjectId);
id_type!(TaskId);
id_type!(TenantId);
id_type!(UserId);

View file

@ -0,0 +1,75 @@
pub mod artifact;
pub mod auth;
pub mod bundle;
pub mod capability;
pub mod checkpoint;
pub mod debug;
pub mod digest;
pub mod environment;
pub mod execution;
pub mod ids;
pub mod limits;
pub mod operator_panel;
pub mod policy;
pub mod project;
pub mod scheduler;
pub mod source;
pub mod transport;
pub mod vfs;
pub use artifact::{
ArtifactDownloadStream, ArtifactMetadata, ArtifactRegistry, ArtifactUnavailable,
DownloadAction, DownloadError, DownloadLink, DownloadPolicy, RetentionPolicy, StorageLocation,
};
pub use auth::{
node_capability_policy_digest, Action, Actor, AuthContext, Authorization, BrowserLoginFlow,
CliLoginFlow, CredentialKind, EnrollmentError, EnrollmentGrant, IdentityKind, NodeCredential,
PublicKeyIdentity, Scope,
};
pub use bundle::{BundleIdentityInputs, BundleMetadata, SelectedInput};
pub use capability::{Capability, CapabilityReportError, EnvironmentBackend, NodeCapabilities, Os};
pub use checkpoint::{
CheckpointBoundary, CompatibilityFailure, RestartDecision, RestartPolicy, RestartRequest,
TaskCheckpoint,
};
pub use debug::{
DebugEpoch, DebugEpochError, DebugParticipant, DebugParticipantKind, DebugRuntimeState,
DebugStopReason, ThreadInspection,
};
pub use digest::Digest;
pub use environment::{
diagnose_environment_references, discover_environments, EnvironmentDiagnostic, EnvironmentKind,
EnvironmentReference, EnvironmentRequirements, EnvironmentResource,
};
pub use execution::{
CommandBackendKind, CommandInvocation, CommandPlan, GuestRuntimeKind, NativeCommandPolicy,
TaskBoundaryValue,
};
pub use ids::{AgentId, ArtifactId, NodeId, ProcessId, ProjectId, TaskId, TenantId, UserId};
pub use limits::{
LargeArgumentPolicy, LimitError, LimitKind, LogBuffer, LogRecord, ResourceLimits,
ResourceMeter, TaskArgumentBudget,
};
pub use operator_panel::{
ControlPlaneAction, PanelError, PanelEvent, PanelEventKind, PanelState, PanelWidget,
PanelWidgetKind, RateLimit,
};
pub use policy::{
CapabilityPolicy, Decision, LocalTrustedPolicy, PolicyReason, ResourceRequest, ServicePolicy,
};
pub use project::{Entrypoint, ProjectModel, ProjectModelError};
pub use scheduler::{
DefaultScheduler, NodeDescriptor, Placement, PlacementError, PlacementRequest, Scheduler,
};
pub use source::{
SourceManifestError, SourcePreparation, SourceProviderKind, SourceProviderManifest,
SourceProviderModule, SourceTransferMode, SourceTransferPolicy,
};
pub use transport::{
BulkTransferDecision, DataPlaneObject, DataPlaneScope, DirectBulkTransferPlan,
NativeQuicTransport, NodeEndpoint, RendezvousRequest, Transport, TransportError, TransportKind,
};
pub use vfs::{
ReuseDecision, SyncPolicy, VfsError, VfsManifest, VfsObject, VfsOverlay, VfsPath,
VfsSyncDecision,
};

View file

@ -0,0 +1,255 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::TaskId;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum LimitKind {
ApiCall,
Spawn,
LogBytes,
MetadataBytes,
DebugReadBytes,
UiEvent,
RendezvousAttempt,
ArtifactDownloadBytes,
HostedFuel,
HostedMemoryBytes,
HostedWallClockMs,
HostedStateBytes,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceLimits {
pub limits: BTreeMap<LimitKind, u64>,
}
impl ResourceLimits {
pub fn community_tier_defaults() -> Self {
Self {
limits: BTreeMap::from([
(LimitKind::ApiCall, 10_000),
(LimitKind::Spawn, 64),
(LimitKind::LogBytes, 256 * 1024),
(LimitKind::MetadataBytes, 512 * 1024),
(LimitKind::DebugReadBytes, 128 * 1024),
(LimitKind::UiEvent, 1_000),
(LimitKind::RendezvousAttempt, 128),
(LimitKind::ArtifactDownloadBytes, 256 * 1024 * 1024),
(LimitKind::HostedFuel, 10_000_000),
(LimitKind::HostedMemoryBytes, 64 * 1024 * 1024),
(LimitKind::HostedWallClockMs, 30_000),
(LimitKind::HostedStateBytes, 1024 * 1024),
]),
}
}
pub fn limit(&self, kind: &LimitKind) -> u64 {
*self.limits.get(kind).unwrap_or(&0)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceMeter {
used: BTreeMap<LimitKind, u64>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum LimitError {
#[error(
"resource limit exceeded for {kind:?}: requested {requested}, used {used}, limit {limit}"
)]
Exceeded {
kind: LimitKind,
requested: u64,
used: u64,
limit: u64,
},
#[error("task argument is too large: {size} bytes exceeds {limit} bytes")]
LargeTaskArgument { size: u64, limit: u64 },
}
impl ResourceMeter {
pub fn can_charge(
&self,
limits: &ResourceLimits,
kind: LimitKind,
amount: u64,
) -> Result<(), LimitError> {
let used = self.used.get(&kind).copied().unwrap_or(0);
let limit = limits.limit(&kind);
if used.saturating_add(amount) > limit {
return Err(LimitError::Exceeded {
kind,
requested: amount,
used,
limit,
});
}
Ok(())
}
pub fn charge(
&mut self,
limits: &ResourceLimits,
kind: LimitKind,
amount: u64,
) -> Result<(), LimitError> {
self.can_charge(limits, kind.clone(), amount)?;
let used = self.used.get(&kind).copied().unwrap_or(0);
self.used.insert(kind, used + amount);
Ok(())
}
pub fn used(&self, kind: &LimitKind) -> u64 {
self.used.get(kind).copied().unwrap_or(0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogRecord {
pub task: TaskId,
pub bytes: Vec<u8>,
pub truncated: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogBuffer {
max_bytes: usize,
used_bytes: usize,
records: Vec<LogRecord>,
backpressured: bool,
}
impl LogBuffer {
pub fn new(max_bytes: usize) -> Self {
Self {
max_bytes,
used_bytes: 0,
records: Vec::new(),
backpressured: false,
}
}
pub fn push(&mut self, task: TaskId, bytes: impl AsRef<[u8]>) {
let bytes = bytes.as_ref();
let remaining = self.max_bytes.saturating_sub(self.used_bytes);
let truncated = bytes.len() > remaining;
let stored = bytes[..bytes.len().min(remaining)].to_vec();
self.used_bytes += stored.len();
if truncated {
self.backpressured = true;
}
self.records.push(LogRecord {
task,
bytes: stored,
truncated,
});
}
pub fn records(&self) -> &[LogRecord] {
&self.records
}
pub fn backpressured(&self) -> bool {
self.backpressured
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LargeArgumentPolicy {
Allow,
Warn,
Reject,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskArgumentBudget {
pub max_inline_bytes: u64,
pub policy: LargeArgumentPolicy,
}
impl TaskArgumentBudget {
pub fn validate(&self, size: u64) -> Result<Option<String>, LimitError> {
if size <= self.max_inline_bytes {
return Ok(None);
}
match self.policy {
LargeArgumentPolicy::Allow => Ok(None),
LargeArgumentPolicy::Warn => Ok(Some(format!(
"task argument is {size} bytes; prefer SourceSnapshot, Blob, Artifact, or VFS handles"
))),
LargeArgumentPolicy::Reject => Err(LimitError::LargeTaskArgument {
size,
limit: self.max_inline_bytes,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_meter_rejects_usage_before_work_starts() {
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::Spawn, 1)]),
};
let mut meter = ResourceMeter::default();
meter.charge(&limits, LimitKind::Spawn, 1).unwrap();
let error = meter.charge(&limits, LimitKind::Spawn, 1).unwrap_err();
assert!(matches!(error, LimitError::Exceeded { .. }));
}
#[test]
fn resource_meter_can_check_limits_without_consuming() {
let limits = ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 4)]),
};
let mut meter = ResourceMeter::default();
meter
.can_charge(&limits, LimitKind::ArtifactDownloadBytes, 4)
.unwrap();
assert_eq!(meter.used(&LimitKind::ArtifactDownloadBytes), 0);
meter
.charge(&limits, LimitKind::ArtifactDownloadBytes, 3)
.unwrap();
assert!(matches!(
meter.can_charge(&limits, LimitKind::ArtifactDownloadBytes, 2),
Err(LimitError::Exceeded { .. })
));
}
#[test]
fn log_buffer_caps_backpressures_and_keeps_task_association() {
let mut logs = LogBuffer::new(4);
logs.push(TaskId::from("task-a"), b"abcdef");
assert!(logs.backpressured());
assert_eq!(logs.records()[0].task, TaskId::from("task-a"));
assert_eq!(logs.records()[0].bytes, b"abcd");
assert!(logs.records()[0].truncated);
}
#[test]
fn large_task_arguments_are_rejected_or_warned() {
let reject = TaskArgumentBudget {
max_inline_bytes: 4,
policy: LargeArgumentPolicy::Reject,
};
assert!(reject.validate(5).is_err());
let warn = TaskArgumentBudget {
max_inline_bytes: 4,
policy: LargeArgumentPolicy::Warn,
};
assert!(warn.validate(5).unwrap().unwrap().contains("Artifact"));
}
}

View file

@ -0,0 +1,375 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{ArtifactId, DownloadAction, DownloadError, ProcessId, ProjectId, TaskId, TenantId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PanelWidgetKind {
Text {
value: String,
},
Progress {
current: u64,
total: u64,
},
Button {
action: String,
},
Toggle {
value: bool,
},
Select {
options: Vec<String>,
selected: String,
},
ArtifactDownload {
artifact: ArtifactId,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PanelWidget {
pub id: String,
pub label: String,
pub kind: PanelWidgetKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControlPlaneAction {
RestartTask(TaskId),
CancelProcess,
DebugProcess,
DownloadArtifact(ArtifactId),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PanelState {
pub tenant: TenantId,
pub project: ProjectId,
pub process: ProcessId,
pub widgets: BTreeMap<String, PanelWidget>,
pub program_ui_events_enabled: bool,
pub control_plane_actions: Vec<ControlPlaneAction>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PanelEventKind {
ButtonClicked,
ToggleChanged(bool),
SelectChanged(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PanelEvent {
pub tenant: TenantId,
pub project: ProjectId,
pub process: ProcessId,
pub widget_id: String,
pub kind: PanelEventKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimit {
pub max_events: u64,
pub used_events: u64,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum PanelError {
#[error("custom HTML or JavaScript is not supported in operator panels")]
CustomContentDenied,
#[error(
"operator panel widget `{0}` is not allowed to collect secrets or OAuth-like credentials"
)]
CredentialCollectionDenied(String),
#[error("panel event scope does not match tenant/project/process")]
ScopeMismatch,
#[error("program UI events are disabled while debug process is stopped")]
ProgramEventsDisabled,
#[error("panel event rate limit exceeded")]
RateLimited,
#[error("unknown panel widget `{0}`")]
UnknownWidget(String),
#[error("artifact download action is unavailable: {0}")]
DownloadUnavailable(String),
}
impl PanelState {
pub fn new(tenant: TenantId, project: ProjectId, process: ProcessId) -> Self {
Self {
tenant,
project,
process,
widgets: BTreeMap::new(),
program_ui_events_enabled: true,
control_plane_actions: Vec::new(),
}
}
pub fn add_widget(&mut self, widget: PanelWidget) -> Result<(), PanelError> {
validate_widget(&widget)?;
self.widgets.insert(widget.id.clone(), widget);
Ok(())
}
pub fn add_download_widget_from_action(
&mut self,
widget_id: impl Into<String>,
label: impl Into<String>,
action: Result<DownloadAction, DownloadError>,
) -> Result<(), PanelError> {
let action = action.map_err(|err| PanelError::DownloadUnavailable(err.to_string()))?;
let artifact = action.artifact;
self.add_widget(PanelWidget {
id: widget_id.into(),
label: label.into(),
kind: PanelWidgetKind::ArtifactDownload {
artifact: artifact.clone(),
},
})?;
self.control_plane_actions
.push(ControlPlaneAction::DownloadArtifact(artifact));
Ok(())
}
pub fn reject_custom_content(_html_or_js: &str) -> Result<(), PanelError> {
Err(PanelError::CustomContentDenied)
}
pub fn freeze_program_ui_events(&mut self) {
self.program_ui_events_enabled = false;
}
pub fn set_control_plane_actions(&mut self, actions: Vec<ControlPlaneAction>) {
self.control_plane_actions = actions;
}
pub fn accept_event(
&self,
event: &PanelEvent,
limit: &mut RateLimit,
) -> Result<(), PanelError> {
if !self.program_ui_events_enabled {
return Err(PanelError::ProgramEventsDisabled);
}
if self.tenant != event.tenant
|| self.project != event.project
|| self.process != event.process
{
return Err(PanelError::ScopeMismatch);
}
if !self.widgets.contains_key(&event.widget_id) {
return Err(PanelError::UnknownWidget(event.widget_id.clone()));
}
if limit.used_events >= limit.max_events {
return Err(PanelError::RateLimited);
}
limit.used_events += 1;
Ok(())
}
pub fn control_plane_actions_available(&self) -> &[ControlPlaneAction] {
&self.control_plane_actions
}
}
fn validate_widget(widget: &PanelWidget) -> Result<(), PanelError> {
let mut checked_text = vec![widget.id.as_str(), widget.label.as_str()];
match &widget.kind {
PanelWidgetKind::Button { action } => checked_text.push(action),
PanelWidgetKind::Select { options, selected } => {
checked_text.push(selected);
checked_text.extend(options.iter().map(String::as_str));
}
PanelWidgetKind::Text { .. }
| PanelWidgetKind::Progress { .. }
| PanelWidgetKind::Toggle { .. }
| PanelWidgetKind::ArtifactDownload { .. } => {}
}
let combined = checked_text.join(" ").to_ascii_lowercase();
if combined.contains("password")
|| combined.contains("token")
|| combined.contains("oauth")
|| combined.contains("secret")
{
return Err(PanelError::CredentialCollectionDenied(widget.id.clone()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn panel() -> PanelState {
PanelState::new(
TenantId::from("tenant"),
ProjectId::from("project"),
ProcessId::from("process"),
)
}
#[test]
fn panel_uses_typed_widgets_and_rejects_custom_content() {
let mut panel = panel();
panel
.add_widget(PanelWidget {
id: "progress".to_owned(),
label: "Build".to_owned(),
kind: PanelWidgetKind::Progress {
current: 1,
total: 2,
},
})
.unwrap();
assert!(PanelState::reject_custom_content("<script>alert(1)</script>").is_err());
assert!(panel.widgets.contains_key("progress"));
}
#[test]
fn panel_rejects_password_or_oauth_collection_widgets() {
let mut panel = panel();
let error = panel
.add_widget(PanelWidget {
id: "oauth_token".to_owned(),
label: "OAuth Token".to_owned(),
kind: PanelWidgetKind::Text {
value: String::new(),
},
})
.unwrap_err();
assert!(matches!(error, PanelError::CredentialCollectionDenied(_)));
}
#[test]
fn panel_rejects_credential_collection_in_interactive_fields() {
let mut panel = panel();
let button_error = panel
.add_widget(PanelWidget {
id: "continue".to_owned(),
label: "Continue".to_owned(),
kind: PanelWidgetKind::Button {
action: "collect-secret".to_owned(),
},
})
.unwrap_err();
assert!(matches!(
button_error,
PanelError::CredentialCollectionDenied(_)
));
let select_error = panel
.add_widget(PanelWidget {
id: "auth-mode".to_owned(),
label: "Auth Mode".to_owned(),
kind: PanelWidgetKind::Select {
options: vec!["password".to_owned(), "public key".to_owned()],
selected: "public key".to_owned(),
},
})
.unwrap_err();
assert!(matches!(
select_error,
PanelError::CredentialCollectionDenied(_)
));
}
#[test]
fn panel_events_are_scoped_and_rate_limited() {
let mut panel = panel();
panel
.add_widget(PanelWidget {
id: "restart".to_owned(),
label: "Restart".to_owned(),
kind: PanelWidgetKind::Button {
action: "restart".to_owned(),
},
})
.unwrap();
let event = PanelEvent {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("process"),
widget_id: "restart".to_owned(),
kind: PanelEventKind::ButtonClicked,
};
let mut limit = RateLimit {
max_events: 1,
used_events: 0,
};
panel.accept_event(&event, &mut limit).unwrap();
assert_eq!(
panel.accept_event(&event, &mut limit),
Err(PanelError::RateLimited)
);
}
#[test]
fn stopped_debug_process_keeps_control_plane_actions_available() {
let mut panel = panel();
panel.freeze_program_ui_events();
panel.set_control_plane_actions(vec![
ControlPlaneAction::RestartTask(TaskId::from("task")),
ControlPlaneAction::DownloadArtifact(ArtifactId::from("artifact")),
]);
let event = PanelEvent {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("process"),
widget_id: "missing".to_owned(),
kind: PanelEventKind::ButtonClicked,
};
let mut limit = RateLimit {
max_events: 1,
used_events: 0,
};
assert_eq!(
panel.accept_event(&event, &mut limit),
Err(PanelError::ProgramEventsDisabled)
);
assert_eq!(panel.control_plane_actions_available().len(), 2);
}
#[test]
fn download_widget_is_only_created_from_available_action() {
let mut panel = panel();
let action = Ok(DownloadAction {
artifact: ArtifactId::from("artifact"),
source: crate::StorageLocation::RetainedNode(crate::NodeId::from("node")),
scoped_token_subject: "tenant/project/process/artifact".to_owned(),
});
panel
.add_download_widget_from_action("download-artifact", "Download", action)
.unwrap();
assert!(matches!(
panel.widgets["download-artifact"].kind,
PanelWidgetKind::ArtifactDownload { .. }
));
assert!(matches!(
panel.control_plane_actions_available()[0],
ControlPlaneAction::DownloadArtifact(_)
));
let before = panel.widgets.len();
let error = panel
.add_download_widget_from_action(
"missing-download",
"Download",
Err(DownloadError::Unavailable),
)
.unwrap_err();
assert_eq!(panel.widgets.len(), before);
assert!(matches!(error, PanelError::DownloadUnavailable(_)));
}
}

View file

@ -0,0 +1,134 @@
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{Action, AuthContext, Capability, Scope};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PolicyReason {
Allowed,
MissingCapability(Capability),
HostedNativeComputeDenied,
HostedContainerDenied,
QuotaExceeded(String),
Unauthorized(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Decision {
pub allowed: bool,
pub reason: PolicyReason,
}
impl Decision {
pub fn allow() -> Self {
Self {
allowed: true,
reason: PolicyReason::Allowed,
}
}
pub fn deny(reason: PolicyReason) -> Self {
Self {
allowed: false,
reason,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceRequest {
pub action: Action,
pub required_capabilities: BTreeSet<Capability>,
pub hosted_control_plane: bool,
}
pub trait CapabilityPolicy {
fn decide(&self, context: &AuthContext, scope: &Scope, request: &ResourceRequest) -> Decision;
}
pub trait ServicePolicy: CapabilityPolicy + Send + Sync {}
impl<T> ServicePolicy for T where T: CapabilityPolicy + Send + Sync {}
#[derive(Clone, Debug, Default)]
pub struct LocalTrustedPolicy;
impl CapabilityPolicy for LocalTrustedPolicy {
fn decide(&self, context: &AuthContext, scope: &Scope, request: &ResourceRequest) -> Decision {
let authz = crate::auth::same_tenant_project(context, scope);
if !authz.allowed {
return Decision::deny(PolicyReason::Unauthorized(authz.reason));
}
if request.hosted_control_plane && request.action == Action::RunNativeCommand {
return Decision::deny(PolicyReason::HostedNativeComputeDenied);
}
if request.hosted_control_plane && request.action == Action::RunContainer {
return Decision::deny(PolicyReason::HostedContainerDenied);
}
Decision::allow()
}
}
#[cfg(test)]
mod tests {
use crate::{Actor, ProjectId, TenantId, UserId};
use super::*;
#[test]
fn public_policy_interface_denies_hosted_native_compute() {
let policy = LocalTrustedPolicy;
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("user")),
};
let scope = Scope {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: None,
task: None,
node: None,
artifact: None,
};
let request = ResourceRequest {
action: Action::RunNativeCommand,
required_capabilities: BTreeSet::new(),
hosted_control_plane: true,
};
let decision = policy.decide(&context, &scope, &request);
assert!(!decision.allowed);
assert_eq!(decision.reason, PolicyReason::HostedNativeComputeDenied);
}
#[test]
fn local_trusted_policy_allows_owner_controlled_native_capability_request() {
let policy = LocalTrustedPolicy;
let context = AuthContext {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
actor: Actor::User(UserId::from("owner")),
};
let scope = Scope {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: None,
task: None,
node: None,
artifact: None,
};
let request = ResourceRequest {
action: Action::RunNativeCommand,
required_capabilities: BTreeSet::from([Capability::Command]),
hosted_control_plane: false,
};
let decision = policy.decide(&context, &scope, &request);
assert!(decision.allowed);
assert_eq!(decision.reason, PolicyReason::Allowed);
}
}

View file

@ -0,0 +1,125 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{discover_environments, environment::EnvironmentError, EnvironmentResource};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entrypoint {
pub name: String,
pub function: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectModel {
pub root: PathBuf,
pub environments: Vec<EnvironmentResource>,
pub entrypoints: BTreeMap<String, Entrypoint>,
pub default_entrypoint: String,
pub required_config_file: Option<PathBuf>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ProjectModelError {
#[error("environment discovery failed: {0}")]
Environment(String),
#[error("unknown Disasmer entrypoint `{name}`; available entrypoints: {available:?}")]
UnknownEntrypoint {
name: String,
available: Vec<String>,
},
}
impl ProjectModel {
pub fn discover_without_config(root: &Path) -> Result<Self, ProjectModelError> {
let environments = discover_environments(root).map_err(|err| {
ProjectModelError::Environment(match err {
EnvironmentError::Read { path, source } => {
format!("failed to read {}: {source}", path.display())
}
})
})?;
Ok(Self {
root: root.to_path_buf(),
environments,
entrypoints: default_entrypoints(),
default_entrypoint: "build".to_owned(),
required_config_file: None,
})
}
pub fn select_entrypoint(&self, name: Option<&str>) -> Result<&Entrypoint, ProjectModelError> {
let name = name.unwrap_or(&self.default_entrypoint);
self.entrypoints
.get(name)
.ok_or_else(|| ProjectModelError::UnknownEntrypoint {
name: name.to_owned(),
available: self.entrypoints.keys().cloned().collect(),
})
}
}
fn default_entrypoints() -> BTreeMap<String, Entrypoint> {
["build", "test", "package", "release", "watch"]
.into_iter()
.map(|name| {
(
name.to_owned(),
Entrypoint {
name: name.to_owned(),
function: format!("{name}_main"),
},
)
})
.collect()
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
#[test]
fn project_works_without_hand_written_configuration_file() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("envs/linux")).unwrap();
fs::write(
temp.path().join("envs/linux/Containerfile"),
"FROM alpine\n",
)
.unwrap();
let model = ProjectModel::discover_without_config(temp.path()).unwrap();
assert_eq!(model.required_config_file, None);
assert_eq!(model.environments[0].name, "linux");
assert_eq!(model.select_entrypoint(None).unwrap().name, "build");
}
#[test]
fn project_can_define_multiple_default_entrypoints() {
let temp = tempfile::tempdir().unwrap();
let model = ProjectModel::discover_without_config(temp.path()).unwrap();
assert_eq!(
model.select_entrypoint(Some("test")).unwrap().function,
"test_main"
);
assert_eq!(
model.select_entrypoint(Some("release")).unwrap().function,
"release_main"
);
}
#[test]
fn unknown_entrypoint_lists_available_choices() {
let temp = tempfile::tempdir().unwrap();
let model = ProjectModel::discover_without_config(temp.path()).unwrap();
let error = model.select_entrypoint(Some("deploy")).unwrap_err();
assert!(matches!(error, ProjectModelError::UnknownEntrypoint { .. }));
}
}

View file

@ -0,0 +1,429 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
ArtifactId, Capability, Digest, EnvironmentRequirements, NodeCapabilities, NodeId, ProjectId,
TenantId,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeDescriptor {
pub id: NodeId,
pub tenant: TenantId,
pub project: ProjectId,
pub capabilities: NodeCapabilities,
pub cached_environments: BTreeSet<Digest>,
pub dependency_caches: BTreeSet<Digest>,
pub source_snapshots: BTreeSet<Digest>,
pub artifact_locations: BTreeSet<ArtifactId>,
pub direct_connectivity: bool,
pub online: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlacementRequest {
pub tenant: TenantId,
pub project: ProjectId,
pub environment: Option<EnvironmentRequirements>,
pub environment_digest: Option<Digest>,
pub required_capabilities: BTreeSet<Capability>,
pub dependency_cache: Option<Digest>,
pub source_snapshot: Option<Digest>,
pub required_artifacts: BTreeSet<ArtifactId>,
pub quota_available: bool,
pub policy_allowed: bool,
pub prefer_node: Option<NodeId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Placement {
pub node: NodeId,
pub score: i64,
pub reasons: Vec<String>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("no capable node for placement: {message}")]
pub struct PlacementError {
pub message: String,
}
pub trait Scheduler {
fn place(
&self,
nodes: &[NodeDescriptor],
request: &PlacementRequest,
) -> Result<Placement, PlacementError>;
}
#[derive(Clone, Debug, Default)]
pub struct DefaultScheduler;
impl Scheduler for DefaultScheduler {
fn place(
&self,
nodes: &[NodeDescriptor],
request: &PlacementRequest,
) -> Result<Placement, PlacementError> {
let mut scored = Vec::new();
let mut rejection_counts = BTreeMap::<String, usize>::new();
for node in nodes {
match compatibility(node, request) {
Ok(mut placement) => {
locality_score(node, request, &mut placement);
scored.push(placement);
}
Err(reasons) => {
for reason in reasons {
*rejection_counts.entry(reason).or_default() += 1;
}
}
}
}
scored
.into_iter()
.max_by_key(|placement| placement.score)
.ok_or_else(|| PlacementError {
message: rejection_counts
.into_iter()
.map(|(reason, count)| format!("{reason} ({count} node(s))"))
.collect::<Vec<_>>()
.join("; "),
})
}
}
fn compatibility(
node: &NodeDescriptor,
request: &PlacementRequest,
) -> Result<Placement, Vec<String>> {
let mut reasons = Vec::new();
if !node.online {
reasons.push("node offline".to_owned());
}
if node.tenant != request.tenant {
reasons.push("tenant mismatch".to_owned());
}
if node.project != request.project {
reasons.push("project mismatch".to_owned());
}
if !request.quota_available {
reasons.push("quota unavailable for placement".to_owned());
}
if !request.policy_allowed {
reasons.push("policy denied placement".to_owned());
}
for capability in &request.required_capabilities {
if !node.capabilities.capabilities.contains(capability) {
reasons.push(format!("missing capability {capability:?}"));
}
}
if let Some(environment) = &request.environment {
if let Some(required_os) = &environment.os {
if &node.capabilities.os != required_os {
reasons.push(format!("environment requires os {required_os:?}"));
}
}
if let Some(required_arch) = &environment.arch {
if &node.capabilities.arch != required_arch {
reasons.push(format!("environment requires arch {required_arch}"));
}
}
for capability in &environment.capabilities {
if !node.capabilities.capabilities.contains(capability) {
reasons.push(format!("environment requires capability {capability:?}"));
}
}
}
let source_transfer_required = request
.source_snapshot
.as_ref()
.is_some_and(|digest| !node.source_snapshots.contains(digest));
if source_transfer_required && !node.direct_connectivity {
reasons.push("source snapshot unavailable and direct connectivity unavailable".to_owned());
}
let missing_artifacts = request
.required_artifacts
.iter()
.filter(|artifact| !node.artifact_locations.contains(*artifact))
.count();
if missing_artifacts > 0 && !node.direct_connectivity {
reasons.push(format!(
"{missing_artifacts} required artifact(s) unavailable and direct connectivity unavailable"
));
}
if reasons.is_empty() {
Ok(Placement {
node: node.id.clone(),
score: 0,
reasons: Vec::new(),
})
} else {
Err(reasons)
}
}
fn locality_score(node: &NodeDescriptor, request: &PlacementRequest, placement: &mut Placement) {
if request.prefer_node.as_ref() == Some(&node.id) {
placement.score += 100;
placement.reasons.push("preferred node".to_owned());
}
if request
.environment_digest
.as_ref()
.is_some_and(|digest| node.cached_environments.contains(digest))
{
placement.score += 50;
placement.reasons.push("warm environment cache".to_owned());
}
if request
.source_snapshot
.as_ref()
.is_some_and(|digest| node.source_snapshots.contains(digest))
{
placement.score += 40;
placement
.reasons
.push("source snapshot already local".to_owned());
}
if request
.dependency_cache
.as_ref()
.is_some_and(|digest| node.dependency_caches.contains(digest))
{
placement.score += 30;
placement.reasons.push("warm dependency cache".to_owned());
}
let artifact_hits = request
.required_artifacts
.iter()
.filter(|artifact| node.artifact_locations.contains(*artifact))
.count() as i64;
if artifact_hits > 0 {
placement.score += 10 * artifact_hits;
placement.reasons.push(format!(
"{artifact_hits} required artifact(s) already local"
));
}
}
#[cfg(test)]
mod tests {
use crate::{EnvironmentBackend, Os};
use super::*;
fn node(id: &str, cached_source: bool) -> NodeDescriptor {
let source = Digest::sha256("source");
NodeDescriptor {
id: NodeId::from(id),
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
capabilities: NodeCapabilities {
os: Os::Linux,
arch: "x86_64".to_owned(),
capabilities: BTreeSet::from([
Capability::Command,
Capability::Containers,
Capability::RootlessPodman,
]),
environment_backends: BTreeSet::from([EnvironmentBackend::Container]),
source_providers: BTreeSet::from(["filesystem".to_owned()]),
},
cached_environments: BTreeSet::from([Digest::sha256("env")]),
dependency_caches: if cached_source {
BTreeSet::from([Digest::sha256("deps")])
} else {
BTreeSet::new()
},
source_snapshots: if cached_source {
BTreeSet::from([source])
} else {
BTreeSet::new()
},
artifact_locations: BTreeSet::new(),
direct_connectivity: true,
online: true,
}
}
#[test]
fn scheduler_prefers_warm_source_and_environment() {
let request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: Some(EnvironmentRequirements::linux_container()),
environment_digest: Some(Digest::sha256("env")),
required_capabilities: BTreeSet::from([Capability::Command]),
dependency_cache: Some(Digest::sha256("deps")),
source_snapshot: Some(Digest::sha256("source")),
required_artifacts: BTreeSet::new(),
quota_available: true,
policy_allowed: true,
prefer_node: None,
};
let placement = DefaultScheduler
.place(&[node("cold", false), node("warm", true)], &request)
.unwrap();
assert_eq!(placement.node, NodeId::from("warm"));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("source")));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("dependency")));
}
#[test]
fn scheduler_failure_names_missing_constraint() {
let mut request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: None,
environment_digest: None,
required_capabilities: BTreeSet::from([Capability::WindowsCommandDev]),
dependency_cache: None,
source_snapshot: None,
required_artifacts: BTreeSet::new(),
quota_available: true,
policy_allowed: true,
prefer_node: None,
};
request.required_capabilities.insert(Capability::Command);
let error = DefaultScheduler
.place(&[node("linux", false)], &request)
.unwrap_err();
assert!(error.message.contains("WindowsCommandDev"));
}
#[test]
fn scheduler_failure_names_environment_constraint() {
let request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: Some(EnvironmentRequirements::windows_command_dev()),
environment_digest: None,
required_capabilities: BTreeSet::new(),
dependency_cache: None,
source_snapshot: None,
required_artifacts: BTreeSet::new(),
quota_available: true,
policy_allowed: true,
prefer_node: None,
};
let error = DefaultScheduler
.place(&[node("linux", false)], &request)
.unwrap_err();
assert!(error.message.contains("environment requires os Windows"));
assert!(error
.message
.contains("environment requires capability WindowsCommandDev"));
}
#[test]
fn scheduler_requires_direct_connectivity_when_transfer_is_needed() {
let mut disconnected = node("disconnected", false);
disconnected.direct_connectivity = false;
let mut local = node("local", true);
local.direct_connectivity = false;
let request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: None,
environment_digest: None,
required_capabilities: BTreeSet::from([Capability::Command]),
dependency_cache: None,
source_snapshot: Some(Digest::sha256("source")),
required_artifacts: BTreeSet::new(),
quota_available: true,
policy_allowed: true,
prefer_node: None,
};
let placement = DefaultScheduler
.place(&[disconnected, local], &request)
.unwrap();
assert_eq!(placement.node, NodeId::from("local"));
let mut disconnected = node("disconnected", false);
disconnected.direct_connectivity = false;
let error = DefaultScheduler
.place(&[disconnected], &request)
.unwrap_err();
assert!(error
.message
.contains("source snapshot unavailable and direct connectivity unavailable"));
}
#[test]
fn scheduler_failure_names_required_artifact_transfer_constraint() {
let mut disconnected = node("disconnected", true);
disconnected.direct_connectivity = false;
let request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: None,
environment_digest: None,
required_capabilities: BTreeSet::from([Capability::Command]),
dependency_cache: None,
source_snapshot: None,
required_artifacts: BTreeSet::from([ArtifactId::from("cache")]),
quota_available: true,
policy_allowed: true,
prefer_node: None,
};
let error = DefaultScheduler
.place(&[disconnected], &request)
.unwrap_err();
assert!(error
.message
.contains("1 required artifact(s) unavailable and direct connectivity unavailable"));
}
#[test]
fn scheduler_failure_names_quota_and_policy_constraints() {
let mut request = PlacementRequest {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
environment: None,
environment_digest: None,
required_capabilities: BTreeSet::from([Capability::Command]),
dependency_cache: None,
source_snapshot: None,
required_artifacts: BTreeSet::new(),
quota_available: false,
policy_allowed: true,
prefer_node: None,
};
let error = DefaultScheduler
.place(&[node("linux", false)], &request)
.unwrap_err();
assert!(error.message.contains("quota unavailable for placement"));
request.quota_available = true;
request.policy_allowed = false;
let error = DefaultScheduler
.place(&[node("linux", false)], &request)
.unwrap_err();
assert!(error.message.contains("policy denied placement"));
}
}

View file

@ -0,0 +1,324 @@
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{Capability, Digest, ProjectId, TenantId};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SourceProviderKind {
Filesystem,
Git,
Custom(String),
}
impl SourceProviderKind {
pub fn provider_id(&self) -> &str {
match self {
SourceProviderKind::Filesystem => "filesystem",
SourceProviderKind::Git => "git",
SourceProviderKind::Custom(provider) => provider,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SourceTransferMode {
RequiredContent,
ExplicitSnapshotChunks,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceTransferPolicy {
pub local_source_bytes_remain_node_local: bool,
pub coordinator_receives_source_bytes_by_default: bool,
pub default_full_repo_tarball: bool,
pub allowed_remote_transfer: BTreeSet<SourceTransferMode>,
}
impl SourceTransferPolicy {
pub fn local_first_snapshot_chunks() -> Self {
Self {
local_source_bytes_remain_node_local: true,
coordinator_receives_source_bytes_by_default: false,
default_full_repo_tarball: false,
allowed_remote_transfer: BTreeSet::from([
SourceTransferMode::RequiredContent,
SourceTransferMode::ExplicitSnapshotChunks,
]),
}
}
}
impl Default for SourceTransferPolicy {
fn default() -> Self {
Self::local_first_snapshot_chunks()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceProviderManifest {
pub kind: SourceProviderKind,
pub digest: Digest,
pub description: String,
#[serde(default)]
pub coordinator_requires_checkout_access: bool,
#[serde(default)]
pub transfer_policy: SourceTransferPolicy,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum SourceManifestError {
#[error("source provider manifest digest is not a valid sha256 digest: {0}")]
InvalidDigest(String),
#[error("custom source provider id `{0}` is invalid")]
InvalidProviderId(String),
#[error("source provider manifest description must be non-empty")]
EmptyDescription,
#[error("source provider manifest description is too long")]
DescriptionTooLong,
#[error("source provider manifest description contains control characters")]
DescriptionControlCharacter,
#[error("source provider manifest would require coordinator checkout access")]
CoordinatorCheckoutAccess,
#[error("source provider manifest would send source bytes to the coordinator by default")]
CoordinatorReceivesSourceBytes,
#[error("source provider manifest would default to a full-repo tarball")]
DefaultFullRepoTarball,
#[error("source provider manifest has no allowed remote transfer mode")]
MissingRemoteTransferMode,
}
pub trait SourceProviderModule {
fn kind(&self) -> SourceProviderKind;
fn manifest(&self) -> SourceProviderManifest;
}
impl SourceProviderManifest {
pub fn local_first(kind: SourceProviderKind, description: impl Into<String>) -> Self {
let transfer_policy = SourceTransferPolicy::local_first_snapshot_chunks();
let digest = Self::digest_for(&kind, false, &transfer_policy);
Self {
kind,
digest,
description: description.into(),
coordinator_requires_checkout_access: false,
transfer_policy,
}
}
pub fn validate_public_mvp(&self) -> Result<(), SourceManifestError> {
self.validate_shape()?;
if self.coordinator_requires_checkout_access {
return Err(SourceManifestError::CoordinatorCheckoutAccess);
}
if self
.transfer_policy
.coordinator_receives_source_bytes_by_default
{
return Err(SourceManifestError::CoordinatorReceivesSourceBytes);
}
if self.transfer_policy.default_full_repo_tarball {
return Err(SourceManifestError::DefaultFullRepoTarball);
}
if self.transfer_policy.allowed_remote_transfer.is_empty() {
return Err(SourceManifestError::MissingRemoteTransferMode);
}
Ok(())
}
fn validate_shape(&self) -> Result<(), SourceManifestError> {
if !self.digest.is_valid_sha256() {
return Err(SourceManifestError::InvalidDigest(
self.digest.as_str().to_owned(),
));
}
if let SourceProviderKind::Custom(provider) = &self.kind {
if !valid_provider_id(provider) {
return Err(SourceManifestError::InvalidProviderId(provider.clone()));
}
}
if self.description.trim().is_empty() {
return Err(SourceManifestError::EmptyDescription);
}
if self.description.len() > 256 {
return Err(SourceManifestError::DescriptionTooLong);
}
if self.description.chars().any(char::is_control) {
return Err(SourceManifestError::DescriptionControlCharacter);
}
Ok(())
}
fn digest_for(
kind: &SourceProviderKind,
coordinator_requires_checkout_access: bool,
transfer_policy: &SourceTransferPolicy,
) -> Digest {
let mut modes = transfer_policy
.allowed_remote_transfer
.iter()
.map(|mode| format!("{mode:?}"))
.collect::<Vec<_>>();
modes.sort();
let mut parts = vec![
b"source-provider-manifest:v2".to_vec(),
kind.provider_id().as_bytes().to_vec(),
coordinator_requires_checkout_access
.to_string()
.into_bytes(),
transfer_policy
.local_source_bytes_remain_node_local
.to_string()
.into_bytes(),
transfer_policy
.coordinator_receives_source_bytes_by_default
.to_string()
.into_bytes(),
transfer_policy
.default_full_repo_tarball
.to_string()
.into_bytes(),
];
parts.extend(modes.into_iter().map(String::into_bytes));
Digest::from_parts(parts)
}
}
fn valid_provider_id(provider: &str) -> bool {
!provider.is_empty()
&& provider.len() <= 64
&& provider
.bytes()
.all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourcePreparation {
pub tenant: TenantId,
pub project: ProjectId,
pub provider: SourceProviderKind,
pub required_capabilities: BTreeSet<Capability>,
pub coordinator_requires_checkout_access: bool,
}
impl SourcePreparation {
pub fn node_task(tenant: TenantId, project: ProjectId, provider: SourceProviderKind) -> Self {
let capability = match provider {
SourceProviderKind::Filesystem => Capability::SourceFilesystem,
SourceProviderKind::Git => Capability::SourceGit,
SourceProviderKind::Custom(_) => Capability::SourceFilesystem,
};
Self {
tenant,
project,
provider,
required_capabilities: BTreeSet::from([capability]),
coordinator_requires_checkout_access: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_preparation_can_be_scheduled_as_node_task() {
let prep = SourcePreparation::node_task(
TenantId::from("tenant"),
ProjectId::from("project"),
SourceProviderKind::Git,
);
assert!(!prep.coordinator_requires_checkout_access);
assert!(prep.required_capabilities.contains(&Capability::SourceGit));
}
#[test]
fn local_first_source_manifest_rejects_bulk_coordinator_paths() {
let manifest = SourceProviderManifest::local_first(
SourceProviderKind::Git,
"node-side Git snapshot provider",
);
assert!(manifest.validate_public_mvp().is_ok());
assert!(!manifest.coordinator_requires_checkout_access);
assert!(
!manifest
.transfer_policy
.coordinator_receives_source_bytes_by_default
);
assert!(!manifest.transfer_policy.default_full_repo_tarball);
assert!(manifest
.transfer_policy
.allowed_remote_transfer
.contains(&SourceTransferMode::ExplicitSnapshotChunks));
}
#[test]
fn source_manifest_validation_treats_manifest_as_hostile_input() {
let mut manifest = SourceProviderManifest::local_first(
SourceProviderKind::Custom("gitlab-lfs".to_owned()),
"custom provider",
);
assert!(manifest.validate_public_mvp().is_ok());
manifest.kind = SourceProviderKind::Custom("../checkout".to_owned());
assert_eq!(
manifest.validate_public_mvp(),
Err(SourceManifestError::InvalidProviderId(
"../checkout".to_owned()
))
);
manifest.kind = SourceProviderKind::Git;
manifest.digest = Digest::sha256("valid");
manifest.coordinator_requires_checkout_access = true;
assert_eq!(
manifest.validate_public_mvp(),
Err(SourceManifestError::CoordinatorCheckoutAccess)
);
manifest.coordinator_requires_checkout_access = false;
manifest
.transfer_policy
.coordinator_receives_source_bytes_by_default = true;
assert_eq!(
manifest.validate_public_mvp(),
Err(SourceManifestError::CoordinatorReceivesSourceBytes)
);
manifest
.transfer_policy
.coordinator_receives_source_bytes_by_default = false;
manifest.transfer_policy.default_full_repo_tarball = true;
assert_eq!(
manifest.validate_public_mvp(),
Err(SourceManifestError::DefaultFullRepoTarball)
);
}
#[test]
fn source_manifest_rejects_malformed_digest_from_json() {
let mut manifest = SourceProviderManifest::local_first(
SourceProviderKind::Filesystem,
"filesystem provider",
);
let value = serde_json::to_value(&manifest).unwrap();
let mut object = value.as_object().unwrap().clone();
object.insert(
"digest".to_owned(),
serde_json::Value::String("sha256:not-a-real-digest".to_owned()),
);
manifest = serde_json::from_value(serde_json::Value::Object(object)).unwrap();
assert_eq!(
manifest.validate_public_mvp(),
Err(SourceManifestError::InvalidDigest(
"sha256:not-a-real-digest".to_owned()
))
);
}
}

View file

@ -0,0 +1,243 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{ArtifactId, Digest, NodeId, ProcessId, ProjectId, TenantId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportKind {
NativeQuic,
}
pub trait Transport {
fn kind(&self) -> TransportKind;
fn authenticated_direct_connections(&self) -> bool;
}
#[derive(Clone, Debug, Default)]
pub struct NativeQuicTransport;
impl Transport for NativeQuicTransport {
fn kind(&self) -> TransportKind {
TransportKind::NativeQuic
}
fn authenticated_direct_connections(&self) -> bool {
true
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataPlaneScope {
pub tenant: TenantId,
pub project: ProjectId,
pub process: ProcessId,
pub object: DataPlaneObject,
pub authorization_subject: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataPlaneObject {
Artifact(ArtifactId),
Blob(Digest),
SourceSnapshot(Digest),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeEndpoint {
pub node: NodeId,
pub advertised_addr: String,
pub public_key_fingerprint: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RendezvousRequest {
pub scope: DataPlaneScope,
pub source: NodeEndpoint,
pub destination: NodeEndpoint,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectBulkTransferPlan {
pub transport: TransportKind,
pub scope: DataPlaneScope,
pub source: NodeEndpoint,
pub destination: NodeEndpoint,
pub authorization_digest: Digest,
pub coordinator_assisted_rendezvous: bool,
pub coordinator_bulk_relay_allowed: bool,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum TransportError {
#[error(
"direct node-to-node connectivity is unavailable for scoped data-plane transfer: {reason}; coordinator bulk relay is disabled"
)]
DirectConnectivityUnavailable { reason: String },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BulkTransferDecision {
DirectAuthenticated { scope: DataPlaneScope },
FailClear { message: String },
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("bulk relay through coordinator is not allowed by default")]
pub struct BulkRelayDenied;
impl NativeQuicTransport {
pub fn plan_authenticated_direct_bulk_transfer(
&self,
request: RendezvousRequest,
direct_connectivity: bool,
failure_reason: impl Into<String>,
) -> Result<DirectBulkTransferPlan, TransportError> {
if !direct_connectivity {
return Err(TransportError::DirectConnectivityUnavailable {
reason: failure_reason.into(),
});
}
let authorization_digest = data_plane_authorization_digest(&request);
Ok(DirectBulkTransferPlan {
transport: self.kind(),
scope: request.scope,
source: request.source,
destination: request.destination,
authorization_digest,
coordinator_assisted_rendezvous: true,
coordinator_bulk_relay_allowed: false,
})
}
}
pub fn direct_bulk_transfer_or_error(
scope: DataPlaneScope,
direct_connectivity: bool,
) -> BulkTransferDecision {
if direct_connectivity {
BulkTransferDecision::DirectAuthenticated { scope }
} else {
BulkTransferDecision::FailClear {
message:
"direct node-to-node connectivity is unavailable; coordinator bulk relay is disabled"
.to_owned(),
}
}
}
fn data_plane_authorization_digest(request: &RendezvousRequest) -> Digest {
let object = match &request.scope.object {
DataPlaneObject::Artifact(artifact) => format!("artifact:{artifact}"),
DataPlaneObject::Blob(digest) => format!("blob:{}", digest.as_str()),
DataPlaneObject::SourceSnapshot(digest) => format!("source:{}", digest.as_str()),
};
Digest::from_parts([
b"dataplane-auth:v1".as_slice(),
request.scope.tenant.as_str().as_bytes(),
request.scope.project.as_str().as_bytes(),
request.scope.process.as_str().as_bytes(),
object.as_bytes(),
request.scope.authorization_subject.as_bytes(),
request.source.node.as_str().as_bytes(),
request.source.public_key_fingerprint.as_str().as_bytes(),
request.destination.node.as_str().as_bytes(),
request
.destination
.public_key_fingerprint
.as_str()
.as_bytes(),
])
}
#[cfg(test)]
mod tests {
use super::*;
fn endpoint(name: &str) -> NodeEndpoint {
NodeEndpoint {
node: NodeId::from(name),
advertised_addr: format!("{name}.mesh.invalid:4433"),
public_key_fingerprint: Digest::sha256(format!("{name}-public-key")),
}
}
fn scope(project: &str) -> DataPlaneScope {
DataPlaneScope {
tenant: TenantId::from("tenant"),
project: ProjectId::from(project),
process: ProcessId::from("process"),
object: DataPlaneObject::Artifact(ArtifactId::from("artifact")),
authorization_subject: "node-a-to-node-b".to_owned(),
}
}
#[test]
fn failed_direct_transfer_does_not_silently_relay() {
let decision = direct_bulk_transfer_or_error(scope("project"), false);
assert!(matches!(decision, BulkTransferDecision::FailClear { .. }));
}
#[test]
fn native_quic_rendezvous_plan_is_scoped_and_disallows_coordinator_bulk_relay() {
let transport = NativeQuicTransport;
let request = RendezvousRequest {
scope: scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
};
let plan = transport
.plan_authenticated_direct_bulk_transfer(request.clone(), true, "")
.unwrap();
let changed_scope_plan = transport
.plan_authenticated_direct_bulk_transfer(
RendezvousRequest {
scope: scope("other-project"),
..request
},
true,
"",
)
.unwrap();
assert_eq!(plan.transport, TransportKind::NativeQuic);
assert_eq!(plan.scope.tenant, TenantId::from("tenant"));
assert_eq!(plan.scope.project, ProjectId::from("project"));
assert_eq!(plan.scope.process, ProcessId::from("process"));
assert_eq!(
plan.scope.object,
DataPlaneObject::Artifact(ArtifactId::from("artifact"))
);
assert_eq!(plan.source.node, NodeId::from("node-a"));
assert_eq!(plan.destination.node, NodeId::from("node-b"));
assert!(plan.coordinator_assisted_rendezvous);
assert!(!plan.coordinator_bulk_relay_allowed);
assert_ne!(
plan.authorization_digest,
changed_scope_plan.authorization_digest
);
}
#[test]
fn failed_direct_rendezvous_reports_clear_error_instead_of_relaying() {
let error = NativeQuicTransport
.plan_authenticated_direct_bulk_transfer(
RendezvousRequest {
scope: scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
},
false,
"nat traversal failed",
)
.unwrap_err();
assert!(error.to_string().contains("nat traversal failed"));
assert!(error
.to_string()
.contains("coordinator bulk relay is disabled"));
}
}

View file

@ -0,0 +1,241 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{Digest, NodeId, TaskId};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct VfsPath(String);
impl VfsPath {
pub fn new(path: impl Into<String>) -> Result<Self, VfsError> {
let path = path.into();
if !path.starts_with("/vfs/") {
return Err(VfsError::InvalidPath(path));
}
Ok(Self(path))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VfsObject {
pub path: VfsPath,
pub digest: Digest,
pub size: u64,
pub producer: TaskId,
pub node: NodeId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VfsManifest {
pub epoch: u64,
pub producer: TaskId,
pub node: NodeId,
pub objects: BTreeMap<VfsPath, VfsObject>,
pub large_bytes_uploaded: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncPolicy {
MetadataOnly,
ExplicitNode(NodeId),
ExplicitStore(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum VfsSyncDecision {
NoBytesMoved,
MoveBytesToNode(NodeId),
MoveBytesToStore(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReuseDecision {
SameNodeZeroCopy,
NeedsTransfer { from: NodeId, to: NodeId },
Unavailable,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum VfsError {
#[error("VFS path must start with /vfs/: {0}")]
InvalidPath(String),
#[error("path is not visible in the published VFS manifest: {0}")]
NotVisible(String),
}
#[derive(Clone, Debug)]
pub struct VfsOverlay {
task: TaskId,
node: NodeId,
epoch: u64,
pending: BTreeMap<VfsPath, VfsObject>,
published: BTreeMap<VfsPath, VfsObject>,
}
impl VfsOverlay {
pub fn new(task: TaskId, node: NodeId) -> Self {
Self {
task,
node,
epoch: 0,
pending: BTreeMap::new(),
published: BTreeMap::new(),
}
}
pub fn write(&mut self, path: VfsPath, digest: Digest, size: u64) -> VfsObject {
let object = VfsObject {
path: path.clone(),
digest,
size,
producer: self.task.clone(),
node: self.node.clone(),
};
self.pending.insert(path, object.clone());
object
}
pub fn flush(&mut self) -> VfsManifest {
self.epoch += 1;
self.published.append(&mut self.pending);
VfsManifest {
epoch: self.epoch,
producer: self.task.clone(),
node: self.node.clone(),
objects: self.published.clone(),
large_bytes_uploaded: false,
}
}
pub fn sync(&self, policy: SyncPolicy) -> VfsSyncDecision {
match policy {
SyncPolicy::MetadataOnly => VfsSyncDecision::NoBytesMoved,
SyncPolicy::ExplicitNode(node) => VfsSyncDecision::MoveBytesToNode(node),
SyncPolicy::ExplicitStore(store) => VfsSyncDecision::MoveBytesToStore(store),
}
}
pub fn read_published<'a>(
manifest: &'a VfsManifest,
path: &VfsPath,
) -> Result<&'a VfsObject, VfsError> {
manifest
.objects
.get(path)
.ok_or_else(|| VfsError::NotVisible(path.as_str().to_owned()))
}
pub fn reuse_for_consumer(
manifest: &VfsManifest,
path: &VfsPath,
consumer_node: &NodeId,
) -> ReuseDecision {
let Some(object) = manifest.objects.get(path) else {
return ReuseDecision::Unavailable;
};
if &object.node == consumer_node {
ReuseDecision::SameNodeZeroCopy
} else {
ReuseDecision::NeedsTransfer {
from: object.node.clone(),
to: consumer_node.clone(),
}
}
}
pub fn discard_unflushed(&mut self) {
self.pending.clear();
}
pub fn pending_len(&self) -> usize {
self.pending.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path() -> VfsPath {
VfsPath::new("/vfs/artifacts/app").unwrap()
}
#[test]
fn flush_publishes_manifest_without_large_byte_upload() {
let mut overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node-a"));
overlay.write(path(), Digest::sha256("binary"), 6);
let manifest = overlay.flush();
assert_eq!(manifest.epoch, 1);
assert!(!manifest.large_bytes_uploaded);
assert!(manifest.objects.contains_key(&path()));
}
#[test]
fn downstream_task_can_read_after_flush_but_not_before() {
let mut overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node-a"));
overlay.write(path(), Digest::sha256("binary"), 6);
let empty = VfsManifest {
epoch: 0,
producer: TaskId::from("task"),
node: NodeId::from("node-a"),
objects: BTreeMap::new(),
large_bytes_uploaded: false,
};
assert!(VfsOverlay::read_published(&empty, &path()).is_err());
let manifest = overlay.flush();
assert!(VfsOverlay::read_published(&manifest, &path()).is_ok());
}
#[test]
fn sync_is_explicit_and_policy_driven() {
let overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node-a"));
assert_eq!(
overlay.sync(SyncPolicy::MetadataOnly),
VfsSyncDecision::NoBytesMoved
);
assert_eq!(
overlay.sync(SyncPolicy::ExplicitStore("s3://bucket/app".to_owned())),
VfsSyncDecision::MoveBytesToStore("s3://bucket/app".to_owned())
);
}
#[test]
fn same_node_reuse_avoids_transfer() {
let mut overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node-a"));
overlay.write(path(), Digest::sha256("binary"), 6);
let manifest = overlay.flush();
assert_eq!(
VfsOverlay::reuse_for_consumer(&manifest, &path(), &NodeId::from("node-a")),
ReuseDecision::SameNodeZeroCopy
);
assert_eq!(
VfsOverlay::reuse_for_consumer(&manifest, &path(), &NodeId::from("node-b")),
ReuseDecision::NeedsTransfer {
from: NodeId::from("node-a"),
to: NodeId::from("node-b")
}
);
}
#[test]
fn unflushed_task_local_changes_can_be_discarded() {
let mut overlay = VfsOverlay::new(TaskId::from("task"), NodeId::from("node-a"));
overlay.write(path(), Digest::sha256("binary"), 6);
overlay.discard_unflushed();
assert_eq!(overlay.pending_len(), 0);
}
}

View file

@ -0,0 +1,16 @@
[package]
name = "disasmer-dap"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[[bin]]
name = "disasmer-debug-dap"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
disasmer-core = { path = "../disasmer-core" }
disasmer-node = { path = "../disasmer-node" }
serde_json.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
[package]
name = "disasmer-macros"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true

View file

@ -0,0 +1,77 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse::Parser, parse_macro_input, Expr, ItemFn, Lit, Meta, Token};
#[proc_macro_attribute]
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn);
let function_name = function.sig.ident.to_string();
let entrypoint_name = descriptor_name(
attr,
function_name
.strip_suffix("_main")
.unwrap_or(&function_name),
);
let descriptor = format_ident!(
"__DISASMER_ENTRYPOINT_{}",
function_name.to_ascii_uppercase()
);
quote! {
#function
#[doc(hidden)]
pub const #descriptor: ::disasmer::EntrypointDescriptor = ::disasmer::EntrypointDescriptor {
name: #entrypoint_name,
function: #function_name,
};
}
.into()
}
#[proc_macro_attribute]
pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn);
let function_name = function.sig.ident.to_string();
let task_name = descriptor_name(attr, &function_name);
let descriptor = format_ident!("__DISASMER_TASK_{}", function_name.to_ascii_uppercase());
quote! {
#function
#[doc(hidden)]
pub const #descriptor: ::disasmer::TaskDescriptor = ::disasmer::TaskDescriptor {
name: #task_name,
function: #function_name,
remotely_startable: true,
};
}
.into()
}
fn descriptor_name(attr: TokenStream, default: &str) -> String {
if attr.is_empty() {
return default.to_owned();
}
let parser = syn::punctuated::Punctuated::<Meta, Token![,]>::parse_terminated;
let Ok(args) = parser.parse(attr) else {
return default.to_owned();
};
for meta in args {
let Meta::NameValue(name_value) = meta else {
continue;
};
if !name_value.path.is_ident("name") {
continue;
}
if let Expr::Lit(expr) = name_value.value {
if let Lit::Str(name) = expr.lit {
return name.value();
}
}
}
default.to_owned()
}

View file

@ -0,0 +1,16 @@
[package]
name = "disasmer-node"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
disasmer-core = { path = "../disasmer-core" }
quinn.workspace = true
rcgen.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
wasmtime.workspace = true

View file

@ -0,0 +1,84 @@
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use disasmer_core::{
CommandInvocation, Digest, EnvironmentKind, EnvironmentRequirements, EnvironmentResource,
NodeId, ProcessId, TaskId, VfsOverlay, VfsPath,
};
use disasmer_node::{LinuxRootlessPodmanBackend, LocalSourceCheckout, StdProcessRunner};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let workspace = create_workspace()?;
let env_dir = workspace.join("envs/linux");
fs::create_dir_all(&env_dir)?;
fs::write(
env_dir.join("Containerfile"),
"FROM docker.io/library/alpine:3.20\nWORKDIR /workspace\n",
)?;
fs::write(workspace.join("input.txt"), "node-local source\n")?;
let env = EnvironmentResource {
name: "linux".to_owned(),
kind: EnvironmentKind::Containerfile,
recipe_path: env_dir.join("Containerfile"),
context_path: env_dir,
digest: Digest::sha256("phase2-podman-smoke-linux-env"),
requirements: EnvironmentRequirements::linux_container(),
};
let invocation = CommandInvocation {
program: "sh".to_owned(),
args: vec![
"-c".to_owned(),
"printf 'podman-ok:' && cat input.txt".to_owned(),
],
env: Some(env),
};
let checkout = LocalSourceCheckout {
host_path: workspace.clone(),
snapshot: Digest::sha256("phase2-podman-smoke-checkout"),
};
let task = TaskId::from("podman-smoke");
let mut overlay = VfsOverlay::new(task.clone(), NodeId::from("node-podman-smoke"));
let mut runner = StdProcessRunner;
let output = LinuxRootlessPodmanBackend.execute_local_checkout_task(
ProcessId::from("vp-podman-smoke"),
task,
&invocation,
checkout,
Some(VfsPath::new("/vfs/artifacts/podman-smoke.txt")?),
&mut runner,
&mut overlay,
)?;
let manifest = overlay.flush();
println!(
"{}",
serde_json::to_string(&json!({
"podman_status": "completed",
"status_code": output.status_code,
"stdout": output.stdout,
"stderr": output.stderr,
"staged_artifact": output.staged_artifact,
"large_bytes_uploaded": manifest.large_bytes_uploaded,
"uses_full_repo_tarball": false,
"coordinator_routed_file_reads": false,
}))?
);
let _ = fs::remove_dir_all(workspace);
Ok(())
}
fn create_workspace() -> Result<PathBuf, std::io::Error> {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
let workspace = std::env::temp_dir().join(format!(
"disasmer-podman-smoke-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&workspace)?;
Ok(workspace)
}

View file

@ -0,0 +1,138 @@
use std::{net::SocketAddr, sync::Arc};
use disasmer_core::{
ArtifactId, DataPlaneObject, DataPlaneScope, Digest, NativeQuicTransport, NodeEndpoint, NodeId,
ProcessId, ProjectId, RendezvousRequest, TenantId, Transport,
};
use quinn::rustls::{
pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer},
RootCertStore,
};
use quinn::{ClientConfig, Endpoint, ServerConfig};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct QuicTransferRequest {
scope: DataPlaneScope,
authorization_digest: Digest,
requested_bytes: u64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let payload = b"artifact-bytes-over-rust-native-quic".to_vec();
let (cert, key) = self_signed_localhost_cert()?;
let server_endpoint = Endpoint::server(
ServerConfig::with_single_cert(vec![cert.clone()], key)?,
"127.0.0.1:0".parse()?,
)?;
let server_addr = server_endpoint.local_addr()?;
let scope = DataPlaneScope {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("vp-quic"),
object: DataPlaneObject::Artifact(ArtifactId::from("quic-artifact")),
authorization_subject: "node-a-to-node-b".to_owned(),
};
let transport = NativeQuicTransport;
let plan = transport.plan_authenticated_direct_bulk_transfer(
RendezvousRequest {
scope: scope.clone(),
source: endpoint("node-a", server_addr),
destination: endpoint("node-b", "127.0.0.1:0".parse()?),
},
true,
"",
)?;
let expected_scope = plan.scope.clone();
let expected_digest = plan.authorization_digest.clone();
let expected_payload = payload.clone();
let server = tokio::spawn(async move {
let incoming = server_endpoint
.accept()
.await
.ok_or("server endpoint closed before accepting a QUIC connection")?;
let connection = incoming.await?;
let (mut send, mut recv) = connection.accept_bi().await?;
let request_bytes = recv.read_to_end(64 * 1024).await?;
let request: QuicTransferRequest = serde_json::from_slice(&request_bytes)?;
if request.scope != expected_scope {
return Err("QUIC request scope did not match the authorized data-plane scope".into());
}
if request.authorization_digest != expected_digest {
return Err(
"QUIC request authorization digest did not match the rendezvous plan".into(),
);
}
send.write_all(&expected_payload).await?;
send.finish()?;
server_endpoint.wait_idle().await;
Ok::<usize, Box<dyn std::error::Error + Send + Sync>>(request_bytes.len())
});
let mut roots = RootCertStore::empty();
roots.add(cert)?;
let client_config = ClientConfig::with_root_certificates(Arc::new(roots))?;
let mut client_endpoint = Endpoint::client("127.0.0.1:0".parse()?)?;
client_endpoint.set_default_client_config(client_config);
let connection = client_endpoint.connect(server_addr, "localhost")?.await?;
let (mut send, mut recv) = connection.open_bi().await?;
let request = QuicTransferRequest {
scope: plan.scope.clone(),
authorization_digest: plan.authorization_digest.clone(),
requested_bytes: payload.len() as u64,
};
let request_bytes = serde_json::to_vec(&request)?;
send.write_all(&request_bytes).await?;
send.finish()?;
let received = recv.read_to_end(64 * 1024).await?;
connection.close(0u32.into(), b"done");
client_endpoint.wait_idle().await;
let server_received_request_bytes = server.await??;
if received != payload {
return Err("QUIC artifact payload did not round trip".into());
}
println!(
"{}",
json!({
"kind": "disasmer_quic_smoke",
"transport": format!("{:?}", transport.kind()),
"rust_native_quic": true,
"authenticated_direct_connection": transport.authenticated_direct_connections(),
"coordinator_assisted_rendezvous": plan.coordinator_assisted_rendezvous,
"coordinator_bulk_relay_allowed": plan.coordinator_bulk_relay_allowed,
"source_node": plan.source.node,
"destination_node": plan.destination.node,
"scope": plan.scope,
"request_bytes": request_bytes.len(),
"server_received_request_bytes": server_received_request_bytes,
"payload_bytes": received.len(),
"authorization_digest": plan.authorization_digest,
})
);
Ok(())
}
fn self_signed_localhost_cert() -> Result<
(CertificateDer<'static>, PrivateKeyDer<'static>),
Box<dyn std::error::Error + Send + Sync>,
> {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()])?;
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()));
Ok((cert.cert.into(), key))
}
fn endpoint(name: &str, addr: SocketAddr) -> NodeEndpoint {
NodeEndpoint {
node: NodeId::from(name),
advertised_addr: addr.to_string(),
public_key_fingerprint: Digest::sha256(format!("{name}-public-key")),
}
}

View file

@ -0,0 +1,133 @@
use std::fs;
use std::path::PathBuf;
use disasmer_core::{CommandInvocation, NodeId, TaskId, VfsPath};
use disasmer_node::{LocalCommandExecutor, VirtualThreadCommand, WasmtimeTaskRuntime};
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
if args.len() == 2 && args[1] == "--host-command" {
return run_host_command_smoke();
}
if args.len() == 6 && args[1] == "--debug-freeze-resume" {
return run_debug_freeze_resume_smoke(&args[2], &args[3], &args[4], &args[5]);
}
if args.len() != 5 {
return Err(
"usage: disasmer-wasmtime-smoke <module.wasm> <export> <i32-arg> <expected-i32> | --host-command | --debug-freeze-resume <module.wasm> <export> <i32-arg> <expected-i32>".into(),
);
}
let module = PathBuf::from(&args[1]);
let export = &args[2];
let arg: i32 = args[3].parse()?;
let expected: i32 = args[4].parse()?;
let wasm = fs::read(&module)?;
let runtime = WasmtimeTaskRuntime::new()?;
let result = runtime.run_i32_export(&wasm, export, arg)?;
if result != expected {
return Err(format!("expected {expected}, got {result} from export `{export}`").into());
}
println!(
"{}",
serde_json::to_string(&json!({
"type": "wasmtime_task_smoke",
"module": module,
"export": export,
"arg": arg,
"result": result,
}))?
);
Ok(())
}
fn run_debug_freeze_resume_smoke(
module: &str,
export: &str,
arg: &str,
expected: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let module = PathBuf::from(module);
let arg: i32 = arg.parse()?;
let expected: i32 = expected.parse()?;
let wasm = fs::read(&module)?;
let runtime = WasmtimeTaskRuntime::new()?;
let probe = runtime.freeze_resume_i32_export_probe(&wasm, export, arg)?;
if probe.result != expected {
return Err(format!(
"expected {expected}, got {} from export `{export}`",
probe.result
)
.into());
}
println!(
"{}",
serde_json::to_string(&json!({
"type": "wasmtime_debug_freeze_resume_smoke",
"module": module,
"export": export,
"task": probe.task,
"frozen_state": probe.frozen_state,
"resumed_state": probe.resumed_state,
"stack_frames": probe.stack_frames,
"local_values": probe.local_values,
"wasm_function": probe.wasm_function,
"wasm_pc": probe.wasm_pc,
"arg": arg,
"result": probe.result,
"node_runtime_reached_wasm_task": true,
"node_runtime_captured_wasm_locals": true,
}))?
);
Ok(())
}
fn run_host_command_smoke() -> Result<(), Box<dyn std::error::Error>> {
let runtime = WasmtimeTaskRuntime::new()?;
let result = runtime.run_i32_export_with_command_import(
r#"
(module
(import "disasmer" "cmd_run" (func $cmd_run (result i32)))
(func (export "compile-linux") (result i32)
call $cmd_run))
"#,
"compile-linux",
LocalCommandExecutor {
node: NodeId::from("node-wasmtime"),
hosted_control_plane: false,
has_command_capability: true,
},
VirtualThreadCommand {
virtual_thread: TaskId::from("compile-linux"),
invocation: CommandInvocation {
program: "sh".to_owned(),
args: vec!["-c".to_owned(), "printf linux-build-artifact".to_owned()],
env: None,
},
stage_stdout_as: Some(VfsPath::new("/vfs/artifacts/linux/app.tar.zst")?),
},
)?;
println!(
"{}",
serde_json::to_string(&json!({
"type": "wasmtime_host_command_smoke",
"export": "compile-linux",
"export_result": result.export_result,
"virtual_thread": result.command_output.virtual_thread,
"stdout": result.command_output.stdout,
"staged_artifact": result.command_output.staged_artifact,
"large_bytes_uploaded": result.manifest.large_bytes_uploaded,
"manifest_objects": result.manifest.objects.len(),
"node_host_import": "disasmer.cmd_run",
"flagship_linux_build_task": true,
"node_executed_host_command": true,
"hosted_control_plane_ran_command": false,
}))?
);
Ok(())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,637 @@
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use disasmer_core::{
Capability, CommandInvocation, NodeCapabilities, NodeId, TaskId, VfsOverlay, VfsPath,
};
use disasmer_node::{CommandOutput, LocalCommandExecutor, VirtualThreadCommand};
use serde_json::{json, Value};
#[derive(Debug)]
struct Args {
coordinator: String,
tenant: String,
project: String,
node: String,
process: String,
task: String,
command: String,
command_args: Vec<String>,
artifact: String,
enrollment_grant: Option<String>,
public_key: Option<String>,
control_poll_ms: u64,
assignment_poll_ms: u64,
emit_ready: bool,
worker: bool,
}
#[derive(Clone, Debug)]
struct RuntimeTask {
process: String,
task: String,
command: String,
command_args: Vec<String>,
artifact: String,
epoch: Option<u64>,
task_assignment_response: Value,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = parse_args()?;
let mut session = CoordinatorSession::connect(&args.coordinator)?;
let registration = register_node(&mut session, &args)?;
let heartbeat = session.request(json!({
"type": "node_heartbeat",
"node": &args.node,
}))?;
let capability_report = session.request(json!({
"type": "report_node_capabilities",
"tenant": &args.tenant,
"project": &args.project,
"node": &args.node,
"capabilities": NodeCapabilities::detect_current(),
"cached_environment_digests": [],
"dependency_cache_digests": [],
"source_snapshots": [],
"artifact_locations": [],
"direct_connectivity": true,
"online": true,
}))?;
if args.worker {
return worker_loop(
&args,
&mut session,
registration,
heartbeat,
capability_report,
);
}
let task_assignment = session.request(json!({
"type": "schedule_task",
"tenant": &args.tenant,
"project": &args.project,
"environment": null,
"environment_digest": null,
"required_capabilities": [Capability::Command],
"dependency_cache": null,
"source_snapshot": null,
"required_artifacts": [],
"quota_available": true,
"policy_allowed": true,
"prefer_node": &args.node,
}))?;
let runtime_task = RuntimeTask {
process: args.process.clone(),
task: args.task.clone(),
command: args.command.clone(),
command_args: args.command_args.clone(),
artifact: args.artifact.clone(),
epoch: None,
task_assignment_response: task_assignment,
};
let report = run_runtime_task(
&args,
&mut session,
runtime_task,
registration,
heartbeat,
capability_report,
)?;
println!("{}", serde_json::to_string(&report)?);
Ok(())
}
fn worker_loop(
args: &Args,
session: &mut CoordinatorSession,
registration: Value,
heartbeat: Value,
capability_report: Value,
) -> Result<(), Box<dyn std::error::Error>> {
if args.emit_ready {
println!(
"{}",
serde_json::to_string(&json!({
"node_status": "ready",
"mode": "worker",
"node": &args.node,
}))?
);
std::io::stdout().flush()?;
}
loop {
let response = session.request(json!({
"type": "poll_task_assignment",
"tenant": &args.tenant,
"project": &args.project,
"node": &args.node,
}))?;
let Some(assignment) = response.get("assignment").filter(|value| !value.is_null()) else {
std::thread::sleep(Duration::from_millis(args.assignment_poll_ms));
continue;
};
let runtime_task = runtime_task_from_assignment(assignment)?;
let report = run_runtime_task(
args,
session,
runtime_task,
registration.clone(),
heartbeat.clone(),
capability_report.clone(),
)?;
println!("{}", serde_json::to_string(&report)?);
std::io::stdout().flush()?;
}
}
fn runtime_task_from_assignment(value: &Value) -> Result<RuntimeTask, Box<dyn std::error::Error>> {
Ok(RuntimeTask {
process: required_string(value, "process")?,
task: required_string(value, "task")?,
command: required_string(value, "command")?,
command_args: value
.get("command_args")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default(),
artifact: required_string(value, "artifact_path")?,
epoch: value.get("epoch").and_then(Value::as_u64),
task_assignment_response: value.clone(),
})
}
fn required_string(value: &Value, field: &str) -> Result<String, Box<dyn std::error::Error>> {
value
.get(field)
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| format!("task assignment missing string field `{field}`").into())
}
fn run_runtime_task(
args: &Args,
session: &mut CoordinatorSession,
task: RuntimeTask,
registration: Value,
heartbeat: Value,
capability_report: Value,
) -> Result<Value, Box<dyn std::error::Error>> {
let epoch = match task.epoch {
Some(epoch) => epoch,
None => {
let started = session.request(json!({
"type": "start_process",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
}))?;
started
.get("epoch")
.and_then(Value::as_u64)
.ok_or("coordinator start_process response missing epoch")?
}
};
session.request(json!({
"type": "reconnect_node",
"node": &args.node,
"process": &task.process,
"epoch": epoch,
}))?;
let debug_command = session.request(json!({
"type": "poll_debug_command",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
}))?;
if args.emit_ready && !args.worker {
println!(
"{}",
serde_json::to_string(&json!({
"node_status": "ready",
"node": &args.node,
"process": &task.process,
"task": &task.task,
}))?
);
std::io::stdout().flush()?;
}
if args.control_poll_ms > 0 && wait_for_cancellation(session, args, &task)? {
let recorded = session.request(json!({
"type": "task_completed",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
"terminal_state": "cancelled",
"status_code": null,
"stdout_bytes": 0,
"stderr_bytes": 0,
"artifact_path": null,
"artifact_digest": null,
"artifact_size_bytes": null,
}))?;
return Ok(cancelled_node_report(
args,
&task,
registration,
heartbeat,
capability_report,
task.task_assignment_response.clone(),
debug_command,
recorded,
session.requests(),
));
}
let executor = LocalCommandExecutor {
node: NodeId::new(args.node.clone()),
hosted_control_plane: false,
has_command_capability: true,
};
let task_id = TaskId::new(task.task.clone());
let mut overlay = VfsOverlay::new(task_id.clone(), NodeId::new(args.node.clone()));
let output = executor.run(
VirtualThreadCommand {
virtual_thread: task_id,
invocation: CommandInvocation {
program: task.command.clone(),
args: task.command_args.clone(),
env: None,
},
stage_stdout_as: Some(VfsPath::new(task.artifact.clone())?),
},
&mut overlay,
)?;
let manifest = overlay.flush();
let staged = output.staged_artifact.as_ref();
let artifact_digest = staged.map(|artifact| artifact.digest.clone());
let artifact_path = staged.map(|artifact| artifact.path.as_str().to_owned());
let artifact_size_bytes = staged.map(|artifact| artifact.size);
let log_event = session.request(json!({
"type": "report_task_log",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
"stdout_bytes": output.stdout.len(),
"stderr_bytes": output.stderr.len(),
"stdout_tail": &output.stdout,
"stderr_tail": &output.stderr,
"stdout_truncated": output.stdout_truncated,
"stderr_truncated": output.stderr_truncated,
"backpressured": output.log_backpressured,
}))?;
let vfs_metadata = session.request(json!({
"type": "report_vfs_metadata",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
"artifact_path": artifact_path,
"artifact_digest": artifact_digest,
"artifact_size_bytes": artifact_size_bytes,
"large_bytes_uploaded": manifest.large_bytes_uploaded,
}))?;
let recorded = session.request(json!({
"type": "task_completed",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
"status_code": output.status_code,
"stdout_bytes": output.stdout.len(),
"stderr_bytes": output.stderr.len(),
"stdout_tail": &output.stdout,
"stderr_tail": &output.stderr,
"stdout_truncated": output.stdout_truncated,
"stderr_truncated": output.stderr_truncated,
"artifact_path": staged.map(|artifact| artifact.path.as_str().to_owned()),
"artifact_digest": staged.map(|artifact| artifact.digest.clone()),
"artifact_size_bytes": artifact_size_bytes,
}))?;
Ok(node_report(
output,
manifest.large_bytes_uploaded,
registration,
heartbeat,
capability_report,
task.task_assignment_response,
debug_command,
log_event,
vfs_metadata,
recorded,
session.requests(),
))
}
fn node_report(
output: CommandOutput,
large_bytes_uploaded: bool,
registration_response: Value,
heartbeat_response: Value,
capability_response: Value,
task_assignment_response: Value,
debug_command_response: Value,
log_event_response: Value,
vfs_metadata_response: Value,
coordinator_response: Value,
session_requests: usize,
) -> Value {
json!({
"node_status": "completed",
"virtual_thread": output.virtual_thread,
"terminal_state": if output.status_code == Some(0) { "completed" } else { "failed" },
"status_code": output.status_code,
"stdout_bytes": output.stdout.len(),
"stderr_bytes": output.stderr.len(),
"stdout_tail": &output.stdout,
"stderr_tail": &output.stderr,
"stdout_truncated": output.stdout_truncated,
"stderr_truncated": output.stderr_truncated,
"log_backpressured": output.log_backpressured,
"staged_artifact": output.staged_artifact,
"large_bytes_uploaded": large_bytes_uploaded,
"registration_response": registration_response,
"heartbeat_response": heartbeat_response,
"capability_response": capability_response,
"task_assignment_response": task_assignment_response,
"debug_command_response": debug_command_response,
"log_event_response": log_event_response,
"vfs_metadata_response": vfs_metadata_response,
"session_requests": session_requests,
"coordinator_response": coordinator_response,
})
}
fn cancelled_node_report(
_args: &Args,
task: &RuntimeTask,
registration_response: Value,
heartbeat_response: Value,
capability_response: Value,
task_assignment_response: Value,
debug_command_response: Value,
coordinator_response: Value,
session_requests: usize,
) -> Value {
json!({
"node_status": "cancelled",
"virtual_thread": &task.task,
"terminal_state": "cancelled",
"status_code": null,
"stdout_bytes": 0,
"stderr_bytes": 0,
"stdout_tail": "",
"stderr_tail": "",
"stdout_truncated": false,
"stderr_truncated": false,
"log_backpressured": false,
"staged_artifact": null,
"large_bytes_uploaded": false,
"registration_response": registration_response,
"heartbeat_response": heartbeat_response,
"capability_response": capability_response,
"task_assignment_response": task_assignment_response,
"debug_command_response": debug_command_response,
"log_event_response": null,
"vfs_metadata_response": null,
"session_requests": session_requests,
"coordinator_response": coordinator_response,
})
}
fn register_node(
session: &mut CoordinatorSession,
args: &Args,
) -> Result<Value, Box<dyn std::error::Error>> {
let public_key = args
.public_key
.clone()
.unwrap_or_else(|| format!("{}-public-key", args.node));
if let Some(grant) = &args.enrollment_grant {
session.request(json!({
"type": "exchange_node_enrollment_grant",
"tenant": &args.tenant,
"project": &args.project,
"node": &args.node,
"public_key": public_key,
"enrollment_grant": grant,
"now_epoch_seconds": 0,
}))
} else {
session.request(json!({
"type": "attach_node",
"tenant": &args.tenant,
"project": &args.project,
"node": &args.node,
"public_key": public_key,
}))
}
}
fn wait_for_cancellation(
session: &mut CoordinatorSession,
args: &Args,
task: &RuntimeTask,
) -> Result<bool, Box<dyn std::error::Error>> {
let deadline = Instant::now() + Duration::from_millis(args.control_poll_ms);
loop {
let control = session.request(json!({
"type": "poll_task_control",
"tenant": &args.tenant,
"project": &args.project,
"process": &task.process,
"node": &args.node,
"task": &task.task,
}))?;
if control
.get("cancel_requested")
.and_then(Value::as_bool)
.unwrap_or(false)
{
return Ok(true);
}
let now = Instant::now();
if now >= deadline {
return Ok(false);
}
std::thread::sleep((deadline - now).min(Duration::from_millis(100)));
}
}
struct CoordinatorSession {
writer: TcpStream,
reader: BufReader<TcpStream>,
requests: usize,
}
impl CoordinatorSession {
fn connect(addr: &str) -> Result<Self, Box<dyn std::error::Error>> {
let transport_addr = json_line_transport_addr(addr);
let writer = TcpStream::connect(&transport_addr)?;
let reader = BufReader::new(writer.try_clone()?);
Ok(Self {
writer,
reader,
requests: 0,
})
}
fn request(&mut self, value: Value) -> Result<Value, Box<dyn std::error::Error>> {
serde_json::to_writer(&mut self.writer, &value)?;
self.writer.write_all(b"\n")?;
self.writer.flush()?;
let mut line = String::new();
if self.reader.read_line(&mut line)? == 0 {
return Err("coordinator closed session without a response".into());
}
let response: Value = serde_json::from_str(&line)?;
self.requests += 1;
if response.get("type").and_then(Value::as_str) == Some("error") {
return Err(format!("coordinator error: {response}").into());
}
Ok(response)
}
fn requests(&self) -> usize {
self.requests
}
}
fn json_line_transport_addr(endpoint: &str) -> String {
let endpoint = endpoint.trim();
for (scheme, default_port) in [("https://", 443), ("http://", 80)] {
if let Some(rest) = endpoint.strip_prefix(scheme) {
let authority = rest.split('/').next().unwrap_or(rest);
if authority.contains(':') {
return authority.to_owned();
}
return format!("{authority}:{default_port}");
}
}
endpoint.to_owned()
}
fn parse_args() -> Result<Args, Box<dyn std::error::Error>> {
let mut coordinator = None;
let mut tenant = "tenant".to_owned();
let mut project = "project".to_owned();
let mut node = "node".to_owned();
let mut process = "process".to_owned();
let mut task = "compile-linux".to_owned();
let mut command = None;
let mut command_args = Vec::new();
let mut artifact = "/vfs/artifacts/node-output.txt".to_owned();
let mut enrollment_grant = None;
let mut public_key = None;
let mut control_poll_ms = 0;
let mut assignment_poll_ms = 500;
let mut emit_ready = false;
let mut worker = false;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--coordinator" => coordinator = args.next(),
"--tenant" => tenant = args.next().ok_or("--tenant requires a value")?,
"--project-id" => project = args.next().ok_or("--project-id requires a value")?,
"--node" => node = args.next().ok_or("--node requires a value")?,
"--process" => process = args.next().ok_or("--process requires a value")?,
"--task" => task = args.next().ok_or("--task requires a value")?,
"--command" => command = args.next(),
"--arg" => command_args.push(args.next().ok_or("--arg requires a value")?),
"--artifact" => artifact = args.next().ok_or("--artifact requires a value")?,
"--enrollment-grant" => enrollment_grant = args.next(),
"--public-key" => public_key = args.next(),
"--control-poll-ms" => {
control_poll_ms = args
.next()
.ok_or("--control-poll-ms requires a value")?
.parse()?
}
"--assignment-poll-ms" => {
assignment_poll_ms = args
.next()
.ok_or("--assignment-poll-ms requires a value")?
.parse()?
}
"--emit-ready" => emit_ready = true,
"--worker" => worker = true,
"--project" => {
let project_path = PathBuf::from(args.next().ok_or("--project requires a path")?);
command_args.extend([
"test".to_owned(),
"--manifest-path".to_owned(),
project_path
.join("Cargo.toml")
.to_string_lossy()
.into_owned(),
]);
}
other => return Err(format!("unknown argument: {other}").into()),
}
}
Ok(Args {
coordinator: coordinator.ok_or("--coordinator is required")?,
tenant,
project,
node,
process,
task,
command: command.unwrap_or_else(|| "cargo".to_owned()),
command_args,
artifact,
enrollment_grant,
public_key,
control_poll_ms,
assignment_poll_ms,
emit_ready,
worker,
})
}
#[cfg(test)]
mod tests {
use super::json_line_transport_addr;
#[test]
fn hosted_operator_url_maps_to_json_line_transport_address() {
assert_eq!(
json_line_transport_addr("https://disasmer.michelpaulissen.com:9443"),
"disasmer.michelpaulissen.com:9443"
);
assert_eq!(
json_line_transport_addr("https://disasmer.michelpaulissen.com:9443/auth/device"),
"disasmer.michelpaulissen.com:9443"
);
assert_eq!(
json_line_transport_addr("http://operator.example.test"),
"operator.example.test:80"
);
assert_eq!(json_line_transport_addr("127.0.0.1:7999"), "127.0.0.1:7999");
}
}

View file

@ -0,0 +1,17 @@
[package]
name = "disasmer-sdk"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "disasmer"
[dependencies]
disasmer-macros = { path = "../disasmer-macros" }
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
futures-executor.workspace = true

View file

@ -0,0 +1,521 @@
use serde::{Deserialize, Serialize};
pub use disasmer_macros::{main, task};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntrypointDescriptor {
pub name: &'static str,
pub function: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskDescriptor {
pub name: &'static str,
pub function: &'static str,
pub remotely_startable: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegisteredProgram {
pub entrypoints: &'static [EntrypointDescriptor],
pub tasks: &'static [TaskDescriptor],
}
impl RegisteredProgram {
pub fn select_entrypoint(&self, name: &str) -> Option<EntrypointDescriptor> {
self.entrypoints
.iter()
.copied()
.find(|entrypoint| entrypoint.name == name)
}
pub fn task(&self, name: &str) -> Option<TaskDescriptor> {
self.tasks.iter().copied().find(|task| task.name == name)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceSnapshot {
pub digest: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Blob {
pub digest: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artifact {
pub id: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvRef {
pub name: &'static str,
}
impl EnvRef {
pub const fn new_static(name: &'static str) -> Self {
Self { name }
}
}
#[macro_export]
macro_rules! env {
($name:literal) => {
$crate::EnvRef::new_static($name)
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TaskArgKind {
SmallSerialized,
Handle,
}
pub trait TaskArg: Serialize {
fn task_arg_kind(&self) -> TaskArgKind {
TaskArgKind::SmallSerialized
}
}
macro_rules! small_task_arg {
($($ty:ty),+ $(,)?) => {
$(
impl TaskArg for $ty {}
)+
};
}
small_task_arg!(
(),
bool,
char,
String,
i8,
i16,
i32,
i64,
isize,
u8,
u16,
u32,
u64,
usize,
f32,
f64,
);
impl TaskArg for SourceSnapshot {
fn task_arg_kind(&self) -> TaskArgKind {
TaskArgKind::Handle
}
}
impl TaskArg for Blob {
fn task_arg_kind(&self) -> TaskArgKind {
TaskArgKind::Handle
}
}
impl TaskArg for Artifact {
fn task_arg_kind(&self) -> TaskArgKind {
TaskArgKind::Handle
}
}
impl<T> TaskArg for Option<T> where T: TaskArg {}
impl<T> TaskArg for Vec<T> where T: TaskArg {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TaskArgBudget {
pub max_inline_bytes: usize,
}
impl Default for TaskArgBudget {
fn default() -> Self {
Self {
max_inline_bytes: 64 * 1024,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaskArgValidation {
pub inline_bytes: usize,
pub kind: TaskArgKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TaskArgError {
Serialization(String),
TooLarge { size: usize, limit: usize },
HostOnly { type_name: &'static str },
}
impl std::fmt::Display for TaskArgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Serialization(error) => write!(f, "task argument could not serialize: {error}"),
Self::TooLarge { size, limit } => write!(
f,
"task argument is {size} bytes; inline task arguments are limited to {limit} bytes, use SourceSnapshot, Blob, Artifact, or VFS handles"
),
Self::HostOnly { type_name } => write!(
f,
"task boundary value `{type_name}` is host-only; use small serialized data or handles"
),
}
}
}
impl std::error::Error for TaskArgError {}
pub fn validate_task_arg<T>(
value: &T,
budget: TaskArgBudget,
) -> Result<TaskArgValidation, TaskArgError>
where
T: TaskArg + ?Sized,
{
let bytes = serde_json::to_vec(value)
.map_err(|error| TaskArgError::Serialization(error.to_string()))?;
let kind = value.task_arg_kind();
if kind == TaskArgKind::SmallSerialized && bytes.len() > budget.max_inline_bytes {
return Err(TaskArgError::TooLarge {
size: bytes.len(),
limit: budget.max_inline_bytes,
});
}
Ok(TaskArgValidation {
inline_bytes: bytes.len(),
kind,
})
}
pub fn reject_host_only_task_arg<T: ?Sized>() -> TaskArgError {
TaskArgError::HostOnly {
type_name: std::any::type_name::<T>(),
}
}
pub mod spawn {
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use crate::{validate_task_arg, EnvRef, TaskArg, TaskArgBudget, TaskArgError};
static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1);
static RUNTIME_THREADS: OnceLock<Mutex<Vec<RuntimeSpawnEvent>>> = OnceLock::new();
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeSpawnEvent {
pub virtual_thread_id: u64,
pub name: &'static str,
pub env: Option<EnvRef>,
pub debugger_visible: bool,
}
fn runtime_threads() -> &'static Mutex<Vec<RuntimeSpawnEvent>> {
RUNTIME_THREADS.get_or_init(|| Mutex::new(Vec::new()))
}
fn register_runtime_thread(
virtual_thread_id: u64,
name: &'static str,
env: Option<EnvRef>,
) -> RuntimeSpawnEvent {
let event = RuntimeSpawnEvent {
virtual_thread_id,
name,
env,
debugger_visible: true,
};
runtime_threads().lock().unwrap().push(event.clone());
event
}
pub fn drain_runtime_spawn_events() -> Vec<RuntimeSpawnEvent> {
runtime_threads().lock().unwrap().drain(..).collect()
}
pub fn runtime_spawn_events() -> Vec<RuntimeSpawnEvent> {
runtime_threads().lock().unwrap().clone()
}
pub fn task<F, R>(entry: F) -> TaskBuilder<F, R>
where
F: FnOnce() -> R,
R: TaskArg,
{
TaskBuilder {
entry: Some(entry),
env: None,
name: "task",
}
}
pub fn task_with_arg<A, F, R>(arg: A, entry: F) -> TaskWithArgBuilder<A, F, R>
where
A: TaskArg,
F: FnOnce(A) -> R,
R: TaskArg,
{
TaskWithArgBuilder {
arg: Some(arg),
entry: Some(entry),
env: None,
name: "task",
arg_budget: TaskArgBudget::default(),
}
}
pub struct TaskBuilder<F, R>
where
F: FnOnce() -> R,
R: TaskArg,
{
entry: Option<F>,
env: Option<EnvRef>,
name: &'static str,
}
impl<F, R> TaskBuilder<F, R>
where
F: FnOnce() -> R,
R: TaskArg,
{
pub fn env(mut self, env: EnvRef) -> Self {
self.env = Some(env);
self
}
pub fn name(mut self, name: &'static str) -> Self {
self.name = name;
self
}
pub async fn start(mut self) -> TaskHandle<R> {
let id = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
let runtime_event = register_runtime_thread(id, self.name, self.env);
let entry = self.entry.take().expect("task entry used once");
TaskHandle {
virtual_thread_id: id,
name: self.name,
env: self.env,
debugger_visible: runtime_event.debugger_visible,
result: Some(entry()),
}
}
}
pub struct TaskWithArgBuilder<A, F, R>
where
A: TaskArg,
F: FnOnce(A) -> R,
R: TaskArg,
{
arg: Option<A>,
entry: Option<F>,
env: Option<EnvRef>,
name: &'static str,
arg_budget: TaskArgBudget,
}
impl<A, F, R> TaskWithArgBuilder<A, F, R>
where
A: TaskArg,
F: FnOnce(A) -> R,
R: TaskArg,
{
pub fn env(mut self, env: EnvRef) -> Self {
self.env = Some(env);
self
}
pub fn name(mut self, name: &'static str) -> Self {
self.name = name;
self
}
pub fn arg_budget(mut self, arg_budget: TaskArgBudget) -> Self {
self.arg_budget = arg_budget;
self
}
pub async fn start(mut self) -> Result<TaskHandle<R>, TaskArgError> {
let arg = self.arg.take().expect("task argument used once");
validate_task_arg(&arg, self.arg_budget)?;
let id = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
let runtime_event = register_runtime_thread(id, self.name, self.env);
let entry = self.entry.take().expect("task entry used once");
Ok(TaskHandle {
virtual_thread_id: id,
name: self.name,
env: self.env,
debugger_visible: runtime_event.debugger_visible,
result: Some(entry(arg)),
})
}
}
pub struct TaskHandle<R> {
virtual_thread_id: u64,
name: &'static str,
env: Option<EnvRef>,
debugger_visible: bool,
result: Option<R>,
}
impl<R> TaskHandle<R>
where
R: TaskArg,
{
pub fn virtual_thread_id(&self) -> u64 {
self.virtual_thread_id
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn env(&self) -> Option<EnvRef> {
self.env
}
pub fn debugger_visible(&self) -> bool {
self.debugger_visible
}
pub async fn join(mut self) -> R {
self.result.take().expect("task joined once")
}
}
}
#[cfg(test)]
mod tests {
use futures_executor::block_on;
#[test]
fn env_macro_creates_logical_environment_reference() {
let env = crate::env!("linux");
assert_eq!(env.name, "linux");
}
#[test]
fn spawn_task_start_join_returns_small_result() {
let result = block_on(async {
let handle = crate::spawn::task(|| 42)
.name("compile linux")
.env(crate::env!("linux"))
.start()
.await;
assert_eq!(handle.name(), "compile linux");
assert_eq!(handle.env().unwrap().name, "linux");
assert!(handle.debugger_visible());
handle.join().await
});
assert_eq!(result, 42);
}
#[test]
fn spawn_task_start_registers_debugger_visible_runtime_thread() {
let handle = block_on(async {
crate::spawn::task(|| 7_u32)
.name("sdk-runtime-thread-test")
.env(crate::env!("linux"))
.start()
.await
});
let events = crate::spawn::runtime_spawn_events();
let event = events
.iter()
.find(|event| event.virtual_thread_id == handle.virtual_thread_id())
.expect("spawn runtime event should exist for task handle");
assert!(handle.debugger_visible());
assert!(event.debugger_visible);
assert_eq!(event.name, "sdk-runtime-thread-test");
assert_eq!(event.env.unwrap().name, "linux");
assert_eq!(block_on(handle.join()), 7);
}
#[test]
fn task_arg_validation_allows_handles_and_rejects_oversized_inline_values() {
let artifact = crate::Artifact {
id: "artifact://build/app".to_owned(),
};
let artifact_validation = crate::validate_task_arg(
&artifact,
crate::TaskArgBudget {
max_inline_bytes: 4,
},
)
.unwrap();
assert_eq!(artifact_validation.kind, crate::TaskArgKind::Handle);
let bytes = vec![1_u8, 2, 3, 4, 5];
let error = crate::validate_task_arg(
&bytes,
crate::TaskArgBudget {
max_inline_bytes: 4,
},
)
.unwrap_err();
assert!(matches!(error, crate::TaskArgError::TooLarge { .. }));
}
#[test]
fn spawn_task_with_arg_rejects_large_inline_argument_before_dispatch() {
let dispatched = std::cell::Cell::new(false);
let result = block_on(async {
crate::spawn::task_with_arg(vec![1_u8, 2, 3, 4, 5], |_| {
dispatched.set(true);
42_u32
})
.arg_budget(crate::TaskArgBudget {
max_inline_bytes: 4,
})
.start()
.await
});
assert!(matches!(result, Err(crate::TaskArgError::TooLarge { .. })));
assert!(!dispatched.get());
}
#[test]
fn spawn_task_with_arg_allows_runtime_handles_under_inline_budget() {
let artifact = crate::Artifact {
id: "artifact://build/app".to_owned(),
};
let result = block_on(async {
let handle = crate::spawn::task_with_arg(artifact, |artifact| artifact.id)
.name("publish artifact")
.env(crate::env!("linux"))
.arg_budget(crate::TaskArgBudget {
max_inline_bytes: 4,
})
.start()
.await
.unwrap();
assert_eq!(handle.name(), "publish artifact");
handle.join().await
});
assert_eq!(result, "artifact://build/app");
}
#[test]
fn host_only_task_arg_error_names_rejected_type() {
let error = crate::reject_host_only_task_arg::<*const u8>();
assert!(error.to_string().contains("*const u8"));
assert!(error.to_string().contains("host-only"));
}
}

View file

@ -0,0 +1,80 @@
use futures_executor::block_on;
#[disasmer::main]
fn build_main() -> u32 {
1
}
#[disasmer::main(name = "release")]
fn release_main() -> u32 {
2
}
#[disasmer::task(name = "compile-linux")]
fn compile_linux() -> u32 {
41
}
#[disasmer::task]
fn package_release() -> disasmer::Artifact {
disasmer::Artifact {
id: "artifact://package/release.tar.zst".to_owned(),
}
}
#[test]
fn disasmer_attributes_and_spawn_api_compile_for_rust_build_workflow() {
let program = disasmer::RegisteredProgram {
entrypoints: &[
__DISASMER_ENTRYPOINT_BUILD_MAIN,
__DISASMER_ENTRYPOINT_RELEASE_MAIN,
],
tasks: &[
__DISASMER_TASK_COMPILE_LINUX,
__DISASMER_TASK_PACKAGE_RELEASE,
],
};
assert_eq!(
program.select_entrypoint("build").unwrap().function,
"build_main"
);
assert_eq!(
program.select_entrypoint("release").unwrap().function,
"release_main"
);
assert_eq!(
program.task("compile-linux").unwrap().function,
"compile_linux"
);
assert!(program.task("compile-linux").unwrap().remotely_startable);
let result = block_on(async {
let handle = disasmer::spawn::task(|| compile_linux() + build_main())
.name("compile linux")
.env(disasmer::env!("linux"))
.start()
.await;
assert!(handle.virtual_thread_id() > 0);
handle.join().await
});
assert_eq!(result, 42);
assert_eq!(release_main(), 2);
}
#[test]
fn task_boundaries_accept_small_values_and_runtime_handles() {
let small = disasmer::validate_task_arg(&7_u32, disasmer::TaskArgBudget::default()).unwrap();
assert_eq!(small.kind, disasmer::TaskArgKind::SmallSerialized);
let artifact = package_release();
let artifact = disasmer::validate_task_arg(
&artifact,
disasmer::TaskArgBudget {
max_inline_bytes: 4,
},
)
.unwrap();
assert_eq!(artifact.kind, disasmer::TaskArgKind::Handle);
}

View file

@ -0,0 +1,17 @@
[package]
name = "launch-build-demo"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
path = "src/build.rs"
crate-type = ["rlib", "cdylib"]
[dependencies]
disasmer = { package = "disasmer-sdk", path = "../../crates/disasmer-sdk" }
serde.workspace = true
[dev-dependencies]
futures-executor.workspace = true

View file

@ -0,0 +1,11 @@
# Launch Build Demo
This demo is the MVP build workflow expressed as Rust source code. It uses `env!("linux")`, recognizes `env!("windows")` when a Windows development node is attached, spawns debugger-visible virtual tasks, and returns artifact/source handles instead of moving large bytes through task arguments.
The Linux environment is defined by `envs/linux/Containerfile`. The Windows environment is a user-attached development contract and does not claim secure managed Windows sandboxing.
Inspect the bundle metadata:
```bash
disasmer bundle inspect --project examples/launch-build-demo
```

View file

@ -0,0 +1,3 @@
FROM docker.io/library/alpine:3.20
RUN apk add --no-cache build-base tar zstd
WORKDIR /workspace

View file

@ -0,0 +1,2 @@
# User-attached Windows development execution contract for the MVP.
# This is not a managed untrusted Windows sandbox.

View file

@ -0,0 +1,97 @@
use disasmer::{Artifact, EnvRef, SourceSnapshot};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildReport {
pub linux_thread: u64,
pub package_thread: u64,
pub linux_artifact: Artifact,
pub package_artifact: Artifact,
pub source: SourceSnapshot,
}
pub fn linux_env() -> EnvRef {
disasmer::env!("linux")
}
pub fn windows_env() -> EnvRef {
disasmer::env!("windows")
}
#[disasmer::task]
pub fn compile_linux() -> Artifact {
Artifact {
id: "artifact://linux/app.tar.zst".to_owned(),
}
}
#[disasmer::task]
#[unsafe(no_mangle)]
pub extern "C" fn task_add_one(input: i32) -> i32 {
input + 1
}
#[disasmer::task]
pub fn package_release() -> Artifact {
Artifact {
id: "artifact://package/release.tar.zst".to_owned(),
}
}
#[disasmer::main]
pub fn build_main() -> &'static str {
"launch-build-demo"
}
pub async fn run_build_workflow() -> BuildReport {
let linux = disasmer::spawn::task(compile_linux)
.name("compile linux")
.env(linux_env())
.start()
.await;
let package = disasmer::spawn::task(package_release)
.name("package artifacts")
.env(linux_env())
.start()
.await;
let linux_thread = linux.virtual_thread_id();
let package_thread = package.virtual_thread_id();
let linux_artifact = linux.join().await;
let package_artifact = package.join().await;
BuildReport {
linux_thread,
package_thread,
linux_artifact,
package_artifact,
source: SourceSnapshot {
digest: "source://local-checkout".to_owned(),
},
}
}
#[cfg(test)]
mod tests {
use futures_executor::block_on;
use super::*;
#[test]
fn flagship_workflow_is_rust_source_with_spawned_virtual_tasks() {
assert_eq!(build_main(), "launch-build-demo");
assert_eq!(task_add_one(41), 42);
assert_eq!(linux_env().name, "linux");
assert_eq!(windows_env().name, "windows");
let report = block_on(run_build_workflow());
assert_ne!(report.linux_thread, report.package_thread);
assert_eq!(report.linux_artifact.id, "artifact://linux/app.tar.zst");
assert_eq!(
report.package_artifact.id,
"artifact://package/release.tar.zst"
);
assert_eq!(report.source.digest, "source://local-checkout");
}
}

View file

@ -0,0 +1,111 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function criterionLines(source) {
return source
.split(/\r?\n/)
.filter((line) => /^- \[[ x]\] \*\*/.test(line));
}
function assertEveryCriterionHasStatus(source, name) {
const lines = criterionLines(source);
assert(lines.length > 0, `${name} must contain acceptance criteria`);
for (const line of lines) {
assert.match(
line,
/^- \[[ x]\] \*\*(Passed|Partial|Open)(?: \([^)]+\))?:\*\*/,
`${name} criterion lacks an explicit status prefix: ${line}`
);
}
}
function assertNoOpenCriteria(source, name) {
const open = criterionLines(source).filter((line) => /\*\*Open(?::| \()/.test(line));
assert.deepStrictEqual(open, [], `${name} still has Open criteria`);
}
const phase2 = read("acceptance_criteria_phase2.md");
const base = read("acceptance_criteria.md");
const docsSmoke = read("scripts/docs-smoke.js");
const releaseBlockerSmoke = read("scripts/release-blocker-smoke.js");
const publicAcceptance = read("scripts/acceptance-public.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
const publicSplit = read("scripts/verify-public-split.sh");
assert.match(
phase2,
/phase 2 superset of `acceptance_criteria\.md`/,
"phase 2 criteria must declare that they are a superset of the base criteria"
);
assert.match(
phase2,
/Existing `acceptance_criteria\.md` remains required unless it conflicts with this stricter release document; this document wins in conflicts/,
"phase 2 criteria must keep base acceptance criteria required unless stricter phase 2 criteria conflict"
);
assert.match(
phase2,
/- \[x\] \*\*Passed:\*\* Existing `acceptance_criteria\.md` remains required unless it conflicts with this stricter release document; this document wins in conflicts\./,
"phase 2 cross-document requirement must be marked passed only when this guard is wired"
);
for (const [source, name] of [
[base, "acceptance_criteria.md"],
[phase2, "acceptance_criteria_phase2.md"],
]) {
assertEveryCriterionHasStatus(source, name);
assertNoOpenCriteria(source, name);
}
for (const file of ["MVP.md", "acceptance_criteria.md", "acceptance_criteria_phase2.md"]) {
assert(
docsSmoke.includes(`"${file}"`),
`docs smoke must include ${file} as user-facing acceptance context`
);
}
assert(
releaseBlockerSmoke.includes('const phase2 = read("acceptance_criteria_phase2.md")'),
"release-blocker smoke must read phase 2 acceptance criteria"
);
assert(
releaseBlockerSmoke.includes('const base = read("acceptance_criteria.md")'),
"release-blocker smoke must read base acceptance criteria"
);
for (const [source, name] of [
[base, "acceptance_criteria.md"],
[phase2, "acceptance_criteria_phase2.md"],
]) {
for (const [label, pattern] of [
["MVP selected locals", /selected (?:top-level )?locals|selected real source locals/],
["MVP task args", /task arguments|task args/],
["MVP handle inspection", /Artifact.*SourceSnapshot.*Blob|Disasmer handles/],
["MVP stdout stderr", /stdout\/stderr/],
["MVP unavailable locals", /cannot be inspected|unavailable-local/],
["MVP required DAP surface", /initialize[\s\S]*launch.*attach[\s\S]*setBreakpoints[\s\S]*configurationDone[\s\S]*threads[\s\S]*stackTrace[\s\S]*scopes[\s\S]*variables[\s\S]*continue[\s\S]*pause/],
]) {
assert.match(source, pattern, `${name} must include MVP debugging criterion: ${label}`);
}
}
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["private acceptance", privateAcceptance],
["public split", publicSplit],
]) {
assert(
script.includes("node scripts/acceptance-doc-contract-smoke.js"),
`${scriptName} must run acceptance-doc-contract-smoke.js`
);
}
console.log("Acceptance doc contract smoke passed");

View file

@ -0,0 +1,248 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(relativePath) {
const absolute = path.join(repo, relativePath);
if (!fs.existsSync(absolute)) return null;
return fs.readFileSync(absolute, "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing acceptance environment evidence: ${name}`);
}
function expectIncludes(source, name, text) {
assert(source.includes(text), `missing acceptance environment evidence: ${name}`);
}
const publicAcceptance = read("scripts/acceptance-public.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const acceptanceReport = read("scripts/acceptance-report.js");
const acceptanceReportSmoke = read("scripts/acceptance-report-smoke.js");
const readme = read("README.md");
const windowsWorkflow = read(".forgejo/workflows/windows-validation.yml");
const publicDryrunServiceSmoke = maybeRead("private/hosted-policy/scripts/public-release-dryrun-service-smoke.js");
const publicDryrunDeployPrep = maybeRead("private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js");
const publicDryrunSystemd = maybeRead("private/hosted-policy/deploy/disasmer-public-release-dryrun.service");
const publicDryrunRunbook = maybeRead("private/hosted-policy/deploy/README.md");
const publicOperatorCompatSmoke = maybeRead("private/hosted-policy/scripts/public-operator-compat-smoke.js");
const hostedService = maybeRead("private/hosted-policy/src/bin/disasmer-hosted-service.rs");
const publicDryrunE2e = read("scripts/public-release-dryrun-e2e.js");
const finalDryrunEvidence = read("scripts/public-release-dryrun-final-evidence.js");
for (const [name, script] of [
["public acceptance", publicAcceptance],
["private acceptance", privateAcceptance],
]) {
expect(script, `${name} writes acceptance environment report first`, /node scripts\/acceptance-report\.js (public|private)[\s\S]*node scripts\/acceptance-report-smoke\.js/);
expectIncludes(
script,
`${name} runs acceptance environment contract`,
"node scripts/acceptance-environment-contract-smoke.js"
);
}
for (const [name, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
for (const smoke of [
"scripts/local-services-smoke.js",
"scripts/node-attach-smoke.js",
"scripts/cli-local-run-smoke.js",
"scripts/vscode-f5-smoke.js",
"scripts/dap-smoke.js",
"scripts/artifact-download-smoke.js",
"scripts/artifact-export-smoke.js",
"scripts/public-local-demo-matrix-smoke.js",
]) {
assert(script.includes(`node ${smoke}`), `${name} must run ${smoke}`);
}
}
expectIncludes(publicAcceptance, "public gate runs rootless Podman backend smoke", "node scripts/podman-backend-smoke.js");
expectIncludes(publicAcceptance, "public gate runs Wasmtime node smoke", "node scripts/wasmtime-node-smoke.js");
expectIncludes(privateAcceptance, "private gate runs hosted deployment smoke", "node private/hosted-policy/scripts/hosted-deployment-smoke.js");
expectIncludes(privateAcceptance, "private gate prepares public dry-run deployment bundle", "node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js");
expectIncludes(privateAcceptance, "private gate runs hosted community smoke", "node private/hosted-policy/scripts/hosted-community-smoke.js");
expectIncludes(privateAcceptance, "private gate runs public operator compatibility smoke", "node private/hosted-policy/scripts/public-operator-compat-smoke.js");
expectIncludes(privateAcceptance, "private gate runs standalone public coordinator smoke", "node scripts/self-hosted-coordinator-smoke.js");
expectIncludes(privateAcceptance, "private gate runs Postgres durable smoke", "node private/hosted-policy/scripts/postgres-durable-smoke.js");
expectIncludes(privateAcceptance, "private gate can run public release dry-run service smoke", "node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js");
expectIncludes(privateAcceptance, "public release dry-run service smoke is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR");
expectIncludes(privateAcceptance, "private gate runs hosted policy cargo tests", "cargo test --manifest-path private/hosted-policy/Cargo.toml");
expectIncludes(publicAcceptance, "public gate can run final dry-run evidence verifier", "node scripts/public-release-dryrun-final-evidence.js");
expectIncludes(publicAcceptance, "public gate can run public release dry-run e2e", "node scripts/public-release-dryrun-e2e.js");
expectIncludes(publicAcceptance, "public release dry-run e2e is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_E2E");
expectIncludes(privateAcceptance, "private gate can run final dry-run evidence verifier", "node scripts/public-release-dryrun-final-evidence.js");
expectIncludes(publicAcceptance, "public final dry-run verifier is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL");
expectIncludes(privateAcceptance, "private final dry-run verifier is env gated", "DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL");
expect(publicSplit, "public split excludes private modules", /--exclude='\.\/private'/);
expect(publicSplit, "public split excludes experiments", /--exclude='\.\/experiments'/);
expect(publicSplit, "public split tests copied workspace", /cargo test --workspace --manifest-path "\$tmp_dir\/Cargo\.toml"/);
expect(publicSplit, "public split builds copied workspace binaries", /cargo build --workspace --bins --manifest-path "\$tmp_dir\/Cargo\.toml"/);
expectIncludes(
publicSplit,
"public split runs acceptance environment contract from copied tree",
'(cd "$tmp_dir" && node scripts/acceptance-environment-contract-smoke.js)'
);
for (const [name, pattern] of [
["commit SHA fallback", /process\.env\.DISASMER_ACCEPTANCE_COMMIT \|\| commandOutput\("git", \["rev-parse", "HEAD"\]\)/],
["tree status", /tree_status: \(commandOutput\("git", \["status", "--short"\]\) \|\| ""\)[\s\S]*\.filter\(Boolean\)/],
["OS report", /platform: os\.platform\(\)[\s\S]*kernel: os\.release\(\)/],
["Rust report", /rustc: commandOutput\("rustc", \["--version"\]\)/],
["Node report", /version: process\.version/],
["Podman report", /function podmanReport\(\)/],
["Postgres report", /postgres: \{[\s\S]*commandOutput\("postgres", \["--version"\]\) \|\|[\s\S]*commandOutput\("psql", \["--version"\]\)/],
["browser harness report", /browser_harness:/],
["VS Code harness report", /vscode_harness:/],
["Windows validation report", /windows_validation: process\.env\.DISASMER_WINDOWS_VALIDATION \|\| "not-run"/],
]) {
expect(acceptanceReport, name, pattern);
}
for (const [name, pattern] of [
["acceptance report validates Podman incomplete state", /assertPodmanReport/],
["acceptance report validates Windows not-run", /assertReport\(runReport\(mode\), mode, "not-run"\)/],
["acceptance report validates Windows runner mode", /DISASMER_WINDOWS_VALIDATION: "forgejo-windows-runner"/],
]) {
expect(acceptanceReportSmoke, name, pattern);
}
expect(readme, "README documents public acceptance script", /scripts\/acceptance-public\.sh/);
expect(readme, "README documents private acceptance script", /scripts\/acceptance-private\.sh/);
expect(readme, "README documents rootless Podman incomplete handling", /Podman backend behavior is marked `incomplete`/);
expect(readme, "README documents Postgres discovery in environment report", /Podman\/Postgres discovery/);
expect(readme, "README documents manual Windows validation", /manual `Windows validation`\s+workflow/);
expect(windowsWorkflow, "Windows workflow is manual", /workflow_dispatch/);
expect(windowsWorkflow, "Windows workflow uses intermittent Windows runner", /runs-on:\s*windows/);
expect(windowsWorkflow, "Windows workflow writes acceptance report", /node scripts\/acceptance-report\.js windows/);
if (publicDryrunServiceSmoke && publicDryrunDeployPrep && publicDryrunSystemd && publicDryrunRunbook) {
for (const [name, pattern] of [
["service smoke requires external service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR is required/],
["service smoke requires OIDC test issuer", /DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL/],
["service smoke loads release manifest", /public-release-manifest\.json/],
["service smoke rejects stale release manifest", /manifest\.source_commit[\s\S]*expectedCommit/],
["service smoke records source commit", /source_commit: release\.sourceCommit/],
["service smoke records release name", /release_name: release\.releaseName/],
["service smoke connects through public domain", /addr\.host[\s\S]*serviceHost/],
["service smoke verifies DNS state", /\["not-published", "published"\]\.includes\(dnsPublicationState\)/],
["service smoke performs OIDC login", /type: "oidc_browser_login"/],
["service smoke creates project", /type: "create_project"/],
["service smoke enrolls node", /type: "create_node_enrollment_token"[\s\S]*exchange_node_enrollment_token/],
["service smoke starts user node process", /type: "start_user_node_process"/],
["service smoke records task", /type: "record_user_node_task_completion"/],
["service smoke reads debug state", /type: "debug_process"/],
["service smoke reads artifact metadata", /type: "artifact_metadata"/],
["service smoke creates download link", /type: "create_artifact_download_link"/],
["service smoke records observability", /type: "observability_snapshot"/],
["service smoke records private hosted coordinator", /operator_implementation:[\s\S]*"private-hosted-coordinator"/],
["service smoke writes evidence report", /public-release-dryrun-service\.json/],
]) {
expect(publicDryrunServiceSmoke, name, pattern);
}
for (const [name, source, pattern] of [
["deployment prep builds hosted service release", publicDryrunDeployPrep, /cargo"[\s\S]*"build"[\s\S]*"--release"[\s\S]*"private\/hosted-policy\/Cargo\.toml"[\s\S]*"disasmer-hosted-service"/],
["deployment prep stages systemd unit", publicDryrunDeployPrep, /disasmer-public-release-dryrun\.service/],
["deployment prep writes manifest", publicDryrunDeployPrep, /deployment-manifest\.json/],
["deployment prep records private hosted coordinator", publicDryrunDeployPrep, /operator_implementation:[\s\S]*"private-hosted-coordinator"/],
["deployment prep records service address", publicDryrunDeployPrep, /service_addr:[\s\S]*`\$\{serviceHost\}:\$\{servicePort\}`/],
["deployment prep records DNS state", publicDryrunDeployPrep, /dns_publication_state: dnsPublicationState/],
["deployment prep records service smoke command", publicDryrunDeployPrep, /public-release-dryrun-service-smoke\.js/],
["systemd binds public TCP 9443", publicDryrunSystemd, /--listen 0\.0\.0\.0:9443/],
["systemd avoids privileged port capability", publicDryrunSystemd, /NoNewPrivileges=true/],
["systemd uses dedicated user", publicDryrunSystemd, /User=disasmer[\s\S]*Group=disasmer/],
["runbook says externally reachable", publicDryrunRunbook, /externally reachable host/],
["runbook documents DNS pending fallback", publicDryrunRunbook, /Until the `disasmer\.michelpaulissen\.com` DNS record is deployed/],
["runbook gives hosts entry", publicDryrunRunbook, /<deployment-ip> disasmer\.michelpaulissen\.com/],
]) {
expect(source, name, pattern);
}
}
if (publicOperatorCompatSmoke && hostedService) {
for (const [name, pattern] of [
["hosted service embeds private coordinator runtime", /private_coordinator: CoordinatorService/],
["hosted service parses public client protocol requests", /serde_json::from_str::<CoordinatorRequest>/],
["hosted service delegates public requests", /handle_public_request/],
["hosted service marks public protocol sessions", /ConnectionProtocol::Public/],
["hosted service seeds public projects from hosted auth", /seed_public_project/],
["hosted service seeds public enrollment grants", /seed_public_node_enrollment_grant/],
]) {
expect(hostedService, name, pattern);
}
for (const [name, pattern] of [
["compat smoke starts hosted service", /disasmer-hosted-service/],
["compat smoke creates hosted project", /type: "create_project"/],
["compat smoke creates hosted enrollment grant", /type: "create_node_enrollment_token"/],
["compat smoke runs public CLI attach", /"disasmer-cli"[\s\S]*"node"[\s\S]*"attach"/],
["compat smoke verifies public enrollment exchange", /node_enrollment_exchanged/],
["compat smoke runs public node runtime", /"disasmer-node"/],
["compat smoke launches through coordinator assignment", /type: "launch_task"/],
["compat smoke verifies assignment polling", /poll_task_assignment/],
["compat smoke verifies public node metadata", /vfs_metadata_recorded/],
["compat smoke writes evidence report", /public-operator-compat\.json/],
]) {
expect(publicOperatorCompatSmoke, name, pattern);
}
}
for (const [name, pattern] of [
["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E/],
["e2e runner requires public domain service address", /serviceAddr[\s\S]*serviceHost/],
["e2e runner downloads release assets", /downloadReleaseAssets/],
["e2e runner verifies release checksums", /verifyChecksums/],
["e2e runner clones public repo", /git"[\s\S]*"clone"[\s\S]*publicRepositoryUrl/],
["e2e runner uses default operator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/],
["e2e runner completes browser login", /--complete-browser-code/],
["e2e runner attaches user node", /node"[\s\S]*"attach"/],
["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/],
["e2e runner launches through coordinator assignment", /type: "launch_task"/],
["e2e runner verifies public assignment polling", /worker_assignment_poll_protocol/],
["e2e runner validates standalone public coordinator", /validateStandalonePublicCoordinator/],
["e2e runner records standalone public coordinator", /public_coordinator_operator_implementation/],
["e2e runner verifies task events", /list_task_events/],
["e2e runner creates artifact download link", /create_artifact_download_link/],
["e2e runner verifies VS Code debugger", /vscode-f5-smoke\.js/],
["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/],
]) {
expect(publicDryrunE2e, name, pattern);
}
for (const [name, pattern] of [
["final verifier requires public release manifest", /public-release-manifest\.json/],
["final verifier requires Forgejo release evidence", /public-release-dryrun-forgejo-release\.json/],
["final verifier requires deployment manifest", /deployment-manifest\.json/],
["final verifier requires service smoke evidence", /public-release-dryrun-service\.json/],
["final verifier requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/],
["final verifier requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/],
["final verifier requires public operator compatibility evidence", /public-operator-compat\.json/],
["final verifier requires current public operator source", /compat\.source_commit[\s\S]*manifest\.source_commit/],
["final verifier requires current public operator release", /compat\.release_name[\s\S]*manifest\.release_name/],
["final verifier requires public coordinator compatibility evidence", /public-coordinator-compat\.json/],
["final verifier requires current public coordinator source", /publicCoordinator\.source_commit[\s\S]*manifest\.source_commit/],
["final verifier requires current public coordinator release", /publicCoordinator\.release_name[\s\S]*manifest\.release_name/],
["final verifier requires public e2e evidence", /public-release-dryrun-e2e\.json/],
["final verifier records both coordinator validations", /coordinator_validation/],
["final verifier writes final evidence", /public-release-dryrun-final\.json/],
]) {
expect(finalDryrunEvidence, name, pattern);
}
console.log("Acceptance environment contract smoke passed");

View file

@ -0,0 +1,236 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing acceptance evidence guard: ${name}`);
}
function expectGate(script, gateName) {
assert(
script.includes("node scripts/acceptance-evidence-contract-smoke.js"),
`${gateName} must run acceptance-evidence-contract-smoke.js`
);
}
const phase2 = read("acceptance_criteria_phase2.md");
const publicAcceptance = read("scripts/acceptance-public.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const localServicesSmoke = read("scripts/local-services-smoke.js");
const nodeAttachSmoke = read("scripts/node-attach-smoke.js");
const cliLocalRunSmoke = read("scripts/cli-local-run-smoke.js");
const vscodeF5Smoke = read("scripts/vscode-f5-smoke.js");
const dapSmoke = read("scripts/dap-smoke.js");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const artifactExportSmoke = read("scripts/artifact-export-smoke.js");
const selfHostedCoordinatorSmoke = read("scripts/self-hosted-coordinator-smoke.js");
const publicLocalDemoMatrix = read("scripts/public-local-demo-matrix-smoke.js");
const publicOperatorCompatSmoke = fs.existsSync(
path.join(repo, "private/hosted-policy/scripts/public-operator-compat-smoke.js")
)
? read("private/hosted-policy/scripts/public-operator-compat-smoke.js")
: null;
const publicDryrunE2e = read("scripts/public-release-dryrun-e2e.js");
const finalDryrunEvidence = read("scripts/public-release-dryrun-final-evidence.js");
expect(
phase2,
"phase 2 rejects type-only acceptance",
/- \[x\] \*\*Passed:\*\* A criterion cannot be accepted solely because a type, trait, schema, mock, or unit-level model exists\./
);
for (const [gateName, script] of [
["public acceptance", publicAcceptance],
["private acceptance", privateAcceptance],
["public split", publicSplit],
]) {
expectGate(script, gateName);
}
expect(publicAcceptance, "public gate includes unit coverage", /cargo test --workspace/);
expect(publicAcceptance, "public gate includes binary build coverage", /cargo build --workspace --bins/);
expect(privateAcceptance, "private gate includes hosted unit coverage", /cargo test --manifest-path private\/hosted-policy\/Cargo\.toml/);
expect(publicSplit, "public split includes copied-tree unit coverage", /cargo test --workspace --manifest-path "\$tmp_dir\/Cargo\.toml"/);
expect(publicSplit, "public split includes copied-tree binary build coverage", /cargo build --workspace --bins --manifest-path "\$tmp_dir\/Cargo\.toml"/);
for (const [gateName, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
for (const smoke of [
"scripts/local-services-smoke.js",
"scripts/node-attach-smoke.js",
"scripts/cli-local-run-smoke.js",
"scripts/vscode-f5-smoke.js",
"scripts/dap-smoke.js",
"scripts/artifact-download-smoke.js",
"scripts/artifact-export-smoke.js",
"scripts/public-local-demo-matrix-smoke.js",
]) {
assert(script.includes(`node ${smoke}`), `${gateName} must run boundary smoke ${smoke}`);
}
}
for (const smoke of [
"private/hosted-policy/scripts/hosted-deployment-smoke.js",
"private/hosted-policy/scripts/hosted-community-smoke.js",
"private/hosted-policy/scripts/public-operator-compat-smoke.js",
"private/hosted-policy/scripts/postgres-durable-smoke.js",
]) {
assert(
privateAcceptance.includes(`node ${smoke}`),
`private acceptance must run hosted boundary smoke ${smoke}`
);
}
assert(
privateAcceptance.includes("node scripts/self-hosted-coordinator-smoke.js"),
"private acceptance must run standalone public coordinator smoke"
);
for (const [name, source, patterns] of [
[
"local services",
localServicesSmoke,
[/cp\.spawn/, /disasmer-coordinator/, /type: "create_node_enrollment_grant"/, /disasmer-node/],
],
[
"node attach",
nodeAttachSmoke,
[/cp\.spawn/, /"node"[\s\S]*"attach"/, /used_enrollment_exchange/, /runAttachedNodeWork/],
],
[
"CLI local run",
cliLocalRunSmoke,
[/cp\.spawn/, /disasmer[\s\S]*run/, /cli_process_started_node_process[\s\S]*true/],
],
[
"VS Code F5",
vscodeF5Smoke,
[
/runtimeBackend[\s\S]*local-services/,
/coordinator_task_events[\s\S]*value === 1/,
/Source Locals/,
/Task Args and Handles/,
/unavailable-local-diagnostic/,
/TaskHandle/,
/linux_thread/,
/linux_artifact/,
/command_spec/,
/stdout_tail/,
/stderr_tail/,
],
],
[
"DAP",
dapSmoke,
[
/runtimeBackend: "local-services"/,
/threads[\s\S]*compile linux/,
/send\("attach"/,
/Source Locals/,
/return_value/,
/TaskHandle/,
/linux_thread/,
/linux_artifact/,
/vfs_mounts/,
/command_spec/,
/stdout_tail/,
/stderr_tail/,
],
],
[
"artifact download",
artifactDownloadSmoke,
[/create_artifact_download_link/, /open_artifact_download_stream/, /crossTenantOpen/],
],
[
"artifact export",
artifactExportSmoke,
[/export_artifact_to_node/, /node-export-receiver/, /coordinator_bulk_relay_allowed[\s\S]*false/],
],
[
"standalone public coordinator",
selfHostedCoordinatorSmoke,
[/disasmer-coordinator/, /standalone-public-coordinator/, /public-coordinator-compat\.json/],
],
[
"public local demo matrix",
publicLocalDemoMatrix,
[/scripts\/cli-install-smoke\.js/, /scripts\/node-attach-smoke\.js/, /scripts\/vscode-f5-smoke\.js/],
],
]) {
for (const pattern of patterns) {
expect(source, name, pattern);
}
}
if (publicOperatorCompatSmoke) {
for (const pattern of [
/disasmer-hosted-service/,
/"disasmer-cli"[\s\S]*"node"[\s\S]*"attach"/,
/node_enrollment_exchanged/,
/"disasmer-node"/,
/node_capabilities_recorded/,
/task_launched/,
/poll_task_assignment/,
/debug_command/,
/task_log_recorded/,
/vfs_metadata_recorded/,
/public-operator-compat\.json/,
]) {
expect(publicOperatorCompatSmoke, "public operator compatibility", pattern);
}
}
for (const pattern of [
/DISASMER_PUBLIC_RELEASE_DRYRUN_E2E/,
/downloadReleaseAssets/,
/verifyChecksums/,
/git"[\s\S]*"clone"/,
/defaultLoginPlan\.coordinator/,
/--complete-browser-code/,
/node_enrollment_exchanged/,
/disasmerNode/,
/launch_task/,
/worker_assignment_poll_verified/,
/validateStandalonePublicCoordinator/,
/public_coordinator_validated/,
/standalone-public-coordinator/,
/task_recorded/,
/list_task_events/,
/create_artifact_download_link/,
/vscode-f5-smoke\.js/,
/downloaded_release_assets: true/,
/vscode_debugger_verified: true/,
/artifact_download_or_export_verified: true/,
/public-release-dryrun-e2e\.json/,
]) {
expect(publicDryrunE2e, "public release e2e evidence", pattern);
}
for (const pattern of [
/public-release-dryrun-forgejo-release\.json/,
/deployment-manifest\.json/,
/public-release-dryrun-service\.json/,
/public-operator-compat\.json/,
/public-coordinator-compat\.json/,
/public-release-dryrun-e2e\.json/,
/coordinator_validation/,
/downloaded_release_assets/,
/vscode_debugger_verified/,
/artifact_download_or_export_verified/,
/public-release-dryrun-final\.json/,
]) {
expect(finalDryrunEvidence, "public release final evidence", pattern);
}
console.log("Acceptance evidence contract smoke passed");

30
scripts/acceptance-private.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo"
node scripts/acceptance-report.js private
node scripts/acceptance-report-smoke.js
node scripts/acceptance-doc-contract-smoke.js
node scripts/acceptance-environment-contract-smoke.js
node scripts/acceptance-evidence-contract-smoke.js
node scripts/public-private-boundary-smoke.js
node scripts/release-blocker-smoke.js
node scripts/resource-metering-contract-smoke.js
node scripts/hostile-input-contract-smoke.js
node scripts/tenant-isolation-contract-smoke.js
scripts/release-source-scan.sh
cargo test --manifest-path private/hosted-policy/Cargo.toml
node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js
node private/hosted-policy/scripts/hosted-deployment-smoke.js
node private/hosted-policy/scripts/hosted-community-smoke.js
node private/hosted-policy/scripts/public-operator-compat-smoke.js
node scripts/self-hosted-coordinator-smoke.js
node private/hosted-policy/scripts/postgres-durable-smoke.js
if [[ -n "${DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR:-}" ]]; then
node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js
fi
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:-}" == "1" ]]; then
node scripts/public-release-dryrun-final-evidence.js
fi

64
scripts/acceptance-public.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo"
node scripts/acceptance-report.js public
node scripts/acceptance-report-smoke.js
node scripts/acceptance-doc-contract-smoke.js
node scripts/acceptance-environment-contract-smoke.js
node scripts/acceptance-evidence-contract-smoke.js
node scripts/public-private-boundary-smoke.js
node scripts/release-blocker-smoke.js
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
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
node scripts/prepare-public-release-dryrun.js
if [[ "${DISASMER_PUBLIC_RELEASE_PREFLIGHT:-}" == "1" ]]; then
node scripts/public-release-dryrun-preflight.js
fi
if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]; then
node scripts/publish-public-release-dryrun.js
fi
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_E2E:-}" == "1" ]]; then
node scripts/public-release-dryrun-e2e.js
fi
if [[ "${DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:-}" == "1" ]]; then
node scripts/public-release-dryrun-final-evidence.js
fi
cargo fmt --all --check
cargo test --workspace
cargo build --workspace --bins
node scripts/docs-smoke.js
node scripts/cli-login-smoke.js
node scripts/cli-browser-login-flow-smoke.js
node scripts/cli-install-smoke.js
node scripts/user-session-token-boundary-smoke.js
node scripts/sdk-spawn-runtime-smoke.js
node scripts/node-lifecycle-contract-smoke.js
node scripts/wasmtime-node-smoke.js
node scripts/podman-backend-smoke.js
node scripts/vscode-extension-smoke.js
node scripts/vscode-f5-smoke.js
node scripts/node-attach-smoke.js
node scripts/local-services-smoke.js
node scripts/cancellation-smoke.js
node scripts/cli-local-run-smoke.js
node scripts/artifact-download-smoke.js
node scripts/artifact-export-smoke.js
node scripts/operator-panel-smoke.js
node scripts/source-preparation-smoke.js
node scripts/scheduler-placement-smoke.js
node scripts/windows-best-effort-smoke.js
node scripts/windows-validation-contract-smoke.js
node scripts/quic-smoke.js
node scripts/dap-smoke.js
node scripts/flagship-demo-smoke.js
scripts/verify-public-split.sh

View file

@ -0,0 +1,113 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function runReport(mode, env = {}) {
const output = cp.execFileSync(
"node",
["scripts/acceptance-report.js", mode],
{
cwd: repo,
encoding: "utf8",
env: { ...process.env, ...env },
}
);
const report = JSON.parse(output);
const persistedPath = path.join(
repo,
"target",
"acceptance",
`${mode}-environment.json`
);
const persisted = JSON.parse(fs.readFileSync(persistedPath, "utf8"));
assert.deepStrictEqual(persisted, report, `${mode} report was not persisted`);
return report;
}
function assertString(value, name) {
assert.strictEqual(typeof value, "string", `${name} must be a string`);
assert(value.length > 0, `${name} must not be empty`);
}
function assertNullableString(value, name) {
if (value === null) return;
assertString(value, name);
}
function assertPodmanReport(podman) {
assert(podman && typeof podman === "object", "podman report must be an object");
assert(
["available", "incomplete"].includes(podman.status),
"podman.status must be available or incomplete"
);
assertNullableString(podman.version, "podman.version");
assertNullableString(podman.rootless, "podman.rootless");
assertNullableString(podman.incomplete_reason, "podman.incomplete_reason");
if (podman.status === "available") {
assertString(podman.version, "podman.version");
assert.strictEqual(podman.rootless, "true");
assert.strictEqual(podman.incomplete_reason, null);
} else {
assertString(podman.incomplete_reason, "podman.incomplete_reason");
}
}
function assertReport(report, mode, expectedWindowsValidation) {
assert.strictEqual(report.kind, "disasmer_acceptance_environment");
assert.strictEqual(report.mode, mode);
assert.match(report.generated_at, /^\d{4}-\d{2}-\d{2}T/);
assert.match(report.commit, /^[0-9a-f]{40}$/);
assert(Array.isArray(report.tree_status), "tree_status must be an array");
assertString(report.os.platform, "os.platform");
assertString(report.os.release, "os.release");
assertString(report.os.kernel, "os.kernel");
assertString(report.os.arch, "os.arch");
assertString(report.rust.rustc, "rust.rustc");
assertString(report.rust.cargo, "rust.cargo");
assertString(report.node.version, "node.version");
assert(report.node.version.startsWith("v"), "node.version must be Node.js style");
assertPodmanReport(report.podman);
assertNullableString(report.postgres.version, "postgres.version");
assertNullableString(report.browser_harness.version, "browser_harness.version");
assertNullableString(report.browser_harness.command, "browser_harness.command");
assertString(report.browser_harness.configured, "browser_harness.configured");
assert(Array.isArray(report.vscode_harness.smokes), "vscode smokes must be listed");
assert(
report.vscode_harness.smokes.includes("scripts/vscode-extension-smoke.js"),
"VS Code extension smoke must be recorded"
);
assert(
report.vscode_harness.smokes.includes("scripts/vscode-f5-smoke.js"),
"VS Code F5 smoke must be recorded"
);
assertString(report.vscode_harness.engine, "vscode_harness.engine");
assertString(
report.vscode_harness.extension_version,
"vscode_harness.extension_version"
);
assert.strictEqual(report.windows_validation, expectedWindowsValidation);
}
for (const mode of ["public", "private"]) {
assertReport(runReport(mode), mode, "not-run");
}
assertReport(
runReport("windows", {
DISASMER_WINDOWS_VALIDATION: "forgejo-windows-runner",
}),
"windows",
"forgejo-windows-runner"
);
console.log("Acceptance report smoke passed");

139
scripts/acceptance-report.js Executable file
View file

@ -0,0 +1,139 @@
#!/usr/bin/env node
const cp = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const mode = process.argv[2] || "public";
function commandOutput(command, args = []) {
try {
return cp
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"]
})
.trim();
} catch (_) {
return null;
}
}
function packageJson() {
return JSON.parse(
fs.readFileSync(path.join(repo, "vscode-extension/package.json"), "utf8")
);
}
function firstCommandOutput(candidates) {
for (const [command, args] of candidates) {
const output = commandOutput(command, args);
if (output) {
return { command, version: output };
}
}
return null;
}
function podmanReport() {
const version = commandOutput("podman", ["--version"]);
if (!version) {
return {
status: "incomplete",
version: null,
rootless: null,
incomplete_reason: "podman command is unavailable"
};
}
const rootless = commandOutput("podman", [
"info",
"--format",
"{{.Host.Security.Rootless}}"
]);
if (!rootless) {
return {
status: "incomplete",
version,
rootless: null,
incomplete_reason: "podman info did not report rootless status"
};
}
if (rootless !== "true") {
return {
status: "incomplete",
version,
rootless,
incomplete_reason: "podman is not running in rootless mode"
};
}
return {
status: "available",
version,
rootless,
incomplete_reason: null
};
}
const extensionPackage = packageJson();
const sourceCommit =
process.env.DISASMER_ACCEPTANCE_COMMIT || commandOutput("git", ["rev-parse", "HEAD"]);
const browserVersion = firstCommandOutput([
["chromium", ["--version"]],
["chromium-browser", ["--version"]],
["google-chrome", ["--version"]],
["firefox", ["--version"]]
]);
const report = {
kind: "disasmer_acceptance_environment",
mode,
commit: sourceCommit,
tree_status: (commandOutput("git", ["status", "--short"]) || "")
.split("\n")
.filter(Boolean),
generated_at: new Date().toISOString(),
os: {
platform: os.platform(),
release: os.release(),
kernel: os.release(),
arch: os.arch()
},
rust: {
rustc: commandOutput("rustc", ["--version"]),
cargo: commandOutput("cargo", ["--version"])
},
node: {
version: process.version
},
podman: podmanReport(),
postgres: {
version:
commandOutput("postgres", ["--version"]) ||
commandOutput("psql", ["--version"])
},
browser_harness: {
version: browserVersion && browserVersion.version,
command: browserVersion && browserVersion.command,
configured: process.env.DISASMER_BROWSER_HARNESS || "not-configured"
},
vscode_harness: {
smokes: [
"scripts/vscode-extension-smoke.js",
"scripts/vscode-f5-smoke.js"
],
engine: extensionPackage.engines && extensionPackage.engines.vscode,
extension_version: extensionPackage.version
},
windows_validation: process.env.DISASMER_WINDOWS_VALIDATION || "not-run"
};
const outDir = path.join(repo, "target/acceptance");
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, `${mode}-environment.json`),
`${JSON.stringify(report, null, 2)}\n`
);
console.log(JSON.stringify(report, null, 2));

View file

@ -0,0 +1,414 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runNode(addr) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-download",
"--process",
"vp-download",
"--task",
"compile-linux",
"--project",
project,
"--artifact",
"/vfs/artifacts/download-output.txt",
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`node process failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
} catch (error) {
reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
function downloadNodeCapabilities() {
return {
os: "Linux",
arch: "x86_64",
capabilities: ["Command", "VfsArtifacts"],
environment_backends: [],
source_providers: ["filesystem"],
};
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0",
],
{ cwd: repo }
);
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const report = await runNode(addr);
assert.strictEqual(report.node_status, "completed");
assert.strictEqual(report.status_code, 0);
assert.strictEqual(report.large_bytes_uploaded, false);
assert.strictEqual(report.staged_artifact.path, "/vfs/artifacts/download-output.txt");
const disconnectedReport = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "node-download",
capabilities: downloadNodeCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: false,
online: true,
});
assert.strictEqual(disconnectedReport.type, "node_capabilities_recorded");
const disconnectedLink = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "disconnected",
now_epoch_seconds: 10,
ttl_seconds: 60,
});
assert.strictEqual(disconnectedLink.type, "error");
assert.match(disconnectedLink.message, /direct connectivity unavailable/);
const connectedReport = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "node-download",
capabilities: downloadNodeCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: true,
online: true,
});
assert.strictEqual(connectedReport.type, "node_capabilities_recorded");
const link = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
now_epoch_seconds: 10,
ttl_seconds: 60,
});
assert.strictEqual(link.type, "artifact_download_link");
assert.strictEqual(link.link.tenant, "tenant");
assert.strictEqual(link.link.project, "project");
assert.strictEqual(link.link.process, "vp-download");
assert.deepStrictEqual(link.link.actor, { User: "user" });
assert.match(link.link.policy_context_digest, /^sha256:[0-9a-f]{64}$/);
assert.strictEqual(link.link.expires_at_epoch_seconds, 70);
assert.match(link.link.url_path, /\/artifacts\/tenant\/project\/vp-download\/download-output\.txt$/);
assert.deepStrictEqual(link.link.source, { RetainedNode: "node-download" });
const crossTenant = await send(addr, {
type: "create_artifact_download_link",
tenant: "other",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
now_epoch_seconds: 10,
ttl_seconds: 60,
});
assert.strictEqual(crossTenant.type, "error");
assert.match(crossTenant.message, /tenant mismatch/);
const crossProject = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "other-project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
now_epoch_seconds: 10,
ttl_seconds: 60,
});
assert.strictEqual(crossProject.type, "error");
assert.match(crossProject.message, /project mismatch/);
const crossTenantOpen = await send(addr, {
type: "open_artifact_download_stream",
tenant: "other",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 1,
});
assert.strictEqual(crossTenantOpen.type, "error");
assert.match(crossTenantOpen.message, /tenant mismatch/);
const crossProjectOpen = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "other-project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 1,
});
assert.strictEqual(crossProjectOpen.type, "error");
assert.match(crossProjectOpen.message, /project mismatch/);
const guessed = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: "sha256:guessed",
now_epoch_seconds: 11,
chunk_bytes: 1,
});
assert.strictEqual(guessed.type, "error");
assert.match(guessed.message, /token is invalid/);
const crossActorOpen = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "other-user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 1,
});
assert.strictEqual(crossActorOpen.type, "error");
assert.match(crossActorOpen.message, /token is invalid/);
const expired = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 71,
chunk_bytes: 1,
});
assert.strictEqual(expired.type, "error");
assert.match(expired.message, /expired/);
await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "node-download",
capabilities: downloadNodeCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: false,
online: true,
});
const disconnectedOpen = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 1,
});
assert.strictEqual(disconnectedOpen.type, "error");
assert.match(disconnectedOpen.message, /direct connectivity unavailable/);
await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "node-download",
capabilities: downloadNodeCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: true,
online: true,
});
const stream = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 16,
});
assert.strictEqual(stream.type, "artifact_download_stream");
assert.strictEqual(stream.streamed_bytes, 16);
assert.strictEqual(stream.charged_download_bytes, 16);
assert.strictEqual(stream.link.artifact, "download-output.txt");
const crossActorRevoke = await send(addr, {
type: "revoke_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "other-user",
artifact: "download-output.txt",
token_digest: link.link.scoped_token_digest,
});
assert.strictEqual(crossActorRevoke.type, "error");
assert.match(crossActorRevoke.message, /token is invalid/);
const revoked = await send(addr, {
type: "revoke_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
token_digest: link.link.scoped_token_digest,
});
assert.strictEqual(revoked.type, "artifact_download_link_revoked");
assert.strictEqual(revoked.link.scoped_token_digest, link.link.scoped_token_digest);
const revokedOpen = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "download-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "nonce",
token_digest: link.link.scoped_token_digest,
now_epoch_seconds: 12,
chunk_bytes: 1,
});
assert.strictEqual(revokedOpen.type, "error");
assert.match(revokedOpen.message, /revoked/);
} finally {
coordinator.kill("SIGTERM");
}
console.log("Artifact download smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,244 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runProducerNode(addr) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-export-source",
"--process",
"vp-export",
"--task",
"compile-linux",
"--project",
project,
"--artifact",
"/vfs/artifacts/export-output.txt",
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`producer node failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
} catch (error) {
reject(new Error(`producer node output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
function nodeCapabilities() {
return {
os: "Linux",
arch: "x86_64",
capabilities: ["Command", "VfsArtifacts"],
environment_backends: [],
source_providers: ["filesystem"],
};
}
async function reportNode(addr, node, { directConnectivity = true, online = true } = {}) {
const response = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node,
capabilities: nodeCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: directConnectivity,
online,
});
assert.strictEqual(response.type, "node_capabilities_recorded");
assert.strictEqual(response.node, node);
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0",
],
{ cwd: repo }
);
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const produced = await runProducerNode(addr);
assert.strictEqual(produced.node_status, "completed");
assert.strictEqual(produced.coordinator_response.type, "task_recorded");
assert.strictEqual(produced.staged_artifact.path, "/vfs/artifacts/export-output.txt");
await reportNode(addr, "node-export-source");
const attachedReceiver = await send(addr, {
type: "attach_node",
tenant: "tenant",
project: "project",
node: "node-export-receiver",
public_key: "node-export-receiver-public-key",
});
assert.strictEqual(attachedReceiver.type, "node_attached");
await reportNode(addr, "node-export-receiver");
const exportPlan = await send(addr, {
type: "export_artifact_to_node",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "export-output.txt",
receiver_node: "node-export-receiver",
direct_connectivity: true,
failure_reason: "",
});
assert.strictEqual(exportPlan.type, "artifact_export_plan");
assert.strictEqual(exportPlan.source_node, "node-export-source");
assert.strictEqual(exportPlan.receiver_node, "node-export-receiver");
assert.strictEqual(exportPlan.plan.transport, "NativeQuic");
assert.strictEqual(exportPlan.plan.scope.tenant, "tenant");
assert.strictEqual(exportPlan.plan.scope.project, "project");
assert.strictEqual(exportPlan.plan.scope.process, "vp-export");
assert.deepStrictEqual(exportPlan.plan.scope.object, { Artifact: "export-output.txt" });
assert.strictEqual(exportPlan.plan.source.node, "node-export-source");
assert.strictEqual(exportPlan.plan.destination.node, "node-export-receiver");
assert.strictEqual(exportPlan.plan.coordinator_assisted_rendezvous, true);
assert.strictEqual(exportPlan.plan.coordinator_bulk_relay_allowed, false);
assert.match(exportPlan.plan.authorization_digest, /^sha256:[0-9a-f]{64}$/);
const crossTenant = await send(addr, {
type: "export_artifact_to_node",
tenant: "other",
project: "project",
actor_user: "user",
artifact: "export-output.txt",
receiver_node: "node-export-receiver",
direct_connectivity: true,
failure_reason: "",
});
assert.strictEqual(crossTenant.type, "error");
assert.match(crossTenant.message, /tenant mismatch/);
const failedDirect = await send(addr, {
type: "export_artifact_to_node",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "export-output.txt",
receiver_node: "node-export-receiver",
direct_connectivity: false,
failure_reason: "nat traversal failed",
});
assert.strictEqual(failedDirect.type, "error");
assert.match(failedDirect.message, /nat traversal failed/);
assert.match(failedDirect.message, /coordinator bulk relay is disabled/);
await reportNode(addr, "node-export-receiver", { online: false });
const offlineReceiver = await send(addr, {
type: "export_artifact_to_node",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "export-output.txt",
receiver_node: "node-export-receiver",
direct_connectivity: true,
failure_reason: "",
});
assert.strictEqual(offlineReceiver.type, "error");
assert.match(offlineReceiver.message, /offline/);
} finally {
coordinator.kill("SIGTERM");
}
console.log("Artifact export smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,263 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function spawnNode(addr) {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-cancel",
"--process",
"vp-cancel",
"--task",
"compile-linux",
"--artifact",
"/vfs/artifacts/cancelled-output.txt",
"--emit-ready",
"--control-poll-ms",
"5000",
],
{ cwd: repo }
);
let buffer = "";
let rawStdout = "";
let stderr = "";
let exitCode = null;
let exitSignal = null;
const messages = [];
const waiters = [];
child.stdout.on("data", (chunk) => {
rawStdout += chunk.toString();
buffer += chunk.toString();
while (true) {
const newline = buffer.indexOf("\n");
if (newline < 0) return;
const line = buffer.slice(0, newline).trim();
buffer = buffer.slice(newline + 1);
if (!line) continue;
let message;
try {
message = JSON.parse(line);
} catch (error) {
rejectWaiters(new Error(`node emitted non-JSON line: ${line}\n${stderr}`));
return;
}
messages.push(message);
flush();
}
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code, signal) => {
exitCode = code;
exitSignal = signal;
flush();
rejectWaiters(
new Error(
`node process exited before expected message with code ${code} signal ${signal}\nstdout:\n${rawStdout}\nstderr:\n${stderr}`
)
);
});
function flush() {
for (const waiter of [...waiters]) {
const message = messages.find(waiter.predicate);
if (!message) continue;
clearTimeout(waiter.timer);
waiters.splice(waiters.indexOf(waiter), 1);
waiter.resolve(message);
}
}
function waitForMessage(predicate, timeoutMs = 120000) {
const existing = messages.find(predicate);
if (existing) return Promise.resolve(existing);
if (exitCode !== null) {
return Promise.reject(
new Error(
`node process already exited with code ${exitCode} signal ${exitSignal}\nstdout:\n${rawStdout}\nstderr:\n${stderr}`
)
);
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`timed out waiting for node message\n${stderr}`));
}, timeoutMs);
waiters.push({ predicate, resolve, reject, timer });
});
}
function rejectWaiters(error) {
for (const waiter of [...waiters]) {
clearTimeout(waiter.timer);
waiters.splice(waiters.indexOf(waiter), 1);
waiter.reject(error);
}
}
function waitForExit() {
if (child.exitCode !== null) {
if (child.exitCode === 0) return Promise.resolve();
return Promise.reject(new Error(`node process failed with code ${child.exitCode}\n${stderr}`));
}
return new Promise((resolve, reject) => {
child.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`node process failed with code ${code}\n${stderr}`));
}
});
});
}
return { child, waitForMessage, waitForExit };
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0",
],
{ cwd: repo }
);
let node;
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
node = spawnNode(addr);
const nodeReady = await node.waitForMessage((message) => message.node_status === "ready");
assert.strictEqual(nodeReady.process, "vp-cancel");
assert.strictEqual(nodeReady.task, "compile-linux");
const cancel = await send(addr, {
type: "cancel_task",
tenant: "tenant",
project: "project",
process: "vp-cancel",
node: "node-cancel",
task: "compile-linux",
});
assert.strictEqual(cancel.type, "task_cancellation_requested");
assert.strictEqual(cancel.process, "vp-cancel");
assert.strictEqual(cancel.task, "compile-linux");
assert.strictEqual(cancel.node, "node-cancel");
const report = await node.waitForMessage((message) => message.node_status === "cancelled");
assert.strictEqual(report.terminal_state, "cancelled");
assert.strictEqual(report.status_code, null);
assert.strictEqual(report.large_bytes_uploaded, false);
assert.strictEqual(report.coordinator_response.type, "task_recorded");
await node.waitForExit();
const events = await send(addr, {
type: "list_task_events",
tenant: "tenant",
project: "project",
actor_user: "user",
process: "vp-cancel"
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "node-cancel");
assert.strictEqual(events.events[0].process, "vp-cancel");
assert.strictEqual(events.events[0].task, "compile-linux");
assert.strictEqual(events.events[0].terminal_state, "cancelled");
assert.strictEqual(events.events[0].status_code, null);
const control = await send(addr, {
type: "poll_task_control",
tenant: "tenant",
project: "project",
process: "vp-cancel",
node: "node-cancel",
task: "compile-linux",
});
assert.strictEqual(control.type, "task_control");
assert.strictEqual(control.cancel_requested, false);
} finally {
if (node && node.child.exitCode === null) node.child.kill("SIGKILL");
coordinator.kill("SIGTERM");
}
console.log("Cancellation smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,206 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const http = require("http");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow");
fs.mkdirSync(tmp, { recursive: true });
function writeOpener() {
const opener = path.join(tmp, "browser-opener.js");
const trace = path.join(tmp, "browser-opener.log");
fs.writeFileSync(
opener,
`#!/usr/bin/env node
const fs = require("fs");
const http = require("http");
const trace = ${JSON.stringify(trace)};
fs.appendFileSync(trace, "started " + process.argv.slice(2).join(" ") + "\\n");
const loginUrl = new URL(process.argv[2]);
const state = loginUrl.searchParams.get("state");
const redirect = new URL(loginUrl.searchParams.get("redirect_uri"));
if (!state || !redirect) {
fs.appendFileSync(trace, "missing state or redirect\\n");
console.error("browser opener did not receive state and redirect_uri");
process.exit(1);
}
redirect.searchParams.set("code", "browser-smoke-code");
redirect.searchParams.set("state", state);
fs.appendFileSync(trace, "callback " + redirect.toString() + "\\n");
http.get(redirect, (response) => {
response.resume();
response.on("end", () => {
fs.appendFileSync(trace, "status " + response.statusCode + "\\n");
process.exit(response.statusCode === 200 ? 0 : 1);
});
}).on("error", (error) => {
fs.appendFileSync(trace, "error " + (error.stack || error.message) + "\\n");
console.error(error.stack || error.message);
process.exit(1);
});
`
);
fs.chmodSync(opener, 0o755);
return opener;
}
function listen(server, host = "127.0.0.1") {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, host, () => {
server.off("error", reject);
resolve(server.address());
});
});
}
async function startCoordinator() {
const requests = [];
const server = net.createServer((socket) => {
let buffered = "";
socket.on("data", (chunk) => {
buffered += chunk.toString("utf8");
if (!buffered.includes("\n")) return;
const [line] = buffered.split(/\r?\n/);
const request = JSON.parse(line);
requests.push(request);
assert.strictEqual(request.type, "oidc_browser_login");
assert.strictEqual(request.authorization_code, "browser-smoke-code");
assert.strictEqual(request.issuer_url, "http://127.0.0.1:1");
assert.strictEqual(request.client_id, "disasmer-smoke");
assert.match(request.redirect_path, /^http:\/\/127\.0\.0\.1:45173\/callback$/);
assert.match(request.state, /^sha256:[a-f0-9]{64}$/);
socket.end(
JSON.stringify({
type: "oidc_browser_session",
session: {
tenant: request.tenant,
project: request.project,
user: request.user,
browser_credential_kind: "BrowserSession",
cli_session_credential_kind: "CliDeviceSession",
provider_tokens_sent_to_nodes: false,
flow: {
authorization_url: "http://127.0.0.1:1/application/o/authorize/",
callback_path: request.redirect_path,
state: request.state,
},
oidc_token_exchange: {
token_endpoint: "http://127.0.0.1:1/application/o/token/",
token_type: "Bearer",
received_access_token: true,
received_id_token: true,
retained_provider_tokens: false,
},
},
}) + "\n"
);
server.close();
});
});
const address = await listen(server);
return {
url: `${address.address}:${address.port}`,
requests,
close: () => new Promise((resolve) => server.close(() => resolve())),
};
}
function runDisasmer(args, env) {
return new Promise((resolve, reject) => {
const child = cp.spawn("cargo", args, {
cwd: repo,
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timeout = setTimeout(() => {
child.kill();
reject(new Error(`disasmer command timed out\n${stderr}`));
}, 30000);
child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.on("close", (code, signal) => {
clearTimeout(timeout);
if (code !== 0) {
reject(
new Error(
`disasmer command failed with ${signal || code}\nSTDERR:\n${stderr}\nSTDOUT:\n${stdout}`
)
);
} else {
resolve(stdout);
}
});
});
}
(async () => {
const opener = writeOpener();
const coordinator = await startCoordinator();
try {
const stdout = await runDisasmer(
[
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"login",
"--browser",
"--json",
"--coordinator",
coordinator.url,
"--oidc-issuer-url",
"http://127.0.0.1:1",
"--oidc-client-id",
"disasmer-smoke",
"--tenant",
"tenant-smoke",
"--project-id",
"project-smoke",
"--user",
"user-smoke",
],
{
...process.env,
DISASMER_BROWSER_OPEN_COMMAND: opener,
DISASMER_BROWSER_LOGIN_TIMEOUT_SECONDS: "5",
}
);
const report = JSON.parse(stdout);
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.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);
} finally {
if (coordinator.requests.length === 0) {
await coordinator.close().catch(() => {});
}
}
console.log("CLI browser login flow smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

58
scripts/cli-install-smoke.js Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const os = require("os");
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 project = path.join(repo, "examples/launch-build-demo");
const binName = process.platform === "win32" ? "disasmer.exe" : "disasmer";
const installedBin = path.join(installRoot, "bin", binName);
try {
cp.execFileSync(
"cargo",
[
"install",
"--path",
"crates/disasmer-cli",
"--bin",
"disasmer",
"--root",
installRoot,
"--debug"
],
{
cwd: repo,
env: {
...process.env,
CARGO_TARGET_DIR: targetDir
},
stdio: "inherit"
}
);
assert(fs.existsSync(installedBin), "installed disasmer binary must exist");
const inspection = JSON.parse(
cp.execFileSync(
installedBin,
["bundle", "inspect", "--project", project],
{ cwd: repo, encoding: "utf8" }
)
);
assert.strictEqual(inspection.project, project);
assert.strictEqual(inspection.metadata.embeds_full_container_images, false);
assert(inspection.metadata.environments.some((env) => env.name === "linux"));
assert(inspection.metadata.selected_inputs.some((input) => input.path === "src/build.rs"));
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
console.log("CLI install smoke passed");

202
scripts/cli-local-run-smoke.js Executable file
View file

@ -0,0 +1,202 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const crypto = require("crypto");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
const agentPublicKey = "agent-cli-smoke-public-key";
function sha256(value) {
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
}
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runCli(args, env = {}) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
{
cwd: repo,
env: {
...process.env,
...env
}
}
);
const cliPid = child.pid;
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`CLI run failed with code ${code}\n${stderr}`));
return;
}
try {
resolve({ pid: cliPid, report: JSON.parse(stdout) });
} catch (error) {
reject(new Error(`CLI output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
assert(Number.isInteger(coordinator.pid));
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const { pid: cliPid, report } = await runCli(
["run", "--coordinator", `${addr.host}:${addr.port}`, "--project", project],
{ DISASMER_AGENT_PUBLIC_KEY: agentPublicKey }
);
assert(Number.isInteger(cliPid));
assert.notStrictEqual(cliPid, coordinator.pid);
assert.strictEqual(report.plan.entry, "build");
assert.deepStrictEqual(report.plan.session, {
AgentPublicKey: {
public_key_fingerprint: sha256(agentPublicKey),
browser_interaction_required: false
}
});
assert.strictEqual(report.boundary.cli_process_started_node_process, true);
assert.strictEqual(report.boundary.cli_process_started_coordinator_process, false);
assert(Number.isInteger(report.boundary.spawned_node_process_id));
assert.notStrictEqual(report.boundary.spawned_node_process_id, cliPid);
assert.notStrictEqual(report.boundary.spawned_node_process_id, coordinator.pid);
assert.strictEqual(report.boundary.node_session_requests, 10);
assert.strictEqual(report.node_report.node_status, "completed");
assert.strictEqual(report.node_report.status_code, 0);
assert.strictEqual(report.node_report.large_bytes_uploaded, false);
assert.strictEqual(report.node_report.capability_response.type, "node_capabilities_recorded");
assert.strictEqual(report.node_report.task_assignment_response.type, "task_placement");
assert.strictEqual(report.node_report.debug_command_response.type, "debug_command");
assert.strictEqual(report.node_report.log_event_response.type, "task_log_recorded");
assert.strictEqual(report.node_report.vfs_metadata_response.type, "vfs_metadata_recorded");
assert.strictEqual(report.node_report.staged_artifact.path, "/vfs/artifacts/cli-run-output.txt");
const events = await send(addr, {
type: "list_task_events",
tenant: "tenant",
project: "project",
actor_user: "user",
process: "vp-cli-local"
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "node-cli-local");
assert.strictEqual(events.events[0].process, "vp-cli-local");
assert.strictEqual(events.events[0].task, "compile-linux");
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/cli-run-output.txt");
} finally {
coordinator.kill("SIGTERM");
}
const { pid: autoCliPid, report: autoReport } = await runCli([
"run",
"--local",
"--project",
project
]);
assert(Number.isInteger(autoCliPid));
assert.strictEqual(autoReport.plan.entry, "build");
assert.deepStrictEqual(autoReport.plan.coordinator, "LocalOnly");
assert.deepStrictEqual(autoReport.plan.session, "Anonymous");
assert.strictEqual(autoReport.boundary.cli_process_started_node_process, true);
assert.strictEqual(autoReport.boundary.cli_process_started_coordinator_process, true);
assert.match(autoReport.boundary.coordinator_address, /^127\.0\.0\.1:\d+$/);
assert(Number.isInteger(autoReport.boundary.coordinator_process_id));
assert(Number.isInteger(autoReport.boundary.spawned_node_process_id));
assert.notStrictEqual(autoReport.boundary.coordinator_process_id, autoCliPid);
assert.notStrictEqual(autoReport.boundary.spawned_node_process_id, autoCliPid);
assert.notStrictEqual(
autoReport.boundary.spawned_node_process_id,
autoReport.boundary.coordinator_process_id
);
assert.strictEqual(autoReport.boundary.node_session_requests, 10);
assert.strictEqual(autoReport.node_report.node_status, "completed");
assert.strictEqual(autoReport.node_report.status_code, 0);
assert.strictEqual(autoReport.node_report.large_bytes_uploaded, false);
assert.strictEqual(autoReport.node_report.capability_response.type, "node_capabilities_recorded");
assert.strictEqual(autoReport.node_report.task_assignment_response.type, "task_placement");
assert.strictEqual(autoReport.node_report.debug_command_response.type, "debug_command");
assert.strictEqual(autoReport.node_report.log_event_response.type, "task_log_recorded");
assert.strictEqual(autoReport.node_report.vfs_metadata_response.type, "vfs_metadata_recorded");
assert.strictEqual(
autoReport.node_report.staged_artifact.path,
"/vfs/artifacts/cli-run-output.txt"
);
console.log("CLI local run smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,47 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const coordinator = "https://coord.example.test";
const defaultOperatorEndpoint = "https://disasmer.michelpaulissen.com:9443";
function disasmer(args) {
return JSON.parse(
cp.execFileSync(
"cargo",
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
{ cwd: repo, encoding: "utf8" }
)
);
}
const device = disasmer(["login", "--coordinator", coordinator]);
assert.strictEqual(device.coordinator, coordinator);
assert(device.human_flow.Device, "default human login should use device flow");
assert.strictEqual(device.human_flow.Device.verification_url, `${coordinator}/auth/device`);
assert.match(device.human_flow.Device.user_code, /^DISASMER-[A-F0-9]{4}-[A-F0-9]{4}$/);
assert.match(device.human_flow.Device.device_code, /^sha256:[a-f0-9]{64}$/);
assert.strictEqual(device.human_flow.Device.expires_in_seconds, 900);
assert.strictEqual(device.human_flow.Device.yields_long_lived_secret_directly, false);
const defaultDevice = disasmer(["login"]);
assert.strictEqual(defaultDevice.coordinator, defaultOperatorEndpoint);
assert.strictEqual(
defaultDevice.human_flow.Device.verification_url,
`${defaultOperatorEndpoint}/auth/device`
);
const browser = disasmer(["login", "--browser", "--plan", "--coordinator", coordinator]);
assert.strictEqual(browser.coordinator, coordinator);
assert(browser.human_flow.Browser, "browser login should be available for human users");
assert.match(
browser.human_flow.Browser.authorization_url,
new RegExp(`^${coordinator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/auth/browser/start\\?`)
);
assert.match(browser.human_flow.Browser.callback_path, /^http:\/\/127\.0\.0\.1:\d+\/callback$/);
assert.match(browser.human_flow.Browser.state, /^sha256:[a-f0-9]{64}$/);
console.log("CLI login smoke passed");

948
scripts/dap-smoke.js Executable file
View file

@ -0,0 +1,948 @@
#!/usr/bin/env node
const cp = require("child_process");
const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
class DapClient {
constructor() {
this.child = cp.spawn(
"cargo",
["run", "-q", "-p", "disasmer-dap", "--bin", "disasmer-debug-dap"],
{ cwd: process.cwd() }
);
this.seq = 1;
this.buffer = Buffer.alloc(0);
this.messages = [];
this.waiters = [];
this.stderr = "";
this.child.stdout.on("data", (chunk) => {
this.buffer = Buffer.concat([this.buffer, chunk]);
this.parse();
});
this.child.stderr.on("data", (chunk) => {
this.stderr += chunk.toString();
});
this.child.on("exit", () => this.flushWaiters());
}
send(command, args = {}) {
const seq = this.seq++;
const message = { seq, type: "request", command, arguments: args };
const payload = Buffer.from(JSON.stringify(message));
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
this.child.stdin.write(payload);
return seq;
}
async response(seq, command) {
const message = await this.waitFor(
(item) =>
item.type === "response" &&
item.request_seq === seq &&
item.command === command
);
if (!message.success) {
throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(message)}`);
}
return message;
}
async failure(seq, command) {
const message = await this.waitFor(
(item) =>
item.type === "response" &&
item.request_seq === seq &&
item.command === command
);
if (message.success) {
throw new Error(`DAP ${command} unexpectedly succeeded`);
}
return message;
}
waitFor(predicate, timeoutMs = 120000) {
const existing = this.messages.find(predicate);
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.child.kill("SIGKILL");
reject(new Error(`timed out waiting for DAP message\n${this.stderr}`));
}, timeoutMs);
this.waiters.push({ predicate, resolve, timer });
});
}
parse() {
while (true) {
const headerEnd = this.buffer.indexOf("\r\n\r\n");
if (headerEnd < 0) return;
const header = this.buffer.slice(0, headerEnd).toString();
const match = header.match(/Content-Length: (\d+)/i);
if (!match) throw new Error(`bad DAP header: ${header}`);
const length = Number(match[1]);
const start = headerEnd + 4;
const end = start + length;
if (this.buffer.length < end) return;
const payload = this.buffer.slice(start, end).toString();
this.buffer = this.buffer.slice(end);
this.messages.push(JSON.parse(payload));
this.flushWaiters();
}
}
flushWaiters() {
for (const waiter of [...this.waiters]) {
const message = this.messages.find(waiter.predicate);
if (!message) continue;
clearTimeout(waiter.timer);
this.waiters.splice(this.waiters.indexOf(waiter), 1);
waiter.resolve(message);
}
}
async close() {
const seq = this.send("disconnect");
await this.response(seq, "disconnect");
this.child.stdin.end();
}
}
function createFailingProject() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-dap-failing-"));
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(
path.join(root, "Cargo.toml"),
`[package]
name = "dap-failing-demo"
version = "0.1.0"
edition = "2021"
`
);
fs.writeFileSync(
path.join(root, "src/lib.rs"),
`#[cfg(test)]
mod tests {
#[test]
fn fails_for_restart_smoke() {
assert_eq!(1, 2);
}
}
`
);
return root;
}
function waitForJsonLine(child, description) {
return new Promise((resolve, reject) => {
let buffer = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(new Error(`${description} did not emit JSON: ${buffer}\n${error.stack || error.message}`));
}
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.once("exit", (code, signal) => {
reject(new Error(`${description} exited before JSON line with code ${code} signal ${signal}\n${stderr}`));
});
});
}
async function startCoordinator() {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: process.cwd() }
);
const ready = await waitForJsonLine(child, "coordinator");
return { child, listen: ready.listen };
}
async function startExplicitWorker(listen) {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
listen,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"dap-live-worker",
"--worker",
"--assignment-poll-ms",
"50",
"--emit-ready"
],
{ cwd: process.cwd() }
);
const ready = await waitForJsonLine(child, "explicit worker");
assert.strictEqual(ready.node_status, "ready");
assert.strictEqual(ready.mode, "worker");
assert.strictEqual(ready.node, "dap-live-worker");
return child;
}
function killChild(child) {
if (child && child.exitCode === null) {
child.kill("SIGTERM");
}
}
async function launchToBreakpoint({
runtimeBackend = "simulated",
breakpointLine,
breakpointLines,
project = path.join(process.cwd(), "examples/launch-build-demo"),
operatorEndpoint,
tenant = "tenant",
projectId = "project",
actorUser = "dap"
}) {
const client = new DapClient();
const initialize = client.send("initialize", {
adapterID: "disasmer",
linesStartAt1: true,
columnsStartAt1: true
});
await client.response(initialize, "initialize");
const launchArgs = {
entry: "build",
project,
runtimeBackend,
tenant,
projectId,
actorUser
};
if (operatorEndpoint) {
launchArgs.operatorEndpoint = operatorEndpoint;
}
const launch = client.send("launch", launchArgs);
await client.response(launch, "launch");
await client.waitFor((message) => message.type === "event" && message.event === "initialized");
const breakpoints = client.send("setBreakpoints", {
source: { path: path.join(project, "src/build.rs") },
breakpoints: (breakpointLines || [breakpointLine]).map((line) => ({ line }))
});
const breakpointResponse = await client.response(breakpoints, "setBreakpoints");
assert.deepStrictEqual(
breakpointResponse.body.breakpoints.map((breakpoint) => breakpoint.verified),
(breakpointLines || [breakpointLine]).map(() => true)
);
const exceptions = client.send("setExceptionBreakpoints", { filters: [] });
await client.response(exceptions, "setExceptionBreakpoints");
const configurationDone = client.send("configurationDone");
await client.response(configurationDone, "configurationDone");
const stopped = await client.waitFor(
(message) => message.type === "event" && message.event === "stopped"
);
assert.strictEqual(stopped.body.allThreadsStopped, true);
assert.strictEqual(stopped.body.reason, "breakpoint");
return { client, stopped };
}
(async () => {
const launchProject = path.join(process.cwd(), "examples/launch-build-demo");
const launchSource = fs.realpathSync(path.join(launchProject, "src/build.rs"));
const dapSource = fs.readFileSync(
path.join(process.cwd(), "crates/disasmer-dap/src/main.rs"),
"utf8"
);
const liveStart = dapSource.indexOf("fn run_live_services_runtime");
const liveEnd = dapSource.indexOf("fn run_with_coordinator", liveStart);
const liveRuntimeSource = dapSource.slice(liveStart, liveEnd);
assert.doesNotMatch(liveRuntimeSource, /run_node_against_coordinator|Command::new|project_binary/);
const { client, stopped } = await launchToBreakpoint({
runtimeBackend: "local-services",
project: launchProject,
breakpointLine: 22
});
assert.strictEqual(stopped.body.threadId, 2);
const threadsRequest = client.send("threads");
const threads = (await client.response(threadsRequest, "threads")).body.threads;
assert.deepStrictEqual(
threads.map((thread) => thread.id),
[1, 2, 3, 4]
);
assert(threads.some((thread) => thread.name.includes("compile linux")));
assert(threads.some((thread) => thread.name.includes("compile windows")));
assert(threads.some((thread) => thread.name.includes("package artifacts")));
const stackRequest = client.send("stackTrace", { threadId: 2, startFrame: 0, levels: 1 });
const stack = (await client.response(stackRequest, "stackTrace")).body.stackFrames;
assert.strictEqual(stack.length, 1);
assert.match(stack[0].name, /compile linux::run/);
assert.strictEqual(stack[0].line, 22);
assert.strictEqual(stack[0].source.path, launchSource);
assert.strictEqual(stack[0].source.sourceReference || 0, 0);
assert.doesNotMatch(stack[0].name, /podman|cmd\.exe|powershell|pid|native child/i);
const sourceRequest = client.send("source", { source: stack[0].source });
const source = (await client.response(sourceRequest, "source")).body;
assert.match(source.content, /compile_linux/);
assert.match(source.mimeType, /rust/);
const nextRequest = client.send("next", { threadId: 2 });
await client.response(nextRequest, "next");
const stepped = await client.waitFor(
(message) => message.type === "event" && message.event === "stopped" && message.body.reason === "step"
);
assert.strictEqual(stepped.body.threadId, 2);
assert.strictEqual(stepped.body.allThreadsStopped, true);
const steppedStackRequest = client.send("stackTrace", { threadId: 2, startFrame: 0, levels: 1 });
const steppedStack = (await client.response(steppedStackRequest, "stackTrace")).body.stackFrames;
assert.strictEqual(steppedStack[0].line, 23);
const steppedSourceRequest = client.send("source", { source: steppedStack[0].source });
const steppedSource = (await client.response(steppedSourceRequest, "source")).body;
assert.match(steppedSource.content, /compile_linux/);
const scopesRequest = client.send("scopes", { frameId: steppedStack[0].id });
const scopes = (await client.response(scopesRequest, "scopes")).body.scopes;
const localsRef = scopes.find((scope) => scope.name === "Source Locals").variablesReference;
const argsRef = scopes.find((scope) => scope.name === "Task Args and Handles").variablesReference;
const runtimeRef = scopes.find((scope) => scope.name === "Disasmer Runtime").variablesReference;
const outputRef = scopes.find((scope) => scope.name === "Recent Output").variablesReference;
const localsRequest = client.send("variables", { variablesReference: localsRef });
const locals = (await client.response(localsRequest, "variables")).body.variables;
assert(
locals.some(
(variable) =>
variable.name === "unavailable-local-diagnostic" &&
String(variable.value).includes("cannot be inspected")
)
);
const argsRequest = client.send("variables", { variablesReference: argsRef });
const args = (await client.response(argsRequest, "variables")).body.variables;
const target = args.find((variable) => variable.name === "target");
const artifact = args.find((variable) => variable.name === "artifact");
const sourceSnapshot = args.find((variable) => variable.name === "source_snapshot");
const returnValue = args.find((variable) => variable.name === "return_value");
const blob = args.find((variable) => variable.name === "blob");
const vfsMounts = args.find((variable) => variable.name === "vfs_mounts");
assert(target);
assert(target.variablesReference > 0);
assert(artifact);
assert.strictEqual(artifact.value, 'Artifact { id = "/vfs/artifacts/dap-output.txt" }');
assert(sourceSnapshot);
assert.match(sourceSnapshot.value, /^SourceSnapshot \{ digest = "source:\/\/local-checkout\/[a-f0-9]{12}" \}$/);
assert(returnValue);
assert.match(returnValue.value, /Artifact/);
assert(blob);
assert.match(blob.value, /^Blob \{ digest = "sha256:[a-f0-9]{64}" \}$/);
assert(vfsMounts);
assert(vfsMounts.variablesReference > 0);
const targetRequest = client.send("variables", { variablesReference: target.variablesReference });
const targetFields = (await client.response(targetRequest, "variables")).body.variables;
assert(targetFields.some((variable) => variable.name === "environment" && variable.value === "linux"));
assert(
targetFields.some(
(variable) => variable.name === "required_capability" && variable.value === "Command"
)
);
const vfsRequest = client.send("variables", { variablesReference: vfsMounts.variablesReference });
const vfs = (await client.response(vfsRequest, "variables")).body.variables;
assert(vfs.some((variable) => variable.name === "/vfs/artifacts"));
assert(vfs.some((variable) => variable.name === "/vfs/sources"));
assert(vfs.some((variable) => variable.name === "/vfs/blobs"));
const runtimeRequest = client.send("variables", { variablesReference: runtimeRef });
const runtime = (await client.response(runtimeRequest, "variables")).body.variables;
const processId = runtime.find((variable) => variable.name === "virtual_process_id");
const commandSpec = runtime.find((variable) => variable.name === "command_spec");
assert(processId);
assert.match(processId.value, /^vp-[a-f0-9]{12}$/);
assert(runtime.some((variable) => variable.name === "debug_epoch" && variable.value === 2));
assert(runtime.some((variable) => variable.name === "runtime_backend" && variable.value === "LocalServices"));
assert(runtime.some((variable) => variable.name === "coordinator_task_events" && variable.value === 1));
assert(runtime.some((variable) => variable.name === "command_status" && String(variable.value).includes("completed through local services")));
assert(commandSpec);
assert(commandSpec.variablesReference > 0);
assert(runtime.some((variable) => variable.name === "stdout_tail"));
assert(runtime.some((variable) => variable.name === "stderr_tail"));
const commandRequest = client.send("variables", {
variablesReference: commandSpec.variablesReference
});
const commandVariables = (await client.response(commandRequest, "variables")).body.variables;
assert(commandVariables.some((variable) => variable.name === "program" && variable.value === "cargo"));
assert(
commandVariables.some(
(variable) => variable.name === "required_capability" && variable.value === "Command"
)
);
const outputRequest = client.send("variables", { variablesReference: outputRef });
const output = (await client.response(outputRequest, "variables")).body.variables;
assert(output.some((variable) => variable.name === "stdout_tail"));
assert(output.some((variable) => variable.name === "stderr_tail"));
assert(output.some((variable) => String(variable.value).includes("all-stop")));
assert(output.some((variable) => String(variable.value).includes("attached node completed task")));
assert(output.some((variable) => String(variable.value).includes("coordinator recorded 1 task event")));
const continueRequest = client.send("continue", { threadId: 2 });
await client.response(continueRequest, "continue");
await client.waitFor((message) => message.type === "event" && message.event === "continued");
const pauseRequest = client.send("pause", { threadId: 2 });
await client.response(pauseRequest, "pause");
const paused = await client.waitFor(
(message) => message.type === "event" && message.event === "stopped" && message.body.reason === "pause"
);
assert.strictEqual(paused.body.allThreadsStopped, true);
const restartRequest = client.send("restartFrame", { frameId: stack[0].id });
await client.response(restartRequest, "restartFrame");
await client.waitFor(
(message) =>
message.type === "event" &&
message.event === "output" &&
String(message.body.output).includes("Restarted selected task")
);
const incompatibleRestartRequest = client.send("restartFrame", {
frameId: stack[0].id,
sourceCompatibility: "incompatible"
});
const incompatibleRestart = await client.failure(incompatibleRestartRequest, "restartFrame");
assert.match(incompatibleRestart.message, /incompatible source edit/i);
assert.match(incompatibleRestart.message, /whole virtual-process restart/i);
const continueAfterRestartRequest = client.send("continue", { threadId: 2 });
await client.response(continueAfterRestartRequest, "continue");
await client.waitFor((message) => message.type === "event" && message.event === "continued");
const freezeFailureRequest = client.send("pause", {
threadId: 2,
simulateFreezeFailure: true
});
const freezeFailure = await client.failure(freezeFailureRequest, "pause");
assert.match(freezeFailure.message, /all-stop failed/i);
assert.match(freezeFailure.message, /could not freeze/i);
await client.close();
const attachClient = new DapClient();
try {
const attachInitialize = attachClient.send("initialize", {
adapterID: "disasmer",
linesStartAt1: true,
columnsStartAt1: true
});
await attachClient.response(attachInitialize, "initialize");
const attachRequest = attachClient.send("attach", {
entry: "build",
project: launchProject,
runtimeBackend: "live-services",
operatorEndpoint: "127.0.0.1:1"
});
await attachClient.response(attachRequest, "attach");
await attachClient.waitFor(
(message) => message.type === "event" && message.event === "initialized"
);
const attachBreakpoints = attachClient.send("setBreakpoints", {
source: { path: path.join(launchProject, "src/build.rs") },
breakpoints: [{ line: 12 }]
});
await attachClient.response(attachBreakpoints, "setBreakpoints");
const attachDone = attachClient.send("configurationDone");
await attachClient.response(attachDone, "configurationDone");
const attachStopped = await attachClient.waitFor(
(message) => message.type === "event" && message.event === "stopped"
);
assert.strictEqual(attachStopped.body.allThreadsStopped, true);
const attachStackRequest = attachClient.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const attachStack = (await attachClient.response(attachStackRequest, "stackTrace")).body
.stackFrames;
const attachScopesRequest = attachClient.send("scopes", { frameId: attachStack[0].id });
const attachScopes = (await attachClient.response(attachScopesRequest, "scopes")).body.scopes;
const attachRuntimeRef = attachScopes.find((scope) => scope.name === "Disasmer Runtime")
.variablesReference;
const attachRuntimeRequest = attachClient.send("variables", {
variablesReference: attachRuntimeRef
});
const attachRuntime = (await attachClient.response(attachRuntimeRequest, "variables")).body
.variables;
assert(
attachRuntime.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("attached to existing virtual process")
)
);
} finally {
await attachClient.close().catch(() => {});
}
let sourceLocalsSession;
try {
sourceLocalsSession = await launchToBreakpoint({
runtimeBackend: "simulated",
project: launchProject,
breakpointLine: 60
});
assert.strictEqual(sourceLocalsSession.stopped.body.threadId, 1);
const localsStackRequest = sourceLocalsSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const localsStack = (await sourceLocalsSession.client.response(localsStackRequest, "stackTrace"))
.body.stackFrames;
assert.strictEqual(localsStack[0].line, 60);
const localsScopesRequest = sourceLocalsSession.client.send("scopes", {
frameId: localsStack[0].id
});
const localsScopes = (await sourceLocalsSession.client.response(localsScopesRequest, "scopes"))
.body.scopes;
const sourceLocalsRef = localsScopes.find((scope) => scope.name === "Source Locals")
.variablesReference;
const sourceLocalsRequest = sourceLocalsSession.client.send("variables", {
variablesReference: sourceLocalsRef
});
const sourceLocals = (
await sourceLocalsSession.client.response(sourceLocalsRequest, "variables")
).body.variables;
assert(
sourceLocals.some(
(variable) =>
variable.name === "linux" &&
String(variable.value).includes("TaskHandle") &&
String(variable.value).includes("compile-linux") &&
String(variable.value).includes("virtual_thread_id = 2")
)
);
assert(
sourceLocals.some((variable) => variable.name === "linux_thread" && variable.value === "2")
);
assert(
sourceLocals.some(
(variable) =>
variable.name === "linux_artifact" && String(variable.value).includes("Artifact")
)
);
} finally {
if (sourceLocalsSession) {
await sourceLocalsSession.client.close().catch(() => {});
}
}
let wasmLocalsSession;
try {
wasmLocalsSession = await launchToBreakpoint({
runtimeBackend: "simulated",
project: launchProject,
breakpointLine: 31
});
assert.strictEqual(wasmLocalsSession.stopped.body.threadId, 1);
const wasmStackRequest = wasmLocalsSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const wasmStack = (await wasmLocalsSession.client.response(wasmStackRequest, "stackTrace"))
.body.stackFrames;
assert.strictEqual(wasmStack[0].line, 31);
const wasmScopesRequest = wasmLocalsSession.client.send("scopes", {
frameId: wasmStack[0].id
});
const wasmScopes = (await wasmLocalsSession.client.response(wasmScopesRequest, "scopes")).body
.scopes;
const wasmLocalsRef = wasmScopes.find((scope) => scope.name === "Wasm Frame Locals")
.variablesReference;
const wasmLocalsRequest = wasmLocalsSession.client.send("variables", {
variablesReference: wasmLocalsRef
});
const wasmLocals = (await wasmLocalsSession.client.response(wasmLocalsRequest, "variables"))
.body.variables;
assert(
wasmLocals.some(
(variable) =>
variable.name === "wasm_local_0" &&
String(variable.value).includes("41") &&
variable.type === "wasm-frame-local"
),
"DAP variables must expose Wasmtime frame-local values from the product node runtime"
);
} finally {
if (wasmLocalsSession) {
await wasmLocalsSession.client.close().catch(() => {});
}
}
let liveCoordinator;
let liveWorker;
let liveSession;
try {
liveCoordinator = await startCoordinator();
liveSession = await launchToBreakpoint({
runtimeBackend: "live-services",
operatorEndpoint: liveCoordinator.listen,
project: launchProject,
breakpointLines: [12, 22]
});
assert.strictEqual(liveSession.stopped.body.threadId, 1);
const liveStackRequest = liveSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const liveStack = (await liveSession.client.response(liveStackRequest, "stackTrace")).body
.stackFrames;
assert.strictEqual(liveStack[0].line, 12);
assert.strictEqual(liveStack[0].source.path, launchSource);
const liveScopesRequest = liveSession.client.send("scopes", { frameId: liveStack[0].id });
const liveScopes = (await liveSession.client.response(liveScopesRequest, "scopes")).body.scopes;
const liveRuntimeRef = liveScopes.find((scope) => scope.name === "Disasmer Runtime")
.variablesReference;
const liveOutputRef = liveScopes.find((scope) => scope.name === "Recent Output")
.variablesReference;
const liveRuntimeRequest = liveSession.client.send("variables", {
variablesReference: liveRuntimeRef
});
const liveRuntime = (await liveSession.client.response(liveRuntimeRequest, "variables")).body
.variables;
assert(
liveRuntime.some(
(variable) => variable.name === "runtime_backend" && variable.value === "LiveServices"
)
);
assert(
liveRuntime.some(
(variable) => variable.name === "coordinator_task_events" && variable.value === 0
)
);
assert(
liveRuntime.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("coordinator-side virtual process started")
)
);
const liveOutputRequest = liveSession.client.send("variables", {
variablesReference: liveOutputRef
});
const liveOutput = (await liveSession.client.response(liveOutputRequest, "variables")).body
.variables;
assert(
liveOutput.some((variable) =>
String(variable.value).includes("Command-capability virtual task")
)
);
liveWorker = await startExplicitWorker(liveCoordinator.listen);
const liveContinueRequest = liveSession.client.send("continue", { threadId: 1 });
await liveSession.client.response(liveContinueRequest, "continue");
await liveSession.client.waitFor(
(message) => message.type === "event" && message.event === "continued"
);
const liveTaskStopped = await liveSession.client.waitFor(
(message) =>
message.type === "event" &&
message.event === "stopped" &&
message.body.reason === "breakpoint" &&
message.body.threadId === 2
);
assert.strictEqual(liveTaskStopped.body.threadId, 2);
const liveTaskStackRequest = liveSession.client.send("stackTrace", {
threadId: 2,
startFrame: 0,
levels: 1
});
const liveTaskStack = (await liveSession.client.response(liveTaskStackRequest, "stackTrace"))
.body.stackFrames;
assert.strictEqual(liveTaskStack[0].line, 22);
const liveTaskScopesRequest = liveSession.client.send("scopes", {
frameId: liveTaskStack[0].id
});
const liveTaskScopes = (await liveSession.client.response(liveTaskScopesRequest, "scopes"))
.body.scopes;
const liveTaskRuntimeRef = liveTaskScopes.find((scope) => scope.name === "Disasmer Runtime")
.variablesReference;
const liveTaskOutputRef = liveTaskScopes.find((scope) => scope.name === "Recent Output")
.variablesReference;
const liveTaskRuntimeRequest = liveSession.client.send("variables", {
variablesReference: liveTaskRuntimeRef
});
const liveTaskRuntime = (await liveSession.client.response(liveTaskRuntimeRequest, "variables"))
.body.variables;
assert(
liveTaskRuntime.some(
(variable) => variable.name === "coordinator_task_events" && variable.value === 1
)
);
assert(
liveTaskRuntime.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("completed through live services")
)
);
assert(liveTaskRuntime.some((variable) => variable.name === "stdout_tail"));
assert(liveTaskRuntime.some((variable) => variable.name === "stderr_tail"));
const liveTaskOutputRequest = liveSession.client.send("variables", {
variablesReference: liveTaskOutputRef
});
const liveTaskOutput = (await liveSession.client.response(liveTaskOutputRequest, "variables"))
.body.variables;
assert(liveTaskOutput.some((variable) => variable.name === "stdout_tail"));
assert(liveTaskOutput.some((variable) => variable.name === "stderr_tail"));
assert(
liveTaskOutput.some((variable) =>
String(variable.value).includes("attached node completed task")
)
);
} finally {
if (liveSession) {
await liveSession.client.close().catch(() => {});
}
killChild(liveWorker);
killChild(liveCoordinator && liveCoordinator.child);
}
const failingProject = createFailingProject();
let failedSession;
try {
failedSession = await launchToBreakpoint({
runtimeBackend: "local-services",
breakpointLine: 42,
project: failingProject
});
assert.strictEqual(failedSession.stopped.body.threadId, 2);
const failedStackRequest = failedSession.client.send("stackTrace", {
threadId: 2,
startFrame: 0,
levels: 1
});
const failedStack = (await failedSession.client.response(failedStackRequest, "stackTrace")).body
.stackFrames;
assert.strictEqual(failedStack.length, 1);
assert.match(failedStack[0].name, /compile linux::run/);
const failedScopesRequest = failedSession.client.send("scopes", { frameId: failedStack[0].id });
const failedScopes = (await failedSession.client.response(failedScopesRequest, "scopes")).body
.scopes;
const failedRuntimeRef = failedScopes.find((scope) => scope.name === "Disasmer Runtime")
.variablesReference;
const failedOutputRef = failedScopes.find((scope) => scope.name === "Recent Output")
.variablesReference;
const failedRuntimeRequest = failedSession.client.send("variables", {
variablesReference: failedRuntimeRef
});
const failedRuntime = (await failedSession.client.response(failedRuntimeRequest, "variables"))
.body.variables;
assert(
failedRuntime.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("failed through local services")
)
);
assert(
failedRuntime.some(
(variable) => variable.name === "state" && String(variable.value).includes("Failed")
)
);
const failedOutputRequest = failedSession.client.send("variables", {
variablesReference: failedOutputRef
});
const failedOutput = (await failedSession.client.response(failedOutputRequest, "variables")).body
.variables;
assert(
failedOutput.some((variable) => String(variable.value).includes("attached node failed task"))
);
const failedRestartRequest = failedSession.client.send("restartFrame", {
frameId: failedStack[0].id,
sourceEdit: { compatibility: "compatible" }
});
await failedSession.client.response(failedRestartRequest, "restartFrame");
await failedSession.client.waitFor(
(message) =>
message.type === "event" &&
message.event === "output" &&
String(message.body.output).includes("Restarted failed task")
);
const failedRuntimeAfterRestartRequest = failedSession.client.send("variables", {
variablesReference: failedRuntimeRef
});
const failedRuntimeAfterRestart = (
await failedSession.client.response(failedRuntimeAfterRestartRequest, "variables")
).body.variables;
assert(
failedRuntimeAfterRestart.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("failed task restarted")
)
);
const failedOutputAfterRestartRequest = failedSession.client.send("variables", {
variablesReference: failedOutputRef
});
const failedOutputAfterRestart = (
await failedSession.client.response(failedOutputAfterRestartRequest, "variables")
).body.variables;
assert(
failedOutputAfterRestart.some((variable) =>
String(variable.value).includes("task restarted from VFS checkpoint")
)
);
} finally {
if (failedSession) {
await failedSession.client.close().catch(() => {});
}
fs.rmSync(failingProject, { recursive: true, force: true });
}
const mainSession = await launchToBreakpoint({
runtimeBackend: "local-services",
project: launchProject,
breakpointLine: 42
});
assert.strictEqual(mainSession.stopped.body.threadId, 1);
const mainStackRequest = mainSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const mainStack = (await mainSession.client.response(mainStackRequest, "stackTrace"))
.body.stackFrames;
assert.strictEqual(mainStack.length, 1);
assert.match(mainStack[0].name, /build virtual process::run/);
assert.strictEqual(mainStack[0].line, 42);
assert.strictEqual(mainStack[0].source.path, launchSource);
assert.strictEqual(mainStack[0].source.sourceReference || 0, 0);
const mainSourceRequest = mainSession.client.send("source", { source: mainStack[0].source });
const mainSource = (await mainSession.client.response(mainSourceRequest, "source")).body;
assert.match(mainSource.content, /build/);
const mainScopesRequest = mainSession.client.send("scopes", { frameId: mainStack[0].id });
const mainScopes = (await mainSession.client.response(mainScopesRequest, "scopes")).body.scopes;
const mainRuntimeRef = mainScopes.find((scope) => scope.name === "Disasmer Runtime")
.variablesReference;
const mainRuntimeRequest = mainSession.client.send("variables", {
variablesReference: mainRuntimeRef
});
const mainRuntime = (await mainSession.client.response(mainRuntimeRequest, "variables")).body
.variables;
assert(
mainRuntime.some(
(variable) => variable.name === "runtime_backend" && variable.value === "LocalServices"
)
);
assert(
mainRuntime.some(
(variable) => variable.name === "coordinator_task_events" && variable.value === 1
)
);
await mainSession.client.close();
const multiMainSession = await launchToBreakpoint({
runtimeBackend: "local-services",
project: launchProject,
breakpointLines: [42, 43]
});
assert.strictEqual(multiMainSession.stopped.body.threadId, 1);
const firstMainStackRequest = multiMainSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const firstMainStack = (await multiMainSession.client.response(firstMainStackRequest, "stackTrace"))
.body.stackFrames;
assert.strictEqual(firstMainStack[0].line, 42);
const continueMainRequest = multiMainSession.client.send("continue", { threadId: 1 });
await multiMainSession.client.response(continueMainRequest, "continue");
await multiMainSession.client.waitFor(
(message) => message.type === "event" && message.event === "continued"
);
const secondMainStop = await multiMainSession.client.waitFor(
(message) =>
message.type === "event" &&
message.event === "stopped" &&
message.body.reason === "breakpoint"
);
assert.strictEqual(secondMainStop.body.threadId, 1);
const secondMainStackRequest = multiMainSession.client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const secondMainStack = (
await multiMainSession.client.response(secondMainStackRequest, "stackTrace")
).body.stackFrames;
assert.strictEqual(secondMainStack[0].line, 43);
await multiMainSession.client.close();
console.log("DAP smoke passed");
})().catch((err) => {
console.error(err.stack || err.message);
process.exit(1);
});

336
scripts/docs-smoke.js Normal file
View file

@ -0,0 +1,336 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const readme = fs.readFileSync(path.join(repo, "README.md"), "utf8");
const userFacingDocs = [
"README.md",
"MVP.md",
"acceptance_criteria.md",
"acceptance_criteria_phase2.md",
].map((file) => [file, fs.readFileSync(path.join(repo, file), "utf8")]);
const publicAcceptance = fs.readFileSync(
path.join(repo, "scripts/acceptance-public.sh"),
"utf8"
);
const publicSplit = fs.readFileSync(
path.join(repo, "scripts/verify-public-split.sh"),
"utf8"
);
const privateAcceptance = fs.readFileSync(
path.join(repo, "scripts/acceptance-private.sh"),
"utf8"
);
const requiredReadmePatterns = [
["quickstart heading", /## Quickstart/],
["workspace build", /cargo build --workspace/],
["CLI install", /cargo install --path crates\/disasmer-cli --bin disasmer/],
["node install", /cargo install --path crates\/disasmer-node --bin disasmer-node/],
[
"coordinator install",
/cargo install --path crates\/disasmer-coordinator --bin disasmer-coordinator/,
],
["DAP install", /cargo install --path crates\/disasmer-dap --bin disasmer-debug-dap/],
["VS Code local extension", /code --extensionDevelopmentPath/],
["local coordinator", /disasmer-coordinator --listen/],
["node attach", /disasmer node attach --coordinator/],
["automatic local run", /disasmer run --local --project examples\/launch-build-demo build/],
["demo run", /disasmer run --local --coordinator/],
["entrypoint selection", /disasmer run \[entry\]/],
["implicit hosted mode", /uses the hosted coordinator/],
["local override", /force local coordinator mode/],
["project override", /--project[\s\S]*overrides the project\s+directory/],
["VS Code debug", /Disasmer: Launch\s+Virtual Process/],
["artifact download smoke", /node scripts\/artifact-download-smoke\.js/],
["artifact export smoke", /node scripts\/artifact-export-smoke\.js/],
["acceptance report smoke", /node scripts\/acceptance-report-smoke\.js/],
["public private boundary smoke", /node scripts\/public-private-boundary-smoke\.js/],
["release blocker smoke", /node scripts\/release-blocker-smoke\.js/],
["explicit export", /attached receiver node or user-provided\s+storage integration/],
["cleanup", /Cleanup for the local quickstart/],
["flush docs", /`flush\(\)` publishes metadata/],
["sync docs", /`sync\(\)` is explicit/],
["storage integration is user code", /User-provided storage\/export integrations are ordinary project code or external\s+commands/],
["no managed artifact store feature", /does not provide\s+or manage an explicit artifact-store feature/],
["best-effort retention", /best-effort retained on nodes/],
[
"secure downloads",
/Download links are scoped to the tenant, project, process, artifact, actor, and policy context, expire after a bounded TTL, can be revoked/,
],
["node trust", /Users attach their own nodes for real work/],
["self-hosted trusted teams", /self-hosted local clouds, trusted teams, or VPN deployments/],
["self-hosted coordinator smoke", /node scripts\/self-hosted-coordinator-smoke\.js/],
["wasmtime node smoke", /node scripts\/wasmtime-node-smoke\.js/],
["wasmtime command host import", /node `disasmer\.cmd_run` host import/],
["Windows sandbox limitation", /Production-grade managed Windows sandboxing is behind an explicit backend stub/],
["hosted community limit", /community tier does not provide arbitrary hosted native commands or hosted containers/],
["browser login", /disasmer login --browser/],
["public-key agents", /disasmer agent enroll --public-key/],
["noninteractive agent CLI", /DISASMER_AGENT_PUBLIC_KEY=<agent-public-key> disasmer run build/],
["agent key lifecycle", /register, list, rotate, and revoke an agent key/],
["capability auto-detect", /auto-detects OS, architecture/],
["capability override", /--cap <name>/],
["non-Git source provider", /Non-Git source providers can implement the public source-provider interface/],
["first-run diagnostics", /## First-Run Diagnostics/],
["missing nodes diagnostic", /Missing nodes/],
["missing environment diagnostic", /Missing environments/],
["quota diagnostic", /Quota limits/],
["unavailable artifact diagnostic", /Unavailable artifacts/],
["auth diagnostic", /Auth failures/],
["debug freeze diagnostic", /Failed debug freezes/],
["source-provider diagnostic", /Source-provider capability gaps/],
["browser and VS Code report metadata", /browser\/VS Code harness metadata/],
["Podman incomplete report", /Linux Podman backend behavior is marked `incomplete`/],
["manual Windows validation workflow", /manual `Windows validation`\s+workflow/],
["intermittent Forgejo Windows runner", /intermittent Windows runner/],
["Windows validation env gate", /DISASMER_WINDOWS_VALIDATION = "forgejo-windows-runner"/],
["Windows validation attach", /runs `disasmer node attach`/],
["public dry-run operator endpoint", /https:\/\/disasmer\.michelpaulissen\.com:9443/],
["public dry-run DNS record", /record is live[\s\S]*no resolver override is required|DNS record is deployed[\s\S]*no resolver override is required/],
["public dry-run real deployment", /real externally reachable service/],
["public dry-run selected users", /shared with selected users/],
["public dry-run Forgejo repo", /git\.michelpaulissen\.com/],
["public dry-run Forgejo Release assets", /Forgejo Release publishes compiled\s+assets/],
["public dry-run filters root markdown", /root `\*\.md` files/],
["public dry-run filters Forgejo workflows by default", /\.forgejo\/\*\*` by\s+default/],
["public dry-run Forgejo workflow opt in", /--include-forgejo-workflows/],
["public dry-run selected-user quickstart", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\*\.md/],
["public dry-run selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\*\.md/],
["public dry-run resolver instructions env", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS=<instructions>/],
["public dry-run fallback hosts entry env", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY="<ip-address> disasmer\.michelpaulissen\.com"/],
["public dry-run deployment IP env", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP=<ip-address>/],
["public dry-run prep script", /node scripts\/prepare-public-release-dryrun\.js/],
["public dry-run publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE=1/],
["public dry-run public repo remote", /DISASMER_PUBLIC_REPO_REMOTE=ssh:\/\/git\.michelpaulissen\.com/],
["public dry-run Forgejo workflow", /Public release dry run assets/],
["public dry-run manifest", /public-release-manifest\.json/],
["public dry-run release publisher", /node scripts\/publish-public-release-dryrun\.js/],
["public dry-run non-e2e preflight", /node scripts\/public-release-dryrun-preflight\.js/],
["public dry-run Forgejo token", /DISASMER_FORGEJO_TOKEN=<token>/],
["public dry-run publisher infers repo", /publisher infers the Forgejo owner and repository name/],
["public dry-run Forgejo repo owner override", /DISASMER_PUBLIC_REPO_OWNER=<owner>/],
["public dry-run Forgejo repo name override", /DISASMER_PUBLIC_REPO_NAME=<public-repo>/],
["public dry-run service smoke", /public-release-dryrun-service-smoke\.js/],
["public dry-run service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR=disasmer\.michelpaulissen\.com:9443/],
["public dry-run private hosted coordinator", /default operator at `disasmer\.michelpaulissen\.com:9443` is the private hosted\s+coordinator from `private\/hosted-policy`/],
["public dry-run not standalone public coordinator", /does not mean deploying the standalone\s+open-source\/public coordinator as the hosted operator/],
["public dry-run validates both coordinators", /dry-run acceptance validates both coordinator implementations/],
["public dry-run validates public coordinator separately", /standalone public\/open-source\s+coordinator is validated separately/],
["public browser login site", /disasmer\.michelpaulissen\.com\/auth\/browser\/start/],
["public browser login local callback", /local callback/],
["public dry-run barebones HTML no CSS", /barebones HTML with\s+no CSS/],
["browser login plan diagnostic", /disasmer login --browser --plan/],
["public dry-run deployment prep", /prepare-public-release-dryrun-deployment\.js/],
["public dry-run systemd unit", /systemd unit that\s+binds `0\.0\.0\.0:9443`/],
["public dry-run e2e runner", /public-release-dryrun-e2e\.js/],
["public dry-run e2e gate env", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/],
["public dry-run e2e service address", /DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR=disasmer\.michelpaulissen\.com:9443/],
["public dry-run final verifier", /public-release-dryrun-final-evidence\.js/],
["public dry-run final gate env", /DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL=1/],
["public dry-run e2e evidence", /public-release-dryrun-e2e\.json/],
["public dry-run e2e checks", /downloaded release assets[\s\S]*VS Code debugger behavior[\s\S]*artifact metadata/],
["public operator compatibility smoke", /public-operator-compat-smoke\.js/],
["public operator compatibility purpose", /public CLI and public\s+node runtime can attach, launch work through coordinator task assignment, and\s+publish debug\/log\/artifact metadata/],
];
for (const [name, pattern] of requiredReadmePatterns) {
assert.match(readme, pattern, `README missing ${name}`);
}
for (const [file, contents] of userFacingDocs) {
assert.doesNotMatch(
contents,
/community-tier/i,
`${file} should use "community tier" in user-facing prose`
);
}
for (const script of [publicAcceptance, publicSplit]) {
assert(
script.includes("node scripts/docs-smoke.js"),
"public acceptance gates must run docs-smoke.js"
);
assert(
script.includes("node scripts/cli-install-smoke.js"),
"public acceptance gates must run cli-install-smoke.js"
);
assert(
script.includes("node scripts/cli-login-smoke.js"),
"public acceptance gates must run cli-login-smoke.js"
);
assert(
script.includes("node scripts/acceptance-report-smoke.js"),
"public acceptance gates must run acceptance-report-smoke.js"
);
assert(
script.includes("node scripts/acceptance-doc-contract-smoke.js"),
"public acceptance gates must run acceptance-doc-contract-smoke.js"
);
assert(
script.includes("node scripts/acceptance-environment-contract-smoke.js"),
"public acceptance gates must run acceptance-environment-contract-smoke.js"
);
assert(
script.includes("node scripts/acceptance-evidence-contract-smoke.js"),
"public acceptance gates must run acceptance-evidence-contract-smoke.js"
);
assert(
script.includes("node scripts/public-private-boundary-smoke.js"),
"public acceptance gates must run public-private-boundary-smoke.js"
);
assert(
script.includes("node scripts/release-blocker-smoke.js"),
"public acceptance gates must run release-blocker-smoke.js"
);
assert(
script.includes("node scripts/resource-metering-contract-smoke.js"),
"public acceptance gates must run resource-metering-contract-smoke.js"
);
assert(
script.includes("node scripts/hostile-input-contract-smoke.js"),
"public acceptance gates must run hostile-input-contract-smoke.js"
);
assert(
script.includes("node scripts/tenant-isolation-contract-smoke.js"),
"public acceptance gates must run tenant-isolation-contract-smoke.js"
);
assert(
script.includes("node scripts/public-story-contract-smoke.js"),
"public acceptance gates must run public-story-contract-smoke.js"
);
assert(
script.includes("node scripts/public-release-dryrun-contract-smoke.js"),
"public acceptance gates must run public-release-dryrun-contract-smoke.js"
);
assert(
script.includes("node scripts/prepare-public-release-dryrun.js"),
"public acceptance gates must run prepare-public-release-dryrun.js"
);
assert(
script.includes("node scripts/self-hosted-coordinator-smoke.js"),
"public acceptance gates must run self-hosted-coordinator-smoke.js"
);
assert(
script.includes("node scripts/public-local-demo-matrix-smoke.js"),
"public acceptance gates must run public-local-demo-matrix-smoke.js"
);
assert(
script.includes("scripts/release-source-scan.sh"),
"public acceptance gates must run release-source-scan.sh"
);
assert(
script.includes("node scripts/flagship-demo-smoke.js"),
"public acceptance gates must run flagship-demo-smoke.js"
);
assert(
script.includes("node scripts/cancellation-smoke.js"),
"public acceptance gates must run cancellation-smoke.js"
);
assert(
script.includes("node scripts/sdk-spawn-runtime-smoke.js"),
"public acceptance gates must run sdk-spawn-runtime-smoke.js"
);
assert(
script.includes("node scripts/node-lifecycle-contract-smoke.js"),
"public acceptance gates must run node-lifecycle-contract-smoke.js"
);
assert(
script.includes("node scripts/artifact-export-smoke.js"),
"public acceptance gates must run artifact-export-smoke.js"
);
assert(
script.includes("node scripts/windows-validation-contract-smoke.js"),
"public acceptance gates must run windows-validation-contract-smoke.js"
);
}
assert(
publicAcceptance.includes("node scripts/podman-backend-smoke.js"),
"public acceptance must run the Linux Podman backend smoke when Podman is available"
);
assert(
publicAcceptance.includes("node scripts/wasmtime-node-smoke.js"),
"public acceptance must run the Wasmtime node smoke"
);
assert(
privateAcceptance.includes("node scripts/resource-metering-contract-smoke.js"),
"private acceptance must run resource-metering-contract-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/hostile-input-contract-smoke.js"),
"private acceptance must run hostile-input-contract-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/tenant-isolation-contract-smoke.js"),
"private acceptance must run tenant-isolation-contract-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/acceptance-doc-contract-smoke.js"),
"private acceptance must run acceptance-doc-contract-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/acceptance-environment-contract-smoke.js"),
"private acceptance must run acceptance-environment-contract-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/acceptance-evidence-contract-smoke.js"),
"private acceptance must run acceptance-evidence-contract-smoke.js"
);
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-deployment-smoke.js"),
"private acceptance must run hosted-deployment-smoke.js"
);
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/public-operator-compat-smoke.js"),
"private acceptance must run public-operator-compat-smoke.js"
);
assert(
privateAcceptance.includes("node scripts/self-hosted-coordinator-smoke.js"),
"private acceptance must run self-hosted-coordinator-smoke.js before final dry-run evidence"
);
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/prepare-public-release-dryrun-deployment.js"),
"private acceptance must prepare public release dry-run deployment bundle"
);
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/public-release-dryrun-service-smoke.js"),
"private acceptance must be able to run public-release-dryrun-service-smoke.js"
);
for (const script of [publicAcceptance, privateAcceptance]) {
assert(
script.includes("node scripts/public-release-dryrun-final-evidence.js"),
"acceptance must be able to run public-release-dryrun-final-evidence.js"
);
assert(
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"),
"acceptance must gate public-release-dryrun-final-evidence.js"
);
}
assert(
publicAcceptance.includes("node scripts/public-release-dryrun-e2e.js"),
"public acceptance must be able to run public-release-dryrun-e2e.js"
);
assert(
publicAcceptance.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"),
"public acceptance must gate public-release-dryrun-e2e.js"
);
console.log("Docs smoke passed");

View file

@ -0,0 +1,76 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const extension = require("../vscode-extension/extension");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
const source = fs.readFileSync(path.join(project, "src/build.rs"), "utf8");
const forbiddenSourceAssumptions =
/\b(?:std::fs|std::process|Command::new|git|podman|docker|localhost|127\.0\.0\.1)|\/home\/|\/Users\/|C:\\Users\\/i;
const envs = extension.discoverEnvironmentNames(project);
assert.deepStrictEqual(envs, ["linux", "windows"]);
assert.deepStrictEqual(extension.diagnoseEnvReferences(source, envs), []);
assert.doesNotMatch(
source,
forbiddenSourceAssumptions,
"flagship build source must not rely on coordinator-side filesystem, Git, shell, container, or machine-local assumptions"
);
const inspection = JSON.parse(
cp.execFileSync(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"bundle",
"inspect",
"--project",
project
],
{ cwd: repo, encoding: "utf8" }
)
);
assert.strictEqual(inspection.project, project);
assert.strictEqual(
inspection.source_provider_manifest.coordinator_requires_checkout_access,
false
);
assert.strictEqual(
inspection.source_provider_manifest.transfer_policy.local_source_bytes_remain_node_local,
true
);
assert.strictEqual(
inspection.source_provider_manifest.transfer_policy.coordinator_receives_source_bytes_by_default,
false
);
assert.strictEqual(
inspection.source_provider_manifest.transfer_policy.default_full_repo_tarball,
false
);
assert.deepStrictEqual(
inspection.source_provider_manifest.transfer_policy.allowed_remote_transfer.sort(),
["ExplicitSnapshotChunks", "RequiredContent"]
);
assert.strictEqual(inspection.metadata.embeds_full_container_images, false);
assert(inspection.metadata.environments.some((env) => env.name === "linux"));
assert(inspection.metadata.environments.some((env) => env.name === "windows"));
assert(inspection.metadata.selected_inputs.some((input) => input.path === "src/build.rs"));
cp.execFileSync("cargo", ["test", "-p", "launch-build-demo"], {
cwd: repo,
stdio: "inherit"
});
console.log("Flagship demo smoke passed");

View file

@ -0,0 +1,176 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(segments) {
const fullPath = path.join(repo, ...segments);
if (!fs.existsSync(fullPath)) return null;
return fs.readFileSync(fullPath, "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing hostile-input evidence: ${name}`);
}
function expectGate(script, gateName) {
assert(
script.includes("node scripts/hostile-input-contract-smoke.js"),
`${gateName} must run hostile-input-contract-smoke.js`
);
}
const coreSource = read("crates/disasmer-core/src/source.rs");
const coreCapabilities = read("crates/disasmer-core/src/capability.rs");
const coordinatorService = read("crates/disasmer-coordinator/src/service.rs");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
for (const [name, pattern] of [
["source manifests validate shape", /pub fn validate_public_mvp\(&self\)[\s\S]*self\.validate_shape\(\)\?/],
["source manifests reject invalid digests", /SourceManifestError::InvalidDigest/],
["source manifests reject invalid custom providers", /SourceManifestError::InvalidProviderId/],
["source manifests reject control characters", /DescriptionControlCharacter/],
["source manifests reject coordinator checkout access", /CoordinatorCheckoutAccess/],
["source manifests reject default source-byte upload", /CoordinatorReceivesSourceBytes/],
]) {
expect(coreSource, name, pattern);
}
for (const [name, pattern] of [
["capability reports validate public shape", /pub fn validate_public_report\(&self\)/],
["capability reports validate architecture labels", /InvalidArchitecture/],
["capability reports validate OS labels", /InvalidOsLabel/],
["capability reports validate source providers", /InvalidSourceProvider/],
["source provider ids reject path traversal", /valid_source_provider_id/],
]) {
expect(coreCapabilities, name, pattern);
}
for (const [name, pattern] of [
["coordinator task log tails are bounded", /MAX_TASK_LOG_TAIL_BYTES: usize = 256 \* 1024/],
["coordinator validates reported stdout tails", /ReportTaskLog[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
["coordinator validates completed task stdout tails", /TaskCompleted[\s\S]*validate_task_log_tail\("stdout_tail", &stdout_tail\)\?/],
["coordinator rejects oversized log tail in unit coverage", /"x"\.repeat\(MAX_TASK_LOG_TAIL_BYTES \+ 1\)/],
]) {
expect(coordinatorService, name, pattern);
}
for (const [name, pattern] of [
["service rejects malformed node capability report", /fn service_rejects_malformed_node_capability_report\(\)/],
["capability report rejection leaves descriptors empty", /assert!\(service\.node_descriptors\.is_empty\(\)\)/],
["node capability report rejects cross-scope writes", /fn service_rejects_node_capability_report_outside_enrollment_scope\(\)/],
["task completion rejects cross-scope writes", /task completion outside node scope|outside/],
]) {
expect(coordinatorService, name, pattern);
}
for (const [name, source, patterns] of [
[
"artifact download smoke",
artifactDownloadSmoke,
[
/const crossTenant = await send/,
/const crossProject = await send/,
/const guessed = await send/,
/const crossActorOpen = await send/,
/token is invalid/,
/tenant mismatch/,
/project mismatch/,
],
],
[
"operator panel smoke",
operatorPanelSmoke,
[
/render_operator_panel/,
/submit_panel_event/,
/assert\(!JSON\.stringify\(panel\)\.includes\("<script"\)\)/,
/assert\(!JSON\.stringify\(panel\)\.toLowerCase\(\)\.includes\("oauth"\)\)/,
/rate limit/i,
/exceeds download limit/,
],
],
[
"scheduler smoke",
schedulerSmoke,
[/const crossTenantReport = await send/, /report_node_capabilities/, /tenant\\\/project scope/],
],
[
"source preparation smoke",
sourcePreparationSmoke,
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
],
]) {
for (const pattern of patterns) {
expect(source, name, pattern);
}
}
expectGate(publicAcceptance, "public acceptance");
expectGate(publicSplit, "public split acceptance");
expectGate(privateAcceptance, "private acceptance");
const hostedService = maybeRead([
"private",
"hosted-policy",
"src",
"bin",
"disasmer-hosted-service.rs",
]);
const hostedSmoke = maybeRead([
"private",
"hosted-policy",
"scripts",
"hosted-community-smoke.js",
]);
if (hostedService && hostedSmoke) {
for (const [name, pattern] of [
["hosted service turns malformed JSON into error responses", /serde_json::from_str::<HostedRequest>[\s\S]*HostedResponse::Error/],
["tenant ids are validated", /fn tenant_id\(value: String\)[\s\S]*validate_identifier\("tenant", &value\)\?/],
["project ids are validated", /fn project_id\(value: String\)[\s\S]*validate_identifier\("project", &value\)\?/],
["user ids are validated", /fn user_id\(value: String\)[\s\S]*validate_identifier\("user", &value\)\?/],
["node ids are validated", /fn node_id\(value: String\)[\s\S]*validate_identifier\("node", &value\)\?/],
["process ids are validated", /fn process_id\(value: String\)[\s\S]*validate_identifier\("process", &value\)\?/],
["task ids are validated", /fn task_id\(value: String\)[\s\S]*validate_identifier\("task", &value\)\?/],
["artifact ids are validated", /fn artifact_id\(value: String\)[\s\S]*validate_identifier\("artifact", &value\)\?/],
["identifiers reject empty and control/path characters", /fn validate_identifier[\s\S]*trim\(\)\.is_empty\(\)[\s\S]*ch\.is_control\(\) \|\| ch == '\/' \|\| ch == '\\\\'/],
["OIDC and agent text fields are bounded", /fn validate_text[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
["tokens are bounded", /fn validate_token[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
["logs are bounded at service boundary", /fn validate_log[\s\S]*512 \* 1024/],
["debug requests validate process and task before inspection", /HostedRequest::DebugProcess[\s\S]*process_id\(process\)\?[\s\S]*task_id\(task\)\?[\s\S]*debug_process/],
["download requests validate artifact before action", /HostedRequest::DownloadAction[\s\S]*artifact_id\(artifact\)\?[\s\S]*download_action/],
["log completion requests validate task and stdout before recording", /HostedRequest::RecordUserNodeTaskCompletion[\s\S]*task_id\(task\)\?[\s\S]*validate_log\("task stdout", &stdout\)\?[\s\S]*record_user_node_task_completion/],
]) {
expect(hostedService, name, pattern);
}
for (const [name, pattern] of [
["running service rejects malformed JSON", /sendRaw\(addr, "\{not-json"\)/],
["running service rejects invalid tenant id", /tenant: " "/],
["running service rejects invalid project id", /project: "project\\nbad"/],
["running service rejects invalid task id", /task: "compile-linux\\nescape"/],
["running service rejects oversized logs", /stdout: "x"\.repeat\(256 \* 1024 \+ 1\)/],
["running service rejects cross-tenant debug", /const crossTenantDebug = await send/],
["running service rejects cross-tenant metadata", /const crossTenantMetadata = await send/],
["running service rejects cross-tenant download", /const crossTenantDownload = await send/],
["foreign snapshots do not reveal objects", /foreignSnapshot[\s\S]*snapshot\.nodes, \[\][\s\S]*snapshot\.logs, \[\][\s\S]*snapshot\.artifacts, \[\]/],
]) {
expect(hostedSmoke, name, pattern);
}
}
console.log("Hostile input contract smoke passed");

View file

@ -0,0 +1,188 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
const line = buffer.slice(0, newline).trim();
try {
resolve(JSON.parse(line));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runNode(addr, enrollmentGrant) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-a",
"--enrollment-grant",
enrollmentGrant,
"--public-key",
"node-a-public-key",
"--process",
"vp-local",
"--task",
"compile-linux",
"--project",
project,
"--artifact",
"/vfs/artifacts/demo-test-output.txt"
],
{ cwd: repo }
);
const nodePid = child.pid;
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`node process failed with code ${code}\n${stderr}`));
return;
}
try {
resolve({ pid: nodePid, report: JSON.parse(stdout.trim().split("\n").at(-1)) });
} catch (error) {
reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
assert(Number.isInteger(coordinator.pid));
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const grant = await send(addr, {
type: "create_node_enrollment_grant",
tenant: "tenant",
project: "project",
actor_user: "user",
grant: "grant-local-services-node",
now_epoch_seconds: 0,
ttl_seconds: 900
});
assert.strictEqual(grant.type, "node_enrollment_grant_created");
const { pid: nodePid, report } = await runNode(addr, grant.grant);
assert(Number.isInteger(nodePid));
assert.notStrictEqual(nodePid, coordinator.pid);
assert.strictEqual(report.node_status, "completed");
assert.strictEqual(report.status_code, 0);
assert.strictEqual(report.large_bytes_uploaded, false);
assert.strictEqual(report.registration_response.type, "node_enrollment_exchanged");
assert.strictEqual(report.heartbeat_response.type, "node_heartbeat");
assert.strictEqual(report.capability_response.type, "node_capabilities_recorded");
assert.strictEqual(report.task_assignment_response.type, "task_placement");
assert.strictEqual(report.debug_command_response.type, "debug_command");
assert.strictEqual(report.log_event_response.type, "task_log_recorded");
assert.strictEqual(report.vfs_metadata_response.type, "vfs_metadata_recorded");
assert.strictEqual(report.session_requests, 10);
assert.strictEqual(report.staged_artifact.path, "/vfs/artifacts/demo-test-output.txt");
assert.strictEqual(report.coordinator_response.type, "task_recorded");
const events = await send(addr, {
type: "list_task_events",
tenant: "tenant",
project: "project",
actor_user: "user",
process: "vp-local"
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "node-a");
assert.strictEqual(events.events[0].process, "vp-local");
assert.strictEqual(events.events[0].task, "compile-linux");
assert.strictEqual(events.events[0].status_code, 0);
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/demo-test-output.txt");
} finally {
coordinator.kill("SIGTERM");
}
console.log("Local services smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

270
scripts/node-attach-smoke.js Executable file
View file

@ -0,0 +1,270 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const demoProject = path.join(repo, "examples/launch-build-demo");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runAttach(addr, grant) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"node",
"attach",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-attach",
"--public-key",
"node-attach-public-key",
"--enrollment-grant",
grant,
"--cap",
"quic-direct",
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`node attach failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout));
} catch (error) {
reject(
new Error(`node attach output was not JSON: ${stdout}\n${error.stack || error.message}`)
);
}
});
});
}
function runAttachedNodeWork(addr) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-attach",
"--process",
"vp-node-attach",
"--task",
"compile-linux",
"--project",
demoProject,
"--artifact",
"/vfs/artifacts/node-attach-output.txt",
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`attached node work failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
} catch (error) {
reject(
new Error(
`attached node work output was not JSON: ${stdout}\n${error.stack || error.message}`
)
);
}
});
});
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0",
],
{ cwd: repo }
);
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const grant = await send(addr, {
type: "create_node_enrollment_grant",
tenant: "tenant",
project: "project",
actor_user: "operator",
grant: "grant-node-attach",
now_epoch_seconds: 0,
ttl_seconds: 900
});
assert.strictEqual(grant.type, "node_enrollment_grant_created");
assert.strictEqual(grant.tenant, "tenant");
assert.strictEqual(grant.project, "project");
assert.strictEqual(grant.grant, "grant-node-attach");
assert.strictEqual(grant.scope, "node:attach");
assert.strictEqual(grant.expires_at_epoch_seconds, 900);
const report = await runAttach(addr, grant.grant);
assert.strictEqual(report.plan.node, "node-attach");
assert.strictEqual(report.plan.coordinator, `${addr.host}:${addr.port}`);
assert.strictEqual(report.plan.enrollment.grant, "grant-node-attach");
assert.match(report.plan.enrollment.public_key_fingerprint, /^sha256:[0-9a-f]{64}$/);
assert.strictEqual(
report.plan.enrollment.exchanges_short_lived_grant_for_long_lived_node_identity,
true
);
assert.ok(report.plan.capabilities.arch.length > 0);
assert.ok(report.plan.capabilities.source_providers.includes("filesystem"));
assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect"));
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
assert.strictEqual(report.boundary.used_enrollment_exchange, true);
assert.strictEqual(report.boundary.coordinator_session_requests, 3);
assert.strictEqual(report.coordinator_response.type, "node_enrollment_exchanged");
assert.strictEqual(report.coordinator_response.node, "node-attach");
assert.strictEqual(report.coordinator_response.credential.node, "node-attach");
assert.strictEqual(report.coordinator_response.credential.scope, "node:attach");
assert.strictEqual(report.coordinator_response.credential.credential_kind, "NodeCredential");
assert.match(
report.coordinator_response.credential.capability_policy_digest,
/^sha256:[0-9a-f]{64}$/
);
assert.strictEqual(report.heartbeat_response.type, "node_heartbeat");
assert.strictEqual(report.capability_response.type, "node_capabilities_recorded");
const heartbeat = await send(addr, {
type: "node_heartbeat",
node: "node-attach",
});
assert.strictEqual(heartbeat.type, "node_heartbeat");
assert.strictEqual(heartbeat.node, "node-attach");
const work = await runAttachedNodeWork(addr);
assert.strictEqual(work.node_status, "completed");
assert.strictEqual(work.virtual_thread, "compile-linux");
assert.strictEqual(work.status_code, 0);
assert.strictEqual(work.large_bytes_uploaded, false);
assert.strictEqual(
work.staged_artifact.path,
"/vfs/artifacts/node-attach-output.txt"
);
assert.strictEqual(work.coordinator_response.type, "task_recorded");
const events = await send(addr, {
type: "list_task_events",
tenant: "tenant",
project: "project",
actor_user: "operator",
process: "vp-node-attach",
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "node-attach");
assert.strictEqual(events.events[0].task, "compile-linux");
assert.strictEqual(
events.events[0].artifact_path,
"/vfs/artifacts/node-attach-output.txt"
);
} finally {
coordinator.kill("SIGTERM");
}
console.log("Node attach smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,138 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing node lifecycle evidence: ${name}`);
}
const nodeMain = read("crates/disasmer-node/src/main.rs");
const nodeLib = read("crates/disasmer-node/src/lib.rs");
const coordinatorCore = read("crates/disasmer-coordinator/src/lib.rs");
const coordinatorService = read("crates/disasmer-coordinator/src/service.rs");
const localServicesSmoke = read("scripts/local-services-smoke.js");
const cancellationSmoke = read("scripts/cancellation-smoke.js");
const wasmtimeSmoke = read("scripts/wasmtime-node-smoke.js");
const debugCore = read("crates/disasmer-core/src/debug.rs");
const readme = read("README.md");
assert.strictEqual(
(nodeMain.match(/CoordinatorSession::connect/g) || []).length,
1,
"node runtime should open one coordinator session in the local process-boundary runtime"
);
for (const [name, pattern] of [
["enrollment exchange over session", /"type": "exchange_node_enrollment_grant"/],
["node attach over session", /"type": "attach_node"/],
["heartbeat over session", /"type": "node_heartbeat"/],
["capability report over session", /"type": "report_node_capabilities"/],
["task placement over session", /"type": "schedule_task"/],
["process start over session", /"type": "start_process"/],
["reconnect over session", /"type": "reconnect_node"/],
["debug command polling over session", /"type": "poll_debug_command"/],
["log event over session", /"type": "report_task_log"/],
["VFS metadata over session", /"type": "report_vfs_metadata"/],
["task control polling over session", /"type": "poll_task_control"/],
["completion over session", /"type": "task_completed"/],
["cancellation uses same session", /wait_for_cancellation\(session, args, &task\)/],
["request count is reported", /session\.requests\(\)/],
]) {
expect(nodeMain, name, pattern);
}
for (const [name, pattern] of [
["local smoke asserts persistent request count", /assert\.strictEqual\(report\.session_requests, 10\)/],
["local smoke uses enrollment exchange", /assert\.strictEqual\(report\.registration_response\.type, "node_enrollment_exchanged"\)/],
["local smoke verifies capability report", /assert\.strictEqual\(report\.capability_response\.type, "node_capabilities_recorded"\)/],
["local smoke verifies task placement", /assert\.strictEqual\(report\.task_assignment_response\.type, "task_placement"\)/],
["local smoke verifies debug command channel", /assert\.strictEqual\(report\.debug_command_response\.type, "debug_command"\)/],
["local smoke verifies log event", /assert\.strictEqual\(report\.log_event_response\.type, "task_log_recorded"\)/],
["local smoke verifies VFS metadata", /assert\.strictEqual\(report\.vfs_metadata_response\.type, "vfs_metadata_recorded"\)/],
["local smoke records task completion", /assert\.strictEqual\(report\.coordinator_response\.type, "task_recorded"\)/],
["local smoke verifies artifact metadata", /assert\.strictEqual\(events\.events\[0\]\.artifact_path, "\/vfs\/artifacts\/demo-test-output\.txt"\)/],
["local smoke verifies output accounting", /assert\.strictEqual\(events\.events\[0\]\.status_code, 0\)/],
]) {
expect(localServicesSmoke, name, pattern);
}
for (const [name, pattern] of [
["cancellation request is external client request", /type: "cancel_task"/],
["node reports cancelled terminal state", /assert\.strictEqual\(report\.terminal_state, "cancelled"\)/],
["cancelled completion is recorded", /assert\.strictEqual\(report\.coordinator_response\.type, "task_recorded"\)/],
["coordinator stores cancelled task event", /assert\.strictEqual\(events\.events\[0\]\.terminal_state, "cancelled"\)/],
["control flag is cleared after terminal state", /assert\.strictEqual\(control\.cancel_requested, false\)/],
]) {
expect(cancellationSmoke, name, pattern);
}
for (const [name, pattern] of [
["coordinator rejects stale process ownership", /fn node_reconnect_rejects_stale_process_epoch_after_restart\(\)/],
["reconnect preserves enrolled node identity", /reconnect_node\(&NodeId::from\("node"\), None\)/],
["stale process epoch is rejected", /CoordinatorError::StaleProcessEpoch/],
]) {
expect(coordinatorCore, name, pattern);
}
for (const [name, pattern] of [
["coordinator delivers cancellation to connected node", /fn service_delivers_cancellation_to_connected_node_and_records_terminal_state\(\)/],
["node polls task control", /CoordinatorRequest::PollTaskControl/],
["cancelled terminal state is recorded", /TaskTerminalState::Cancelled/],
]) {
expect(coordinatorService, name, pattern);
}
for (const [name, pattern] of [
["native lifecycle test exists", /fn linux_task_lifecycle_supports_cancel_and_all_stop_freeze_resume\(\)/],
["native freeze succeeds when supported", /lifecycle\.freeze_for_debug_epoch\(\)\.unwrap\(\)/],
["native resume succeeds", /lifecycle\.resume_after_debug_epoch\(\)/],
["native cancel reaches lifecycle", /lifecycle\.cancel\(\)/],
["unsupported freeze errors", /BackendError::DebugFreezeUnsupported/],
["wasmtime runtime exposes freeze resume probe", /pub fn freeze_resume_i32_export_probe/],
["wasmtime runtime captures Wasm frame locals", /debug_i32_export_snapshot[\s\S]*local_values/],
["wasmtime runtime creates Wasm debug participant", /kind: DebugParticipantKind::WasmTask/],
["wasmtime debug participant carries local values", /local_values: snapshot\.local_values\.clone\(\)/],
["wasmtime runtime resumes after freeze", /epoch\.continue_all\(\)/],
]) {
expect(nodeLib, name, pattern);
}
for (const [name, pattern] of [
["wasmtime smoke runs debug freeze resume mode", /--debug-freeze-resume/],
["wasmtime smoke verifies frozen state", /debugReport\.frozen_state, "Frozen"/],
["wasmtime smoke verifies resumed state", /debugReport\.resumed_state, "Running"/],
["wasmtime smoke verifies frame local values", /debugReport\.local_values[\s\S]*wasm_local_0/],
["wasmtime smoke proves node runtime reached wasm task", /node_runtime_reached_wasm_task/],
["wasmtime smoke proves node captured locals", /node_runtime_captured_wasm_locals/],
]) {
expect(wasmtimeSmoke, name, pattern);
}
for (const [name, pattern] of [
["debug model freezes wasm and command participants", /fn breakpoint_creates_all_stop_debug_epoch_for_wasm_and_command_tasks\(\)/],
["debug model rejects unsupported freeze", /fn debug_epoch_reports_freeze_failure_instead_of_claiming_all_stop\(\)/],
["debug model resumes frozen participants", /fn continue_resumes_every_frozen_participant\(\)/],
["debug model includes captured locals", /local_values/],
["wasm participants are modeled", /DebugParticipantKind::WasmTask/],
["controlled native command participants are modeled", /DebugParticipantKind::ControlledNativeCommand/],
]) {
expect(debugCore, name, pattern);
}
for (const [name, pattern] of [
["docs describe one node coordinator session", /keeps one node-to-coordinator JSON-line session open/],
["docs describe cancellation terminal event", /records a cancelled terminal event/],
["docs describe failed freeze diagnostic", /Failed debug freezes/],
]) {
expect(readme, name, pattern);
}
console.log("Node lifecycle contract smoke passed");

356
scripts/operator-panel-smoke.js Executable file
View file

@ -0,0 +1,356 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runNode(addr) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"panel-node",
"--process",
"vp-panel",
"--task",
"compile-linux",
"--project",
project,
"--artifact",
"/vfs/artifacts/panel-output.txt"
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`node process failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout.trim().split("\n").at(-1)));
} catch (error) {
reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
function widget(panel, id) {
const item = panel.widgets[id];
assert(item, `missing panel widget ${id}`);
return item;
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const report = await runNode(addr);
assert.strictEqual(report.node_status, "completed");
assert.strictEqual(report.coordinator_response.type, "task_recorded");
const rendered = await send(addr, {
type: "render_operator_panel",
tenant: "tenant",
project: "project",
process: "vp-panel",
actor_user: "user",
max_download_bytes: 1024 * 1024,
stopped: false
});
assert.strictEqual(rendered.type, "operator_panel");
const panel = rendered.panel;
assert.strictEqual(panel.tenant, "tenant");
assert.strictEqual(panel.project, "project");
assert.strictEqual(panel.process, "vp-panel");
assert.strictEqual(panel.program_ui_events_enabled, true);
assert.deepStrictEqual(widget(panel, "process-status").kind, {
Text: { value: "running" }
});
assert.deepStrictEqual(widget(panel, "task-progress").kind, {
Progress: { current: 1, total: 1 }
});
assert.match(
widget(panel, "task-summary").kind.Text.value,
/compile-linux:Some\(0\):panel-node/
);
assert.match(widget(panel, "recent-logs").kind.Text.value, /stdout=\d+ stderr=\d+/);
const downloadWidget = widget(panel, "download-artifact").kind;
assert.deepStrictEqual(downloadWidget, {
ArtifactDownload: { artifact: "panel-output.txt" }
});
assert(!JSON.stringify(downloadWidget).includes("url_path"));
assert(!JSON.stringify(downloadWidget).includes("scoped_token_digest"));
assert.deepStrictEqual(widget(panel, "debug-process").kind, {
Button: { action: "debug-process" }
});
assert.deepStrictEqual(widget(panel, "cancel-process").kind, {
Button: { action: "cancel-process" }
});
assert.deepStrictEqual(widget(panel, "restart-selected-task").kind, {
Button: { action: "restart-task" }
});
assert(panel.control_plane_actions.includes("DebugProcess"));
assert(panel.control_plane_actions.includes("CancelProcess"));
assert(
panel.control_plane_actions.some(
(action) => action.RestartTask === "compile-linux"
)
);
assert(
panel.control_plane_actions.some(
(action) => action.DownloadArtifact === "panel-output.txt"
)
);
assert(!JSON.stringify(panel).includes("<script"));
assert(!JSON.stringify(panel).toLowerCase().includes("oauth"));
const panelLink = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: downloadWidget.ArtifactDownload.artifact,
max_bytes: 1024 * 1024,
token_nonce: "panel-button",
now_epoch_seconds: 10,
ttl_seconds: 60
});
assert.strictEqual(panelLink.type, "artifact_download_link");
assert.strictEqual(panelLink.link.artifact, downloadWidget.ArtifactDownload.artifact);
assert.match(panelLink.link.policy_context_digest, /^sha256:[0-9a-f]{64}$/);
assert.deepStrictEqual(panelLink.link.source, { RetainedNode: "panel-node" });
assert.match(panelLink.link.url_path, /\/artifacts\/tenant\/project\/vp-panel\/panel-output\.txt$/);
const panelStream = await send(addr, {
type: "open_artifact_download_stream",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: downloadWidget.ArtifactDownload.artifact,
max_bytes: 1024 * 1024,
token_nonce: "panel-button",
token_digest: panelLink.link.scoped_token_digest,
now_epoch_seconds: 11,
chunk_bytes: 16
});
assert.strictEqual(panelStream.type, "artifact_download_stream");
assert.strictEqual(panelStream.link.artifact, downloadWidget.ArtifactDownload.artifact);
assert.deepStrictEqual(panelStream.link.source, { RetainedNode: "panel-node" });
assert.strictEqual(panelStream.streamed_bytes, 16);
const apiTooLarge = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: downloadWidget.ArtifactDownload.artifact,
max_bytes: 1,
token_nonce: "too-small",
now_epoch_seconds: 10,
ttl_seconds: 60
});
assert.strictEqual(apiTooLarge.type, "error");
assert.match(apiTooLarge.message, /exceeds download limit/);
const panelTooLarge = await send(addr, {
type: "render_operator_panel",
tenant: "tenant",
project: "project",
process: "vp-panel",
actor_user: "user",
max_download_bytes: 1,
stopped: false
});
assert.strictEqual(panelTooLarge.type, "error");
assert.match(panelTooLarge.message, /exceeds download limit/);
const accepted = await send(addr, {
type: "submit_panel_event",
tenant: "tenant",
project: "project",
process: "vp-panel",
widget_id: "debug-process",
kind: "ButtonClicked",
max_events: 1
});
assert.strictEqual(accepted.type, "panel_event_accepted");
assert.strictEqual(accepted.used_events, 1);
assert.strictEqual(accepted.max_events, 1);
const rateLimited = await send(addr, {
type: "submit_panel_event",
tenant: "tenant",
project: "project",
process: "vp-panel",
widget_id: "debug-process",
kind: "ButtonClicked",
max_events: 1
});
assert.strictEqual(rateLimited.type, "error");
assert.match(rateLimited.message, /rate limit/i);
const stopped = await send(addr, {
type: "render_operator_panel",
tenant: "tenant",
project: "project",
process: "vp-panel",
actor_user: "user",
max_download_bytes: 1024 * 1024,
stopped: true
});
assert.strictEqual(stopped.type, "operator_panel");
assert.strictEqual(stopped.panel.program_ui_events_enabled, false);
assert.deepStrictEqual(widget(stopped.panel, "process-status").kind, {
Text: { value: "stopped" }
});
assert.deepStrictEqual(
widget(stopped.panel, "task-progress").kind,
widget(panel, "task-progress").kind
);
assert.deepStrictEqual(
widget(stopped.panel, "task-summary").kind,
widget(panel, "task-summary").kind
);
assert.deepStrictEqual(
widget(stopped.panel, "recent-logs").kind,
widget(panel, "recent-logs").kind
);
assert.deepStrictEqual(
widget(stopped.panel, "download-artifact").kind,
widget(panel, "download-artifact").kind
);
assert(stopped.panel.control_plane_actions.includes("DebugProcess"));
assert(
stopped.panel.control_plane_actions.some(
(action) => action.DownloadArtifact === "panel-output.txt"
)
);
const frozenEvent = await send(addr, {
type: "submit_panel_event",
tenant: "tenant",
project: "project",
process: "vp-panel",
widget_id: "debug-process",
kind: "ButtonClicked",
max_events: 10
});
assert.strictEqual(frozenEvent.type, "error");
assert.match(frozenEvent.message, /program UI events are disabled/i);
const crossTenant = await send(addr, {
type: "render_operator_panel",
tenant: "other",
project: "project",
process: "vp-panel",
actor_user: "user",
max_download_bytes: 1024 * 1024,
stopped: false
});
assert.strictEqual(crossTenant.type, "error");
assert.match(crossTenant.message, /scope|tenant|project/i);
} catch (error) {
if (coordinatorStderr) {
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
}
throw error;
} finally {
coordinator.kill("SIGTERM");
}
console.log("Operator panel smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

80
scripts/podman-backend-smoke.js Executable file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const baseImage = "docker.io/library/alpine:3.20";
function run(command, args, options = {}) {
return cp.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: options.stdio || ["ignore", "pipe", "pipe"]
});
}
function incomplete(reason) {
const error = new Error(`Linux Podman backend incomplete: ${reason}`);
error.code = "DISASMER_PODMAN_INCOMPLETE";
throw error;
}
function ensurePodmanBaseImage() {
try {
run("podman", ["--version"]);
} catch (error) {
incomplete(`podman command is unavailable (${error.message})`);
}
let rootless;
try {
rootless = run("podman", ["info", "--format", "{{.Host.Security.Rootless}}"]).trim();
} catch (error) {
incomplete(`podman info did not report rootless status (${error.message})`);
}
if (rootless !== "true") {
incomplete(`podman is not running in rootless mode (reported ${JSON.stringify(rootless)})`);
}
try {
run("podman", ["image", "exists", baseImage]);
} catch (_) {
try {
run("podman", ["pull", baseImage], { stdio: "inherit" });
} catch (error) {
incomplete(`unable to make ${baseImage} available (${error.message})`);
}
}
}
try {
ensurePodmanBaseImage();
const stdout = run("cargo", [
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-podman-smoke"
]);
const report = JSON.parse(stdout.trim().split("\n").at(-1));
assert.strictEqual(report.podman_status, "completed");
assert.strictEqual(report.status_code, 0);
assert.strictEqual(report.stdout, "podman-ok:node-local source\n");
assert.strictEqual(report.large_bytes_uploaded, false);
assert.strictEqual(report.uses_full_repo_tarball, false);
assert.strictEqual(report.coordinator_routed_file_reads, false);
assert.strictEqual(report.staged_artifact.path, "/vfs/artifacts/podman-smoke.txt");
console.log("Podman backend smoke passed");
} catch (error) {
if (error.code === "DISASMER_PODMAN_INCOMPLETE") {
console.error(error.message);
process.exit(2);
}
throw error;
}

View file

@ -0,0 +1,617 @@
#!/usr/bin/env node
const crypto = require("crypto");
const cp = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const outputRoot = path.resolve(
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
path.join(repo, "target/public-release-dryrun")
);
const publicTree = path.join(outputRoot, "public-tree");
const assetsDir = path.join(outputRoot, "assets");
const stagingDir = path.join(outputRoot, "staging");
const defaultOperatorEndpoint = "https://disasmer.michelpaulissen.com:9443";
const forgejoHost = "git.michelpaulissen.com";
const args = new Set(process.argv.slice(2));
const includeForgejoWorkflows =
args.has("--include-forgejo-workflows") ||
/^(1|true|yes)$/i.test(process.env.DISASMER_INCLUDE_FORGEJO_WORKFLOWS || "");
const filteredTopLevel = ["private", "experiments", ".git", "target"];
const filteredDirectoryNames = [".disasmer"];
const publicRepoBranch = process.env.DISASMER_PUBLIC_REPO_BRANCH || "main";
const publicBinaries = [
"disasmer",
"disasmer-coordinator",
"disasmer-node",
"disasmer-debug-dap",
];
function commandOutput(command, args, options = {}) {
try {
return cp
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
...options,
})
.trim();
} catch (_) {
return null;
}
}
function run(command, args, options = {}) {
cp.execFileSync(command, args, {
cwd: repo,
stdio: "inherit",
...options,
});
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function filteredOutPatterns() {
return [
"private/**",
"experiments/**",
".git",
"target",
"/*.md",
"**/.disasmer/**",
...(includeForgejoWorkflows ? [] : [".forgejo/**"]),
];
}
function isRootMarkdown(relativePath, entry) {
return (
!relativePath.includes(path.sep) &&
(entry.isFile() || entry.isSymbolicLink()) &&
path.extname(entry.name).toLowerCase() === ".md"
);
}
function shouldFilter(entry, relativePath) {
const parts = relativePath.split(path.sep).filter(Boolean);
const topLevel = parts[0];
if (filteredTopLevel.includes(topLevel)) return true;
if (parts.some((part) => filteredDirectoryNames.includes(part))) return true;
if (topLevel === ".forgejo" && !includeForgejoWorkflows) return true;
if (isRootMarkdown(relativePath, entry)) return true;
return false;
}
function copyFilteredTree(src, dest, relative = "") {
ensureDir(dest);
const entries = fs
.readdirSync(src, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name));
for (const entry of entries) {
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
if (shouldFilter(entry, childRelative)) {
continue;
}
const from = path.join(src, entry.name);
const to = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyFilteredTree(from, to, childRelative);
} else if (entry.isSymbolicLink()) {
fs.symlinkSync(fs.readlinkSync(from), to);
} else if (entry.isFile()) {
fs.copyFileSync(from, to);
fs.chmodSync(to, fs.statSync(from).mode & 0o777);
}
}
}
function assertFilteredTree() {
for (const excluded of ["private", "experiments"]) {
if (fs.existsSync(path.join(publicTree, excluded))) {
throw new Error(`${excluded}/ leaked into the dry-run public tree`);
}
}
for (const file of walkFiles(publicTree)) {
if (file.split(path.sep).includes(".disasmer")) {
throw new Error(`generated .disasmer view state leaked into the dry-run public tree: ${file}`);
}
}
if (!includeForgejoWorkflows && fs.existsSync(path.join(publicTree, ".forgejo"))) {
throw new Error(".forgejo/ leaked into the host-neutral dry-run public tree");
}
for (const entry of fs.readdirSync(publicTree, { withFileTypes: true })) {
if (isRootMarkdown(entry.name, entry)) {
throw new Error(`root Markdown file leaked into the dry-run public tree: ${entry.name}`);
}
}
}
function walkFiles(root, relative = "") {
const dir = path.join(root, relative);
const entries = fs
.readdirSync(dir, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name));
const files = [];
for (const entry of entries) {
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
if (entry.isDirectory()) {
files.push(...walkFiles(root, childRelative));
} else if (entry.isFile()) {
files.push(childRelative);
} else if (entry.isSymbolicLink()) {
files.push(childRelative);
}
}
return files;
}
function hashTree(root) {
const hash = crypto.createHash("sha256");
for (const file of walkFiles(root)) {
const absolute = path.join(root, file);
const stat = fs.lstatSync(absolute);
hash.update(file.replaceAll(path.sep, "/"));
hash.update("\0");
hash.update(String(stat.mode & 0o777));
hash.update("\0");
if (stat.isSymbolicLink()) {
hash.update("symlink");
hash.update("\0");
hash.update(fs.readlinkSync(absolute));
} else {
hash.update(fs.readFileSync(absolute));
}
hash.update("\0");
}
return `sha256:${hash.digest("hex")}`;
}
function sha256File(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
function tarGz(output, cwd, inputs) {
run("tar", ["-czf", output, "-C", cwd, ...inputs]);
}
function platformName() {
return `${os.platform()}-${os.arch()}`;
}
function binaryName(name) {
return process.platform === "win32" ? `${name}.exe` : name;
}
function buildPublicBinaries() {
run("cargo", ["build", "--workspace", "--bins", "--release"], {
cwd: publicTree,
});
}
function stageBinaryAssets(releaseName) {
const stageRoot = path.join(stagingDir, "binaries");
const binDir = path.join(stageRoot, "bin");
fs.rmSync(stageRoot, { recursive: true, force: true });
ensureDir(binDir);
for (const binary of publicBinaries) {
const fileName = binaryName(binary);
const built = path.join(publicTree, "target", "release", fileName);
if (!fs.existsSync(built)) {
throw new Error(`expected release binary ${built}`);
}
const staged = path.join(binDir, fileName);
fs.copyFileSync(built, staged);
fs.chmodSync(staged, 0o755);
}
const archive = path.join(
assetsDir,
`disasmer-public-binaries-${releaseName}-${platformName()}.tar.gz`
);
tarGz(archive, stageRoot, ["."]);
return archive;
}
function stageSourceAsset(releaseName) {
const archive = path.join(assetsDir, `disasmer-public-source-${releaseName}.tar.gz`);
tarGz(archive, publicTree, ["."]);
return archive;
}
function stageExtensionAsset() {
const packageJson = JSON.parse(
fs.readFileSync(path.join(publicTree, "vscode-extension/package.json"), "utf8")
);
const archive = path.join(
assetsDir,
`${packageJson.name}-${packageJson.version}.vsix`
);
run(
"npx",
[
"--yes",
"@vscode/vsce",
"package",
"--allow-missing-repository",
"--out",
archive,
],
{ cwd: path.join(publicTree, "vscode-extension") }
);
return archive;
}
function resolverInstructions() {
if (process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS) {
return process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS.trim();
}
if (process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY) {
return [
"Add the controlled hosts entry supplied for this dry run:",
"",
"```",
process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY.trim(),
"```",
].join("\n");
}
if (process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP) {
return [
"Add this controlled hosts entry for the dry run:",
"",
"```",
`${process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP} disasmer.michelpaulissen.com`,
"```",
].join("\n");
}
return [
"`disasmer.michelpaulissen.com` should resolve through public DNS. If it",
"does not resolve yet, wait for DNS propagation or use the fallback hosts",
"entry supplied with the invitation.",
].join("\n");
}
function writeGettingStartedAsset(releaseName, publicTreeIdentity, resolution) {
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-${releaseName}.md`);
fs.writeFileSync(
file,
`# Disasmer Public Dry Run
This dry run is a real Disasmer deployment for selected external users. If
public DNS has not propagated yet, use the fallback resolution instructions
below.
The default operator is the private hosted coordinator at
\`https://disasmer.michelpaulissen.com:9443\`. The word \`public\` in this dry run
refers to the public Forgejo repository, release downloads, selected-user
network access, and public client protocol compatibility. It does not mean the
server is the standalone open-source/public coordinator.
## DNS
${resolution}
## Install
1. Download the binary archive for your platform from the Forgejo Release at
\`git.michelpaulissen.com\`.
2. Verify it against \`SHA256SUMS\`.
3. Extract the archive and put the \`bin/\` directory on your \`PATH\`.
4. Download \`disasmer-vscode-*.vsix\` if you want the debugger and Disasmer
side views, then install it:
\`\`\`bash
code --install-extension disasmer-vscode-*.vsix
\`\`\`
## Connect
Use the default operator endpoint:
\`\`\`bash
disasmer login --browser
disasmer bundle inspect --project examples/launch-build-demo
\`\`\`
\`disasmer login --browser\` opens your browser through the barebones
\`https://disasmer.michelpaulissen.com\` login site and returns to the CLI
through a local callback.
Enroll a node identity with the grant supplied by the invitation:
\`\`\`bash
disasmer node attach --coordinator https://disasmer.michelpaulissen.com:9443 --enrollment-grant <grant> --public-key <public-key>
\`\`\`
That command proves the identity/enrollment path and then exits. To actually run
work for the flagship workflow, leave a worker process running in another
terminal. Use a fresh enrollment grant, or skip the attach command above and use
the worker command directly:
\`\`\`bash
disasmer-node --coordinator https://disasmer.michelpaulissen.com:9443 --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready
\`\`\`
Then run the flagship workflow from the filtered public repository while that
worker is still running:
\`\`\`bash
disasmer run --project examples/launch-build-demo build
\`\`\`
The dry-run public tree identity is \`${publicTreeIdentity}\`.
The release name is \`${releaseName}\`.
`,
"utf8"
);
return file;
}
function writeInviteAsset(releaseName, publicTreeIdentity, resolution) {
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_INVITE-${releaseName}.md`);
const publicRepo =
process.env.DISASMER_PUBLIC_REPO_URL ||
"https://git.michelpaulissen.com/<owner>/<public-repo>";
const releaseUrl =
process.env.DISASMER_FORGEJO_RELEASE_URL ||
"the Forgejo Release attached to the public repository";
fs.writeFileSync(
file,
`# Disasmer Public Dry Run Invite
This invite is for selected external users, such as friends helping test the
MVP release experience. The deployment is real and externally reachable, but it
is intentionally not broadly advertised yet.
The default operator behind this invite is the private hosted coordinator at
\`https://disasmer.michelpaulissen.com:9443\`. The public part is the Forgejo
repository, release downloads, network-reachable dry run, and public client
protocol used by the binaries; the hosted server is not the standalone public
coordinator.
If public DNS has not propagated yet, make sure \`disasmer.michelpaulissen.com\`
resolves to the deployment host before running the CLI.
## Links
- Public repository: ${publicRepo}
- Release downloads: ${releaseUrl}
- Default operator endpoint: ${defaultOperatorEndpoint}
- Public tree identity: ${publicTreeIdentity}
- Release name: ${releaseName}
## DNS
${resolution}
## First run
1. Download the binary archive for your platform and \`SHA256SUMS\` from the
Forgejo Release.
2. Extract the archive and put \`bin/\` on your \`PATH\`.
3. Optionally install \`disasmer-vscode-*.vsix\` with
\`code --install-extension disasmer-vscode-*.vsix\`.
4. Run \`disasmer login --browser\`; it opens the barebones
\`https://disasmer.michelpaulissen.com\` login site and returns to the CLI
through a local callback.
5. Enroll a node identity with the enrollment grant supplied out of band.
6. Start a long-lived worker with a fresh enrollment grant, or use the worker
command directly instead of step 5:
\`disasmer-node --coordinator https://disasmer.michelpaulissen.com:9443 --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready\`.
7. Run \`disasmer run --project examples/launch-build-demo build\` from the
public repository checkout while the worker process is still running.
`,
"utf8"
);
return file;
}
function writeSha256Sums(assets) {
const sumsPath = path.join(assetsDir, "SHA256SUMS");
const lines = assets
.map((asset) => `${sha256File(asset)} ${path.basename(asset)}`)
.sort()
.join("\n");
fs.writeFileSync(sumsPath, `${lines}\n`);
return sumsPath;
}
function boolEnv(name) {
return /^(1|true|yes)$/i.test(process.env[name] || "");
}
function writePublicTreeProvenance(sourceCommit, releaseName) {
const provenance = {
kind: "disasmer-filtered-public-tree",
source_commit: sourceCommit,
release_name: releaseName,
filtered_out: filteredOutPatterns(),
public_export: {
host_neutral: !includeForgejoWorkflows,
include_forgejo_workflows: includeForgejoWorkflows,
},
forgejo_host: forgejoHost,
default_operator_endpoint: defaultOperatorEndpoint,
};
fs.writeFileSync(
path.join(publicTree, "DISASMER_PUBLIC_TREE.json"),
`${JSON.stringify(provenance, null, 2)}\n`
);
}
function publishPublicTree(releaseName, sourceCommit, publicTreeIdentity) {
const remote =
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
process.env.DISASMER_PUBLIC_REPO_URL ||
null;
const enabled = boolEnv("DISASMER_PUBLISH_PUBLIC_TREE");
const result = {
enabled,
remote,
branch: publicRepoBranch,
commit: null,
pushed: false,
};
if (!enabled) {
return result;
}
if (!remote) {
throw new Error(
"DISASMER_PUBLISH_PUBLIC_TREE requires DISASMER_PUBLIC_REPO_REMOTE or DISASMER_PUBLIC_REPO_URL"
);
}
if (!remote.includes(forgejoHost)) {
throw new Error(`public repo remote must point at ${forgejoHost}: ${remote}`);
}
run("git", ["init"], { cwd: publicTree });
run("git", ["checkout", "-B", publicRepoBranch], { cwd: publicTree });
run("git", ["config", "user.name", "Disasmer release dry run"], {
cwd: publicTree,
});
run("git", ["config", "user.email", "release-dryrun@disasmer.invalid"], {
cwd: publicTree,
});
run("git", ["add", "."], { cwd: publicTree });
run(
"git",
[
"commit",
"-m",
`Public dry run ${releaseName}`,
"-m",
`Source commit: ${sourceCommit}`,
"-m",
`Public tree identity: ${publicTreeIdentity}`,
],
{ cwd: publicTree }
);
result.commit = commandOutput("git", ["rev-parse", "HEAD"], { cwd: publicTree });
run("git", ["remote", "add", "public", remote], { cwd: publicTree });
const pushArgs = ["push", "public", `HEAD:${publicRepoBranch}`];
if (boolEnv("DISASMER_PUBLIC_REPO_PUSH_FORCE_WITH_LEASE")) {
run(
"git",
[
"fetch",
"public",
`refs/heads/${publicRepoBranch}:refs/remotes/public/${publicRepoBranch}`,
],
{ cwd: publicTree }
);
pushArgs.splice(1, 0, "--force-with-lease");
}
run("git", pushArgs, { cwd: publicTree });
result.pushed = true;
return result;
}
function main() {
const sourceCommit =
process.env.DISASMER_ACCEPTANCE_COMMIT ||
commandOutput("git", ["rev-parse", "HEAD"]) ||
"unknown";
const shortCommit = sourceCommit === "unknown" ? "unknown" : sourceCommit.slice(0, 12);
const releaseName = process.env.DISASMER_PUBLIC_RELEASE_NAME || `dryrun-${shortCommit}`;
const sourceStatus = commandOutput("git", ["status", "--short"]);
const sourceTreeClean = sourceStatus === null ? null : sourceStatus === "";
fs.rmSync(outputRoot, { recursive: true, force: true });
ensureDir(publicTree);
ensureDir(assetsDir);
ensureDir(stagingDir);
copyFilteredTree(repo, publicTree);
assertFilteredTree();
writePublicTreeProvenance(sourceCommit, releaseName);
const publicTreeIdentity = hashTree(publicTree);
const sourceArchive = stageSourceAsset(releaseName);
run("node", ["scripts/public-release-dryrun-contract-smoke.js"], {
cwd: publicTree,
});
const publicTreePublish = publishPublicTree(
releaseName,
sourceCommit,
publicTreeIdentity
);
buildPublicBinaries();
const binaryArchive = stageBinaryAssets(releaseName);
const extensionArchive = stageExtensionAsset();
const resolution = resolverInstructions();
const gettingStarted = writeGettingStartedAsset(
releaseName,
publicTreeIdentity,
resolution
);
const invite = writeInviteAsset(releaseName, publicTreeIdentity, resolution);
const assets = [
sourceArchive,
binaryArchive,
extensionArchive,
gettingStarted,
invite,
];
const sha256Sums = writeSha256Sums(assets);
const manifest = {
kind: "disasmer-public-release-dryrun",
release_name: releaseName,
source_commit: sourceCommit,
source_tree_clean: sourceTreeClean,
public_tree_identity: publicTreeIdentity,
public_tree: publicTree,
filtered_out: filteredOutPatterns(),
public_export: {
host_neutral: !includeForgejoWorkflows,
include_forgejo_workflows: includeForgejoWorkflows,
},
forgejo_host: forgejoHost,
public_repo_url: process.env.DISASMER_PUBLIC_REPO_URL || publicTreePublish.remote,
public_repo_remote: process.env.DISASMER_PUBLIC_REPO_REMOTE || null,
public_tree_publish: publicTreePublish,
forgejo_release_url: process.env.DISASMER_FORGEJO_RELEASE_URL || null,
default_operator_endpoint: defaultOperatorEndpoint,
dns_publication_state:
process.env.DISASMER_DNS_PUBLICATION_STATE || "published",
resolver_override:
process.env.DISASMER_RESOLVER_OVERRIDE || "none-required-public-dns",
platform: platformName(),
tool_versions: {
node: process.version,
rustc: commandOutput("rustc", ["--version"]) || null,
cargo: commandOutput("cargo", ["--version"]) || null,
tar: commandOutput("tar", ["--version"]) || null,
},
commands: [
"node scripts/public-release-dryrun-contract-smoke.js",
...(publicTreePublish.enabled
? [`git push public HEAD:${publicRepoBranch}`]
: []),
"cargo build --workspace --bins --release",
],
assets: [...assets, sha256Sums].map((asset) => ({
file: asset,
name: path.basename(asset),
sha256: sha256File(asset),
})),
notes: [
"Upload the assets to the Forgejo Release for the filtered public repository.",
"The real service deployment and full e2e dry run remain separate acceptance evidence.",
],
};
const manifestPath = path.join(outputRoot, "public-release-manifest.json");
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(JSON.stringify({ manifest: manifestPath, assets: assetsDir }, null, 2));
}
main();

View file

@ -0,0 +1,71 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const infraRepo = path.resolve(repo, "..", "michelpaulissen.com");
function read(relativePath, base = repo) {
return fs.readFileSync(path.join(base, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing public browser login evidence: ${name}`);
}
const cliSource = read("crates/disasmer-cli/src/main.rs");
const cliSmoke = read("scripts/cli-browser-login-flow-smoke.js");
const publicAcceptance = read("scripts/acceptance-public.sh");
const phase2 = read("acceptance_criteria_phase2.md");
const base = read("acceptance_criteria.md");
for (const [name, source] of [
["base acceptance", base],
["phase 2 acceptance", phase2],
]) {
expect(source, `${name} released binary browser criterion`, /Public released binaries default to a real human browser\/account flow/);
}
expect(cliSource, "browser login start constant", /DEFAULT_BROWSER_LOGIN_START: &str = "https:\/\/disasmer\.michelpaulissen\.com\/auth\/browser\/start"/);
expect(cliSource, "default OIDC issuer", /DEFAULT_OIDC_ISSUER_URL: &str = "https:\/\/auth\.michelpaulissen\.com"/);
expect(cliSource, "fixed localhost callback", /BROWSER_CALLBACK_ADDR: &str = "127\.0\.0\.1:45173"/);
expect(cliSource, "diagnostic plan flag", /plan: bool/);
expect(cliSource, "interactive browser branch", /args\.browser && !args\.plan[\s\S]*execute_interactive_browser_login/);
expect(cliSource, "browser command override", /DISASMER_BROWSER_OPEN_COMMAND/);
expect(cliSource, "callback listener", /TcpListener::bind\(BROWSER_CALLBACK_ADDR\)/);
expect(cliSource, "callback completion", /execute_browser_login_completion_for_plan\(args, plan\)/);
expect(cliSmoke, "smoke uses fake browser opener", /DISASMER_BROWSER_OPEN_COMMAND/);
expect(cliSmoke, "smoke verifies callback code", /browser-smoke-code/);
expect(cliSmoke, "smoke verifies coordinator completion", /scoped_cli_session_received/);
expect(publicAcceptance, "public acceptance runs browser flow smoke", /node scripts\/cli-browser-login-flow-smoke\.js/);
if (fs.existsSync(infraRepo)) {
const stack = read("modules/stack.nix", infraRepo);
const hypervisor = read("hosts/hypervisor/default.nix", infraRepo);
const oauth = read("modules/oauth-bootstrap.nix", infraRepo);
const site = read("disasmer-site/index.html", infraRepo);
expect(stack, "Disasmer public host", /publicHost = "disasmer\.michelpaulissen\.com"/);
expect(stack, "Disasmer API port", /apiPort = 9443/);
expect(hypervisor, "nginx Disasmer vhost", /\$\{stack\.hosts\.disasmer\.publicHost\}/);
expect(hypervisor, "nginx Disasmer site root", /root = \.\.\/\.\.\/disasmer-site/);
expect(hypervisor, "nginx browser start route", /locations\."= \/auth\/browser\/start"\.return/);
expect(hypervisor, "nginx redirects to Authentik", /https:\/\/\$\{stack\.hosts\.authentik\.publicHost\}\/application\/o\/authorize/);
expect(hypervisor, "nginx passes state", /state=\$arg_state/);
expect(hypervisor, "nginx passes redirect URI", /redirect_uri=\$arg_redirect_uri/);
expect(hypervisor, "hosted service uses configured API port", /stack\.hosts\.disasmer\.apiPort/);
expect(oauth, "Disasmer Authentik provider", /name: disasmer-provider/);
expect(oauth, "Disasmer public OIDC client", /client_type: public/);
expect(oauth, "Disasmer client id", /client_id: \${disasmerClientId}/);
expect(oauth, "Disasmer localhost redirect", /http:\/\/127\.0\.0\.1:45173\/callback/);
expect(oauth, "Disasmer Authentik application", /slug: disasmer/);
expect(site, "barebones site describes CLI login", /disasmer login --browser/);
expect(site, "barebones site states UX deferral", /Layout and UX work are deferred/);
assert.doesNotMatch(site, /<style|stylesheet|\.css|style=|class=/i, "Disasmer dry-run site must remain barebones HTML with no CSS");
} else {
console.warn("Skipping VPS config checks because ../michelpaulissen.com is not present");
}
console.log("Public browser login contract smoke passed");

View file

@ -0,0 +1,69 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing public local demo evidence: ${name}`);
}
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const cliInstall = read("scripts/cli-install-smoke.js");
const flagship = read("scripts/flagship-demo-smoke.js");
const cliLocalRun = read("scripts/cli-local-run-smoke.js");
const nodeAttach = read("scripts/node-attach-smoke.js");
const vscodeExtension = read("scripts/vscode-extension-smoke.js");
const vscodeF5 = read("scripts/vscode-f5-smoke.js");
const artifactDownload = read("scripts/artifact-download-smoke.js");
const artifactExport = read("scripts/artifact-export-smoke.js");
for (const script of [publicAcceptance, publicSplit]) {
for (const smoke of [
"scripts/cli-install-smoke.js",
"scripts/flagship-demo-smoke.js",
"scripts/cli-local-run-smoke.js",
"scripts/node-attach-smoke.js",
"scripts/vscode-extension-smoke.js",
"scripts/vscode-f5-smoke.js",
"scripts/artifact-download-smoke.js",
"scripts/artifact-export-smoke.js",
]) {
assert(script.includes(`node ${smoke}`), `public demo gate must run ${smoke}`);
}
}
expect(cliInstall, "CLI install from project path", /cargo[\s\S]*install[\s\S]*crates\/disasmer-cli/);
expect(cliInstall, "CLI install smoke targets flagship project", /const project = path\.join\(repo, "examples\/launch-build-demo"\)/);
expect(cliInstall, "installed CLI inspects flagship project", /installedBin[\s\S]*\["bundle", "inspect", "--project", project\]/);
expect(flagship, "flagship project source is Rust workflow", /examples\/launch-build-demo[\s\S]*src\/build\.rs/);
expect(flagship, "flagship source avoids local machine assumptions", /forbiddenSourceAssumptions/);
expect(flagship, "flagship cargo test runs", /cargo[\s\S]*test[\s\S]*launch-build-demo/);
expect(cliLocalRun, "local run starts node process", /cli_process_started_node_process[\s\S]*true/);
expect(cliLocalRun, "local run records logs and metadata", /log_event_response[\s\S]*task_log_recorded[\s\S]*vfs_metadata_response[\s\S]*vfs_metadata_recorded/);
expect(cliLocalRun, "local run stages artifact", /\/vfs\/artifacts\/cli-run-output\.txt/);
expect(nodeAttach, "Linux node attach creates enrollment grant", /create_node_enrollment_grant/);
expect(nodeAttach, "Linux node attach uses enrollment exchange", /used_enrollment_exchange[\s\S]*true/);
expect(nodeAttach, "attached Linux node runs work", /runAttachedNodeWork[\s\S]*node_status[\s\S]*completed/);
expect(vscodeExtension, "extension contributes debugger", /contributes\.debuggers[\s\S]*type === "disasmer"/);
expect(vscodeF5, "F5 uses local-services backend", /runtimeBackend[\s\S]*local-services/);
expect(vscodeF5, "F5 exposes virtual thread", /compile linux[\s\S]*virtual thread/);
expect(vscodeF5, "F5 records coordinator task event", /coordinator_task_events[\s\S]*value === 1/);
expect(artifactDownload, "artifact download creates scoped link", /create_artifact_download_link/);
expect(artifactDownload, "artifact download opens stream", /open_artifact_download_stream/);
expect(artifactExport, "artifact export targets receiver node", /export_artifact_to_node[\s\S]*node-export-receiver/);
expect(artifactExport, "artifact export rejects coordinator bulk relay", /coordinator_bulk_relay_allowed[\s\S]*false/);
console.log("Public local demo matrix smoke passed");

View file

@ -0,0 +1,182 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function walk(relativePath, files = []) {
const fullPath = path.join(repo, relativePath);
if (!fs.existsSync(fullPath)) return files;
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
const base = path.basename(relativePath);
if (["target", "node_modules", ".git"].includes(base)) return files;
for (const entry of fs.readdirSync(fullPath)) {
walk(path.join(relativePath, entry), files);
}
return files;
}
files.push(relativePath);
return files;
}
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function copyPublicTree(sourceRoot, destinationRoot, relativePath = ".") {
const skippedDirectories = new Set([
".git",
"target",
"node_modules",
"private",
"experiments",
]);
const parts = relativePath.split(path.sep).filter(Boolean);
if (parts.some((part) => skippedDirectories.has(part))) {
return;
}
const source = path.join(sourceRoot, relativePath);
const destination = path.join(destinationRoot, relativePath);
const stat = fs.statSync(source);
if (stat.isDirectory()) {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source)) {
copyPublicTree(sourceRoot, destinationRoot, path.join(relativePath, entry));
}
return;
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
}
function assertPublicSplitTreeIsCoherent() {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-public-split-"));
try {
copyPublicTree(repo, tmp);
for (const forbidden of ["private", "experiments"]) {
assert(
!fs.existsSync(path.join(tmp, forbidden)),
`${forbidden} must be absent from the public split tree`
);
}
const workspaceToml = fs.readFileSync(path.join(tmp, "Cargo.toml"), "utf8");
const membersBlock = workspaceToml.match(/members\s*=\s*\[([\s\S]*?)\]/);
assert(membersBlock, "public workspace must define members");
const members = [...membersBlock[1].matchAll(/"([^"]+)"/g)].map(
(match) => match[1]
);
assert(members.length > 0, "public workspace must list members");
for (const member of members) {
assert(
!member.startsWith("private/") && !member.startsWith("experiments/"),
`public workspace member must not point at filtered source: ${member}`
);
assert(
fs.existsSync(path.join(tmp, member, "Cargo.toml")),
`public workspace member is missing after split: ${member}`
);
}
for (const required of [
"scripts/acceptance-public.sh",
"scripts/verify-public-split.sh",
"scripts/public-private-boundary-smoke.js",
"scripts/public-local-demo-matrix-smoke.js",
"vscode-extension/package.json",
"examples/launch-build-demo/Cargo.toml",
]) {
assert(
fs.existsSync(path.join(tmp, required)),
`public split tree is missing ${required}`
);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}
function publicSourceFiles() {
return [
...walk("crates"),
...walk("examples"),
...walk("scripts"),
...walk("vscode-extension"),
"Cargo.toml",
].filter((file) => {
if (file === "scripts/acceptance-private.sh") return false;
if (file === "scripts/acceptance-evidence-contract-smoke.js") return false;
if (file === "scripts/acceptance-environment-contract-smoke.js") return false;
if (file === "scripts/docs-smoke.js") return false;
if (file === "scripts/public-private-boundary-smoke.js") return false;
if (file === "scripts/release-blocker-smoke.js") return false;
return /\.(rs|toml|js|json|sh)$/.test(file);
});
}
const forbiddenPublicPatterns = [
/\bdisasmer[_-]hosted[_-]policy\b/,
/private\/hosted-policy/,
/path\s*=\s*["'][^"']*private\//,
/include!\s*\([^)]*private\//,
/mod\s+private_hosted/,
];
for (const file of publicSourceFiles()) {
const content = read(file);
for (const pattern of forbiddenPublicPatterns) {
assert(
!pattern.test(content),
`public source ${file} must not reference private hosted code via ${pattern}`
);
}
}
const workspace = read("Cargo.toml");
assert(
!/members\s*=\s*\[[\s\S]*private\//.test(workspace),
"public workspace members must not include private hosted crates"
);
assertPublicSplitTreeIsCoherent();
const privateHostedRoot = path.join(repo, "private", "hosted-policy");
if (fs.existsSync(privateHostedRoot)) {
const privateCargo = read("private/hosted-policy/Cargo.toml");
assert.match(privateCargo, /name\s*=\s*"disasmer-hosted-policy"/);
assert.match(privateCargo, /disasmer-core\s*=\s*\{/);
assert.match(privateCargo, /disasmer-coordinator\s*=\s*\{/);
const privateLib = read("private/hosted-policy/src/lib.rs");
for (const required of [
"AuthentikOidcConfig",
"CommunityTierPolicy",
"AdminControls",
"preflight_zero_capability_hosted_wasm",
"impl CapabilityPolicy for CommunityTierPolicy",
"AuthContext",
]) {
assert(
privateLib.includes(required),
`private hosted policy is missing ${required}`
);
}
const privateFiles = walk("private").filter((file) => !file.includes("/target/"));
const privateNodeRuntimeFiles = privateFiles.filter((file) =>
/(^|\/)(disasmer-node|node-runtime)(\/|$)/.test(file)
);
assert.deepStrictEqual(
privateNodeRuntimeFiles,
[],
"hosted mode must not carry a forked private node runtime"
);
}
console.log("Public/private boundary smoke passed");

View file

@ -0,0 +1,283 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443";
const serviceHost = "disasmer.michelpaulissen.com";
const forgejoHost = "git.michelpaulissen.com";
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(relativePath) {
const file = path.join(repo, relativePath);
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
}
function expect(source, name, pattern) {
assert(source !== null && source !== undefined, `missing source for ${name}`);
assert.match(source, pattern, `missing public release dry-run evidence: ${name}`);
}
const phase2 = maybeRead("acceptance_criteria_phase2.md");
const readme = maybeRead("README.md");
const cliSource = read("crates/disasmer-cli/src/main.rs");
const cliLoginSmoke = read("scripts/cli-login-smoke.js");
const vscodePackage = JSON.parse(read("vscode-extension/package.json"));
const vscodeExtension = read("vscode-extension/extension.js");
const vscodeSmoke = read("scripts/vscode-extension-smoke.js");
const prepScript = read("scripts/prepare-public-release-dryrun.js");
const publishScript = read("scripts/publish-public-release-dryrun.js");
const preflightScript = read("scripts/public-release-dryrun-preflight.js");
const e2eScript = read("scripts/public-release-dryrun-e2e.js");
const finalEvidenceScript = read("scripts/public-release-dryrun-final-evidence.js");
const workflow = maybeRead(".forgejo/workflows/public-release-dryrun.yml");
const publicAcceptance = read("scripts/acceptance-public.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
const publicSplit = read("scripts/verify-public-split.sh");
for (const [name, source] of [
["phase 2 criteria", phase2],
["README", readme],
]) {
if (source) {
expect(source, name, new RegExp(serviceHost.replaceAll(".", "\\.")));
expect(source, `${name} DNS publication state`, /DNS record|public DNS|dns[-_]publication/i);
expect(source, `${name} resolver fallback`, /no resolver override|required resolver override|hosts entry|controlled resolution|fallback/i);
expect(source, `${name} Forgejo host`, new RegExp(forgejoHost.replaceAll(".", "\\.")));
expect(source, `${name} Forgejo Release`, /Forgejo Release/);
expect(source, `${name} compiled assets`, /compiled\s+(release\s+)?assets|compiled release assets/);
expect(source, `${name} filtered tree`, /private\/\*\*[\s\S]*experiments\/\*\*/);
expect(source, `${name} root Markdown export filter`, /root `\*\.md`|root Markdown/);
expect(source, `${name} host-neutral workflow filter`, /\.forgejo[\s\S]*--include-forgejo-workflows/);
expect(source, `${name} GitHub release out of scope`, /GitHub[\s\S]*outside this dry run|public GitHub-release[\s\S]*out of scope/);
}
}
expect(cliSource, "CLI default operator constant", new RegExp(`DEFAULT_OPERATOR_ENDPOINT: &str = "${serviceEndpoint.replaceAll(".", "\\.")}"`));
expect(cliSource, "login uses default operator", /default_value_t = default_operator_endpoint\(\)/);
expect(cliSource, "hosted run records operator endpoint", /operator_endpoint[\s\S]*Some\(default_operator_endpoint\(\)\)/);
expect(cliSource, "hosted URL maps to JSON-line transport", /fn json_line_transport_addr[\s\S]*\("https:\/\/", 443\)/);
expect(cliSource, "JSON-line session uses mapped transport", /TcpStream::connect\(&transport_addr\)/);
expect(cliSource, "default operator maps to port 9443", /json_line_transport_addr\(DEFAULT_OPERATOR_ENDPOINT\)[\s\S]*"disasmer\.michelpaulissen\.com:9443"/);
assert.doesNotMatch(cliSource, /coord\.disasmer\.invalid/);
expect(cliLoginSmoke, "CLI login smoke covers default operator", new RegExp(`defaultOperatorEndpoint = "${serviceEndpoint.replaceAll(".", "\\.")}"`));
expect(vscodeExtension, "VS Code default operator constant", new RegExp(`DEFAULT_OPERATOR_ENDPOINT = "${serviceEndpoint.replaceAll(".", "\\.")}"`));
assert.strictEqual(
vscodePackage.contributes.debuggers[0].configurationAttributes.launch.properties.operatorEndpoint.default,
serviceEndpoint
);
expect(vscodeSmoke, "VS Code smoke covers operator endpoint", /operatorEndpoint\.default/);
for (const binary of [
"disasmer",
"disasmer-coordinator",
"disasmer-node",
"disasmer-debug-dap",
]) {
expect(prepScript, `release prep includes ${binary}`, new RegExp(`"${binary}"`));
}
for (const [name, pattern] of [
["release prep filters private and experiments", /filteredTopLevel = \["private", "experiments", "\.git", "target"\]/],
["release prep filters generated view state", /filteredDirectoryNames = \["\.disasmer"\]/],
["release prep filters root Markdown", /isRootMarkdown[\s\S]*path\.extname\(entry\.name\)\.toLowerCase\(\) === "\.md"/],
["release prep filters .forgejo by default", /topLevel === "\.forgejo" && !includeForgejoWorkflows/],
["release prep has Forgejo workflow opt-in argument", /--include-forgejo-workflows/],
["release prep builds public release bins", /cargo"[\s\S]*"build"[\s\S]*"--workspace"[\s\S]*"--bins"[\s\S]*"--release"/],
["release prep writes binary archive", /disasmer-public-binaries-\$\{releaseName\}-\$\{platformName\(\)\}\.tar\.gz/],
["release prep writes source archive", /disasmer-public-source-\$\{releaseName\}\.tar\.gz/],
["release prep writes extension VSIX", /\$\{packageJson\.name\}-\$\{packageJson\.version\}\.vsix[\s\S]*@vscode\/vsce[\s\S]*package/],
["release prep writes selected-user guide", /DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-\$\{releaseName\}\.md/],
["release prep writes selected-user invite", /DISASMER_PUBLIC_DRYRUN_INVITE-\$\{releaseName\}\.md/],
["release prep accepts resolver instructions", /DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS/],
["release prep accepts hosts entry", /DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY/],
["release prep accepts deployment IP", /DISASMER_PUBLIC_RELEASE_DRYRUN_IP/],
["selected-user guide documents DNS or fallback", /public DNS[\s\S]*(fallback|hosts entry)|DNS record[\s\S]*controlled resolution/],
["selected-user guide says private hosted coordinator", /default operator is the private hosted coordinator/],
["selected-user guide rejects standalone public coordinator", /standalone open-source\/public coordinator/],
["selected-user invite documents friends dry run", /friends helping test[\s\S]*not broadly advertised/],
["selected-user invite says hosted server is private", /hosted server is not the standalone public\s+coordinator/],
["selected-user guide documents default login", /disasmer login --browser/],
["selected-user guide documents VSIX install", /code --install-extension disasmer-vscode-\*\.vsix/],
["selected-user guide documents node attach", /disasmer node attach --coordinator https:\/\/disasmer\.michelpaulissen\.com:9443/],
["release prep writes checksums", /SHA256SUMS/],
["release prep writes manifest", /public-release-manifest\.json/],
["release prep writes public tree provenance", /DISASMER_PUBLIC_TREE\.json/],
["release prep records public tree identity", /public_tree_identity: publicTreeIdentity/],
["release prep records DNS state", /dns_publication_state:/],
["release prep records resolver override", /resolver_override:/],
["release prep records Forgejo URLs", /public_repo_url:[\s\S]*public_repo_remote:[\s\S]*forgejo_release_url:/],
["release prep requires publish opt-in", /DISASMER_PUBLISH_PUBLIC_TREE/],
["release prep requires Forgejo remote", /remote\.includes\(forgejoHost\)/],
["release prep can push filtered tree", /git"[\s\S]*"push"[\s\S]*"public"[\s\S]*`HEAD:\$\{publicRepoBranch\}`/],
["release prep records publish result", /public_tree_publish: publicTreePublish/],
["release prep publishes tree before building target", /const publicTreePublish = publishPublicTree[\s\S]*buildPublicBinaries\(\)/],
]) {
expect(prepScript, name, pattern);
}
for (const [name, pattern] of [
["release publisher requires Forgejo token", /DISASMER_FORGEJO_TOKEN/],
["release publisher infers Forgejo repo identity", /function resolveRepoIdentity\(manifest\)[\s\S]*parseForgejoRepoIdentity/],
["release publisher uses manifest public repo URL", /manifest\.public_repo_url[\s\S]*manifest\.public_repo_remote/],
["release publisher uses token auth", /Authorization: `token \$\{token\}`/],
["release publisher lists releases", /\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases\?limit=100/],
["release publisher creates release", /POST[\s\S]*\/repos\/\$\{encodeURIComponent\(owner\)\}\/\$\{encodeURIComponent\(repoName\)\}\/releases/],
["release publisher reloads release details", /function loadRelease\(releaseId\)[\s\S]*\/releases\/\$\{releaseId\}/],
["release publisher uploads assets", /releases\/\$\{release\.id\}\/assets\?name=\$\{encodeURIComponent\(asset\.name\)\}/],
["release publisher uses attachment multipart field", /multipartFile\("attachment", asset\.file\)/],
["release publisher skips already attached assets", /existingAssetByName\(release, asset\.name\)/],
["release publisher rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
["release publisher checks manifest kind", /manifest\.kind !== "disasmer-public-release-dryrun"/],
["release publisher requires public tree push", /public tree must be pushed before publishing the Forgejo Release/],
["release publisher writes evidence report", /public-release-dryrun-forgejo-release\.json/],
["release publisher records reused assets", /reused_assets:/],
]) {
expect(publishScript, name, pattern);
}
for (const [name, pattern] of [
["preflight rejects stale release manifests", /manifest\.source_commit[\s\S]*currentSourceCommit/],
["preflight verifies public branch commit", /remoteMain[\s\S]*manifest\.public_tree_publish\.commit/],
["preflight verifies local asset checksums", /parseSha256Sums[\s\S]*checksum mismatch/],
["preflight records pending external gates", /external_gates:[\s\S]*forgejo_release_publication[\s\S]*public_release_e2e/],
["preflight writes evidence report", /public-release-dryrun-preflight\.json/],
]) {
expect(preflightScript, name, pattern);
}
for (const [name, pattern] of [
["e2e runner requires explicit opt-in", /DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1/],
["e2e runner rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
["e2e runner downloads Forgejo Release assets", /downloadReleaseAssets/],
["e2e runner verifies SHA256SUMS", /verifyChecksums/],
["e2e runner clones public repo", /"git", \["clone", "--depth", "1"/],
["e2e runner rejects private tree leaks", /private[\s\S]*experiments[\s\S]*target/],
["e2e runner rejects root Markdown leaks", /root Markdown file leaked into public repo/],
["e2e runner rejects .forgejo leaks by default", /\.forgejo\/ leaked into public repo/],
["e2e runner rejects generated view state", /generated \.disasmer view state leaked into public repo/],
["e2e runner checks public tree identity", /hashTree\(checkout\)[\s\S]*manifest\.public_tree_identity/],
["e2e runner extracts public binaries", /tar"[\s\S]*"-xzf"/],
["e2e runner loads public coordinator binary", /executable\(installDir, "disasmer-coordinator"\)/],
["e2e runner checks default operator", /defaultLoginPlan\.coordinator[\s\S]*serviceEndpoint/],
["e2e runner completes browser login", /--complete-browser-code/],
["e2e runner uses public CLI node attach", /"node"[\s\S]*"attach"[\s\S]*serviceEndpoint/],
["e2e runner starts public worker runtime", /workerArgs[\s\S]*"--worker"[\s\S]*cp\.spawn\(disasmerNode/],
["e2e runner launches task through coordinator", /type: "launch_task"/],
["e2e runner verifies assignment polling", /worker_assignment_poll_verified/],
["e2e runner validates standalone public coordinator", /validateStandalonePublicCoordinator/],
["e2e runner records public coordinator validation", /public_coordinator_validated/],
["e2e runner validates released live DAP", /validateReleasedLiveDap/],
["e2e runner starts live DAP worker after coordinator-side breakpoint", /worker_started_after_coordinator_side_breakpoint/],
["e2e runner verifies task events", /type: "list_task_events"/],
["e2e runner verifies download link", /type: "create_artifact_download_link"/],
["e2e runner verifies VS Code debugger", /scripts\/vscode-f5-smoke\.js/],
["e2e runner writes e2e report", /public-release-dryrun-e2e\.json/],
]) {
expect(e2eScript, name, pattern);
}
for (const [name, pattern] of [
["final evidence reads manifest", /public-release-manifest\.json/],
["final evidence rejects stale release manifests", /manifest\.source_commit[\s\S]*expectedSourceCommit\(\)/],
["final evidence verifies filtered export", /function assertFilteredOut\(manifest\)/],
["final evidence reads Forgejo Release report", /public-release-dryrun-forgejo-release\.json/],
["final evidence reads deployment manifest", /deployment-manifest\.json/],
["final evidence reads service smoke report", /public-release-dryrun-service\.json/],
["final evidence reads public operator compatibility report", /public-operator-compat\.json/],
["final evidence reads public coordinator compatibility report", /public-coordinator-compat\.json/],
["final evidence requires public e2e report", /public-release-dryrun-e2e\.json/],
["final evidence verifies Forgejo host", /forgejoHost = "git\.michelpaulissen\.com"/],
["final evidence verifies default operator", /serviceEndpoint = "https:\/\/disasmer\.michelpaulissen\.com:9443"/],
["final evidence requires private hosted coordinator", /operator_implementation[\s\S]*"private-hosted-coordinator"/],
["final evidence requires service private coordinator marker", /service\.operator_implementation[\s\S]*"private-hosted-coordinator"/],
["final evidence requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/],
["final evidence requires current service smoke release", /service\.release_name[\s\S]*manifest\.release_name/],
["final evidence requires current public operator source", /compat\.source_commit[\s\S]*manifest\.source_commit/],
["final evidence requires current public operator release", /compat\.release_name[\s\S]*manifest\.release_name/],
["final evidence requires current public coordinator source", /publicCoordinator\.source_commit[\s\S]*manifest\.source_commit/],
["final evidence requires current public coordinator release", /publicCoordinator\.release_name[\s\S]*manifest\.release_name/],
["final evidence requires service launch task", /service\.evidence\.public_launch_task[\s\S]*"task_launched"/],
["final evidence requires launch task", /launch_task_response[\s\S]*"task_launched"/],
["final evidence requires assignment polling", /worker_assignment_poll_verified/],
["final evidence requires public coordinator validation", /public_coordinator_validated/],
["final evidence requires standalone public coordinator", /standalone-public-coordinator/],
["final evidence requires released live DAP", /released_live_dap_verified/],
["final evidence requires pushed public tree", /public_tree_publish[\s\S]*pushed/],
["final evidence requires release assets", /downloaded_release_assets/],
["final evidence requires VS Code debugger", /vscode_debugger_verified/],
["final evidence writes final report", /public-release-dryrun-final\.json/],
]) {
expect(finalEvidenceScript, name, pattern);
}
if (workflow) {
for (const [name, pattern] of [
["manual Forgejo workflow", /workflow_dispatch:/],
["Linux Forgejo asset job", /linux-assets:[\s\S]*runs-on: docker/],
["Windows Forgejo asset job", /windows-assets:[\s\S]*runs-on: windows/],
["Windows runner caveat", /intermittently online/],
["workflow runs release prep", /node scripts\/prepare-public-release-dryrun\.js/],
["workflow uploads assets", /actions\/upload-artifact@v4[\s\S]*target\/public-release-dryrun\/assets\/\*/],
]) {
expect(workflow, name, pattern);
}
}
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
assert(
script.includes("node scripts/public-release-dryrun-contract-smoke.js"),
`${scriptName} must run public-release-dryrun-contract-smoke.js`
);
assert(
script.includes("node scripts/prepare-public-release-dryrun.js"),
`${scriptName} must run prepare-public-release-dryrun.js`
);
assert(
script.includes("node scripts/publish-public-release-dryrun.js"),
`${scriptName} must be able to publish the Forgejo Release`
);
assert(
script.includes("DISASMER_FORGEJO_TOKEN"),
`${scriptName} must gate Forgejo Release publishing on a token`
);
assert(
script.includes('if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]'),
`${scriptName} must not require owner/repo env when the manifest can infer the Forgejo repository`
);
}
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["private acceptance", privateAcceptance],
]) {
if (scriptName === "public acceptance") {
assert(
script.includes("node scripts/public-release-dryrun-e2e.js"),
"public acceptance must be able to run public-release-dryrun-e2e.js"
);
assert(
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E"),
"public acceptance must gate public-release-dryrun-e2e.js"
);
}
assert(
script.includes("node scripts/public-release-dryrun-final-evidence.js"),
`${scriptName} must be able to run public-release-dryrun-final-evidence.js`
);
assert(
script.includes("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL"),
`${scriptName} must gate final public release dry-run evidence`
);
}
console.log("Public release dry-run contract smoke passed");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,440 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const releaseRoot = path.resolve(
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
path.join(repo, "target/public-release-dryrun")
);
const acceptanceRoot = path.join(repo, "target/acceptance");
const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443";
const serviceHost = "disasmer.michelpaulissen.com";
const serviceAddr = `${serviceHost}:9443`;
const forgejoHost = "git.michelpaulissen.com";
const inputs = {
manifest: process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
path.join(releaseRoot, "public-release-manifest.json"),
forgejoRelease: process.env.DISASMER_PUBLIC_RELEASE_FORGEJO_REPORT ||
path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json"),
deployment: process.env.DISASMER_PUBLIC_RELEASE_DEPLOYMENT_MANIFEST ||
path.join(releaseRoot, "deployment/stage/deployment-manifest.json"),
service: process.env.DISASMER_PUBLIC_RELEASE_SERVICE_REPORT ||
path.join(acceptanceRoot, "public-release-dryrun-service.json"),
publicOperatorCompat: process.env.DISASMER_PUBLIC_OPERATOR_COMPAT_REPORT ||
path.join(acceptanceRoot, "public-operator-compat.json"),
publicCoordinatorCompat: process.env.DISASMER_PUBLIC_COORDINATOR_COMPAT_REPORT ||
path.join(acceptanceRoot, "public-coordinator-compat.json"),
e2e: process.env.DISASMER_PUBLIC_RELEASE_E2E_REPORT ||
path.join(acceptanceRoot, "public-release-dryrun-e2e.json"),
};
function readJson(name, file) {
assert(fs.existsSync(file), `missing ${name} evidence: ${file}`);
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function assertIncludes(value, expected, message) {
assert(
typeof value === "string" && value.includes(expected),
`${message}: expected ${JSON.stringify(value)} to include ${expected}`
);
}
function assetNames(manifest) {
assert(Array.isArray(manifest.assets), "manifest assets must be an array");
return new Set(manifest.assets.map((asset) => asset.name));
}
function uploadedAssetNames(report) {
return new Set(
[...(report.uploaded_assets || []), ...(report.reused_assets || [])].map(
(asset) => asset.name
)
);
}
function assertEvidenceBooleans(report, fields) {
for (const field of fields) {
assert.strictEqual(report[field], true, `e2e report must set ${field}=true`);
}
}
function compactToolVersions(...reports) {
const versions = {};
for (const [name, report] of reports) {
if (report && report.tool_versions) {
versions[name] = report.tool_versions;
}
}
return versions;
}
function assertDnsState(value, name) {
assert(
["not-published", "published"].includes(value),
`${name} has unexpected DNS publication state: ${value}`
);
}
function assertFilteredOut(manifest) {
assert(Array.isArray(manifest.filtered_out), "manifest filtered_out must be an array");
for (const expected of [
"private/**",
"experiments/**",
".git",
"target",
"/*.md",
"**/.disasmer/**",
]) {
assert(
manifest.filtered_out.includes(expected),
`manifest filtered_out must include ${expected}`
);
}
const includeForgejoWorkflows =
manifest.public_export && manifest.public_export.include_forgejo_workflows;
if (!includeForgejoWorkflows) {
assert(
manifest.filtered_out.includes(".forgejo/**"),
"manifest filtered_out must include .forgejo/** for the host-neutral export"
);
}
}
function commandOutput(command, args) {
try {
return require("child_process")
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
})
.trim();
} catch (_) {
return null;
}
}
function expectedSourceCommit() {
return (
process.env.DISASMER_ACCEPTANCE_COMMIT ||
commandOutput("git", ["rev-parse", "HEAD"]) ||
"unknown"
);
}
const manifest = readJson("public release manifest", inputs.manifest);
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
assert.strictEqual(
manifest.source_commit,
expectedSourceCommit(),
"final public release evidence must be generated from the current acceptance commit"
);
assert.strictEqual(manifest.default_operator_endpoint, serviceEndpoint);
assert.strictEqual(manifest.forgejo_host, forgejoHost);
assertDnsState(manifest.dns_publication_state, "manifest");
assert(manifest.resolver_override, "manifest must record resolver override");
assertFilteredOut(manifest);
assert.strictEqual(
manifest.public_tree_publish && manifest.public_tree_publish.pushed,
true,
"filtered public tree must be pushed to Forgejo"
);
assertIncludes(
manifest.public_repo_url || manifest.public_repo_remote || "",
forgejoHost,
"manifest public repository must point at Forgejo"
);
const manifestAssets = assetNames(manifest);
for (const pattern of [
/^disasmer-public-source-/,
/^disasmer-public-binaries-/,
/^disasmer-vscode-/,
/^DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-/,
/^DISASMER_PUBLIC_DRYRUN_INVITE-/,
/^SHA256SUMS$/,
]) {
assert(
[...manifestAssets].some((name) => pattern.test(name)),
`manifest is missing asset matching ${pattern}`
);
}
const forgejoRelease = readJson("Forgejo Release report", inputs.forgejoRelease);
assert.strictEqual(
forgejoRelease.kind,
"disasmer-public-release-dryrun-forgejo-release"
);
assertIncludes(forgejoRelease.forgejo_url, forgejoHost, "Forgejo Release host");
assert.strictEqual(forgejoRelease.release_name, manifest.release_name);
assert.strictEqual(
forgejoRelease.default_operator_endpoint,
manifest.default_operator_endpoint
);
assert.strictEqual(
forgejoRelease.public_tree_identity,
manifest.public_tree_identity
);
assert.strictEqual(forgejoRelease.source_commit, manifest.source_commit);
const releaseAssets = uploadedAssetNames(forgejoRelease);
for (const asset of manifestAssets) {
assert(releaseAssets.has(asset), `Forgejo Release is missing asset ${asset}`);
}
const deployment = readJson("deployment manifest", inputs.deployment);
assert.strictEqual(deployment.kind, "disasmer-public-release-dryrun-deployment");
assert.strictEqual(deployment.default_operator_endpoint, serviceEndpoint);
assert.strictEqual(deployment.operator_implementation, "private-hosted-coordinator");
assert.match(
deployment.public_release_meaning || "",
/public repository[\s\S]*public release assets[\s\S]*public client protocol/
);
assert.strictEqual(deployment.service_host, serviceHost);
assert.strictEqual(deployment.service_addr, serviceAddr);
assertDnsState(deployment.dns_publication_state, "deployment");
assert(deployment.resolver_override, "deployment manifest must record resolver override");
assert(deployment.deployed_service_commit, "deployment must record deployed service commit");
const service = readJson("live service smoke report", inputs.service);
assert.strictEqual(service.kind, "disasmer-public-release-dryrun-service");
assert.strictEqual(service.source_commit, manifest.source_commit);
assert.strictEqual(service.release_name, manifest.release_name);
assert.strictEqual(service.endpoint, serviceEndpoint);
assert.strictEqual(service.service_addr, serviceAddr);
assert.strictEqual(service.operator_implementation, "private-hosted-coordinator");
assertDnsState(service.dns_publication_state, "service smoke");
assert(service.resolver_override, "service smoke must record resolver override");
assert.strictEqual(
service.public_tree_identity || manifest.public_tree_identity,
manifest.public_tree_identity
);
assert.strictEqual(
service.deployed_service_commit || deployment.deployed_service_commit,
deployment.deployed_service_commit
);
for (const key of [
"ping",
"login",
"project",
"node_credential",
"heartbeat",
"public_worker_credential",
"public_worker_capabilities",
"public_launch_task",
"public_assignment_poll",
"public_worker_completion",
"process",
"task",
"debug",
"artifact_metadata",
"download_action",
"download_link",
"observability",
]) {
assert(service.evidence && service.evidence[key], `service smoke missing ${key}`);
}
assert.strictEqual(service.evidence.public_launch_task, "task_launched");
assert.strictEqual(service.evidence.public_assignment_poll, "task_assignment");
assert.strictEqual(service.evidence.public_worker_completion, "task_recorded");
if (service.acceptance_result) {
assert.strictEqual(service.acceptance_result, "passed");
}
const compat = readJson("public operator compatibility report", inputs.publicOperatorCompat);
assert.strictEqual(compat.kind, "disasmer-public-operator-compatibility");
assert.strictEqual(compat.source_commit, manifest.source_commit);
assert.strictEqual(compat.release_name, manifest.release_name);
assert.strictEqual(
compat.public_cli_attach.coordinator_response,
"node_enrollment_exchanged"
);
assert.strictEqual(compat.public_cli_attach.heartbeat_response, "node_heartbeat");
assert.strictEqual(compat.public_launch_task.process_started_response, "process_started");
assert.strictEqual(compat.public_launch_task.launch_response, "task_launched");
assert.strictEqual(compat.public_node_runtime.node_status, "completed");
assert.strictEqual(compat.public_node_runtime.task_assignment_response, "task_assignment");
assert.strictEqual(
compat.public_node_runtime.task_assignment_node,
compat.public_launch_task.assignment_node
);
assert.strictEqual(
compat.public_node_runtime.coordinator_response,
"task_recorded"
);
assert(compat.public_task_events >= 1, "public operator compat must record task events");
const publicCoordinator = readJson(
"public coordinator compatibility report",
inputs.publicCoordinatorCompat
);
assert.strictEqual(publicCoordinator.kind, "disasmer-public-coordinator-compatibility");
assert.strictEqual(publicCoordinator.source_commit, manifest.source_commit);
assert.strictEqual(publicCoordinator.release_name, manifest.release_name);
assert.strictEqual(
publicCoordinator.operator_implementation,
"standalone-public-coordinator"
);
assert.strictEqual(publicCoordinator.task_placement, "task_placement");
assert.strictEqual(publicCoordinator.task_completion, "task_recorded");
assert(publicCoordinator.task_events >= 1, "public coordinator compat must record task events");
assert.strictEqual(publicCoordinator.artifact_export_plan, "artifact_export_plan");
const e2e = readJson("public repository e2e report", inputs.e2e);
assert.strictEqual(e2e.kind, "disasmer-public-release-dryrun-e2e");
assert.strictEqual(e2e.default_operator_endpoint, serviceEndpoint);
assert.strictEqual(e2e.service_addr, serviceAddr);
assert.strictEqual(e2e.launch_task_verified, true);
assert.strictEqual(e2e.worker_assignment_poll_verified, true);
assert.strictEqual(e2e.worker_assignment_poll_protocol, "poll_task_assignment");
assert.strictEqual(e2e.launch_task_response, "task_launched");
assert.strictEqual(e2e.worker_assignment_process, e2e.process);
assert.strictEqual(e2e.public_coordinator_validated, true);
assert.strictEqual(
e2e.public_coordinator_operator_implementation,
"standalone-public-coordinator"
);
assert.strictEqual(e2e.public_coordinator_launch_task_response, "task_launched");
assert.strictEqual(e2e.public_coordinator_assignment_response, "task_assignment");
assert(e2e.public_coordinator_task_events >= 1, "e2e must record public coordinator task events");
assert.strictEqual(e2e.released_live_dap_verified, true);
assert.strictEqual(
e2e.released_live_dap_operator_implementation,
"private-hosted-coordinator"
);
assert.strictEqual(e2e.released_live_dap_worker_after_launch, true);
assert(e2e.released_live_dap_task_events >= 1, "e2e must record released live-DAP task events");
assert(
e2e.evidence &&
e2e.evidence.released_live_dap &&
e2e.evidence.released_live_dap.coordinator_side_started_without_worker === true,
"e2e must prove released DAP starts the coordinator-side process before a worker is attached"
);
assert(
e2e.evidence &&
e2e.evidence.released_live_dap &&
e2e.evidence.released_live_dap.variables_verified === true,
"e2e must prove released live-DAP variables are inspectable"
);
if (e2e.dns_publication_state) {
assertDnsState(e2e.dns_publication_state, "public repository e2e");
}
if (e2e.acceptance_result) {
assert.strictEqual(e2e.acceptance_result, "passed");
}
assertIncludes(e2e.public_repository_url, forgejoHost, "e2e public repo URL");
assert.strictEqual(e2e.release_name, manifest.release_name);
assert.strictEqual(e2e.public_tree_identity, manifest.public_tree_identity);
assert.strictEqual(e2e.source_commit, manifest.source_commit);
assertEvidenceBooleans(e2e, [
"downloaded_release_assets",
"verified_checksums",
"clean_public_checkout",
"public_repo_build_or_install",
"default_operator_selected",
"browser_or_cli_login",
"attached_user_node",
"public_coordinator_validated",
"released_live_dap_verified",
"ran_flagship_workflow",
"vscode_debugger_verified",
"logs_verified",
"artifact_metadata_verified",
"artifact_download_or_export_verified",
]);
assert(Array.isArray(e2e.commands) && e2e.commands.length > 0);
assert(Array.isArray(e2e.tool_versions) || typeof e2e.tool_versions === "object");
const finalReport = {
kind: "disasmer-public-release-dryrun-final-evidence",
release_name: manifest.release_name,
source_commit: manifest.source_commit,
public_tree_identity: manifest.public_tree_identity,
deployed_service_commit: deployment.deployed_service_commit,
default_operator_endpoint: serviceEndpoint,
service_addr: serviceAddr,
dns_publication_state: {
manifest: manifest.dns_publication_state,
deployment: deployment.dns_publication_state,
service: service.dns_publication_state,
public_repository_e2e: e2e.dns_publication_state || null,
},
resolver_override: {
manifest: manifest.resolver_override,
deployment: deployment.resolver_override,
service: service.resolver_override,
public_repository_e2e: e2e.resolver_override || null,
},
deployment_config: {
repo: service.deployment_config_repo || null,
commit: service.deployment_config_commit || null,
system_generation: service.deployment_system_generation || null,
service_unit: service.service_unit || null,
},
coordinator_validation: {
private_hosted_default_operator: {
operator_implementation: service.operator_implementation,
service_addr: service.service_addr,
launch_task: service.evidence.public_launch_task,
assignment_poll: service.evidence.public_assignment_poll,
released_live_dap: e2e.evidence.released_live_dap,
},
standalone_public_coordinator: {
operator_implementation: publicCoordinator.operator_implementation,
task_placement: publicCoordinator.task_placement,
task_events: publicCoordinator.task_events,
release_binary_e2e: {
launch_task: e2e.public_coordinator_launch_task_response,
assignment_poll: e2e.public_coordinator_assignment_response,
task_events: e2e.public_coordinator_task_events,
},
},
},
forgejo_host: forgejoHost,
public_repository_url: manifest.public_repo_url,
forgejo_release: {
owner: forgejoRelease.owner,
repo: forgejoRelease.repo,
release_id: forgejoRelease.release_id,
asset_count: releaseAssets.size,
},
tool_versions: compactToolVersions(
["manifest", manifest],
["service_smoke", service],
["public_repository_e2e", e2e]
),
acceptance_results: {
public_release_preparation: {
commands: manifest.commands,
result: "passed",
},
deployment_bundle: {
commands: deployment.commands,
result: "passed",
},
live_service_smoke: {
command: service.acceptance_command || null,
result: service.acceptance_result || "passed",
evidence: service.evidence,
},
public_coordinator_compatibility: {
result: "passed",
evidence: publicCoordinator,
},
public_repository_e2e: {
commands: e2e.commands,
result: "passed",
evidence: e2e.evidence,
},
},
evidence_files: inputs,
};
const output = path.join(acceptanceRoot, "public-release-dryrun-final.json");
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, `${JSON.stringify(finalReport, null, 2)}\n`);
console.log(`Public release dry-run final evidence passed: ${output}`);

View file

@ -0,0 +1,247 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const releaseRoot = path.resolve(
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
path.join(repo, "target/public-release-dryrun")
);
const acceptanceRoot = path.join(repo, "target/acceptance");
const manifestPath =
process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
path.join(releaseRoot, "public-release-manifest.json");
const reportPath =
process.env.DISASMER_PUBLIC_RELEASE_PREFLIGHT_REPORT ||
path.join(acceptanceRoot, "public-release-dryrun-preflight.json");
function commandOutput(command, args, options = {}) {
try {
return cp
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
...options,
})
.trim();
} catch (_) {
return null;
}
}
function expectedSourceCommit() {
return (
process.env.DISASMER_ACCEPTANCE_COMMIT ||
commandOutput("git", ["rev-parse", "HEAD"]) ||
"unknown"
);
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function sha256File(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
function parseSha256Sums(file) {
const sums = new Map();
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
if (!line.trim()) continue;
const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line.trim());
assert(match, `malformed SHA256SUMS line: ${line}`);
sums.set(path.basename(match[2]), match[1]);
}
return sums;
}
function remoteHead(remote) {
const output = commandOutput("git", ["ls-remote", remote, "HEAD", "refs/heads/main"]);
if (!output) return null;
const lines = output.split(/\r?\n/).filter(Boolean);
const head = lines.find((line) => line.endsWith("\tHEAD")) || lines[0];
return head && head.split(/\s+/)[0];
}
function envState(name) {
return process.env[name] ? "set" : "unset";
}
function staleEvidence(file, currentSourceCommit) {
if (!fs.existsSync(file)) {
return { file, status: "missing", source_commit: null, release_name: null };
}
const evidence = readJson(file);
if (!evidence.source_commit) {
return {
file,
status: "unversioned",
source_commit: null,
release_name: evidence.release_name || null,
};
}
return {
file,
status: evidence.source_commit === currentSourceCommit ? "current" : "stale",
source_commit: evidence.source_commit,
release_name: evidence.release_name || null,
};
}
assert(fs.existsSync(manifestPath), `missing public release manifest: ${manifestPath}`);
const manifest = readJson(manifestPath);
const currentSourceCommit = expectedSourceCommit();
const currentTreeStatus = commandOutput("git", ["status", "--short"]) || "";
assert.strictEqual(
currentTreeStatus,
"",
"public release preflight requires a clean source tree"
);
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
assert.strictEqual(
manifest.source_commit,
currentSourceCommit,
"public release manifest must be regenerated for the current acceptance commit"
);
assert.strictEqual(manifest.source_tree_clean, true, "public release prep must start clean");
assert.strictEqual(
manifest.public_tree_publish && manifest.public_tree_publish.pushed,
true,
"filtered public tree must be pushed to Forgejo before release publication"
);
const publicRepoRemote = manifest.public_repo_url || manifest.public_repo_remote;
assert(publicRepoRemote, "manifest must record public repository URL or remote");
const remoteMain = remoteHead(publicRepoRemote);
assert.strictEqual(
remoteMain,
manifest.public_tree_publish.commit,
"Forgejo public repository main branch must match the prepared public tree commit"
);
assert(Array.isArray(manifest.assets) && manifest.assets.length > 0, "manifest assets missing");
const checksumAsset = manifest.assets.find((asset) => asset.name === "SHA256SUMS");
assert(checksumAsset, "manifest must include SHA256SUMS");
assert(fs.existsSync(checksumAsset.file), `missing checksum asset: ${checksumAsset.file}`);
const checksums = parseSha256Sums(checksumAsset.file);
const assets = manifest.assets.map((asset) => {
assert(fs.existsSync(asset.file), `missing release asset: ${asset.file}`);
const actual = sha256File(asset.file);
const expected = checksums.get(asset.name);
if (asset.name !== "SHA256SUMS") {
assert.strictEqual(actual, expected, `checksum mismatch for ${asset.name}`);
}
return {
name: asset.name,
file: asset.file,
bytes: fs.statSync(asset.file).size,
sha256: actual,
};
});
const evidence = [
staleEvidence(
path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json"),
currentSourceCommit
),
staleEvidence(
path.join(acceptanceRoot, "public-release-dryrun-service.json"),
currentSourceCommit
),
staleEvidence(
path.join(acceptanceRoot, "public-operator-compat.json"),
currentSourceCommit
),
staleEvidence(
path.join(acceptanceRoot, "public-coordinator-compat.json"),
currentSourceCommit
),
staleEvidence(
path.join(acceptanceRoot, "public-release-dryrun-e2e.json"),
currentSourceCommit
),
staleEvidence(
path.join(acceptanceRoot, "public-release-dryrun-final.json"),
currentSourceCommit
),
];
const report = {
kind: "disasmer-public-release-dryrun-preflight",
source_commit: currentSourceCommit,
release_name: manifest.release_name,
public_tree_commit: manifest.public_tree_publish.commit,
public_repo_url: publicRepoRemote,
public_repo_remote_head: remoteMain,
source_tree_clean: currentTreeStatus === "",
local_assets_ready: true,
assets,
evidence,
external_gates: {
forgejo_release_publication: {
status: envState("DISASMER_FORGEJO_TOKEN") === "set" ? "ready" : "pending",
env: {
DISASMER_FORGEJO_TOKEN: envState("DISASMER_FORGEJO_TOKEN"),
},
},
live_service_smoke: {
status:
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR") === "set" &&
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL") === "set" &&
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE") === "set"
? "ready"
: "pending",
env: {
DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR: envState(
"DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR"
),
DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL: envState(
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL"
),
DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE: envState(
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE"
),
},
},
public_release_e2e: {
status:
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_E2E") === "set" &&
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E === "1" &&
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL") === "set" &&
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE") === "set"
? "ready"
: "pending",
env: {
DISASMER_PUBLIC_RELEASE_DRYRUN_E2E:
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E || "unset",
DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL: envState(
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL"
),
DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE: envState(
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE"
),
},
},
final_evidence: {
status:
envState("DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL") === "set" &&
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL === "1"
? "ready"
: "pending",
env: {
DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL:
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_FINAL || "unset",
},
},
},
};
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(JSON.stringify({ report: reportPath, release_name: report.release_name }, null, 2));

View file

@ -0,0 +1,99 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing public-story evidence: ${name}`);
}
const readme = read("README.md");
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const cliLocalRunSmoke = read("scripts/cli-local-run-smoke.js");
const dapSmoke = read("scripts/dap-smoke.js");
const flagshipDemoSmoke = read("scripts/flagship-demo-smoke.js");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const artifactExportSmoke = read("scripts/artifact-export-smoke.js");
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
assert(
script.includes("node scripts/public-story-contract-smoke.js"),
`${scriptName} must run public-story-contract-smoke.js`
);
}
for (const [name, pattern] of [
[
"README states public story",
/one virtual process, many virtual threads\/tasks, ordinary debugger controls, attached user nodes, and explicit artifact handling/,
],
[
"README states local-first bytes policy",
/local source checkouts and large outputs stay node-local unless user code or policy explicitly moves bytes/,
],
[
"README states normal debugger controls",
/ordinary debugger\s+controls for breakpoints, continue, pause, and restart/,
],
]) {
expect(readme, name, pattern);
}
for (const [name, pattern] of [
["CLI local run starts a node process", /cli_process_started_node_process/],
["CLI local run checks node is separate from CLI", /assert\.notStrictEqual\(report\.boundary\.spawned_node_process_id, cliPid\)/],
["CLI local run checks node is separate from coordinator", /assert\.notStrictEqual\(report\.boundary\.spawned_node_process_id, coordinator\.pid\)/],
["CLI local run records one node task event", /assert\.strictEqual\(events\.events\.length, 1\)/],
["CLI local run records artifact metadata", /assert\.strictEqual\(events\.events\[0\]\.artifact_path, "\/vfs\/artifacts\/cli-run-output\.txt"\)/],
["CLI local-only run starts coordinator", /cli_process_started_coordinator_process[\s\S]*true/],
["CLI local-only run hides coordinator address requirement", /\["run"[\s\S]*"--local"[\s\S]*"--project"[\s\S]*project/],
]) {
expect(cliLocalRunSmoke, name, pattern);
}
for (const [name, pattern] of [
["DAP smoke uses local-services runtime", /runtimeBackend: "local-services"/],
["DAP smoke exposes one target with four virtual threads", /threads\.map\(\(thread\) => thread\.id\)[\s\S]*\[1, 2, 3, 4\]/],
["DAP smoke binds main breakpoint", /assert\.match\(mainStack\[0\]\.name, \/build virtual process::run\/\)/],
["DAP smoke binds Linux task breakpoint", /assert\.match\(stack\[0\]\.name, \/compile linux::run\/\)/],
["DAP smoke all-stops on breakpoint", /assert\.strictEqual\(stopped\.body\.allThreadsStopped, true\)/],
["DAP smoke supports pause all-stop", /assert\.strictEqual\(paused\.body\.allThreadsStopped, true\)/],
["DAP smoke supports selected restart", /Restarted selected task/],
["DAP smoke supports failed task restart", /Restarted failed task/],
["DAP smoke avoids native child debugger claims", /doesNotMatch\(stack\[0\]\.name, \/podman\|cmd\\\.exe\|powershell\|pid\|native child\/i\)/],
["DAP smoke crosses coordinator-node boundary", /coordinator_task_events[\s\S]*value === 1/],
]) {
expect(dapSmoke, name, pattern);
}
for (const [name, pattern] of [
["flagship source does not need coordinator checkout", /coordinator_requires_checkout_access[\s\S]*false/],
["flagship source bytes remain node-local", /local_source_bytes_remain_node_local[\s\S]*true/],
["flagship avoids default source upload", /coordinator_receives_source_bytes_by_default[\s\S]*false/],
["flagship avoids default repo tarball", /default_full_repo_tarball[\s\S]*false/],
]) {
expect(flagshipDemoSmoke, name, pattern);
}
for (const [name, source, pattern] of [
["artifact download creates scoped link", artifactDownloadSmoke, /create_artifact_download_link/],
["artifact download records retained-node source", artifactDownloadSmoke, /assert\.deepStrictEqual\(link\.link\.source, \{ RetainedNode: "node-download" \}\)/],
["artifact download opens scoped stream", artifactDownloadSmoke, /open_artifact_download_stream/],
["artifact export targets attached receiver node", artifactExportSmoke, /export_artifact_to_node[\s\S]*node-export-receiver/],
["artifact export disables coordinator bulk relay", artifactExportSmoke, /coordinator_bulk_relay_allowed[\s\S]*false/],
]) {
expect(source, name, pattern);
}
console.log("Public story contract smoke passed");

View file

@ -0,0 +1,336 @@
#!/usr/bin/env node
const fs = require("fs");
const https = require("https");
const path = require("path");
const cp = require("child_process");
const repo = path.resolve(__dirname, "..");
const releaseRoot = path.resolve(
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
path.join(repo, "target/public-release-dryrun")
);
const manifestPath = path.join(releaseRoot, "public-release-manifest.json");
const reportPath = path.join(
repo,
"target/acceptance/public-release-dryrun-forgejo-release.json"
);
const forgejoUrl = (
process.env.DISASMER_FORGEJO_URL || "https://git.michelpaulissen.com"
).replace(/\/+$/, "");
const token = process.env.DISASMER_FORGEJO_TOKEN;
let owner = process.env.DISASMER_PUBLIC_REPO_OWNER;
let repoName = process.env.DISASMER_PUBLIC_REPO_NAME;
function requireEnv(name, value) {
if (!value) {
throw new Error(`${name} is required`);
}
}
function commandOutput(command, args) {
try {
return cp
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
})
.trim();
} catch (_) {
return null;
}
}
function expectedSourceCommit() {
return (
process.env.DISASMER_ACCEPTANCE_COMMIT ||
commandOutput("git", ["rev-parse", "HEAD"]) ||
"unknown"
);
}
function parseForgejoRepoIdentity(remote) {
if (!remote) {
return null;
}
let pathname = remote;
try {
pathname = new URL(remote).pathname;
} catch (_) {
const scpLike = /^[^@/]+@[^:]+:(.+)$/.exec(remote);
if (scpLike) {
pathname = scpLike[1];
}
}
const parts = pathname
.replace(/^\/+/, "")
.replace(/\.git$/, "")
.split("/")
.filter(Boolean);
if (parts.length < 2) {
return null;
}
return {
owner: parts[parts.length - 2],
repoName: parts[parts.length - 1],
};
}
function resolveRepoIdentity(manifest) {
if (owner && repoName) {
return;
}
const inferred = parseForgejoRepoIdentity(
manifest.public_repo_url ||
manifest.public_repo_remote ||
process.env.DISASMER_PUBLIC_REPO_REMOTE
);
owner = owner || (inferred && inferred.owner);
repoName = repoName || (inferred && inferred.repoName);
if (!owner || !repoName) {
throw new Error(
"DISASMER_PUBLIC_REPO_OWNER and DISASMER_PUBLIC_REPO_NAME are required when the manifest does not contain a parseable Forgejo repository URL"
);
}
}
function apiPath(pathname) {
return `/api/v1${pathname}`;
}
function request(method, pathname, { body, headers = {} } = {}) {
const url = new URL(apiPath(pathname), forgejoUrl);
const payload =
body === undefined
? null
: Buffer.isBuffer(body)
? body
: Buffer.from(JSON.stringify(body));
const requestHeaders = {
Accept: "application/json",
Authorization: `token ${token}`,
...headers,
};
if (payload) {
requestHeaders["Content-Length"] = payload.length;
if (!requestHeaders["Content-Type"]) {
requestHeaders["Content-Type"] = "application/json";
}
}
return new Promise((resolve, reject) => {
const req = https.request(
url,
{
method,
headers: requestHeaders,
},
(res) => {
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
let parsed = null;
if (text.trim()) {
try {
parsed = JSON.parse(text);
} catch (_) {
parsed = text;
}
}
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(
new Error(
`${method} ${url.pathname} failed with ${res.statusCode}: ${text}`
)
);
return;
}
resolve({ status: res.statusCode, body: parsed });
});
}
);
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
function multipartFile(fieldName, file) {
const boundary = `disasmer-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const name = path.basename(file);
const header = Buffer.from(
`--${boundary}\r\n` +
`Content-Disposition: form-data; name="${fieldName}"; filename="${name}"\r\n` +
"Content-Type: application/octet-stream\r\n\r\n"
);
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
return {
body: Buffer.concat([header, fs.readFileSync(file), footer]),
contentType: `multipart/form-data; boundary=${boundary}`,
};
}
async function existingRelease(tagName) {
const releases = await request(
"GET",
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases?limit=100`
);
return (releases.body || []).find((release) => release.tag_name === tagName) || null;
}
async function createOrReuseRelease(manifest) {
const tagName = manifest.release_name;
const found = await existingRelease(tagName);
if (found) {
return { release: found, created: false };
}
const targetCommitish =
(manifest.public_tree_publish && manifest.public_tree_publish.commit) ||
process.env.DISASMER_PUBLIC_RELEASE_TARGET ||
"main";
const body = [
"Disasmer public release dry run.",
"",
`Default operator endpoint: ${manifest.default_operator_endpoint}`,
`DNS publication state: ${manifest.dns_publication_state}`,
`Resolver override: ${manifest.resolver_override}`,
`Public tree identity: ${manifest.public_tree_identity}`,
`Source commit: ${manifest.source_commit}`,
].join("\n");
const created = await request(
"POST",
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases`,
{
body: {
tag_name: tagName,
target_commitish: targetCommitish,
name: tagName,
body,
draft: false,
prerelease: true,
},
}
);
return { release: created.body, created: true };
}
async function loadRelease(releaseId) {
const response = await request(
"GET",
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases/${releaseId}`
);
return response.body;
}
async function uploadAsset(release, asset) {
const { body, contentType } = multipartFile("attachment", asset.file);
const response = await request(
"POST",
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}/releases/${release.id}/assets?name=${encodeURIComponent(asset.name)}`,
{
body,
headers: {
"Content-Type": contentType,
},
}
);
return response.body;
}
function existingAssetByName(release, name) {
return Array.isArray(release.assets)
? release.assets.find((asset) => asset.name === name) || null
: null;
}
async function main() {
requireEnv("DISASMER_FORGEJO_TOKEN", token);
if (!fs.existsSync(manifestPath)) {
throw new Error(`missing public release manifest: ${manifestPath}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
resolveRepoIdentity(manifest);
if (manifest.kind !== "disasmer-public-release-dryrun") {
throw new Error(`unexpected public release manifest kind: ${manifest.kind}`);
}
if (manifest.source_commit !== expectedSourceCommit()) {
throw new Error(
"public release manifest is stale; regenerate it for the current acceptance commit before publishing the Forgejo Release"
);
}
const publicTreeAlreadyPushed =
process.env.DISASMER_PUBLIC_TREE_ALREADY_PUSHED === "1";
if (
!publicTreeAlreadyPushed &&
(!manifest.public_tree_publish || manifest.public_tree_publish.pushed !== true)
) {
throw new Error(
"public tree must be pushed before publishing the Forgejo Release; run prepare-public-release-dryrun.js with DISASMER_PUBLISH_PUBLIC_TREE=1"
);
}
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
throw new Error("public release manifest has no assets");
}
const { release: releaseResult, created } = await createOrReuseRelease(manifest);
const release = await loadRelease(releaseResult.id);
const uploaded = [];
const reused = [];
for (const asset of manifest.assets) {
if (!fs.existsSync(asset.file)) {
throw new Error(`missing release asset: ${asset.file}`);
}
const existing = existingAssetByName(release, asset.name);
if (existing) {
reused.push(existing);
continue;
}
uploaded.push(await uploadAsset(release, asset));
}
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
const report = {
kind: "disasmer-public-release-dryrun-forgejo-release",
forgejo_url: forgejoUrl,
owner,
repo: repoName,
release_id: release.id,
release_name: release.name || manifest.release_name,
tag_name: release.tag_name || manifest.release_name,
release_created: created,
default_operator_endpoint: manifest.default_operator_endpoint,
public_tree_identity: manifest.public_tree_identity,
source_commit: manifest.source_commit,
uploaded_assets: uploaded.map((asset) => ({
id: asset.id,
name: asset.name,
size: asset.size,
browser_download_url: asset.browser_download_url,
})),
reused_assets: reused.map((asset) => ({
id: asset.id,
name: asset.name,
size: asset.size,
browser_download_url: asset.browser_download_url,
})),
};
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(
JSON.stringify(
{ report: reportPath, uploaded: uploaded.length, reused: reused.length },
null,
2
)
);
}
main().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

152
scripts/quic-smoke.js Executable file
View file

@ -0,0 +1,152 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function rendezvousRequest(overrides = {}) {
return {
type: "request_rendezvous",
scope: {
tenant: "tenant",
project: "project",
process: "vp-quic",
object: { Artifact: "quic-artifact" },
authorization_subject: "node-a-to-node-b",
},
source: {
node: "node-a",
advertised_addr: "node-a.mesh.invalid:4433",
public_key_fingerprint: "sha256:node-a-public-key",
},
destination: {
node: "node-b",
advertised_addr: "node-b.mesh.invalid:4433",
public_key_fingerprint: "sha256:node-b-public-key",
},
direct_connectivity: true,
failure_reason: "",
...overrides,
};
}
(async () => {
const output = cp.execFileSync(
"cargo",
["run", "-q", "-p", "disasmer-node", "--bin", "disasmer-quic-smoke"],
{ cwd: repo, encoding: "utf8" }
);
const report = JSON.parse(output.trim().split("\n").at(-1));
assert.strictEqual(report.kind, "disasmer_quic_smoke");
assert.strictEqual(report.transport, "NativeQuic");
assert.strictEqual(report.rust_native_quic, true);
assert.strictEqual(report.authenticated_direct_connection, true);
assert.strictEqual(report.coordinator_assisted_rendezvous, true);
assert.strictEqual(report.coordinator_bulk_relay_allowed, false);
assert.strictEqual(report.source_node, "node-a");
assert.strictEqual(report.destination_node, "node-b");
assert.strictEqual(report.scope.tenant, "tenant");
assert.strictEqual(report.scope.project, "project");
assert.strictEqual(report.scope.process, "vp-quic");
assert.deepStrictEqual(report.scope.object, { Artifact: "quic-artifact" });
assert.ok(report.authorization_digest.startsWith("sha256:"));
assert.ok(report.request_bytes > 0);
assert.strictEqual(report.server_received_request_bytes, report.request_bytes);
assert.ok(report.payload_bytes > 0);
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0",
],
{ cwd: repo }
);
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
const plan = await send(addr, rendezvousRequest());
assert.strictEqual(plan.type, "rendezvous_plan");
assert.strictEqual(plan.charged_rendezvous_attempts, 1);
assert.strictEqual(plan.plan.transport, "NativeQuic");
assert.strictEqual(plan.plan.scope.tenant, "tenant");
assert.strictEqual(plan.plan.scope.project, "project");
assert.strictEqual(plan.plan.source.node, "node-a");
assert.strictEqual(plan.plan.destination.node, "node-b");
assert.strictEqual(plan.plan.coordinator_assisted_rendezvous, true);
assert.strictEqual(plan.plan.coordinator_bulk_relay_allowed, false);
assert.ok(plan.plan.authorization_digest.startsWith("sha256:"));
const failed = await send(
addr,
rendezvousRequest({
direct_connectivity: false,
failure_reason: "nat traversal failed",
})
);
assert.strictEqual(failed.type, "error");
assert.match(failed.message, /nat traversal failed/);
assert.match(failed.message, /coordinator bulk relay is disabled/);
} finally {
coordinator.kill("SIGTERM");
}
console.log("QUIC smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

190
scripts/release-blocker-smoke.js Executable file
View file

@ -0,0 +1,190 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(relativePath) {
const fullPath = path.join(repo, relativePath);
if (!fs.existsSync(fullPath)) return null;
return fs.readFileSync(fullPath, "utf8");
}
function section(source, heading) {
const marker = `## ${heading}`;
const start = source.indexOf(marker);
assert(start >= 0, `missing section ${marker}`);
const next = source.indexOf("\n## ", start + marker.length);
return source.slice(start, next >= 0 ? next : source.length);
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing release-blocker evidence: ${name}`);
}
const hiddenDemoBlockerPattern = new RegExp(
[
"flagship demo requires",
["undocumented", "manual state"].join(" "),
["hard-coded", "local paths"].join(" "),
["demo-only", "credentials"].join(" "),
["hidden", "setup"].join(" "),
].join("[\\s\\S]*")
);
const hiddenDemoScanPattern = new RegExp(
`demo_setup_pattern='${[
["undocumented", "manual state"].join(" "),
["hidden", "setup"].join(" "),
["demo-only", "credentials?"].join(" "),
["hard-coded", "local paths?"].join(" "),
]
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("\\\\|")}'`
);
const phase2 = read("acceptance_criteria_phase2.md");
const base = read("acceptance_criteria.md");
const publicAcceptance = read("scripts/acceptance-public.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
const hostedCommunitySmoke = maybeRead("private/hosted-policy/scripts/hosted-community-smoke.js");
const releaseSourceScan = read("scripts/release-source-scan.sh");
const flagshipDemoSmoke = read("scripts/flagship-demo-smoke.js");
const releaseBlockers = section(phase2, "20. Release blockers");
expect(
releaseBlockers,
"cross-tenant access is listed as a release blocker",
/Cross-tenant access succeeds for projects, nodes, processes, logs, artifacts, downloads, debug state, panels, capabilities, source manifests, credentials, or metadata/
);
expect(
releaseBlockers,
"manual-state flagship blocker is listed",
hiddenDemoBlockerPattern
);
expect(
section(base, "21. Authorization and tenant isolation"),
"base criteria require tenant isolation failures to block release",
/Tenant isolation failures are treated as release blockers/
);
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
for (const smoke of [
"scripts/artifact-download-smoke.js",
"scripts/operator-panel-smoke.js",
"scripts/source-preparation-smoke.js",
"scripts/scheduler-placement-smoke.js",
"scripts/flagship-demo-smoke.js",
]) {
assert(
script.includes(`node ${smoke}`),
`${scriptName} must run ${smoke} as part of tenant-isolation release blocking`
);
}
assert(
script.includes("scripts/release-source-scan.sh"),
`${scriptName} must run release-source-scan.sh as part of release blocking`
);
}
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-community-smoke.js"),
"private acceptance must run hosted community cross-tenant checks"
);
assert(
privateAcceptance.includes("node private/hosted-policy/scripts/hosted-deployment-smoke.js"),
"private acceptance must run hosted deployment checks"
);
const boundaryEvidence = [
[
"artifact download",
artifactDownloadSmoke,
[/const crossTenant = await send/, /const crossTenantOpen = await send/, /tenant mismatch/],
],
[
"operator panel",
operatorPanelSmoke,
[/const crossTenant = await send/, /render_operator_panel/, /scope\|tenant\|project/],
],
[
"source preparation",
sourcePreparationSmoke,
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
],
[
"scheduler/node capability",
schedulerSmoke,
[/const crossTenantReport = await send/, /report_node_capabilities/, /tenant\\\/project scope/],
],
];
if (hostedCommunitySmoke) {
boundaryEvidence.push([
"hosted community",
hostedCommunitySmoke,
[
/const foreignAgentList = await send/,
/const crossTenantMetadata = await send/,
/const crossTenantDownload = await send/,
/tenant mismatch/,
],
]);
}
for (const [name, source, patterns] of boundaryEvidence) {
for (const pattern of patterns) {
expect(source, name, pattern);
}
}
for (const [name, pattern] of [
["release source scan rejects manual demo state", hiddenDemoScanPattern],
["release source scan rejects hidden local paths", /hidden_local_pattern='file:\/\/\|\/home\/\[.*\]_.-\]\+\/\|\/Users\/\[.*\]_.-\]\+\/\|C:\\\\Users\\\\\|https\?:\/\/\(localhost\|127\\\.0\\\.0\\\.1\)/],
]) {
expect(releaseSourceScan, name, pattern);
}
for (const [name, pattern] of [
["public split excludes private modules", /--exclude='\.\/private'/],
["public split excludes experiments", /--exclude='\.\/experiments'/],
["public split tests copied workspace", /cargo test --workspace --manifest-path "\$tmp_dir\/Cargo\.toml"/],
["public split builds copied workspace bins", /cargo build --workspace --bins --manifest-path "\$tmp_dir\/Cargo\.toml"/],
["public split installs CLI from copied tree", /\(cd "\$tmp_dir" && node scripts\/cli-install-smoke\.js\)/],
["public split installs VS Code extension from copied tree", /\(cd "\$tmp_dir" && node scripts\/vscode-extension-smoke\.js\)/],
["public split attaches node from copied tree", /\(cd "\$tmp_dir" && node scripts\/node-attach-smoke\.js\)/],
["public split runs local services from copied tree", /\(cd "\$tmp_dir" && node scripts\/local-services-smoke\.js\)/],
["public split runs CLI local workflow from copied tree", /\(cd "\$tmp_dir" && node scripts\/cli-local-run-smoke\.js\)/],
["public split runs artifact download from copied tree", /\(cd "\$tmp_dir" && node scripts\/artifact-download-smoke\.js\)/],
["public split runs artifact export from copied tree", /\(cd "\$tmp_dir" && node scripts\/artifact-export-smoke\.js\)/],
["public split runs DAP smoke from copied tree", /\(cd "\$tmp_dir" && node scripts\/dap-smoke\.js\)/],
["public split runs flagship demo smoke from copied tree", /\(cd "\$tmp_dir" && node scripts\/flagship-demo-smoke\.js\)/],
]) {
expect(publicSplit, name, pattern);
}
for (const [name, pattern] of [
["flagship demo rejects local machine assumptions", /forbiddenSourceAssumptions/],
["flagship demo rejects coordinator checkout access", /coordinator_requires_checkout_access[\s\S]*false/],
["flagship demo asserts local source bytes stay node-local", /local_source_bytes_remain_node_local[\s\S]*true/],
["flagship demo asserts coordinator receives no source bytes by default", /coordinator_receives_source_bytes_by_default[\s\S]*false/],
["flagship demo asserts no default full repo tarball", /default_full_repo_tarball[\s\S]*false/],
]) {
expect(flagshipDemoSmoke, name, pattern);
}
console.log("Release blocker smoke passed");

71
scripts/release-source-scan.sh Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo"
release_paths=(
Cargo.toml
Cargo.lock
README.md
crates
examples
private
scripts
vscode-extension
)
existing_release_paths=()
for path in "${release_paths[@]}"; do
if [[ -e "$path" ]]; then
existing_release_paths+=("$path")
fi
done
prose_scan_paths=(
README.md
crates
examples
private
scripts
vscode-extension
)
existing_prose_scan_paths=()
for path in "${prose_scan_paths[@]}"; do
if [[ -e "$path" ]]; then
existing_prose_scan_paths+=("$path")
fi
done
scan_globs=(
--glob '!**/target/**'
--glob '!**/node_modules/**'
--glob '!scripts/release-source-scan.sh'
)
placeholder_pattern='debugger-gate|experiments/debugger-gate|DISASMER-DEMO|device-code-placeholder|artifact://demo|vp-local-demo'
if rg -n "${scan_globs[@]}" "$placeholder_pattern" "${existing_release_paths[@]}"; then
echo "release source scan failed: stale experiment/demo placeholder reference found" >&2
exit 1
fi
demo_setup_pattern='undocumented manual state|hidden setup|demo-only credentials?|hard-coded local paths?'
if rg -n "${scan_globs[@]}" "$demo_setup_pattern" "${existing_prose_scan_paths[@]}"; then
echo "release source scan failed: demo requires hidden setup or demo-only state" >&2
exit 1
fi
hidden_local_pattern='file://|/home/[[:alnum:]_.-]+/|/Users/[[:alnum:]_.-]+/|C:\\Users\\|https?://(localhost|127\.0\.0\.1)[^[:space:]]*/artifacts/'
if rg -n "${scan_globs[@]}" "$hidden_local_pattern" "${existing_release_paths[@]}"; then
echo "release source scan failed: hidden local path or local artifact URL found" >&2
exit 1
fi
public_wording_pattern='reddit|hacker news|lobsters|launch forum|traffic source|free tier'
if rg -n "${scan_globs[@]}" "$public_wording_pattern" "${existing_prose_scan_paths[@]}"; then
echo "release source scan failed: public-facing launch-forum/free-tier wording found" >&2
exit 1
fi
echo "Release source scan passed"

View file

@ -0,0 +1,166 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(segments) {
const fullPath = path.join(repo, ...segments);
if (!fs.existsSync(fullPath)) return null;
return fs.readFileSync(fullPath, "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing resource metering evidence: ${name}`);
}
function expectGate(script, gateName) {
assert(
script.includes("node scripts/resource-metering-contract-smoke.js"),
`${gateName} must run resource-metering-contract-smoke.js`
);
}
const coreLimits = read("crates/disasmer-core/src/limits.rs");
const coordinatorService = read("crates/disasmer-coordinator/src/service.rs");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
const quicSmoke = read("scripts/quic-smoke.js");
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
for (const [name, pattern] of [
["API call limit kind", /LimitKind::ApiCall/],
["spawn limit kind", /LimitKind::Spawn/],
["log bytes limit kind", /LimitKind::LogBytes/],
["metadata bytes limit kind", /LimitKind::MetadataBytes/],
["debug read bytes limit kind", /LimitKind::DebugReadBytes/],
["UI event limit kind", /LimitKind::UiEvent/],
["rendezvous attempt limit kind", /LimitKind::RendezvousAttempt/],
["artifact download bytes limit kind", /LimitKind::ArtifactDownloadBytes/],
["hosted fuel limit kind", /LimitKind::HostedFuel/],
[
"preflight can check without consuming",
/pub fn can_charge\([\s\S]*used\.saturating_add\(amount\) > limit/,
],
[
"charge goes through preflight",
/pub fn charge\([\s\S]*self\.can_charge\(limits, kind\.clone\(\), amount\)\?/,
],
]) {
expect(coreLimits, name, pattern);
}
for (const [name, pattern] of [
[
"rendezvous charges before transport planning",
/CoordinatorRequest::RequestRendezvous[\s\S]*rendezvous_meter\.charge\([\s\S]*LimitKind::RendezvousAttempt[\s\S]*plan_authenticated_direct_bulk_transfer/,
],
[
"artifact link creation preflights downloadable bytes before link creation",
/CoordinatorRequest::CreateArtifactDownloadLink[\s\S]*downloadable_size[\s\S]*download_meter\.can_charge\([\s\S]*LimitKind::ArtifactDownloadBytes[\s\S]*create_download_link/,
],
[
"artifact stream opening and chunks charge download bytes",
/CoordinatorRequest::OpenArtifactDownloadStream[\s\S]*open_download_stream\([\s\S]*&mut self\.download_meter[\s\S]*stream_download_chunk\([\s\S]*charged_download_bytes/,
],
]) {
expect(coordinatorService, name, pattern);
}
for (const [name, source, patterns] of [
[
"rendezvous smoke",
quicSmoke,
[/charged_rendezvous_attempts, 1/, /coordinator bulk relay is disabled/],
],
[
"artifact download smoke",
artifactDownloadSmoke,
[
/charged_download_bytes, 16/,
/revoked/,
],
],
[
"operator panel smoke",
operatorPanelSmoke,
[
/type: "submit_panel_event"/,
/max_events: 1/,
/used_events, 1/,
/rate limit/i,
/max_download_bytes: 1/,
/exceeds download limit/,
],
],
]) {
for (const pattern of patterns) {
expect(source, name, pattern);
}
}
expectGate(publicAcceptance, "public acceptance");
expectGate(publicSplit, "public split acceptance");
expectGate(privateAcceptance, "private acceptance");
const privateHostedLib = maybeRead(["private", "hosted-policy", "src", "lib.rs"]);
const privateHostedSmoke = maybeRead([
"private",
"hosted-policy",
"scripts",
"hosted-community-smoke.js",
]);
if (privateHostedLib && privateHostedSmoke) {
for (const [name, pattern] of [
[
"hosted API authorization meters API calls before policy decisions",
/fn authorize\([\s\S]*LimitKind::ApiCall[\s\S]*policy\.decide/,
],
[
"hosted spawn preflights before process start",
/pub fn start_user_node_process\([\s\S]*LimitKind::Spawn[\s\S]*let active = self\.coordinator\.start_process/,
],
[
"hosted zero-capability Wasm preflights through trial meter",
/preflight_zero_capability_hosted_wasm\([\s\S]*let mut trial_meter = self\.meter\.clone\(\)[\s\S]*LimitKind::HostedFuel[\s\S]*LimitKind::HostedMemoryBytes[\s\S]*LimitKind::HostedWallClockMs[\s\S]*LimitKind::HostedStateBytes[\s\S]*LimitKind::LogBytes[\s\S]*LimitKind::MetadataBytes[\s\S]*LimitKind::UiEvent[\s\S]*LimitKind::ApiCall[\s\S]*self\.meter = trial_meter/,
],
[
"hosted log recording preflights log bytes before storing logs",
/record_user_node_task_completion\([\s\S]*LimitKind::LogBytes[\s\S]*self\.logs\.push/,
],
[
"hosted debug reads preflight before inspection is created",
/debug_process\([\s\S]*LimitKind::DebugReadBytes[\s\S]*DebugEpoch::pause/,
],
[
"hosted tests cover each zero-capability Wasm budget",
/hosted_zero_capability_wasm_preflight_rejects_each_budget_over_limit\([\s\S]*LimitKind::HostedFuel[\s\S]*LimitKind::LogBytes[\s\S]*LimitKind::MetadataBytes[\s\S]*LimitKind::UiEvent[\s\S]*LimitKind::ApiCall/,
],
]) {
expect(privateHostedLib, name, pattern);
}
for (const [name, pattern] of [
["running service denies hosted native compute", /hosted_native_command_preflight/],
["running service rejects non-zero hosted capabilities", /required_capabilities: \["Network"\]/],
["running service rejects hosted fuel overage", /resource limit exceeded.*HostedFuel/i],
["running service rejects spawn quota before retry succeeds", /quotaExhausted[\s\S]*resource limit exceeded.*Spawn/i],
["spawn quota survives fresh client retry", /quotaRetryFromFreshClient[\s\S]*resource limit exceeded.*Spawn/i],
["spawn quota survives node reconnect", /quotaAfterNodeReconnect[\s\S]*resource limit exceeded.*Spawn/i],
["running service rejects over-limit logs", /resource limit exceeded.*LogBytes/i],
["running service exposes scoped debug inspection", /type: "debug_process"[\s\S]*debug_inspection/],
]) {
expect(privateHostedSmoke, name, pattern);
}
}
console.log("Resource metering contract smoke passed");

View file

@ -0,0 +1,303 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function linuxCapabilities() {
return {
os: "Linux",
arch: "x86_64",
capabilities: [
"Command",
"Containers",
"RootlessPodman",
"VfsArtifacts"
],
environment_backends: ["Container"],
source_providers: ["filesystem"]
};
}
async function attachNode(addr, node) {
const attached = await send(addr, {
type: "attach_node",
tenant: "tenant",
project: "project",
node,
public_key: `${node}-public-key`
});
assert.strictEqual(attached.type, "node_attached");
assert.strictEqual(attached.node, node);
}
async function reportNode(addr, node, locality) {
const recorded = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node,
capabilities: linuxCapabilities(),
cached_environment_digests: locality.cached_environment_digests,
dependency_cache_digests: locality.dependency_cache_digests,
source_snapshots: locality.source_snapshots,
artifact_locations: locality.artifact_locations,
direct_connectivity: locality.direct_connectivity !== false,
online: true
});
assert.strictEqual(recorded.type, "node_capabilities_recorded");
assert.strictEqual(recorded.node, node);
return recorded;
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
await attachNode(addr, "cold-node");
await attachNode(addr, "warm-node");
const cold = await reportNode(addr, "cold-node", {
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: false
});
assert.strictEqual(cold.node_descriptors, 1);
const warm = await reportNode(addr, "warm-node", {
cached_environment_digests: ["sha256:env-linux-container"],
dependency_cache_digests: ["sha256:deps-toolchain"],
source_snapshots: ["sha256:source-tree"],
artifact_locations: ["toolchain-cache"]
});
assert.strictEqual(warm.node_descriptors, 2);
const reportedNodes = new Set([cold.node, warm.node]);
assert.strictEqual(reportedNodes.size, 2);
assert(reportedNodes.has("cold-node"));
assert(reportedNodes.has("warm-node"));
const inspected = await send(addr, {
type: "list_node_descriptors",
tenant: "tenant",
project: "project",
actor_user: "operator"
});
assert.strictEqual(inspected.type, "node_descriptors");
assert.strictEqual(inspected.actor, "operator");
assert.strictEqual(inspected.descriptors.length, 2);
const warmDescriptor = inspected.descriptors.find(
(descriptor) => descriptor.id === "warm-node"
);
assert(warmDescriptor, "warm node descriptor must be visible to inspector state");
assert(warmDescriptor.capabilities.capabilities.includes("Command"));
assert(warmDescriptor.capabilities.capabilities.includes("RootlessPodman"));
assert(warmDescriptor.cached_environments.includes("sha256:env-linux-container"));
assert(warmDescriptor.dependency_caches.includes("sha256:deps-toolchain"));
assert(warmDescriptor.source_snapshots.includes("sha256:source-tree"));
assert(warmDescriptor.artifact_locations.includes("toolchain-cache"));
const crossScopeInspection = await send(addr, {
type: "list_node_descriptors",
tenant: "other-tenant",
project: "project",
actor_user: "operator"
});
assert.strictEqual(crossScopeInspection.type, "node_descriptors");
assert.strictEqual(crossScopeInspection.descriptors.length, 0);
const crossTenantReport = await send(addr, {
type: "report_node_capabilities",
tenant: "other-tenant",
project: "project",
node: "warm-node",
capabilities: linuxCapabilities(),
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: true,
online: true
});
assert.strictEqual(crossTenantReport.type, "error");
assert.match(crossTenantReport.message, /tenant\/project scope/);
const placement = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: {
os: "Linux",
arch: null,
capabilities: ["Containers", "RootlessPodman"]
},
environment_digest: "sha256:env-linux-container",
required_capabilities: ["Command"],
dependency_cache: "sha256:deps-toolchain",
source_snapshot: "sha256:source-tree",
required_artifacts: ["toolchain-cache"],
prefer_node: null
});
assert.strictEqual(placement.type, "task_placement");
assert.strictEqual(placement.placement.node, "warm-node");
assert.ok(placement.placement.score > 0);
assert.ok(placement.placement.reasons.includes("warm environment cache"));
assert.ok(placement.placement.reasons.includes("warm dependency cache"));
assert.ok(placement.placement.reasons.includes("source snapshot already local"));
assert.ok(
placement.placement.reasons.includes("1 required artifact(s) already local")
);
const impossible = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: null,
environment_digest: null,
required_capabilities: ["WindowsCommandDev"],
dependency_cache: null,
source_snapshot: null,
required_artifacts: [],
prefer_node: null
});
assert.strictEqual(impossible.type, "error");
assert.match(impossible.message, /WindowsCommandDev/);
const quotaDenied = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: null,
environment_digest: null,
required_capabilities: ["Command"],
dependency_cache: null,
source_snapshot: null,
required_artifacts: [],
quota_available: false,
policy_allowed: true,
prefer_node: null
});
assert.strictEqual(quotaDenied.type, "error");
assert.match(quotaDenied.message, /quota unavailable for placement/);
const policyDenied = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: null,
environment_digest: null,
required_capabilities: ["Command"],
dependency_cache: null,
source_snapshot: null,
required_artifacts: [],
quota_available: true,
policy_allowed: false,
prefer_node: null
});
assert.strictEqual(policyDenied.type, "error");
assert.match(policyDenied.message, /policy denied placement/);
await reportNode(addr, "warm-node", {
cached_environment_digests: [],
dependency_cache_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: false
});
const disconnectedTransfer = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: null,
environment_digest: null,
required_capabilities: ["Command"],
dependency_cache: null,
source_snapshot: "sha256:source-tree",
required_artifacts: ["toolchain-cache"],
prefer_node: null
});
assert.strictEqual(disconnectedTransfer.type, "error");
assert.match(disconnectedTransfer.message, /source snapshot unavailable/);
assert.match(disconnectedTransfer.message, /required artifact\(s\) unavailable/);
assert.match(disconnectedTransfer.message, /direct connectivity unavailable/);
} catch (error) {
if (coordinatorStderr) {
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
}
throw error;
} finally {
coordinator.kill("SIGTERM");
}
console.log("Scheduler placement smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,30 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const sdk = fs.readFileSync(path.join(repo, "crates/disasmer-sdk/src/lib.rs"), "utf8");
assert.match(sdk, /pub struct RuntimeSpawnEvent/);
assert.match(sdk, /pub debugger_visible: bool/);
assert.match(sdk, /fn register_runtime_thread/);
assert.match(sdk, /register_runtime_thread\(id, self\.name, self\.env\)/);
assert.match(sdk, /pub fn debugger_visible\(&self\) -> bool/);
cp.execFileSync(
"cargo",
[
"test",
"-p",
"disasmer-sdk",
"spawn_task_start_registers_debugger_visible_runtime_thread",
"--",
"--exact",
],
{ cwd: repo, stdio: "inherit" }
);
console.log("SDK spawn runtime smoke passed");

View file

@ -0,0 +1,299 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const teamArtifactDigest = `sha256:${"a".repeat(64)}`;
const releaseManifestPath =
process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
path.join(repo, "target/public-release-dryrun/public-release-manifest.json");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function linuxNodeCapabilities() {
return {
os: "Linux",
arch: "x86_64",
capabilities: ["Command", "RootlessPodman", "VfsArtifacts", "QuicDirect"],
environment_backends: ["Container"],
source_providers: ["filesystem", "git"]
};
}
function commandOutput(command, args) {
try {
return cp
.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
})
.trim();
} catch (_) {
return null;
}
}
function expectedSourceCommit() {
return (
process.env.DISASMER_ACCEPTANCE_COMMIT ||
commandOutput("git", ["rev-parse", "HEAD"])
);
}
function readReleaseManifest() {
if (!fs.existsSync(releaseManifestPath)) return null;
const manifest = JSON.parse(fs.readFileSync(releaseManifestPath, "utf8"));
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
const expectedCommit = expectedSourceCommit();
if (expectedCommit) {
assert.strictEqual(
manifest.source_commit,
expectedCommit,
"self-hosted coordinator smoke must use the manifest for the current acceptance commit"
);
}
return manifest;
}
function releaseIdentity() {
const manifest = readReleaseManifest();
return {
sourceCommit: manifest ? manifest.source_commit : expectedSourceCommit(),
releaseName:
(manifest && manifest.release_name) ||
process.env.DISASMER_PUBLIC_RELEASE_NAME ||
null,
};
}
async function attachTrustedNode(addr, node) {
const attached = await send(addr, {
type: "attach_node",
tenant: "team",
project: "self-hosted",
node,
public_key: `${node}-public-key`
});
assert.strictEqual(attached.type, "node_attached");
const heartbeat = await send(addr, {
type: "node_heartbeat",
node
});
assert.strictEqual(heartbeat.type, "node_heartbeat");
const reported = await send(addr, {
type: "report_node_capabilities",
tenant: "team",
project: "self-hosted",
node,
capabilities: linuxNodeCapabilities(),
cached_environment_digests: ["sha256:env-team-linux"],
dependency_cache_digests: ["sha256:cargo-cache"],
source_snapshots: ["sha256:source-team"],
artifact_locations: [],
direct_connectivity: true,
online: true
});
assert.strictEqual(reported.type, "node_capabilities_recorded");
}
(async () => {
const release = releaseIdentity();
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
await attachTrustedNode(addr, "team-linux-a");
await attachTrustedNode(addr, "team-linux-b");
const placement = await send(addr, {
type: "schedule_task",
tenant: "team",
project: "self-hosted",
environment: {
os: "Linux",
arch: "x86_64",
capabilities: ["Command", "QuicDirect"]
},
environment_digest: "sha256:env-team-linux",
required_capabilities: ["VfsArtifacts"],
source_snapshot: "sha256:source-team",
required_artifacts: [],
prefer_node: "team-linux-a"
});
assert.strictEqual(placement.type, "task_placement");
assert.strictEqual(placement.placement.node, "team-linux-a");
assert(placement.placement.reasons.includes("preferred node"));
assert(placement.placement.reasons.includes("warm environment cache"));
assert(placement.placement.reasons.includes("source snapshot already local"));
const started = await send(addr, {
type: "start_process",
tenant: "team",
project: "self-hosted",
process: "vp-team-build"
});
assert.strictEqual(started.type, "process_started");
const reconnected = await send(addr, {
type: "reconnect_node",
node: "team-linux-a",
process: "vp-team-build",
epoch: started.epoch
});
assert.strictEqual(reconnected.type, "node_reconnected");
const completed = await send(addr, {
type: "task_completed",
tenant: "team",
project: "self-hosted",
process: "vp-team-build",
node: "team-linux-a",
task: "compile-linux",
status_code: 0,
stdout_bytes: 18,
stderr_bytes: 0,
artifact_path: "/vfs/artifacts/team-output.txt",
artifact_digest: teamArtifactDigest,
artifact_size_bytes: 18
});
assert.strictEqual(completed.type, "task_recorded");
const events = await send(addr, {
type: "list_task_events",
tenant: "team",
project: "self-hosted",
actor_user: "developer",
process: "vp-team-build"
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "team-linux-a");
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/team-output.txt");
const link = await send(addr, {
type: "create_artifact_download_link",
tenant: "team",
project: "self-hosted",
actor_user: "developer",
artifact: "team-output.txt",
max_bytes: 1024 * 1024,
token_nonce: "team-download",
now_epoch_seconds: 10,
ttl_seconds: 60
});
assert.strictEqual(link.type, "artifact_download_link");
assert.deepStrictEqual(link.link.source, { RetainedNode: "team-linux-a" });
const exportPlan = await send(addr, {
type: "export_artifact_to_node",
tenant: "team",
project: "self-hosted",
actor_user: "developer",
artifact: "team-output.txt",
receiver_node: "team-linux-b",
direct_connectivity: true,
failure_reason: ""
});
assert.strictEqual(exportPlan.type, "artifact_export_plan");
assert.strictEqual(exportPlan.source_node, "team-linux-a");
assert.strictEqual(exportPlan.receiver_node, "team-linux-b");
assert.strictEqual(exportPlan.plan.transport, "NativeQuic");
assert.strictEqual(exportPlan.plan.coordinator_assisted_rendezvous, true);
assert.strictEqual(exportPlan.plan.coordinator_bulk_relay_allowed, false);
const reportPath = path.join(repo, "target/acceptance/public-coordinator-compat.json");
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(
reportPath,
`${JSON.stringify(
{
kind: "disasmer-public-coordinator-compatibility",
source_commit: release.sourceCommit,
release_name: release.releaseName,
operator_implementation: "standalone-public-coordinator",
coordinator_addr: ready.listen,
ping: "pong",
nodes: ["team-linux-a", "team-linux-b"],
task_placement: placement.type,
process_started: started.type,
task_completion: completed.type,
task_events: events.events.length,
artifact_download_link: link.type,
artifact_export_plan: exportPlan.type,
},
null,
2
)}\n`
);
} finally {
coordinator.kill("SIGTERM");
}
console.log("Self-hosted coordinator smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,212 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function sourceCapableNode(sourceProviders = ["git"]) {
return {
os: "Linux",
arch: "x86_64",
capabilities: ["Command", "SourceFilesystem", "SourceGit"],
environment_backends: [],
source_providers: sourceProviders
};
}
(async () => {
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const pending = await send(addr, {
type: "request_source_preparation",
tenant: "tenant",
project: "project",
provider: "Git"
});
assert.strictEqual(pending.type, "source_preparation");
assert.strictEqual(pending.status.preparation.tenant, "tenant");
assert.strictEqual(pending.status.preparation.project, "project");
assert.strictEqual(pending.status.preparation.provider, "Git");
assert.strictEqual(
pending.status.preparation.coordinator_requires_checkout_access,
false
);
assert.deepStrictEqual(pending.status.preparation.required_capabilities, [
"SourceGit"
]);
assert.match(pending.status.disposition.Pending.reason, /waiting|node/i);
for (const node of ["source-cold", "source-ready"]) {
const attached = await send(addr, {
type: "attach_node",
tenant: "tenant",
project: "project",
node,
public_key: `${node}-public-key`
});
assert.strictEqual(attached.type, "node_attached");
}
const cold = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "source-cold",
capabilities: sourceCapableNode(),
cached_environment_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: true,
online: true
});
assert.strictEqual(cold.type, "node_capabilities_recorded");
const readyReport = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "source-ready",
capabilities: sourceCapableNode(),
cached_environment_digests: [],
source_snapshots: [],
artifact_locations: [],
direct_connectivity: true,
online: true
});
assert.strictEqual(readyReport.type, "node_capabilities_recorded");
const assigned = await send(addr, {
type: "request_source_preparation",
tenant: "tenant",
project: "project",
provider: "Git"
});
assert.strictEqual(assigned.type, "source_preparation");
assert(["source-cold", "source-ready"].includes(assigned.status.disposition.Assigned.node));
assert.strictEqual(
assigned.status.preparation.coordinator_requires_checkout_access,
false
);
const crossTenantCompletion = await send(addr, {
type: "complete_source_preparation",
tenant: "other",
project: "project",
node: "source-ready",
provider: "Git",
source_snapshot: "sha256:source-prepared"
});
assert.strictEqual(crossTenantCompletion.type, "error");
assert.match(crossTenantCompletion.message, /tenant\/project scope/i);
const completed = await send(addr, {
type: "complete_source_preparation",
tenant: "tenant",
project: "project",
node: "source-ready",
provider: "Git",
source_snapshot: "sha256:source-prepared"
});
assert.strictEqual(completed.type, "source_preparation_completed");
assert.strictEqual(completed.node, "source-ready");
assert.strictEqual(completed.provider, "Git");
assert.strictEqual(completed.source_snapshot, "sha256:source-prepared");
const placement = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: null,
environment_digest: null,
required_capabilities: ["SourceGit"],
source_snapshot: "sha256:source-prepared",
required_artifacts: [],
prefer_node: null
});
assert.strictEqual(placement.type, "task_placement");
assert.strictEqual(placement.placement.node, "source-ready");
assert(
placement.placement.reasons.includes("source snapshot already local"),
"completed source preparation must update node source locality"
);
} catch (error) {
if (coordinatorStderr) {
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
}
throw error;
} finally {
coordinator.kill("SIGTERM");
}
console.log("Source preparation smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,179 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function maybeRead(segments) {
const fullPath = path.join(repo, ...segments);
if (!fs.existsSync(fullPath)) return null;
return fs.readFileSync(fullPath, "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing tenant-isolation evidence: ${name}`);
}
function expectGate(script, gateName) {
assert(
script.includes("node scripts/tenant-isolation-contract-smoke.js"),
`${gateName} must run tenant-isolation-contract-smoke.js`
);
}
const auth = read("crates/disasmer-core/src/auth.rs");
const artifact = read("crates/disasmer-core/src/artifact.rs");
const operatorPanel = read("crates/disasmer-core/src/operator_panel.rs");
const source = read("crates/disasmer-core/src/source.rs");
const coordinatorService = read("crates/disasmer-coordinator/src/service.rs");
const artifactDownloadSmoke = read("scripts/artifact-download-smoke.js");
const operatorPanelSmoke = read("scripts/operator-panel-smoke.js");
const schedulerSmoke = read("scripts/scheduler-placement-smoke.js");
const sourcePreparationSmoke = read("scripts/source-preparation-smoke.js");
const releaseBlockerSmoke = read("scripts/release-blocker-smoke.js");
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
const privateAcceptance = read("scripts/acceptance-private.sh");
for (const [name, pattern] of [
["auth contexts carry tenant and project", /pub struct AuthContext[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["enrollment grants carry tenant and project", /pub struct EnrollmentGrant[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["node credentials carry tenant and project", /pub struct NodeCredential[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["same tenant/project denies tenant mismatch", /pub fn same_tenant_project[\s\S]*tenant mismatch/],
["same tenant/project denies project mismatch", /pub fn same_tenant_project[\s\S]*project mismatch/],
["auth unit denies cross-tenant access", /fn tenant_project_scope_denies_cross_tenant_access\(\)/],
]) {
expect(auth, name, pattern);
}
for (const [name, pattern] of [
["artifact metadata carries tenant and project", /pub struct ArtifactMetadata[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["download links carry tenant project process and actor", /pub struct DownloadLink[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId[\s\S]*pub actor: Actor/],
["downloads authorize against scoped metadata", /let authz = same_tenant_project\(context, &scope\)/],
["artifact test denies cross-tenant download", /fn cross_tenant_download_is_denied_even_with_known_artifact_id\(\)/],
["artifact test denies cross-project download", /fn cross_project_download_is_denied_even_with_known_artifact_id\(\)/],
]) {
expect(artifact, name, pattern);
}
for (const [name, pattern] of [
["panel state carries tenant project and process", /pub struct PanelState[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId/],
["panel events carry tenant project and process", /pub struct PanelEvent[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId[\s\S]*pub process: ProcessId/],
["panel events reject scope mismatch", /PanelError::ScopeMismatch/],
["panel scope validation compares tenant project process", /self\.tenant != event\.tenant[\s\S]*self\.project != event\.project[\s\S]*self\.process != event\.process/],
]) {
expect(operatorPanel, name, pattern);
}
expect(
source,
"source preparation carries tenant and project",
/pub struct SourcePreparation[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/
);
for (const [name, pattern] of [
["project creation rejects foreign tenant reuse", /project id is outside the signed-in tenant scope/],
["project selection rejects foreign tenant", /project is outside the signed-in tenant scope/],
["project listing uses tenant-scoped context", /CoordinatorRequest::ListProjects[\s\S]*list_projects\(&context\)/],
["node capability reports check enrollment tenant scope", /node capability report is outside the enrolled tenant\/project scope/],
["operator panel stop state is tenant/project/process keyed", /type PanelStopKey = \(TenantId, ProjectId, ProcessId\)/],
["download service tests tenant mismatch", /cross_tenant\.to_string\(\)\.contains\("tenant mismatch"\)/],
["download service tests project mismatch", /cross_project\.to_string\(\)\.contains\("project mismatch"\)/],
["node capability test rejects cross-scope report", /fn service_rejects_node_capability_report_outside_enrollment_scope\(\)/],
["source preparation test rejects cross-scope completion", /fn service_rejects_source_preparation_completion_outside_node_scope\(\)/],
]) {
expect(coordinatorService, name, pattern);
}
for (const [name, sourceText, patterns] of [
[
"artifact download smoke",
artifactDownloadSmoke,
[
/const crossTenant = await send/,
/const crossProject = await send/,
/const crossTenantOpen = await send/,
/const crossProjectOpen = await send/,
/tenant mismatch/,
/project mismatch/,
/token is invalid/,
],
],
[
"operator panel smoke",
operatorPanelSmoke,
[/const crossTenant = await send/, /render_operator_panel/, /scope\|tenant\|project/],
],
[
"scheduler and capability smoke",
schedulerSmoke,
[
/const crossScopeInspection = await send/,
/assert\.strictEqual\(crossScopeInspection\.descriptors\.length, 0\)/,
/const crossTenantReport = await send/,
/tenant\\\/project scope/,
],
],
[
"source preparation smoke",
sourcePreparationSmoke,
[/const crossTenantCompletion = await send/, /complete_source_preparation/, /tenant\\\/project scope/i],
],
]) {
for (const pattern of patterns) {
expect(sourceText, name, pattern);
}
}
expect(releaseBlockerSmoke, "release blockers list tenant-isolation failure", /cross-tenant access is listed as a release blocker/);
expectGate(publicAcceptance, "public acceptance");
expectGate(publicSplit, "public split acceptance");
expectGate(privateAcceptance, "private acceptance");
const hostedLib = maybeRead(["private", "hosted-policy", "src", "lib.rs"]);
const hostedSmoke = maybeRead([
"private",
"hosted-policy",
"scripts",
"hosted-community-smoke.js",
]);
if (hostedLib && hostedSmoke) {
for (const [name, pattern] of [
["hosted agent keys are tenant/project keyed", /agent_public_keys: BTreeMap<\(TenantId, ProjectId, AgentId\), AgentPublicKeyRecord>/],
["hosted agent records carry tenant and project", /pub struct AgentPublicKeyRecord[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted node statuses carry tenant and project", /pub struct HostedNodeStatus[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted process statuses carry tenant and project", /pub struct HostedProcessStatus[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted log entries carry tenant and project", /pub struct HostedLogEntry[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted debug statuses carry tenant and project", /pub struct HostedDebugSessionStatus[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted observability snapshots carry tenant and project", /pub struct HostedObservabilitySnapshot[\s\S]*pub tenant: TenantId[\s\S]*pub project: ProjectId/],
["hosted observability filters nodes by tenant/project", /node_statuses[\s\S]*filter\(\|status\| status\.tenant == context\.tenant && status\.project == context\.project\)/],
["hosted observability filters logs by tenant/project", /log_entries[\s\S]*filter\(\|entry\| entry\.tenant == context\.tenant && entry\.project == context\.project\)/],
["hosted observability filters artifacts by tenant/project", /metadata\.tenant == context\.tenant && metadata\.project == context\.project/],
["hosted observability filters debug by tenant/project", /debug_sessions[\s\S]*filter\(\|status\| status\.tenant == context\.tenant && status\.project == context\.project\)/],
["hosted artifact metadata inspection authorizes scoped metadata", /artifact_metadata_for_context[\s\S]*self\.authorize\(context, &scope, Action::Inspect/],
["hosted scheduling rejects foreign nodes", /hosted scheduling targets only authorized user-attached project nodes/],
]) {
expect(hostedLib, name, pattern);
}
for (const [name, pattern] of [
["foreign agent list is empty", /const foreignAgentList = await send[\s\S]*records, \[\]/],
["foreign start is denied", /const foreignStart = await send[\s\S]*authorized user-attached project nodes\|tenant mismatch/],
["cross-project debug is denied", /const crossProjectDebug = await send[\s\S]*tenant\|project\|debug/],
["cross-actor debug is denied", /const crossActorDebug = await send[\s\S]*debug\|permission\|actor\|user/],
["cross-tenant metadata is denied", /const crossTenantMetadata = await send[\s\S]*tenant mismatch/],
["foreign snapshot reveals no objects", /foreignSnapshot[\s\S]*snapshot\.nodes, \[\][\s\S]*snapshot\.processes, \[\][\s\S]*snapshot\.logs, \[\][\s\S]*snapshot\.artifacts, \[\][\s\S]*snapshot\.debug_sessions, \[\]/],
["cross-tenant download is denied", /const crossTenantDownload = await send[\s\S]*tenant mismatch/],
]) {
expect(hostedSmoke, name, pattern);
}
}
console.log("Tenant isolation contract smoke passed");

View file

@ -0,0 +1,107 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(file) {
return fs.readFileSync(path.join(repo, file), "utf8");
}
function extractBalancedBlock(source, marker) {
const start = source.indexOf(marker);
assert(start >= 0, `missing marker ${marker}`);
const open = source.indexOf("{", start);
assert(open >= 0, `missing opening brace for ${marker}`);
let depth = 0;
for (let index = open; index < source.length; index += 1) {
const char = source[index];
if (char === "{") depth += 1;
if (char === "}") {
depth -= 1;
if (depth === 0) return source.slice(start, index + 1);
}
}
throw new Error(`missing closing brace for ${marker}`);
}
function extractEnumVariant(source, variant) {
const marker = ` ${variant} {`;
return extractBalancedBlock(source, marker);
}
const forbiddenUserSessionCredentials =
/\b(BrowserSession|CliDeviceSession|BrowserLoginFlow|CliLoginFlow|authorization_url|verification_url|device_code|access_token|refresh_token|provider_token|provider_tokens|session_token|user_token|oauth_token)\b/i;
function assertNoUserSessionCredential(surface, text) {
assert.doesNotMatch(
text,
forbiddenUserSessionCredentials,
`${surface} must not carry user OAuth/browser/session credentials`
);
}
const coordinatorService = read("crates/disasmer-coordinator/src/service.rs");
for (const variant of [
"AttachNode",
"NodeHeartbeat",
"ReportNodeCapabilities",
"RequestRendezvous",
"RequestSourcePreparation",
"CompleteSourcePreparation",
"StartProcess",
"ReconnectNode",
"CancelTask",
"PollTaskControl",
"TaskCompleted",
]) {
assertNoUserSessionCredential(
`CoordinatorRequest::${variant}`,
extractEnumVariant(coordinatorService, variant)
);
}
const nodeRuntime = read("crates/disasmer-node/src/lib.rs");
for (const marker of [
"pub struct LinuxCommandRunPlan",
"pub struct LinuxCommandTaskOutput",
"pub struct CapturedCommandLogs",
"pub struct VirtualThreadCommand",
"pub struct CommandOutput",
]) {
assertNoUserSessionCredential(marker, extractBalancedBlock(nodeRuntime, marker));
}
const coreExecution = read("crates/disasmer-core/src/execution.rs");
assertNoUserSessionCredential(
"CommandInvocation",
extractBalancedBlock(coreExecution, "pub struct CommandInvocation")
);
const dapAdapter = read("crates/disasmer-dap/src/main.rs");
assertNoUserSessionCredential(
"DAP variables response",
extractBalancedBlock(dapAdapter, "fn variables_response")
);
const panel = read("crates/disasmer-core/src/operator_panel.rs");
assertNoUserSessionCredential(
"PanelEvent",
extractBalancedBlock(panel, "pub struct PanelEvent")
);
const auth = read("crates/disasmer-core/src/auth.rs");
assert.match(
auth,
/task_credentials_do_not_contain_user_session/,
"core auth must keep the task credential user-session guard"
);
assert.match(
auth,
/CredentialKind::BrowserSession \| CredentialKind::CliDeviceSession/,
"task credential guard must reject browser and CLI sessions"
);
console.log("User session token boundary smoke passed");

70
scripts/verify-public-split.sh Executable file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source_commit="$(git -C "$repo_root" rev-parse HEAD)"
export DISASMER_ACCEPTANCE_COMMIT="$source_commit"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
tar \
--exclude='./.git' \
--exclude='./target' \
--exclude='./private' \
--exclude='./experiments' \
-C "$repo_root" \
-cf - . | tar -C "$tmp_dir" -xf -
if find "$tmp_dir" -path "$tmp_dir/private" -print -quit | grep -q .; then
echo "private directory leaked into public split" >&2
exit 1
fi
if find "$tmp_dir" -path "$tmp_dir/experiments" -print -quit | grep -q .; then
echo "experiments directory leaked into public split" >&2
exit 1
fi
cargo test --workspace --manifest-path "$tmp_dir/Cargo.toml"
cargo build --workspace --bins --manifest-path "$tmp_dir/Cargo.toml"
(cd "$tmp_dir" && node scripts/acceptance-report-smoke.js)
(cd "$tmp_dir" && node scripts/acceptance-doc-contract-smoke.js)
(cd "$tmp_dir" && node scripts/acceptance-environment-contract-smoke.js)
(cd "$tmp_dir" && node scripts/acceptance-evidence-contract-smoke.js)
(cd "$tmp_dir" && node scripts/public-private-boundary-smoke.js)
(cd "$tmp_dir" && node scripts/release-blocker-smoke.js)
(cd "$tmp_dir" && node scripts/resource-metering-contract-smoke.js)
(cd "$tmp_dir" && node scripts/hostile-input-contract-smoke.js)
(cd "$tmp_dir" && node scripts/tenant-isolation-contract-smoke.js)
(cd "$tmp_dir" && node scripts/public-story-contract-smoke.js)
(cd "$tmp_dir" && node scripts/public-release-dryrun-contract-smoke.js)
(cd "$tmp_dir" && node scripts/self-hosted-coordinator-smoke.js)
(cd "$tmp_dir" && node scripts/public-local-demo-matrix-smoke.js)
(cd "$tmp_dir" && scripts/release-source-scan.sh)
(cd "$tmp_dir" && node scripts/prepare-public-release-dryrun.js)
if [[ -n "${DISASMER_FORGEJO_TOKEN:-}" ]]; then
(cd "$tmp_dir" && node scripts/publish-public-release-dryrun.js)
fi
(cd "$tmp_dir" && node scripts/docs-smoke.js)
(cd "$tmp_dir" && node scripts/cli-login-smoke.js)
(cd "$tmp_dir" && node scripts/cli-browser-login-flow-smoke.js)
(cd "$tmp_dir" && node scripts/cli-install-smoke.js)
(cd "$tmp_dir" && node scripts/user-session-token-boundary-smoke.js)
(cd "$tmp_dir" && node scripts/sdk-spawn-runtime-smoke.js)
(cd "$tmp_dir" && node scripts/node-lifecycle-contract-smoke.js)
(cd "$tmp_dir" && node scripts/vscode-extension-smoke.js)
(cd "$tmp_dir" && node scripts/vscode-f5-smoke.js)
(cd "$tmp_dir" && node scripts/node-attach-smoke.js)
(cd "$tmp_dir" && node scripts/local-services-smoke.js)
(cd "$tmp_dir" && node scripts/cancellation-smoke.js)
(cd "$tmp_dir" && node scripts/cli-local-run-smoke.js)
(cd "$tmp_dir" && node scripts/artifact-download-smoke.js)
(cd "$tmp_dir" && node scripts/artifact-export-smoke.js)
(cd "$tmp_dir" && node scripts/operator-panel-smoke.js)
(cd "$tmp_dir" && node scripts/source-preparation-smoke.js)
(cd "$tmp_dir" && node scripts/scheduler-placement-smoke.js)
(cd "$tmp_dir" && node scripts/windows-best-effort-smoke.js)
(cd "$tmp_dir" && node scripts/windows-validation-contract-smoke.js)
(cd "$tmp_dir" && node scripts/quic-smoke.js)
(cd "$tmp_dir" && node scripts/dap-smoke.js)
(cd "$tmp_dir" && node scripts/flagship-demo-smoke.js)

179
scripts/vscode-extension-smoke.js Executable file
View file

@ -0,0 +1,179 @@
#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const assert = require("assert");
const extension = require("../vscode-extension/extension");
const packageJson = require("../vscode-extension/package.json");
const extensionSource = fs.readFileSync(
path.join(__dirname, "../vscode-extension/extension.js"),
"utf8"
);
const readme = fs.readFileSync(path.join(__dirname, "../README.md"), "utf8");
const defaultOperatorEndpoint = "https://disasmer.michelpaulissen.com:9443";
const repo = path.resolve(__dirname, "..");
assert.strictEqual(packageJson.main, "./extension.js");
assert(fs.existsSync(path.join(__dirname, "../vscode-extension", packageJson.main)));
assert.deepStrictEqual(packageJson.dependencies || {}, {});
assert.match(readme, /code --extensionDevelopmentPath "\$\(pwd\)\/vscode-extension"/);
const root = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-vscode-"));
fs.mkdirSync(path.join(root, "envs/linux"), { recursive: true });
fs.writeFileSync(path.join(root, "envs/linux/Containerfile"), "FROM alpine\n");
fs.mkdirSync(path.join(root, ".disasmer"), { recursive: true });
fs.writeFileSync(
extension.disasmerViewStatePath(root),
JSON.stringify({
nodes: [{ id: "node-linux", status: "online", capabilities: "Command RootlessPodman" }],
processes: [{ id: "vp-build", status: "running", entry: "build" }],
logs: [{ task: "compile-linux", message: "stdout=12 stderr=0", bytes: 12 }],
artifacts: [{ path: "/vfs/artifacts/app.tar.zst", status: "retained", size: 12 }],
inspector: [{ label: "debug", value: "attached" }]
})
);
const envs = extension.discoverEnvironmentNames(root);
assert.deepStrictEqual(envs, ["linux"]);
const diagnostics = extension.diagnoseEnvReferences(
'let _ = env!("linux"); let _ = env!("windows");',
envs
);
assert.strictEqual(diagnostics.length, 1);
assert.strictEqual(diagnostics[0].name, "windows");
assert.match(diagnostics[0].message, /envs\/windows\/Containerfile/);
const inspectCommand = extension.bundleInspectCommand(root, "/repo");
assert.strictEqual(inspectCommand.command, "cargo");
assert.deepStrictEqual(inspectCommand.args.slice(0, 8), [
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"bundle"
]);
assert(inspectCommand.args.includes("inspect"));
assert(inspectCommand.args.includes("--project"));
assert(inspectCommand.args.includes(root));
const refreshed = extension.refreshBundleBeforeLaunch(root, "/repo", (command, args, options) => {
assert.strictEqual(command, "cargo");
assert(args.includes("bundle"));
assert.strictEqual(options.cwd, "/repo");
return {
status: 0,
stdout: JSON.stringify({ metadata: { identity: "sha256:abc" } }),
stderr: ""
};
});
assert.strictEqual(refreshed.metadata.identity, "sha256:abc");
const launch = extension.resolveDisasmerDebugConfiguration(
{ uri: { fsPath: root } },
{}
);
assert.deepStrictEqual(launch, {
name: "Disasmer: Launch Virtual Process",
type: "disasmer",
request: "launch",
entry: "build",
project: root,
runtimeBackend: "local-services",
operatorEndpoint: defaultOperatorEndpoint
});
assert.strictEqual(extension.defaultOperatorEndpoint(), defaultOperatorEndpoint);
const adapter = extension.debugAdapterExecutableSpec(root, repo);
assert.strictEqual(adapter.command, "cargo");
assert.deepStrictEqual(adapter.args, [
"run",
"-q",
"-p",
"disasmer-dap",
"--bin",
"disasmer-debug-dap"
]);
assert.deepStrictEqual(adapter.options, { cwd: repo });
const releasedAdapterPath = path.join(root, process.platform === "win32" ? "disasmer-debug-dap.exe" : "disasmer-debug-dap");
fs.writeFileSync(releasedAdapterPath, "");
const releasedAdapter = extension.debugAdapterExecutableSpec(root, repo);
assert.strictEqual(releasedAdapter.command, releasedAdapterPath);
assert.deepStrictEqual(releasedAdapter.args, []);
assert.deepStrictEqual(releasedAdapter.options, { cwd: root });
assert.throws(
() =>
extension.refreshBundleBeforeLaunch(root, "/repo", () => ({
status: 1,
stdout: "",
stderr: "missing environment linux"
})),
/missing environment linux/
);
assert(
packageJson.contributes.viewsContainers.activitybar.some(
(container) => container.id === "disasmer" && container.title === "Disasmer"
),
"package.json must contribute a Disasmer activity-bar container"
);
assert(
fs.existsSync(path.join(__dirname, "../vscode-extension/resources/disasmer.svg")),
"Disasmer activity-bar icon must exist"
);
const packageViewIds = packageJson.contributes.views.disasmer.map((view) => view.id).sort();
const descriptorViewIds = extension.disasmerViewDescriptors().map((view) => view.id).sort();
assert.deepStrictEqual(descriptorViewIds, [
"disasmer.artifacts",
"disasmer.inspector",
"disasmer.logs",
"disasmer.nodes",
"disasmer.processes"
]);
assert.deepStrictEqual(packageViewIds, descriptorViewIds);
for (const viewId of descriptorViewIds) {
const items = extension.disasmerViewItems(
extension.loadDisasmerViewState(root),
viewId
);
assert(
items.length > 0 && !items[0].label.startsWith("No "),
`${viewId} should render state-backed items`
);
}
assert.deepStrictEqual(
extension.disasmerViewItems(extension.loadDisasmerViewState(root), "disasmer.nodes")[0],
{
label: "node-linux",
description: "online Command RootlessPodman"
}
);
assert(
packageJson.contributes.debuggers.some((debuggerContribution) => debuggerContribution.type === "disasmer"),
"package.json must contribute the disasmer debugger type"
);
for (const viewId of descriptorViewIds) {
assert(
packageJson.activationEvents.includes(`onView:${viewId}`),
`${viewId} should activate the extension when opened`
);
}
const launchProperties =
packageJson.contributes.debuggers[0].configurationAttributes.launch.properties;
assert.strictEqual(launchProperties.runtimeBackend.default, "local-services");
assert(launchProperties.runtimeBackend.enum.includes("live-services"));
assert.strictEqual(launchProperties.operatorEndpoint.default, defaultOperatorEndpoint);
assert.match(extensionSource, /registerDebugAdapterDescriptorFactory\("disasmer"/);
assert.match(extensionSource, /disasmer-debug-dap/);
assert.match(extensionSource, /\.disasmer\/views\.json/);
fs.rmSync(root, { recursive: true, force: true });
console.log("VS Code extension smoke passed");

371
scripts/vscode-f5-smoke.js Executable file
View file

@ -0,0 +1,371 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const path = require("path");
const extension = require("../vscode-extension/extension");
class DapClient {
constructor(spec) {
this.child = cp.spawn(spec.command, spec.args, {
cwd: spec.options && spec.options.cwd,
detached: process.platform !== "win32"
});
this.seq = 1;
this.buffer = Buffer.alloc(0);
this.messages = [];
this.waiters = [];
this.stderr = "";
this.child.stdout.on("data", (chunk) => {
this.buffer = Buffer.concat([this.buffer, chunk]);
this.parse();
});
this.child.stderr.on("data", (chunk) => {
this.stderr += chunk.toString();
});
this.child.on("exit", () => this.flushWaiters());
}
send(command, args = {}) {
const seq = this.seq++;
const message = { seq, type: "request", command, arguments: args };
const payload = Buffer.from(JSON.stringify(message));
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
this.child.stdin.write(payload);
return seq;
}
async response(seq, command) {
const message = await this.waitFor(
(item) =>
item.type === "response" &&
item.request_seq === seq &&
item.command === command
);
if (!message.success) {
throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(message)}`);
}
return message;
}
terminate() {
if (this.child.exitCode !== null) return;
if (process.platform === "win32") {
this.child.kill("SIGKILL");
return;
}
try {
process.kill(-this.child.pid, "SIGKILL");
} catch (_) {
this.child.kill("SIGKILL");
}
}
waitFor(predicate, timeoutMs = 240000) {
const existing = this.messages.find(predicate);
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.terminate();
reject(new Error(`timed out waiting for DAP message\n${this.stderr}`));
}, timeoutMs);
this.waiters.push({ predicate, resolve, timer });
});
}
parse() {
while (true) {
const headerEnd = this.buffer.indexOf("\r\n\r\n");
if (headerEnd < 0) return;
const header = this.buffer.slice(0, headerEnd).toString();
const match = header.match(/Content-Length: (\d+)/i);
if (!match) throw new Error(`bad DAP header: ${header}`);
const length = Number(match[1]);
const start = headerEnd + 4;
const end = start + length;
if (this.buffer.length < end) return;
const payload = this.buffer.slice(start, end).toString();
this.buffer = this.buffer.slice(end);
this.messages.push(JSON.parse(payload));
this.flushWaiters();
}
}
flushWaiters() {
for (const waiter of [...this.waiters]) {
const message = this.messages.find(waiter.predicate);
if (!message) continue;
clearTimeout(waiter.timer);
this.waiters.splice(this.waiters.indexOf(waiter), 1);
waiter.resolve(message);
}
}
async close() {
if (this.child.exitCode !== null) return;
const seq = this.send("disconnect");
await this.response(seq, "disconnect");
this.child.stdin.end();
}
}
(async () => {
const repo = path.resolve(__dirname, "..");
const project = path.join(repo, "examples/launch-build-demo");
const launchConfig = extension.resolveDisasmerDebugConfiguration(
{ uri: { fsPath: project } },
{}
);
assert.strictEqual(launchConfig.type, "disasmer");
assert.strictEqual(launchConfig.request, "launch");
assert.strictEqual(launchConfig.entry, "build");
assert.strictEqual(launchConfig.project, project);
assert.strictEqual(launchConfig.runtimeBackend, "local-services");
const inspection = extension.refreshBundleBeforeLaunch(project, repo);
assert.match(inspection.metadata.identity, /^sha256:/);
const client = new DapClient(extension.debugAdapterExecutableSpec(repo));
try {
const initialize = client.send("initialize", {
adapterID: "disasmer",
linesStartAt1: true,
columnsStartAt1: true
});
await client.response(initialize, "initialize");
const launch = client.send("launch", launchConfig);
await client.response(launch, "launch");
await client.waitFor(
(message) => message.type === "event" && message.event === "initialized"
);
const breakpoints = client.send("setBreakpoints", {
source: { path: path.join(project, "src/build.rs") },
breakpoints: [{ line: 22 }, { line: 31 }, { line: 60 }]
});
const breakpointResponse = await client.response(breakpoints, "setBreakpoints");
assert.strictEqual(breakpointResponse.body.breakpoints[0].verified, true);
const configurationDone = client.send("configurationDone");
await client.response(configurationDone, "configurationDone");
const stopped = await client.waitFor(
(message) => message.type === "event" && message.event === "stopped"
);
assert.strictEqual(stopped.body.allThreadsStopped, true);
assert.strictEqual(stopped.body.reason, "breakpoint");
const threadsRequest = client.send("threads");
const threads = (await client.response(threadsRequest, "threads")).body.threads;
const linuxThread = threads.find((thread) => thread.name.includes("compile linux"));
assert(linuxThread, "F5 launch must expose the Linux task as a virtual thread");
const stackRequest = client.send("stackTrace", {
threadId: linuxThread.id,
startFrame: 0,
levels: 1
});
const stack = (await client.response(stackRequest, "stackTrace")).body.stackFrames;
assert.strictEqual(stack[0].line, 22);
assert.strictEqual(stack[0].source.path, path.join(project, "src/build.rs"));
assert.strictEqual(stack[0].source.sourceReference || 0, 0);
const sourceRequest = client.send("source", { source: stack[0].source });
const source = (await client.response(sourceRequest, "source")).body;
assert.match(source.content, /compile_linux/);
const nextRequest = client.send("next", { threadId: linuxThread.id });
await client.response(nextRequest, "next");
const stepped = await client.waitFor(
(message) =>
message.type === "event" &&
message.event === "stopped" &&
message.body.reason === "step"
);
assert.strictEqual(stepped.body.threadId, linuxThread.id);
const steppedStackRequest = client.send("stackTrace", {
threadId: linuxThread.id,
startFrame: 0,
levels: 1
});
const steppedStack = (await client.response(steppedStackRequest, "stackTrace")).body.stackFrames;
assert.strictEqual(steppedStack[0].line, 23);
const scopesRequest = client.send("scopes", { frameId: steppedStack[0].id });
const scopes = (await client.response(scopesRequest, "scopes")).body.scopes;
const localsScope = scopes.find((scope) => scope.name === "Source Locals");
const argsScope = scopes.find((scope) => scope.name === "Task Args and Handles");
const runtimeScope = scopes.find((scope) => scope.name === "Disasmer Runtime");
const outputScope = scopes.find((scope) => scope.name === "Recent Output");
assert(localsScope, "F5 launch must expose source locals scope");
assert(argsScope, "F5 launch must expose task args and handles");
assert(runtimeScope, "F5 launch must expose Disasmer runtime state");
assert(outputScope, "F5 launch must expose recent output state");
const localsRequest = client.send("variables", {
variablesReference: localsScope.variablesReference
});
const locals = (await client.response(localsRequest, "variables")).body.variables;
assert(
locals.some(
(variable) =>
variable.name === "unavailable-local-diagnostic" &&
String(variable.value).includes("cannot be inspected")
),
"source locals scope must report unavailable real Rust locals explicitly"
);
const argsRequest = client.send("variables", {
variablesReference: argsScope.variablesReference
});
const args = (await client.response(argsRequest, "variables")).body.variables;
assert(args.some((variable) => variable.name === "return_value"));
assert(args.some((variable) => variable.name === "artifact"));
assert(args.some((variable) => variable.name === "source_snapshot"));
assert(args.some((variable) => variable.name === "blob"));
assert(args.some((variable) => variable.name === "vfs_mounts"));
const runtimeRequest = client.send("variables", {
variablesReference: runtimeScope.variablesReference
});
const runtime = (await client.response(runtimeRequest, "variables")).body.variables;
assert(
runtime.some(
(variable) => variable.name === "runtime_backend" && variable.value === "LocalServices"
),
"extension-resolved F5 launch must use the real local-services backend"
);
assert(
runtime.some(
(variable) => variable.name === "coordinator_task_events" && variable.value === 1
),
"F5 launch must cross the coordinator/node boundary and record a task event"
);
assert(
runtime.some(
(variable) =>
variable.name === "command_status" &&
String(variable.value).includes("completed through local services")
)
);
assert(runtime.some((variable) => variable.name === "command_spec"));
assert(runtime.some((variable) => variable.name === "stdout_tail"));
assert(runtime.some((variable) => variable.name === "stderr_tail"));
const outputRequest = client.send("variables", {
variablesReference: outputScope.variablesReference
});
const output = (await client.response(outputRequest, "variables")).body.variables;
assert(output.some((variable) => variable.name === "stdout_tail"));
assert(output.some((variable) => variable.name === "stderr_tail"));
const continueToWasmLocals = client.send("continue", { threadId: linuxThread.id });
await client.response(continueToWasmLocals, "continue");
await client.waitFor((message) => message.type === "event" && message.event === "continued");
const wasmLocalsStop = await client.waitFor(
(message) =>
message.type === "event" &&
message.event === "stopped" &&
message.body.reason === "breakpoint" &&
message.body.threadId === 1
);
assert.strictEqual(wasmLocalsStop.body.allThreadsStopped, true);
const wasmLocalsStackRequest = client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const wasmLocalsStack = (await client.response(wasmLocalsStackRequest, "stackTrace")).body
.stackFrames;
assert.strictEqual(wasmLocalsStack[0].line, 31);
const wasmLocalsScopesRequest = client.send("scopes", {
frameId: wasmLocalsStack[0].id
});
const wasmLocalsScopes = (await client.response(wasmLocalsScopesRequest, "scopes")).body.scopes;
const wasmLocalsScope = wasmLocalsScopes.find((scope) => scope.name === "Wasm Frame Locals");
assert(wasmLocalsScope, "F5 launch must expose Wasm frame locals scope");
const wasmLocalsRequest = client.send("variables", {
variablesReference: wasmLocalsScope.variablesReference
});
const wasmLocals = (await client.response(wasmLocalsRequest, "variables")).body.variables;
assert(
wasmLocals.some(
(variable) =>
variable.name === "wasm_local_0" &&
String(variable.value).includes("41") &&
variable.type === "wasm-frame-local"
),
"Wasm frame locals must expose runtime values in the VS Code variables path"
);
const continueToLocals = client.send("continue", { threadId: 1 });
await client.response(continueToLocals, "continue");
await client.waitFor((message) => message.type === "event" && message.event === "continued");
const sourceLocalsStop = await client.waitFor(
(message) =>
message.type === "event" &&
message.event === "stopped" &&
message.body.reason === "breakpoint" &&
message.body.threadId === 1
);
assert.strictEqual(sourceLocalsStop.body.allThreadsStopped, true);
const sourceLocalsStackRequest = client.send("stackTrace", {
threadId: 1,
startFrame: 0,
levels: 1
});
const sourceLocalsStack = (await client.response(sourceLocalsStackRequest, "stackTrace")).body
.stackFrames;
assert.strictEqual(sourceLocalsStack[0].line, 60);
const sourceLocalsScopesRequest = client.send("scopes", {
frameId: sourceLocalsStack[0].id
});
const sourceLocalsScopes = (await client.response(sourceLocalsScopesRequest, "scopes")).body
.scopes;
const sourceLocalsScope = sourceLocalsScopes.find((scope) => scope.name === "Source Locals");
assert(sourceLocalsScope, "source-local breakpoint must expose source locals");
const sourceLocalsRequest = client.send("variables", {
variablesReference: sourceLocalsScope.variablesReference
});
const sourceLocals = (await client.response(sourceLocalsRequest, "variables")).body.variables;
assert(
sourceLocals.some(
(variable) =>
variable.name === "linux" &&
String(variable.value).includes("TaskHandle") &&
String(variable.value).includes("compile-linux") &&
String(variable.value).includes("virtual_thread_id = 2")
),
"source locals must include the spawned Linux task handle value"
);
assert(
sourceLocals.some((variable) => variable.name === "linux_thread" && variable.value === "2"),
"source locals must include the Linux virtual thread id value"
);
assert(
sourceLocals.some(
(variable) =>
variable.name === "linux_artifact" && String(variable.value).includes("Artifact")
),
"source locals must include the Linux artifact handle value"
);
await client.close();
} catch (error) {
client.terminate();
throw error;
}
console.log("VS Code F5 smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,115 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const wasmTarget = path.join(
repo,
"target",
"wasm32-unknown-unknown",
"debug",
"launch_build_demo.wasm"
);
cp.execFileSync(
"cargo",
["build", "-p", "launch-build-demo", "--target", "wasm32-unknown-unknown"],
{ cwd: repo, stdio: "inherit" }
);
assert(fs.existsSync(wasmTarget), `missing compiled Wasm module at ${wasmTarget}`);
const output = cp.execFileSync(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-wasmtime-smoke",
"--",
wasmTarget,
"task_add_one",
"41",
"42",
],
{ cwd: repo, encoding: "utf8" }
);
const report = JSON.parse(output);
assert.strictEqual(report.type, "wasmtime_task_smoke");
assert.strictEqual(report.export, "task_add_one");
assert.strictEqual(report.arg, 41);
assert.strictEqual(report.result, 42);
const debugOutput = cp.execFileSync(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-wasmtime-smoke",
"--",
"--debug-freeze-resume",
wasmTarget,
"task_add_one",
"41",
"42",
],
{ cwd: repo, encoding: "utf8" }
);
const debugReport = JSON.parse(debugOutput);
assert.strictEqual(debugReport.type, "wasmtime_debug_freeze_resume_smoke");
assert.strictEqual(debugReport.export, "task_add_one");
assert.strictEqual(debugReport.task, "task_add_one");
assert.strictEqual(debugReport.frozen_state, "Frozen");
assert.strictEqual(debugReport.resumed_state, "Running");
assert(debugReport.stack_frames.some((frame) => String(frame).includes("task_add_one")));
assert(
debugReport.local_values.some(
([name, value]) => name === "wasm_local_0" && String(value).includes("41")
),
"Wasmtime debug snapshot must expose the real i32 argument as a frame local"
);
assert.strictEqual(debugReport.node_runtime_captured_wasm_locals, true);
assert.strictEqual(debugReport.result, 42);
assert.strictEqual(debugReport.node_runtime_reached_wasm_task, true);
const hostCommandOutput = cp.execFileSync(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-wasmtime-smoke",
"--",
"--host-command",
],
{ cwd: repo, encoding: "utf8" }
);
const hostCommandReport = JSON.parse(hostCommandOutput);
assert.strictEqual(hostCommandReport.type, "wasmtime_host_command_smoke");
assert.strictEqual(hostCommandReport.export, "compile-linux");
assert.strictEqual(hostCommandReport.export_result, 0);
assert.strictEqual(hostCommandReport.virtual_thread, "compile-linux");
assert.strictEqual(hostCommandReport.stdout, "linux-build-artifact");
assert.strictEqual(
hostCommandReport.staged_artifact.path,
"/vfs/artifacts/linux/app.tar.zst"
);
assert.strictEqual(hostCommandReport.large_bytes_uploaded, false);
assert.strictEqual(hostCommandReport.manifest_objects, 1);
assert.strictEqual(hostCommandReport.node_host_import, "disasmer.cmd_run");
assert.strictEqual(hostCommandReport.flagship_linux_build_task, true);
assert.strictEqual(hostCommandReport.node_executed_host_command, true);
assert.strictEqual(hostCommandReport.hosted_control_plane_ran_command, false);
console.log("Wasmtime node smoke passed");

View file

@ -0,0 +1,235 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const nodeMain = fs.readFileSync(path.join(repo, "crates/disasmer-node/src/main.rs"), "utf8");
const nodeLib = fs.readFileSync(path.join(repo, "crates/disasmer-node/src/lib.rs"), "utf8");
const executionCore = fs.readFileSync(
path.join(repo, "crates/disasmer-core/src/execution.rs"),
"utf8"
);
const dapMain = fs.readFileSync(path.join(repo, "crates/disasmer-dap/src/main.rs"), "utf8");
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function windowsCapabilities() {
return {
os: "Windows",
arch: "x86_64",
capabilities: ["Command", "WindowsCommandDev", "VfsArtifacts"],
environment_backends: ["WindowsCommandDev"],
source_providers: ["filesystem"]
};
}
function assertWindowsBackendBoundary() {
assert.match(nodeMain, /CoordinatorSession::connect\(&args\.coordinator\)/);
assert.match(nodeMain, /"type": "task_completed"/);
assert.doesNotMatch(nodeMain, /WindowsCommandDev|windows-command-dev|cfg\(windows\)/);
const linuxStart = nodeLib.indexOf("impl CommandBackend for LinuxRootlessPodmanBackend");
const windowsStart = nodeLib.indexOf("pub struct WindowsCommandDevBackend");
assert(linuxStart >= 0, "Linux backend implementation must be present");
assert(windowsStart > linuxStart, "Windows backend must be separate from Linux backend");
const linuxBackend = nodeLib.slice(linuxStart, windowsStart);
assert.doesNotMatch(linuxBackend, /WindowsCommandDev|WindowsSandbox|windows-command-dev/);
assert.match(nodeLib, /impl CommandBackend for WindowsCommandDevBackend/);
assert.match(nodeLib, /impl CommandBackend for WindowsSandboxStubBackend/);
assert.match(executionCore, /\bLinuxRootlessPodman\b/);
assert.match(executionCore, /\bWindowsCommandDev\b/);
assert.match(executionCore, /\bStubbedWindowsSandbox\b/);
assert.match(dapMain, /thread\(WINDOWS_THREAD, "compile-windows", "compile windows"/);
assert.match(dapMain, /fn launch_threads_include_windows_task_in_same_virtual_process\(\)/);
assert.match(dapMain, /fn windows_thread_runtime_variables_share_virtual_process\(\)/);
}
(async () => {
assertWindowsBackendBoundary();
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const attached = await send(addr, {
type: "attach_node",
tenant: "tenant",
project: "project",
node: "windows-node",
public_key: "windows-node-public-key"
});
assert.strictEqual(attached.type, "node_attached");
assert.strictEqual(attached.node, "windows-node");
const recorded = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "windows-node",
capabilities: windowsCapabilities(),
cached_environment_digests: ["sha256:env-windows-command-dev"],
source_snapshots: ["sha256:source-tree"],
artifact_locations: [],
direct_connectivity: true,
online: true
});
assert.strictEqual(recorded.type, "node_capabilities_recorded");
assert.strictEqual(recorded.node, "windows-node");
const placement = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: {
os: "Windows",
arch: null,
capabilities: ["WindowsCommandDev"]
},
environment_digest: "sha256:env-windows-command-dev",
required_capabilities: ["Command"],
source_snapshot: "sha256:source-tree",
required_artifacts: [],
prefer_node: null
});
assert.strictEqual(placement.type, "task_placement");
assert.strictEqual(placement.placement.node, "windows-node");
assert.ok(placement.placement.reasons.includes("warm environment cache"));
assert.ok(placement.placement.reasons.includes("source snapshot already local"));
const started = await send(addr, {
type: "start_process",
tenant: "tenant",
project: "project",
process: "vp-windows"
});
assert.strictEqual(started.type, "process_started");
assert.strictEqual(started.process, "vp-windows");
const reconnected = await send(addr, {
type: "reconnect_node",
node: "windows-node",
process: "vp-windows",
epoch: started.epoch
});
assert.strictEqual(reconnected.type, "node_reconnected");
const recordedTask = await send(addr, {
type: "task_completed",
tenant: "tenant",
project: "project",
process: "vp-windows",
node: "windows-node",
task: "windows-command-dev",
status_code: 0,
stdout_bytes: 18,
stderr_bytes: 0,
artifact_path: "/vfs/artifacts/windows-output.txt",
artifact_digest: "sha256:windows-artifact",
artifact_size_bytes: 18
});
assert.strictEqual(recordedTask.type, "task_recorded");
assert.strictEqual(recordedTask.task, "windows-command-dev");
const events = await send(addr, {
type: "list_task_events",
tenant: "tenant",
project: "project",
actor_user: "user",
process: "vp-windows"
});
assert.strictEqual(events.type, "task_events");
assert.strictEqual(events.events.length, 1);
assert.strictEqual(events.events[0].node, "windows-node");
assert.strictEqual(events.events[0].artifact_path, "/vfs/artifacts/windows-output.txt");
const link = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "windows-output.txt",
max_bytes: 1024,
token_nonce: "nonce"
});
assert.strictEqual(link.type, "artifact_download_link");
assert.strictEqual(link.link.process, "vp-windows");
assert.deepStrictEqual(link.link.source, { RetainedNode: "windows-node" });
} catch (error) {
if (coordinatorStderr) {
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
}
throw error;
} finally {
coordinator.kill("SIGTERM");
}
console.log("Windows best-effort smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

457
scripts/windows-runner-smoke.js Executable file
View file

@ -0,0 +1,457 @@
#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const net = require("net");
const path = require("path");
const repo = path.resolve(__dirname, "..");
class DapClient {
constructor() {
this.child = cp.spawn(
"cargo",
["run", "-q", "-p", "disasmer-dap", "--bin", "disasmer-debug-dap"],
{ cwd: repo }
);
this.seq = 1;
this.buffer = Buffer.alloc(0);
this.messages = [];
this.waiters = [];
this.stderr = "";
this.child.stdout.on("data", (chunk) => {
this.buffer = Buffer.concat([this.buffer, chunk]);
this.parse();
});
this.child.stderr.on("data", (chunk) => {
this.stderr += chunk.toString();
});
this.child.on("exit", () => this.flushWaiters());
}
send(command, args = {}) {
const seq = this.seq++;
const message = { seq, type: "request", command, arguments: args };
const payload = Buffer.from(JSON.stringify(message));
this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`);
this.child.stdin.write(payload);
return seq;
}
async response(seq, command) {
const message = await this.waitFor(
(item) =>
item.type === "response" &&
item.request_seq === seq &&
item.command === command
);
if (!message.success) {
throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(message)}`);
}
return message;
}
waitFor(predicate, timeoutMs = 120000) {
const existing = this.messages.find(predicate);
if (existing) return Promise.resolve(existing);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.child.kill("SIGKILL");
reject(new Error(`timed out waiting for DAP message\n${this.stderr}`));
}, timeoutMs);
this.waiters.push({ predicate, resolve, timer });
});
}
parse() {
while (true) {
const headerEnd = this.buffer.indexOf("\r\n\r\n");
if (headerEnd < 0) return;
const header = this.buffer.slice(0, headerEnd).toString();
const match = header.match(/Content-Length: (\d+)/i);
if (!match) throw new Error(`bad DAP header: ${header}`);
const length = Number(match[1]);
const start = headerEnd + 4;
const end = start + length;
if (this.buffer.length < end) return;
const payload = this.buffer.slice(start, end).toString();
this.buffer = this.buffer.slice(end);
this.messages.push(JSON.parse(payload));
this.flushWaiters();
}
}
flushWaiters() {
for (const waiter of [...this.waiters]) {
const message = this.messages.find(waiter.predicate);
if (!message) continue;
clearTimeout(waiter.timer);
this.waiters.splice(this.waiters.indexOf(waiter), 1);
waiter.resolve(message);
}
}
async close() {
if (this.child.exitCode !== null) return;
const seq = this.send("disconnect");
await this.response(seq, "disconnect");
this.child.stdin.end();
}
}
function waitForJsonLine(child) {
return new Promise((resolve, reject) => {
let buffer = "";
child.stdout.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
try {
resolve(JSON.parse(buffer.slice(0, newline).trim()));
} catch (error) {
reject(error);
}
});
child.once("exit", (code) => {
reject(new Error(`process exited before JSON line with code ${code}`));
});
});
}
function send(addr, message) {
return new Promise((resolve, reject) => {
const socket = net.connect(addr.port, addr.host, () => {
socket.write(`${JSON.stringify(message)}\n`);
});
let buffer = "";
socket.on("data", (chunk) => {
buffer += chunk.toString();
const newline = buffer.indexOf("\n");
if (newline < 0) return;
socket.end();
try {
resolve(JSON.parse(buffer.slice(0, newline)));
} catch (error) {
reject(error);
}
});
socket.on("error", reject);
});
}
function runWindowsAttach(addr, grant) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"node",
"attach",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"forgejo-windows-node",
"--public-key",
"forgejo-windows-node-public-key",
"--enrollment-grant",
grant,
"--cap",
"windows-command-dev"
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`Windows node attach failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout));
} catch (error) {
reject(
new Error(`Windows node attach output was not JSON: ${stdout}\n${error.stack || error.message}`)
);
}
});
});
}
function runWindowsNode(addr, grant) {
return new Promise((resolve, reject) => {
const child = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
"--coordinator",
`${addr.host}:${addr.port}`,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"forgejo-windows-node",
"--public-key",
"forgejo-windows-node-public-key",
"--enrollment-grant",
grant,
"--process",
"vp-forgejo-windows",
"--task",
"windows-command-dev",
"--command",
"cmd",
"--arg",
"/C",
"--arg",
"echo disasmer-windows-runner",
"--artifact",
"/vfs/artifacts/windows-runner-output.txt"
],
{ cwd: repo }
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`Windows node smoke failed with code ${code}\n${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1)));
} catch (error) {
reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`));
}
});
});
}
async function assertDebuggerShowsWindowsThread() {
const client = new DapClient();
try {
const initialize = client.send("initialize", {
adapterID: "disasmer",
linesStartAt1: true,
columnsStartAt1: true
});
await client.response(initialize, "initialize");
const launch = client.send("launch", {
entry: "build",
project: path.join(repo, "examples/launch-build-demo"),
runtimeBackend: "simulated"
});
await client.response(launch, "launch");
await client.waitFor(
(message) => message.type === "event" && message.event === "initialized"
);
const configurationDone = client.send("configurationDone");
await client.response(configurationDone, "configurationDone");
const threadsRequest = client.send("threads");
const threads = (await client.response(threadsRequest, "threads")).body.threads;
assert(
threads.some((thread) => thread.name.includes("compile windows")),
"debugger must represent the Windows task as a virtual thread"
);
await client.close();
} catch (error) {
client.child.kill("SIGKILL");
throw error;
}
}
(async () => {
if (process.platform !== "win32") {
throw new Error(
`Windows runner validation requires win32; current platform is ${process.platform}`
);
}
const coordinator = cp.spawn(
"cargo",
[
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
"--listen",
"127.0.0.1:0"
],
{ cwd: repo }
);
let coordinatorStderr = "";
coordinator.stderr.on("data", (chunk) => {
coordinatorStderr += chunk.toString();
});
try {
const ready = await waitForJsonLine(coordinator);
const [host, portText] = ready.listen.split(":");
const addr = { host, port: Number(portText) };
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
const attachGrant = await send(addr, {
type: "create_node_enrollment_grant",
tenant: "tenant",
project: "project",
actor_user: "operator",
grant: "grant-forgejo-windows-attach",
now_epoch_seconds: 0,
ttl_seconds: 900
});
assert.strictEqual(attachGrant.type, "node_enrollment_grant_created");
assert.strictEqual(attachGrant.scope, "node:attach");
const attach = await runWindowsAttach(addr, attachGrant.grant);
assert.strictEqual(attach.plan.node, "forgejo-windows-node");
assert.strictEqual(attach.boundary.cli_contacted_coordinator, true);
assert.strictEqual(attach.boundary.used_enrollment_exchange, true);
assert.strictEqual(attach.coordinator_response.type, "node_enrollment_exchanged");
assert.strictEqual(attach.coordinator_response.credential.scope, "node:attach");
assert(attach.plan.capabilities.capabilities.includes("WindowsCommandDev"));
const runtimeGrant = await send(addr, {
type: "create_node_enrollment_grant",
tenant: "tenant",
project: "project",
actor_user: "operator",
grant: "grant-forgejo-windows-runtime",
now_epoch_seconds: 0,
ttl_seconds: 900
});
assert.strictEqual(runtimeGrant.type, "node_enrollment_grant_created");
const report = await runWindowsNode(addr, runtimeGrant.grant);
assert.strictEqual(report.node_status, "completed");
assert.strictEqual(report.virtual_thread, "windows-command-dev");
assert.strictEqual(report.status_code, 0);
assert.strictEqual(report.large_bytes_uploaded, false);
assert.strictEqual(
report.staged_artifact.path,
"/vfs/artifacts/windows-runner-output.txt"
);
assert.strictEqual(report.registration_response.type, "node_enrollment_exchanged");
assert.strictEqual(report.registration_response.credential.scope, "node:attach");
assert.strictEqual(report.coordinator_response.type, "task_recorded");
const recorded = await send(addr, {
type: "report_node_capabilities",
tenant: "tenant",
project: "project",
node: "forgejo-windows-node",
capabilities: {
os: "Windows",
arch: "x86_64",
capabilities: ["Command", "WindowsCommandDev", "VfsArtifacts"],
environment_backends: ["WindowsCommandDev"],
source_providers: ["filesystem"]
},
cached_environment_digests: ["sha256:env-windows-command-dev"],
source_snapshots: ["sha256:source-tree"],
artifact_locations: ["windows-runner-output.txt"],
direct_connectivity: true,
online: true
});
assert.strictEqual(recorded.type, "node_capabilities_recorded");
const placement = await send(addr, {
type: "schedule_task",
tenant: "tenant",
project: "project",
environment: {
os: "Windows",
arch: null,
capabilities: ["WindowsCommandDev"]
},
environment_digest: "sha256:env-windows-command-dev",
required_capabilities: ["Command"],
source_snapshot: "sha256:source-tree",
required_artifacts: ["windows-runner-output.txt"],
prefer_node: null
});
assert.strictEqual(placement.type, "task_placement");
assert.strictEqual(placement.placement.node, "forgejo-windows-node");
const link = await send(addr, {
type: "create_artifact_download_link",
tenant: "tenant",
project: "project",
actor_user: "user",
artifact: "windows-runner-output.txt",
max_bytes: 1024,
token_nonce: "nonce"
});
assert.strictEqual(link.type, "artifact_download_link");
assert.deepStrictEqual(link.link.source, {
RetainedNode: "forgejo-windows-node"
});
} catch (error) {
if (coordinatorStderr) {
error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`;
}
throw error;
} finally {
coordinator.kill("SIGTERM");
}
await assertDebuggerShowsWindowsThread();
const outDir = path.join(repo, "target", "acceptance");
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, "windows-runner.json"),
`${JSON.stringify(
{
kind: "disasmer_windows_runner_validation",
platform: process.platform,
runner: process.env.FORGEJO_RUNNER_NAME || process.env.RUNNER_NAME || null,
validated: true
},
null,
2
)}\n`
);
console.log("Windows runner smoke passed");
})().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});

View file

@ -0,0 +1,59 @@
#!/usr/bin/env node
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const repo = path.resolve(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repo, relativePath), "utf8");
}
function expect(source, name, pattern) {
assert.match(source, pattern, `missing Windows validation evidence: ${name}`);
}
const workflow = read(".forgejo/workflows/windows-validation.yml");
const runnerSmoke = read("scripts/windows-runner-smoke.js");
const readme = read("README.md");
const publicAcceptance = read("scripts/acceptance-public.sh");
const publicSplit = read("scripts/verify-public-split.sh");
expect(workflow, "manual workflow dispatch", /workflow_dispatch/);
expect(workflow, "intermittent Forgejo Windows runner note", /intermittently online/);
expect(workflow, "Windows runner label", /runs-on:\s*windows/);
expect(workflow, "Windows validation acceptance report env", /DISASMER_WINDOWS_VALIDATION:\s*forgejo-windows-runner/);
expect(workflow, "acceptance report step", /node scripts\/acceptance-report\.js windows/);
expect(workflow, "Windows runtime unit coverage", /cargo test -p disasmer-node windows_backend_is_labeled_user_attached_dev_execution/);
expect(workflow, "Windows runner smoke step", /node scripts\/windows-runner-smoke\.js/);
expect(runnerSmoke, "real Windows platform guard", /process\.platform !== "win32"/);
expect(runnerSmoke, "CLI node attach function", /function runWindowsAttach/);
expect(runnerSmoke, "CLI node attach command", /"node"[\s\S]*"attach"[\s\S]*"windows-command-dev"/);
expect(runnerSmoke, "enrollment grant creation", /create_node_enrollment_grant[\s\S]*grant-forgejo-windows-attach/);
expect(runnerSmoke, "CLI enrollment exchange asserted", /used_enrollment_exchange[\s\S]*true/);
expect(runnerSmoke, "runtime enrollment grant", /grant-forgejo-windows-runtime/);
expect(runnerSmoke, "runtime uses enrollment grant", /"--enrollment-grant"[\s\S]*grant/);
expect(runnerSmoke, "Windows command task runs", /"windows-command-dev"[\s\S]*"cmd"[\s\S]*"echo disasmer-windows-runner"/);
expect(runnerSmoke, "artifact metadata is asserted", /windows-runner-output\.txt[\s\S]*artifact_download_link/);
expect(runnerSmoke, "Windows placement is asserted", /schedule_task[\s\S]*WindowsCommandDev[\s\S]*forgejo-windows-node/);
expect(runnerSmoke, "debugger shows Windows virtual thread", /compile windows/);
expect(runnerSmoke, "validation result artifact", /disasmer_windows_runner_validation/);
expect(readme, "docs mention manual Windows workflow", /manual `Windows validation`\s+workflow/);
expect(readme, "docs mention intermittent runner", /intermittent Windows runner/);
expect(readme, "docs mention CLI attach in Windows validation", /runs `disasmer node attach`/);
expect(readme, "docs keep Windows unvalidated when report is not-run", /windows_validation: "not-run"[\s\S]*best-effort and unvalidated/);
for (const [scriptName, script] of [
["public acceptance", publicAcceptance],
["public split", publicSplit],
]) {
assert(
script.includes("node scripts/windows-validation-contract-smoke.js"),
`${scriptName} must run windows-validation-contract-smoke.js`
);
}
console.log("Windows validation contract smoke passed");

View file

@ -0,0 +1,414 @@
const fs = require("fs");
const path = require("path");
const childProcess = require("child_process");
const DEFAULT_OPERATOR_ENDPOINT = "https://disasmer.michelpaulissen.com:9443";
let vscode = null;
try {
vscode = require("vscode");
} catch (_) {
vscode = null;
}
function discoverEnvironmentNames(projectRoot) {
const envsDir = path.join(projectRoot, "envs");
if (!fs.existsSync(envsDir)) return [];
return fs
.readdirSync(envsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => {
const dir = path.join(envsDir, name);
return (
fs.existsSync(path.join(dir, "Containerfile")) ||
fs.existsSync(path.join(dir, "Dockerfile"))
);
})
.sort();
}
function findEnvReferences(text) {
const references = [];
const pattern = /env!\(\s*"([^"]+)"\s*\)/g;
let match;
while ((match = pattern.exec(text)) !== null) {
references.push({
name: match[1],
start: match.index,
end: pattern.lastIndex
});
}
return references;
}
function diagnoseEnvReferences(text, environmentNames) {
const known = new Set(environmentNames);
return findEnvReferences(text)
.filter((reference) => !known.has(reference.name))
.map((reference) => ({
...reference,
message: `Missing Disasmer environment "${reference.name}". Add envs/${reference.name}/Containerfile or envs/${reference.name}/Dockerfile.`
}));
}
function binaryCandidates(projectRoot, name) {
const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
return names.map((candidate) => path.join(projectRoot, candidate));
}
function executableWorkspaceBinary(projectRoot, name) {
if (!projectRoot) return null;
return binaryCandidates(projectRoot, name).find((candidate) => {
try {
return fs.statSync(candidate).isFile();
} catch (_) {
return false;
}
}) || null;
}
function hasCargoWorkspace(root) {
return fs.existsSync(path.join(root, "Cargo.toml"));
}
function resolveProjectPath(folder, value) {
const workspaceRoot = folder && folder.uri && folder.uri.fsPath;
if (!value || value === "${workspaceFolder}") return workspaceRoot;
if (workspaceRoot && typeof value === "string") {
return value.replace(/\$\{workspaceFolder\}/g, workspaceRoot);
}
return value;
}
function bundleInspectCommand(projectRoot, adapterWorkspace = path.resolve(__dirname, "..")) {
const workspaceCli = executableWorkspaceBinary(projectRoot, "disasmer");
if (workspaceCli) {
return {
command: workspaceCli,
args: ["bundle", "inspect", "--project", projectRoot],
options: {
cwd: projectRoot,
encoding: "utf8"
}
};
}
return {
command: "cargo",
args: [
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"bundle",
"inspect",
"--project",
projectRoot
],
options: {
cwd: adapterWorkspace,
encoding: "utf8"
}
};
}
function refreshBundleBeforeLaunch(
projectRoot,
adapterWorkspace = path.resolve(__dirname, ".."),
runner = childProcess.spawnSync
) {
const command = bundleInspectCommand(projectRoot, adapterWorkspace);
const result = runner(command.command, command.args, command.options);
if (result.error) {
throw new Error(`Disasmer bundle refresh failed: ${result.error.message}`);
}
if (result.status !== 0) {
const detail = (result.stderr || result.stdout || "").trim();
throw new Error(
`Disasmer bundle refresh failed before debug launch${detail ? `: ${detail}` : "."}`
);
}
let inspection;
try {
inspection = JSON.parse(result.stdout);
} catch (error) {
throw new Error(`Disasmer bundle refresh returned invalid metadata: ${error.message}`);
}
if (!inspection.metadata || !inspection.metadata.identity) {
throw new Error("Disasmer bundle refresh did not return a bundle identity.");
}
return inspection;
}
function disasmerViewDescriptors() {
return [
{ id: "disasmer.nodes", emptyLabel: "No attached nodes" },
{ id: "disasmer.processes", emptyLabel: "No virtual processes" },
{ id: "disasmer.logs", emptyLabel: "No log streams" },
{ id: "disasmer.artifacts", emptyLabel: "No artifacts" },
{ id: "disasmer.inspector", emptyLabel: "No inspector state" }
];
}
function disasmerViewStatePath(projectRoot) {
return path.join(projectRoot, ".disasmer", "views.json");
}
function emptyDisasmerViewState() {
return {
nodes: [],
processes: [],
logs: [],
artifacts: [],
inspector: []
};
}
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function loadDisasmerViewState(projectRoot, reader = fs.readFileSync) {
const statePath = disasmerViewStatePath(projectRoot);
if (!fs.existsSync(statePath)) {
return emptyDisasmerViewState();
}
let parsed;
try {
parsed = JSON.parse(reader(statePath, "utf8"));
} catch (error) {
return {
...emptyDisasmerViewState(),
inspector: [
{
label: "View state error",
value: error.message
}
]
};
}
return {
nodes: asArray(parsed.nodes),
processes: asArray(parsed.processes),
logs: asArray(parsed.logs),
artifacts: asArray(parsed.artifacts),
inspector: asArray(parsed.inspector)
};
}
function item(label, description = "") {
return {
label: String(label || ""),
description: String(description || "")
};
}
function disasmerViewItems(state, viewId) {
switch (viewId) {
case "disasmer.nodes":
return state.nodes.map((node) =>
item(node.id || node.name || "node", [node.status, node.capabilities].filter(Boolean).join(" "))
);
case "disasmer.processes":
return state.processes.map((process) =>
item(process.id || process.process || "process", [process.status, process.entry].filter(Boolean).join(" "))
);
case "disasmer.logs":
return state.logs.map((log) =>
item(log.task || log.stream || "log", [log.message, log.bytes].filter(Boolean).join(" "))
);
case "disasmer.artifacts":
return state.artifacts.map((artifact) =>
item(artifact.path || artifact.id || "artifact", [artifact.status, artifact.size].filter(Boolean).join(" "))
);
case "disasmer.inspector":
return state.inspector.map((entry) =>
item(entry.label || entry.name || "inspector", entry.value || entry.detail || "")
);
default:
return [];
}
}
function treeItemsForView(projectRoot, descriptor) {
const state = projectRoot ? loadDisasmerViewState(projectRoot) : emptyDisasmerViewState();
const items = disasmerViewItems(state, descriptor.id);
return items.length > 0 ? items : [item(descriptor.emptyLabel)];
}
function toVsCodeTreeItem(viewItem) {
const treeItem = new vscode.TreeItem(
viewItem.label,
vscode.TreeItemCollapsibleState.None
);
treeItem.description = viewItem.description;
return treeItem;
}
function resolveDisasmerDebugConfiguration(folder, config = {}) {
const project = resolveProjectPath(folder, config.project);
return {
...config,
name: config.name || "Disasmer: Launch Virtual Process",
type: "disasmer",
request: "launch",
entry: config.entry || "build",
project,
runtimeBackend: config.runtimeBackend || "local-services",
operatorEndpoint: config.operatorEndpoint || DEFAULT_OPERATOR_ENDPOINT
};
}
function defaultOperatorEndpoint() {
return DEFAULT_OPERATOR_ENDPOINT;
}
function debugAdapterExecutableSpec(
projectRoot,
adapterWorkspace = path.resolve(__dirname, "..")
) {
const workspaceAdapter = executableWorkspaceBinary(projectRoot, "disasmer-debug-dap");
if (workspaceAdapter) {
return {
command: workspaceAdapter,
args: [],
options: { cwd: projectRoot }
};
}
if (!hasCargoWorkspace(adapterWorkspace)) {
return {
command: "disasmer-debug-dap",
args: [],
options: { cwd: projectRoot || process.cwd() }
};
}
return {
command: "cargo",
args: ["run", "-q", "-p", "disasmer-dap", "--bin", "disasmer-debug-dap"],
options: { cwd: adapterWorkspace }
};
}
function registerDisasmerViews(context) {
if (!vscode) return;
const workspaceRoot =
vscode.workspace.workspaceFolders &&
vscode.workspace.workspaceFolders[0] &&
vscode.workspace.workspaceFolders[0].uri.fsPath;
const emitters = [];
for (const descriptor of disasmerViewDescriptors()) {
const emitter = new vscode.EventEmitter();
emitters.push(emitter);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(descriptor.id, {
onDidChangeTreeData: emitter.event,
getTreeItem(item) {
return item;
},
getChildren() {
return treeItemsForView(workspaceRoot, descriptor).map(toVsCodeTreeItem);
}
}),
emitter
);
}
if (workspaceRoot && vscode.workspace.createFileSystemWatcher) {
const watcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(workspaceRoot, ".disasmer/views.json")
);
const refresh = () => emitters.forEach((emitter) => emitter.fire());
context.subscriptions.push(
watcher,
watcher.onDidChange(refresh),
watcher.onDidCreate(refresh),
watcher.onDidDelete(refresh)
);
}
}
function activate(context) {
if (!vscode) return;
const diagnostics = vscode.languages.createDiagnosticCollection("disasmer");
context.subscriptions.push(diagnostics);
registerDisasmerViews(context);
const refresh = (document) => {
if (document.languageId !== "rust") return;
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
if (!folder) return;
const environmentNames = discoverEnvironmentNames(folder.uri.fsPath);
const items = diagnoseEnvReferences(document.getText(), environmentNames).map((diagnostic) => {
const start = document.positionAt(diagnostic.start);
const end = document.positionAt(diagnostic.end);
return new vscode.Diagnostic(
new vscode.Range(start, end),
diagnostic.message,
vscode.DiagnosticSeverity.Error
);
});
diagnostics.set(document.uri, items);
};
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(refresh),
vscode.workspace.onDidChangeTextDocument((event) => refresh(event.document)),
vscode.workspace.onDidSaveTextDocument(refresh),
vscode.debug.registerDebugAdapterDescriptorFactory("disasmer", {
createDebugAdapterDescriptor(session) {
const folder = session.workspaceFolder;
const project = session.configuration.project || (folder && folder.uri.fsPath);
if (!project) {
throw new Error("Disasmer debug launch requires a workspace folder or project path.");
}
const adapterWorkspace = path.resolve(__dirname, "..");
refreshBundleBeforeLaunch(project, adapterWorkspace);
const spec = debugAdapterExecutableSpec(project, adapterWorkspace);
return new vscode.DebugAdapterExecutable(
spec.command,
spec.args,
spec.options
);
}
}),
vscode.debug.registerDebugConfigurationProvider("disasmer", {
resolveDebugConfiguration(folder, config) {
return resolveDisasmerDebugConfiguration(folder, config);
}
})
);
for (const document of vscode.workspace.textDocuments) {
refresh(document);
}
}
function deactivate() {}
module.exports = {
activate,
deactivate,
bundleInspectCommand,
debugAdapterExecutableSpec,
defaultOperatorEndpoint,
diagnoseEnvReferences,
disasmerViewDescriptors,
disasmerViewItems,
disasmerViewStatePath,
discoverEnvironmentNames,
findEnvReferences,
loadDisasmerViewState,
registerDisasmerViews,
refreshBundleBeforeLaunch,
resolveDisasmerDebugConfiguration
};

Some files were not shown because too many files have changed in this diff Show more