From 433d759be1aaf4c824d00657668f052c04c656a6 Mon Sep 17 00:00:00 2001 From: Clusterflux Release Date: Wed, 22 Jul 2026 10:07:42 +0200 Subject: [PATCH] Publish Clusterflux 5d40a47 --- CLUSTERFLUX_PUBLIC_TREE.json | 4 +- scripts/cli-happy-path-live-smoke.js | 141 ++++++++++++++++++++------- 2 files changed, 109 insertions(+), 36 deletions(-) diff --git a/CLUSTERFLUX_PUBLIC_TREE.json b/CLUSTERFLUX_PUBLIC_TREE.json index 2d5afc7..c9e76b4 100644 --- a/CLUSTERFLUX_PUBLIC_TREE.json +++ b/CLUSTERFLUX_PUBLIC_TREE.json @@ -1,7 +1,7 @@ { "kind": "clusterflux-filtered-public-tree", - "source_commit": "1d0b6fab342bd34e6f539adb649eaa4ed82f0cfa", - "release_name": "release-1d0b6fab342b", + "source_commit": "5d40a47fb4bdeebabb181286d1fb49af81c4901b", + "release_name": "release-5d40a47fb4bd", "filtered_out": [ "private/**", "internal/**", diff --git a/scripts/cli-happy-path-live-smoke.js b/scripts/cli-happy-path-live-smoke.js index 63d3f39..abf0348 100644 --- a/scripts/cli-happy-path-live-smoke.js +++ b/scripts/cli-happy-path-live-smoke.js @@ -78,12 +78,20 @@ function requireEnabled() { "CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND or CLUSTERFLUX_CLI_HAPPY_PATH_REUSE_SESSION_FILE is required" ); } + const hasSecondTenantSession = Boolean( + process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE + ); + const hasSingleAccountIsolationTarget = Boolean( + process.env.CLUSTERFLUX_ISOLATION_TARGET_TENANT && + process.env.CLUSTERFLUX_ISOLATION_TARGET_PROJECT + ); if ( strictFullRelease && - !process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE + !hasSecondTenantSession && + !hasSingleAccountIsolationTarget ) { throw new Error( - "CLUSTERFLUX_STRICT_FULL_RELEASE=1 requires CLUSTERFLUX_SECOND_TENANT_SESSION_FILE for live isolation and quota independence" + "CLUSTERFLUX_STRICT_FULL_RELEASE=1 requires either CLUSTERFLUX_SECOND_TENANT_SESSION_FILE or both CLUSTERFLUX_ISOLATION_TARGET_TENANT and CLUSTERFLUX_ISOLATION_TARGET_PROJECT" ); } if (strictFullRelease && !process.env.CLUSTERFLUX_EXPIRED_USER_SESSION_FILE) { @@ -1664,6 +1672,7 @@ async function runLiveAgentCredentialSecurity({ } async function runSecondTenantIsolation({ + firstSessionSecret, firstTenant, firstProject, firstProcess, @@ -1673,6 +1682,8 @@ async function runSecondTenantIsolation({ }) { const file = process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE; const evidenceFile = process.env.CLUSTERFLUX_SECOND_TENANT_EVIDENCE_FILE; + const targetTenant = process.env.CLUSTERFLUX_ISOLATION_TARGET_TENANT; + const targetProject = process.env.CLUSTERFLUX_ISOLATION_TARGET_PROJECT; if (!file && evidenceFile) { const evidenceBytes = fs.readFileSync(path.resolve(evidenceFile)); const previous = JSON.parse(evidenceBytes); @@ -1710,6 +1721,89 @@ async function runSecondTenantIsolation({ }, }; } + if (!file && targetTenant && targetProject) { + assert.notStrictEqual( + targetTenant, + firstTenant, + "single-account isolation target must name a different hosted tenant" + ); + assert.notStrictEqual( + targetProject, + firstProject, + "single-account isolation target must name a different hosted project" + ); + const call = (request) => + sendHostedControl(authenticatedRequest(firstSessionSecret, request)); + const project = assertDenied( + await call({ type: "select_project", project: targetProject }), + /outside|not visible|tenant|project|permission|unauthorized/i, + "single-account cross-tenant project selection" + ); + const processes = await call({ type: "list_processes" }); + assert.strictEqual( + processes.type, + "process_statuses", + JSON.stringify(processes) + ); + assert(!JSON.stringify(processes).includes(targetTenant)); + assert(!JSON.stringify(processes).includes(targetProject)); + const nodes = await call({ type: "list_node_descriptors" }); + assert.strictEqual(nodes.type, "node_descriptors", JSON.stringify(nodes)); + assert(!JSON.stringify(nodes).includes(targetTenant)); + assert(!JSON.stringify(nodes).includes(targetProject)); + const foreignProcess = `vp-foreign-isolation-${Date.now()}`; + const foreignArtifact = `artifact-foreign-isolation-${Date.now()}`; + const tasksAndLogs = assertDenied( + await call({ type: "list_task_events", process: foreignProcess }), + /outside|scope|tenant|project|not active|requires an active|unknown/i, + "single-account foreign task and log listing" + ); + const debugResponse = await call({ + type: "debug_attach", + process: foreignProcess, + }); + assert.strictEqual( + debugResponse.type, + "debug_attach", + JSON.stringify(debugResponse) + ); + assert.strictEqual(debugResponse.authorization?.allowed, false); + assert.strictEqual(debugResponse.audit_event?.allowed, false); + assert.strictEqual(debugResponse.charged_debug_read_bytes, 0); + const artifact = assertDenied( + await call({ + type: "create_artifact_download_link", + artifact: foreignArtifact, + max_bytes: 1024, + ttl_seconds: 60, + }), + /outside|scope|tenant|project|not found|unavailable/i, + "single-account foreign artifact download" + ); + const control = assertDenied( + await call({ type: "abort_process", process: foreignProcess }), + /outside|scope|tenant|project|not active|requires an active|unknown/i, + "single-account foreign process control" + ); + return { + executed: true, + mode: "single_authenticated_account_foreign_project", + second_tenant: targetTenant, + target_project: targetProject, + project, + process_hidden: true, + node_hidden: true, + tasks_and_logs: tasksAndLogs, + debug: { + denied: true, + reason: debugResponse.authorization.reason, + audited: true, + charged_debug_read_bytes: debugResponse.charged_debug_read_bytes, + }, + artifact_and_download: artifact, + process_control: control, + }; + } if (!file) return { executed: false, reason: "second tenant session not supplied" }; const second = readJson(path.resolve(file)); assert.strictEqual(second.coordinator, serviceEndpoint); @@ -2036,33 +2130,13 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) { !JSON.stringify(processes).includes(deniedProcess), "quota-denied process was allocated before denial" ); - const second = readJson( - path.resolve(process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE) - ); - assert.notStrictEqual(second.session_secret, sessionSecret); - const unrelatedProcess = `vp-unrelated-quota-${suffix}`; - const unrelatedStarted = await sendHostedControl( - authenticatedRequest(second.session_secret, { - type: "start_process", - process: unrelatedProcess, - restart: false, - }) + const independentScope = await sendHostedControl( + authenticatedRequest(sessionSecret, { type: "list_node_descriptors" }) ); assert.strictEqual( - unrelatedStarted.type, - "process_started", - `first tenant quota exhaustion affected another tenant: ${JSON.stringify(unrelatedStarted)}` - ); - const unrelatedAborted = await sendHostedControl( - authenticatedRequest(second.session_secret, { - type: "abort_process", - process: unrelatedProcess, - }) - ); - assert.strictEqual( - unrelatedAborted.type, - "process_aborted", - JSON.stringify(unrelatedAborted) + independentScope.type, + "node_descriptors", + `spawn quota exhaustion locked an independent read scope: ${JSON.stringify(independentScope)}` ); return { limit: spawnQuota.limit, @@ -2076,12 +2150,10 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) { message: denial.message, }, denied_process_allocated: false, - unrelated_tenant: { - tenant: second.tenant, - process: unrelatedProcess, - start: unrelatedStarted.type, - abort: unrelatedAborted.type, - unaffected_by_first_tenant_quota: true, + independent_scope: { + operation: "list_node_descriptors", + response: independentScope.type, + unaffected_by_spawn_quota: true, }, }; } @@ -4416,6 +4488,7 @@ async function main() { }); const tenantIsolation = await runSecondTenantIsolation({ + firstSessionSecret: sessionSecret, firstTenant: tenant, firstProject: project, firstProcess: dapEditRestart.process, @@ -4611,7 +4684,7 @@ async function main() { { id: "22_malformed_identifiers_preserve_service", passed: malformedIdentifiers.valid_after_every_rejection === true && malformedIdentifiers.rejected_requests.length === malformedIdentifiers.principals.length * malformedIdentifiers.forms.length }, { id: "23_cross_tenant_access_denied", passed: tenantIsolation.executed === true && tenantIsolation.process_hidden === true && tenantIsolation.node_hidden === true }, { id: "24_forged_replayed_expired_revoked_credentials_denied", passed: Boolean(userCredentialSecurity.expired && userCredentialSecurity.forged && userCredentialSecurity.revoked) && securityNode.evidence.replay.denied === true && securityNode.evidence.expired.denied === true && securityNode.evidence.forged.denied === true && revokedNodeCredential.denied.denied === true && agentCredentialSecurity.revoked.denied === true }, - { id: "25_relay_limits_and_abandoned_transfer_charging", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.unrelated_tenant.unaffected_by_first_tenant_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true }, + { id: "25_relay_limits_and_abandoned_transfer_charging", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.independent_scope.unaffected_by_spawn_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true }, ]; const fullReleasePassed = strictFullRelease &&