QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 7s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Failing after 6s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
Save the current implementation and README, status, and known-issue documentation at the user-requested stop. No completed release or deployment is claimed. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: GPT-6 Codex <noreply@openai.com>
251 lines
11 KiB
Rust
251 lines
11 KiB
Rust
/* Copyright (C) 2026 Kingsrook, LLC.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
//! Fixed trusted compatibility cases. These are not hostile-runtime qualification.
|
|
#![allow(clippy::expect_used)]
|
|
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use qqq_javy_runtime::{Failure, process};
|
|
use serde_json::{Value, json};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
/// Synthetic exact runtime identity; an actual supervisor binds its loaded binary digest.
|
|
fn runtime_id() -> String {
|
|
"ab".repeat(32)
|
|
}
|
|
|
|
/// Prepare only fixed test module sources; this is not TypeScript compilation.
|
|
fn prepare(files: Vec<(&str, &str)>) -> Value {
|
|
prepare_result(files).expect("trusted preparation")
|
|
}
|
|
|
|
/// Keep compiler refusals observable without relaxing the trusted cache boundary.
|
|
fn prepare_result(files: Vec<(&str, &str)>) -> Result<Value, Failure> {
|
|
let bundle = json!({"format":"qqq-esm/1","entryPoint":"main.js","files":files.into_iter().map(|(path,content)|json!({"path":path,"content":content})).collect::<Vec<_>>()}).to_string();
|
|
let input = json!({"operation":"prepare","runtimeSha256":runtime_id(),"artifactSha256":format!("{:x}",Sha256::digest(bundle.as_bytes())),"bundle":bundle});
|
|
let output = process(input.to_string().as_bytes(), |_| panic!("prepare must not broker"))?;
|
|
Ok(serde_json::from_slice(&output).expect("prepared JSON"))
|
|
}
|
|
|
|
/// Create the exact hot request shape used by the persistent transport, without bundle.
|
|
fn request(prepared: &Value) -> Value {
|
|
json!({"protocol":"qbit-scripting-worker/1","type":"execute","requestId":"trusted-test","artifactId":"fixed",
|
|
"artifactSha256":prepared["artifactSha256"],"entryPoint":"main.ts","event":"CONTEXT","payload":{"actor":"alpha"},"capabilityToken":"synthetic-only",
|
|
"limits":{"wallMillis":2000,"cpuMillis":1000,"memoryBytes":64*1024*1024,"outputBytes":65536,"brokerCalls":2}})
|
|
}
|
|
|
|
/// Invoke through the public private-envelope boundary.
|
|
fn run(prepared: &Value, request: Value, broker: impl Fn(&[u8]) -> Result<Vec<u8>, Failure> + 'static) -> Result<Value, Failure> {
|
|
let input = json!({"operation":"run","runtimeSha256":runtime_id(),"preparedSha256":prepared["preparedSha256"],"prepared":prepared,"request":request});
|
|
let output = process(input.to_string().as_bytes(), broker)?;
|
|
Ok(serde_json::from_slice(&output).expect("result JSON"))
|
|
}
|
|
|
|
/// Exact existing broker response, containing only synthetic data.
|
|
fn allow(bytes: &[u8]) -> Result<Vec<u8>, Failure> {
|
|
let request: Value = serde_json::from_slice(bytes).expect("broker request");
|
|
assert_eq!(request["operation"], "query");
|
|
assert_eq!(request["token"], "synthetic-only");
|
|
Ok(
|
|
json!({"protocol":"qbit-scripting-worker/1","type":"brokerResult","ok":true,"rows":[{"id":7}],"truncated":false})
|
|
.to_string()
|
|
.into_bytes(),
|
|
)
|
|
}
|
|
|
|
/// Preparation must not execute module top-level effects or throw statements.
|
|
#[test]
|
|
fn preparation_does_not_evaluate_author_module() {
|
|
let prepared = prepare(vec![(
|
|
"main.js",
|
|
"throw new Error('fixed top level'); export function main(){return {ok:true}}",
|
|
)]);
|
|
let result = run(&prepared, request(&prepared), |_| panic!("no broker")).expect("bounded result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
}
|
|
|
|
/// Java's artifact character limit is not an ASCII-only byte limit.
|
|
#[test]
|
|
fn unicode_artifact_above_one_megabyte_remains_compatible() {
|
|
let source = format!(
|
|
"const marker='{}'; export function main(){{return {{length:marker.length}}}}",
|
|
"界".repeat(400_000)
|
|
);
|
|
assert!(source.len() > 1024 * 1024);
|
|
assert!(source.encode_utf16().count() < 1024 * 1024);
|
|
let prepared = prepare(vec![("main.js", source.as_str())]);
|
|
let result = run(&prepared, request(&prepared), |_| panic!("no broker")).expect("bounded Unicode result");
|
|
assert_eq!(result["message"]["outcome"], "OK");
|
|
assert_eq!(result["message"]["result"]["length"], 400_000);
|
|
}
|
|
|
|
/// Separate fixed module, await and broker cases localize compatibility failures.
|
|
#[test]
|
|
fn fixed_compatibility_boundaries() {
|
|
for (name, source) in [
|
|
("relative", "import {id} from './state.js'; export function main(){return {id}}"),
|
|
("tla", "await Promise.resolve(); export function main(){return {id:7}}"),
|
|
(
|
|
"broker",
|
|
"export async function main(c,q){const rows=await q.query('purchaseOrder',['id']);return {id:rows[0].id}}",
|
|
),
|
|
] {
|
|
let prepared = prepare(vec![("main.js", source), ("state.js", "export const id=7;")]);
|
|
let result = run(&prepared, request(&prepared), allow).expect("fixed boundary result");
|
|
assert_eq!(result["message"]["outcome"], "OK", "boundary: {name}");
|
|
assert_eq!(result["message"]["result"]["id"], 7);
|
|
}
|
|
}
|
|
|
|
/// Bytecode loading restores strict runtime import checks after nested module reads.
|
|
#[test]
|
|
fn nested_bytecode_and_dynamic_imports_preserve_attribute_policy() {
|
|
for attributes in ["", ", {with:{type:'json'}}"] {
|
|
let source = format!(
|
|
"import {{id}} from './nested/state.js'; export async function main(){{const extra=await import('./extra.js'{attributes});return {{id,extra:extra.id}}}}"
|
|
);
|
|
let prepared = prepare(vec![
|
|
("main.js", &source),
|
|
("nested/state.js", "export {id} from '../value.js';"),
|
|
("value.js", "export const id=7;"),
|
|
("extra.js", "export const id=8;"),
|
|
]);
|
|
let result = run(&prepared, request(&prepared), |_| panic!("no broker")).expect("bounded dynamic result");
|
|
assert_eq!(result["message"]["outcome"], if attributes.is_empty() { "OK" } else { "SCRIPT_ERROR" });
|
|
if attributes.is_empty() {
|
|
assert_eq!(result["message"]["result"], json!({"id":7,"extra":8}));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attributed source imports never enter the private compiled-bytecode cache.
|
|
#[test]
|
|
fn attributed_static_import_is_refused_at_preparation() {
|
|
for attributed_path in ["main.js", "unused.js"] {
|
|
let mut files = vec![
|
|
(
|
|
attributed_path,
|
|
"import {id} from './state.js' with {type:'json'}; export function main(){return {id}}",
|
|
),
|
|
("state.js", "export const id=7;"),
|
|
];
|
|
if attributed_path != "main.js" {
|
|
files.push(("main.js", "export function main(){return {ok:true}}"));
|
|
}
|
|
assert_eq!(prepare_result(files).expect_err("unsupported import attributes"), Failure::Compile);
|
|
}
|
|
}
|
|
|
|
/// Relative modules, live bindings, TLA, async main and Promise broker remain observable.
|
|
#[test]
|
|
fn fresh_modules_time_random_and_async_broker() {
|
|
let prepared = prepare(vec![
|
|
(
|
|
"state.js",
|
|
"export let count=0; export const stamp=Date.now(); export const random=Math.random(); export function bump(){count++}",
|
|
),
|
|
(
|
|
"main.js",
|
|
"import {count,stamp,random,bump} from './state.js'; await Promise.resolve(); export async function main(context,qqq){const rows=await qqq.query('purchaseOrder',['id']); bump(); return {count,stamp,random,id:rows[0].id,actor:context.payload.actor}}",
|
|
),
|
|
]);
|
|
let first = run(&prepared, request(&prepared), allow).expect("first invocation");
|
|
thread::sleep(Duration::from_millis(5));
|
|
let mut second_request = request(&prepared);
|
|
second_request["payload"]["actor"] = json!("beta");
|
|
let second = run(&prepared, second_request, allow).expect("second invocation");
|
|
assert_eq!(first["message"]["outcome"], "OK");
|
|
assert_eq!(second["message"]["outcome"], "OK");
|
|
let a = &first["message"]["result"];
|
|
let b = &second["message"]["result"];
|
|
assert_eq!(a["count"], 1);
|
|
assert_eq!(b["count"], 1);
|
|
assert_eq!(a["id"], 7);
|
|
assert_eq!(b["actor"], "beta");
|
|
assert_ne!(a["stamp"], b["stamp"]);
|
|
assert_ne!(a["random"], b["random"]);
|
|
}
|
|
|
|
/// Pending promises cannot be treated as successful completion.
|
|
#[test]
|
|
fn idle_pending_main_is_refused() {
|
|
let prepared = prepare(vec![("main.js", "export function main(){return new Promise(()=>{})}")]);
|
|
let result = run(&prepared, request(&prepared), |_| panic!("no broker")).expect("bounded result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
}
|
|
|
|
/// A second unawaited SDK call is refused by the existing one-pending-call rule.
|
|
#[test]
|
|
fn simultaneous_broker_calls_are_refused() {
|
|
let prepared = prepare(vec![(
|
|
"main.js",
|
|
"export async function main(c,q){await Promise.all([q.query('purchaseOrder',['id']),q.query('purchaseOrder',['id'])]);return {ok:true}}",
|
|
)]);
|
|
let calls = Rc::new(Cell::new(0));
|
|
let observed = Rc::clone(&calls);
|
|
let result = run(&prepared, request(&prepared), move |bytes| {
|
|
observed.set(observed.get() + 1);
|
|
allow(bytes)
|
|
})
|
|
.expect("bounded result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
assert!(calls.get() <= 1);
|
|
}
|
|
|
|
/// Current broker refusal is not converted into an authorized row result.
|
|
#[test]
|
|
fn broker_denial_is_generic() {
|
|
let prepared = prepare(vec![(
|
|
"main.js",
|
|
"export async function main(c,q){return {rows:await q.query('purchaseOrder',['id'])}}",
|
|
)]);
|
|
let result = run(&prepared, request(&prepared), |_| Err(Failure::Broker)).expect("bounded result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
assert_eq!(result["message"]["error"]["message"], "Script execution failed");
|
|
}
|
|
|
|
/// Cached code is bound to both publication bytes and runtime identity before execution.
|
|
#[test]
|
|
fn altered_bytecode_or_runtime_never_brokers() {
|
|
let prepared = prepare(vec![("main.js", "export function main(){return {ok:true}}")]);
|
|
let mut changed = prepared.clone();
|
|
changed["modules"][0]["bytecode"] = json!("AA==");
|
|
assert_eq!(
|
|
run(&changed, request(&prepared), |_| panic!("must not broker")).expect_err("changed bytes"),
|
|
Failure::Identity
|
|
);
|
|
changed = prepared.clone();
|
|
changed["runtimeSha256"] = json!("cd".repeat(32));
|
|
assert_eq!(
|
|
run(&changed, request(&prepared), |_| panic!("must not broker")).expect_err("changed runtime"),
|
|
Failure::Identity
|
|
);
|
|
}
|
|
|
|
/// Author serialization callbacks cannot create an unchecked detached rejection.
|
|
#[test]
|
|
fn detached_rejection_during_result_serialization_is_refused() {
|
|
let prepared = prepare(vec![(
|
|
"main.js",
|
|
"export function main(){return {toJSON(){Promise.reject(new Error('fixed trusted rejection'));return {ok:true}}}}",
|
|
)]);
|
|
let result = run(&prepared, request(&prepared), |_| panic!("no broker")).expect("bounded serialization result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
}
|
|
|
|
/// Return values are bounded plain objects and preserve declared output limits.
|
|
#[test]
|
|
fn output_limit_and_array_results_refused() {
|
|
for source in ["export function main(){return {text:'x'.repeat(200)}}", "export function main(){return [1,2]}"] {
|
|
let prepared = prepare(vec![("main.js", source)]);
|
|
let mut input = request(&prepared);
|
|
input["limits"]["outputBytes"] = json!(64);
|
|
let result = run(&prepared, input, |_| panic!("no broker")).expect("bounded result");
|
|
assert_eq!(result["message"]["outcome"], "SCRIPT_ERROR");
|
|
}
|
|
}
|