///|
priv enum MetadataProcessState {
Fresh
Retained(@metadata_wire.Snapshot)
Refused
Disposed
}
///|
priv enum MetadataQuiescence {
Confirmed
Unconfirmed(String)
}
///|
/// Bind the explicit process authority without acquisition during planning.
/// Missing grants refuse only when metadata is actually requested. A qualified
/// session owns binary transport and cleanup; saved frames grant no authority.
#cfg(any(target="native", target="js"))
pub fn Host::register_process_metadata_provider(
self : Host,
engine : @engine.Engine,
grants : @metadata_grants.Grants?,
workspace_root : String,
invocation_root : String,
timeout~ : Int32 = 120000,
cancel_file~ : String = "",
observer~ : @observe.Observer = @observe.Observer::noop(),
audit_sink~ : MetadataAuditSink = MetadataAuditSink::noop(),
) -> Unit raise {
if timeout < 6001 || timeout > 600000 {
raise @config.Invalid("metadata.restore_invalid_deadline")
}
self.register_requested_metadata_provider(engine, fn(scope) raise {
let operation = @platform.observation_id()
let project = @identity.sha256(
@utf8.encode(
Json::array(
[
workspace_root,
scope.workspace_root,
scope.project_root,
scope.project_name,
].map(Json::string),
).stringify(),
),
)
let audit = MetadataAuditContext::{
sink: audit_sink,
operation,
project,
evidence: None,
}
metadata_observe(
observer,
operation,
project,
Resolve,
"metadata.acquire",
fn() raise {
guard grants is Some(granted) else {
raise @linked_metadata.Invalid(
"metadata.restore_host_grants_required",
scope.project_root,
"Select an explicit --metadata-grants file",
)
}
let selected = granted.select(scope, workspace_root, invocation_root)
audit.evidence = Some({
service: @metadata_wire.SERVICE_ID,
service_sha256: selected.service_sha256,
service_verified: false,
release: selected.release,
native_revision: None,
profile: "local-library-mvp",
profile_version: "0.1.0-draft.1",
assurance: "portable",
profile_verified: false,
})
metadata_process_session(
self, scope, selected, operation, project, timeout, cancel_file, observer,
audit,
)
},
audit=Some(audit),
)
})
}
///|
/// Native and JS adapters call this helper. Pure targets keep the shared code.
#warnings("-unused_value")
fn metadata_process_session(
host : Host,
scope : @engine.ProjectMetadataScope,
grant : @metadata_grants.GrantedService,
operation : String,
project : String,
timeout : Int32,
cancel_file : String,
observer : @observe.Observer,
audit : MetadataAuditContext,
) -> MetadataSession raise {
if !@platform.file_chunks_supported() || !@platform.process_supported() {
raise @config.Invalid("metadata.restore_runtime_unavailable")
}
let deadline = @platform.monotonic_seconds() + timeout.to_double() / 1000.0
let directory = @platform.temporary()
let mut state = MetadataProcessState::Fresh
let mut quiescence = MetadataQuiescence::Confirmed
let mut cleanup_deadline : Double? = Some(deadline)
fn check() -> Unit raise {
if state is Disposed {
raise @config.Invalid("metadata.restore_disposed")
}
if (host.cancelled)() ||
(cancel_file != "" && @platform.kind(cancel_file) != 0) {
raise @protocol.Invalid("Cancelled")
}
if @platform.monotonic_seconds() >= deadline {
raise @config.Invalid("metadata.restore_deadline")
}
}
let (policy, lock) = {
errdefer dispose_build_tree(directory)
let policy = metadata_read_file(
grant.policy,
@metadata_wire.MAX_REQUEST_BYTES,
check,
)
let lock = metadata_read_file(
grant.lock,
@metadata_wire.MAX_REQUEST_BYTES,
check,
)
if policy.length() + lock.length() > @metadata_wire.MAX_REQUEST_BYTES {
raise @config.Invalid("metadata.restore_request_limit")
}
(policy, lock)
}
let origins = match scope.request {
SharedLocalRestore(r) =>
[
r.service.origin,
r.policy.origin,
r.lock.origin,
r.registry.origin,
r.state.origin,
]
Disabled => []
}
let frozen_scope = @identity.sha256(
@utf8.encode(
Json::array(
([
project,
grant.node,
grant.node_sha256,
grant.helper,
grant.helper_bundle_sha256,
grant.service,
grant.service_sha256,
grant.policy,
@identity.sha256(policy),
grant.lock,
@identity.sha256(lock),
grant.registry,
grant.state,
grant.release,
"portable",
"local-library-mvp",
"0.1.0-draft.1",
] +
origins).map(Json::string),
).stringify(),
),
)
let expected : @metadata_wire.Expected = {
invocation: operation,
project_scope: frozen_scope,
release: grant.release,
policy_sha256: @identity.sha256(policy),
lock_sha256: @identity.sha256(lock),
}
fn identity() -> Unit raise {
if audit.evidence is Some(e) {
audit.evidence = Some({ ..e, service_verified: false, })
}
check()
if metadata_hash_file(grant.node, check) != grant.node_sha256 {
raise @config.Invalid("metadata.restore_executable_changed: Node pin")
}
if metadata_bundle_hash(grant.helper, check) != grant.helper_bundle_sha256 {
raise @config.Invalid(
"metadata.restore_executable_changed: host helper bundle pin",
)
}
if metadata_hash_file(grant.service, check) != grant.service_sha256 {
raise @config.Invalid(
"metadata.restore_executable_changed: selected service pin",
)
}
if audit.evidence is Some(e) {
audit.evidence = Some({ ..e, service_verified: true, })
}
}
fn exchange(name : String, input : Bytes) -> Bytes raise {
check()
identity()
let path = directory + "/" + name
@platform.mkdir(path)
@platform.write_bytes(path + "/request.ionb", input)
let budget = ((deadline - @platform.monotonic_seconds()) * 1000.0).to_int()
if budget < 6001 {
raise @config.Invalid("metadata.restore_deadline")
}
@platform.write(
path + "/control.json",
(
{
"profile": "morphir-metadata-exchange-v1",
"invocation": operation,
"projectScope": frozen_scope,
"service": grant.service,
"serviceSha256": grant.service_sha256,
"input": path + "/request.ionb",
"inputSha256": @identity.sha256(input),
"output": path + "/response.ionb",
"receipt": path + "/receipt.json",
"budgetMs": budget,
"cancelFile": cancel_file,
} : Json).stringify(),
)
quiescence = Unconfirmed(path)
let result = @platform.process(
grant.node,
[grant.helper, path + "/control.json"],
directory,
"",
budget,
)
let receipt = metadata_receipt(path, operation, frozen_scope) catch {
_ =>
raise @config.Invalid(
"metadata.restore_cleanup_unconfirmed: scratch retained",
)
}
if build_field(receipt, "quiescent") is True {
quiescence = Confirmed
}
if build_field(result, "exitCode") != Json::number(0.0) {
let code = build_text(receipt, "code")
if code == "execution.cancelled" {
raise @protocol.Invalid("Cancelled")
}
raise @config.Invalid(
if code.has_prefix("metadata.restore_") || code.has_prefix("execution.") {
code
} else {
"metadata.restore_transport_failed"
},
)
}
guard quiescence is Confirmed else {
raise @config.Invalid("metadata.restore_cleanup_unconfirmed")
}
if build_text(result, "stdout").length() > 16384 ||
build_field(receipt, "result") != Json::string("succeeded") ||
@wire.parse(build_text(result, "stdout")) != receipt ||
build_text(receipt, "serviceSha256") != grant.service_sha256 {
raise @config.Invalid("metadata.restore_invalid_receipt")
}
identity()
let bytes = @platform.read_bytes(path + "/response.ionb")
if build_text(receipt, "responseSha256") != @identity.sha256(bytes) ||
build_field(receipt, "responseBytes") !=
Json::number(bytes.length().to_double()) {
raise @config.Invalid("metadata.restore_response_changed")
}
check()
bytes
}
fn snapshot() -> @metadata_wire.Snapshot raise {
match state {
Retained(snapshot) => return snapshot
Fresh => ()
Refused => raise @config.Invalid("metadata.restore_session_refused")
Disposed => raise @config.Invalid("metadata.restore_disposed")
}
errdefer {
state = Refused
}
metadata_observe(
observer,
operation,
project,
ProfileCheck,
"metadata.probe",
fn() raise {
let probe = exchange(
"probe",
@metadata_wire.capabilities_request(operation, frozen_scope),
)
@metadata_wire.admit_capabilities(probe, operation, frozen_scope)
if audit.evidence is Some(e) {
audit.evidence = Some({ ..e, profile_verified: true, })
}
},
audit=Some(audit),
)
let response = metadata_observe(
observer,
operation,
project,
Resolve,
"metadata.restore",
fn() raise {
exchange(
"restore",
@metadata_wire.snapshot_request(
expected,
policy,
lock,
grant.registry,
grant.state,
directory + "/restored",
),
)
},
audit=Some(audit),
)
let snapshot = metadata_observe(
observer,
operation,
project,
Decode,
"metadata.decode",
fn() raise {
let accepted = @metadata_wire.Snapshot::decode(response, expected)
check()
if audit.evidence is Some(e) {
audit.evidence = Some({
..e,
native_revision: Some(accepted.native_revision()),
})
}
accepted
},
audit=Some(audit),
)
state = Retained(snapshot)
snapshot
}
{
capabilities: {
restore_library: Some(fn(predicate) raise {
if !@linked_metadata.is_target_names_predicate(predicate) {
raise @linked_metadata.Invalid(
"metadata.provider_unavailable", predicate, "The grant is for the target-name provider",
)
}
metadata_process_boundary(scope.project_root, fn() raise {
check()
let restored = snapshot().restored()
check()
restored
})
}),
load_context: Some(fn(resource, base) raise {
metadata_process_boundary(resource, fn() raise {
check()
metadata_observe(
observer,
operation,
project,
Resolve,
"metadata.context",
fn() raise {
let context = snapshot().context(resource, base)
check()
context
},
audit=Some(audit),
)
})
}),
target_names_interpreter: true,
provider_observer: audit.provider_observer(),
},
dispose: fn() raise {
if state is Disposed {
return
}
metadata_observe(
observer,
operation,
project,
Cleanup,
"metadata.dispose",
fn() raise {
if quiescence is Unconfirmed(path) {
let receipt = metadata_receipt(path, operation, frozen_scope) catch {
_ =>
raise @config.Invalid(
"metadata.restore_cleanup_unconfirmed: scratch retained",
)
}
if build_field(receipt, "quiescent") is True {
quiescence = Confirmed
}
}
guard quiescence is Confirmed else {
raise @config.Invalid(
"metadata.restore_cleanup_unconfirmed: scratch retained",
)
}
let until = cleanup_deadline.unwrap_or(
@platform.monotonic_seconds() + 5.0,
)
cleanup_deadline = None
metadata_dispose_tree(directory, until)
state = Disposed
},
audit=Some(audit),
)
},
}
}