feat(core): add ontology-backed asset and host runtime
This commit is contained in:
@@ -8,7 +8,7 @@ const replayedIntermediateConstraintMigrations = Object.freeze([
|
||||
"009_device_sensitive_reference_commands.sql",
|
||||
"011_device_control_resource_commands.sql",
|
||||
]);
|
||||
const finalCommandKindMigration = "014_device_registry_profile_commands.sql";
|
||||
const finalCommandKindMigration = "016_device_asset_infrastructure_ontology.sql";
|
||||
const finalCommandKinds = Object.freeze([
|
||||
"owner_scope.ensure",
|
||||
"project.ensure",
|
||||
@@ -31,6 +31,14 @@ const finalCommandKinds = Object.freeze([
|
||||
"device_binding.revoke",
|
||||
"device_configuration_revision.create",
|
||||
"device_configuration_desired.set",
|
||||
"asset.ensure",
|
||||
"asset_binding.ensure",
|
||||
"asset_binding.close",
|
||||
"infrastructure_host.ensure",
|
||||
"infrastructure_endpoint.ensure",
|
||||
"infrastructure_deployment.ensure",
|
||||
"infrastructure_service_instance.ensure",
|
||||
"health_observation.record",
|
||||
]);
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/003_device_management_commands.sql",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createControlCoreApp } from "../src/app.mjs";
|
||||
|
||||
const managementToken = "test-only-management-token-with-32-bytes";
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const projectId = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
test("management API forwards a canonical asset command", async () => {
|
||||
let executed;
|
||||
const runtime = await startServer({
|
||||
executeManagementCommand: async (input) => {
|
||||
executed = input;
|
||||
return { replayed: false, result: { created: true } };
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${runtime.baseUrl}/internal/v1/management/assets:ensure`, {
|
||||
method: "POST",
|
||||
headers: managementHeaders("ontology-asset-0001"),
|
||||
body: JSON.stringify({
|
||||
projectRef: `project:${projectId}`,
|
||||
assetKey: "trike-001",
|
||||
displayName: "Trike 001",
|
||||
assetTypeRef: "asset-type:delivery-trike",
|
||||
}),
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(executed.commandKind, "asset.ensure");
|
||||
assert.equal(executed.command.assetKey, "trike-001");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("query API exposes the ontology projection through Core", async () => {
|
||||
let actor;
|
||||
const runtime = await startServer({
|
||||
getProjectOntologyProjection: async (value, requestedProjectId) => {
|
||||
actor = value;
|
||||
assert.equal(requestedProjectId, projectId);
|
||||
return {
|
||||
ontology: { catalogHash: "229c61c02a790906" },
|
||||
assets: [],
|
||||
hosts: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/query/projects/${projectId}/ontology`,
|
||||
{ headers: managementHeaders("ontology-query-0001") },
|
||||
);
|
||||
const body = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.projection.ontology.catalogHash, "229c61c02a790906");
|
||||
assert.equal(actor.userRef, "user:test-owner");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function startServer(repositoryOverrides) {
|
||||
const server = createControlCoreApp({
|
||||
managementApiEnabled: true,
|
||||
managementToken,
|
||||
identifierPepper,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
||||
...repositoryOverrides,
|
||||
},
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) =>
|
||||
server.close((error) => error ? reject(error) : resolve())),
|
||||
};
|
||||
}
|
||||
|
||||
function managementHeaders(idempotencyKey) {
|
||||
return {
|
||||
Authorization: `Bearer ${managementToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": idempotencyKey,
|
||||
"X-NODEDC-User-Ref": "user:test-owner",
|
||||
"X-NODEDC-Hub-Role": "owner",
|
||||
"X-NODEDC-Group-Refs": "",
|
||||
"X-NODEDC-Owner-Scopes": "company=organization:test",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_ONTOLOGY_CATALOG_HASH,
|
||||
normalizeOntologyManagementCommand,
|
||||
} from "../src/ontology-management.mjs";
|
||||
import {
|
||||
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
|
||||
normalizeDeviceManagementCommand,
|
||||
} from "../src/management-command.mjs";
|
||||
|
||||
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
||||
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
||||
const assetRef = "asset:33333333-3333-4333-8333-333333333333";
|
||||
const hostRef = "host:44444444-4444-4444-8444-444444444444";
|
||||
const deploymentRef = "deployment:55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("publishes the production ontology catalog contract", () => {
|
||||
assert.equal(DEVICE_ONTOLOGY_CATALOG_HASH, "229c61c02a790906");
|
||||
for (const kind of [
|
||||
"asset.ensure",
|
||||
"asset_binding.ensure",
|
||||
"infrastructure_host.ensure",
|
||||
"health_observation.record",
|
||||
]) {
|
||||
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("normalizes an asset and a temporal tracker binding", () => {
|
||||
const asset = normalizeDeviceManagementCommand("asset.ensure", {
|
||||
projectRef,
|
||||
assetKey: "trike-001",
|
||||
displayName: "Trike 001",
|
||||
assetTypeRef: "asset-type:delivery-trike",
|
||||
});
|
||||
const binding = normalizeDeviceManagementCommand("asset_binding.ensure", {
|
||||
projectRef,
|
||||
bindingKey: "trike-001-primary-tracker",
|
||||
deviceRef,
|
||||
assetRef,
|
||||
bindingKind: "tracking",
|
||||
validFrom: "2026-08-22T10:00:00.000Z",
|
||||
provenanceRef: "onboarding:direct-b2",
|
||||
});
|
||||
|
||||
assert.equal(asset.assetKey, "trike-001");
|
||||
assert.equal(binding.deviceId, deviceRef.slice("device:".length));
|
||||
assert.equal(binding.assetId, assetRef.slice("asset:".length));
|
||||
assert.equal(binding.bindingKind, "tracking");
|
||||
});
|
||||
|
||||
test("normalizes provider-neutral host topology without browser credentials", () => {
|
||||
const host = normalizeOntologyManagementCommand("infrastructure_host.ensure", {
|
||||
projectRef,
|
||||
hostKey: "b2-edge-moscow",
|
||||
displayName: "B2 Edge Moscow",
|
||||
providerRef: "provider:beget",
|
||||
externalRef: "provider-resource:vps-123",
|
||||
managementCredentialRef: "secret-ref:device-core/b2-edge-moscow",
|
||||
lifecycleState: "active",
|
||||
});
|
||||
const deployment = normalizeOntologyManagementCommand(
|
||||
"infrastructure_deployment.ensure",
|
||||
{
|
||||
projectRef,
|
||||
hostRef,
|
||||
deploymentKey: "device-edge-001",
|
||||
displayName: "Device Edge 001",
|
||||
artifactRef: "artifact:device-edge/1.0.0",
|
||||
artifactDigest: `sha256:${"a".repeat(64)}`,
|
||||
},
|
||||
);
|
||||
const service = normalizeOntologyManagementCommand(
|
||||
"infrastructure_service_instance.ensure",
|
||||
{
|
||||
projectRef,
|
||||
hostRef,
|
||||
deploymentRef,
|
||||
serviceKey: "device-edge",
|
||||
displayName: "Device Edge",
|
||||
serviceRole: "device.edge",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(host.managementCredentialRef.startsWith("secret-ref:"), true);
|
||||
assert.equal(deployment.hostId, hostRef.slice("host:".length));
|
||||
assert.equal(service.serviceRole, "device.edge");
|
||||
assert.equal("password" in host, false);
|
||||
});
|
||||
|
||||
test("rejects credential-bearing endpoints and secret-shaped health evidence", () => {
|
||||
assert.throws(
|
||||
() => normalizeOntologyManagementCommand("infrastructure_endpoint.ensure", {
|
||||
projectRef,
|
||||
hostRef,
|
||||
endpointKey: "ssh",
|
||||
purpose: "management",
|
||||
endpointUri: "ssh://root:password@example.test:22/",
|
||||
}),
|
||||
/device_endpoint_uri_invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeOntologyManagementCommand("health_observation.record", {
|
||||
projectRef,
|
||||
subjectKind: "host",
|
||||
subjectRef: hostRef,
|
||||
observedState: "reachable",
|
||||
evidenceClass: "management_probe",
|
||||
sourceRef: "probe:device-core",
|
||||
schemaRef: "schema:health.v1",
|
||||
evidence: { token: "forbidden" },
|
||||
observedAt: "2026-08-22T10:00:00.000Z",
|
||||
expiresAt: "2026-08-22T10:01:00.000Z",
|
||||
}),
|
||||
/forbidden_device_field/,
|
||||
);
|
||||
});
|
||||
|
||||
test("health is a bounded observation and not a permanent online flag", () => {
|
||||
const command = normalizeOntologyManagementCommand(
|
||||
"health_observation.record",
|
||||
{
|
||||
projectRef,
|
||||
subjectKind: "host",
|
||||
subjectRef: hostRef,
|
||||
observedState: "reachable",
|
||||
evidenceClass: "management_probe",
|
||||
sourceRef: "probe:device-core",
|
||||
schemaRef: "schema:health.v1",
|
||||
evidence: { latencyMs: 42 },
|
||||
observedAt: "2026-08-22T10:00:00.000Z",
|
||||
expiresAt: "2026-08-22T10:01:00.000Z",
|
||||
},
|
||||
);
|
||||
assert.equal(command.observedState, "reachable");
|
||||
assert.equal(command.expiresAt, "2026-08-22T10:01:00.000Z");
|
||||
assert.throws(
|
||||
() => normalizeOntologyManagementCommand("health_observation.record", {
|
||||
projectRef,
|
||||
subjectKind: "host",
|
||||
subjectRef: hostRef,
|
||||
observedState: "reachable",
|
||||
evidenceClass: "management_probe",
|
||||
sourceRef: "probe:device-core",
|
||||
schemaRef: "schema:health.v1",
|
||||
evidence: {},
|
||||
observedAt: command.observedAt,
|
||||
expiresAt: command.observedAt,
|
||||
}),
|
||||
/device_health_freshness_window_invalid/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/016_device_asset_infrastructure_ontology.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
|
||||
|
||||
test("ontology migration is additive and encodes the official entity identifiers", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
for (const table of [
|
||||
"device_assets",
|
||||
"device_asset_bindings",
|
||||
"device_infrastructure_hosts",
|
||||
"device_infrastructure_deployments",
|
||||
"device_infrastructure_service_instances",
|
||||
"device_health_observations",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`create table if not exists ${table}`, "i"));
|
||||
}
|
||||
for (const entityId of [
|
||||
"asset.asset",
|
||||
"device.asset_binding",
|
||||
"infrastructure.host",
|
||||
"infrastructure.deployment",
|
||||
"infrastructure.service_instance",
|
||||
"observation.health_observation",
|
||||
]) {
|
||||
assert.equal(sql.includes(`'${entityId}'`), true);
|
||||
}
|
||||
assert.equal(sql.includes("229c61c02a790906"), true);
|
||||
for (const commandKind of [
|
||||
"asset.ensure",
|
||||
"asset_binding.ensure",
|
||||
"asset_binding.close",
|
||||
"infrastructure_host.ensure",
|
||||
"infrastructure_endpoint.ensure",
|
||||
"infrastructure_deployment.ensure",
|
||||
"infrastructure_service_instance.ensure",
|
||||
"health_observation.record",
|
||||
]) {
|
||||
assert.equal(sql.includes(`'${commandKind}'`), true);
|
||||
}
|
||||
assert.doesNotMatch(sql, /insert\s+into\s+device_(?:assets|infrastructure_hosts)/i);
|
||||
assert.doesNotMatch(sql, /gelios|arusnavi|beget|hetzner|aws/i);
|
||||
});
|
||||
|
||||
test("ontology migration follows integration identity", async () => {
|
||||
const source = await readFile(repositoryUrl, "utf8");
|
||||
assert.ok(
|
||||
source.indexOf("015_device_integration_identity.sql")
|
||||
< source.indexOf("016_device_asset_infrastructure_ontology.sql"),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user