feat(manager): manage ontology assets and infrastructure

This commit is contained in:
DCCONSTRUCTIONS
2026-08-22 15:04:32 +03:00
parent b302b6ba1a
commit 088686d67f
8 changed files with 1027 additions and 27 deletions
@@ -13,6 +13,14 @@ const commandRoutes = new Map([
["enrollment-intents:ensure", "/internal/v1/management/enrollment-intents:ensure"],
["devices:claim", "/internal/v1/management/devices:claim"],
["devices:update", "/internal/v1/management/devices:update"],
["assets:ensure", "/internal/v1/management/assets:ensure"],
["asset-bindings:ensure", "/internal/v1/management/asset-bindings:ensure"],
["asset-bindings:close", "/internal/v1/management/asset-bindings:close"],
["infrastructure-hosts:ensure", "/internal/v1/management/infrastructure-hosts:ensure"],
["infrastructure-endpoints:ensure", "/internal/v1/management/infrastructure-endpoints:ensure"],
["infrastructure-deployments:ensure", "/internal/v1/management/infrastructure-deployments:ensure"],
["infrastructure-service-instances:ensure", "/internal/v1/management/infrastructure-service-instances:ensure"],
["health-observations:record", "/internal/v1/management/health-observations:record"],
["device-bindings:ensure", "/internal/v1/management/device-bindings:ensure"],
["device-bindings:revoke", "/internal/v1/management/device-bindings:revoke"],
[
@@ -61,8 +69,13 @@ export function createDeviceCoreClient({ baseUrl, token, fetchImpl = fetch } = {
},
async getWorkspace(actor, projectRef) {
const projectId = entityId(projectRef, "project");
return request(`/internal/v1/query/projects/${projectId}/workspace`, actor)
.then((body) => body.workspace);
const [workspace, ontology] = await Promise.all([
request(`/internal/v1/query/projects/${projectId}/workspace`, actor)
.then((body) => body.workspace),
request(`/internal/v1/query/projects/${projectId}/ontology`, actor)
.then((body) => body.projection),
]);
return { ...workspace, ontology };
},
async execute(command, actor, input, idempotencyKey) {
const pathname = commandRoutes.get(command);
@@ -100,6 +113,13 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) {
const configurationStates = new Map();
const auditEvents = [];
const commands = new Map();
const assets = new Map();
const assetBindings = new Map();
const hosts = new Map();
const endpoints = new Map();
const deployments = new Map();
const serviceInstances = new Map();
const healthObservations = new Map();
function now() {
return new Date().toISOString();
@@ -166,6 +186,53 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) {
identifierProjection: fixture === "arusnavi-b2" ? "authorized-full" : "masked-only",
auditPayloadProjection: "metadata-only",
},
ontology: ontologyProjection(projectRef),
};
}
function ontologyProjection(projectRef) {
const projectHosts = projectValues(hosts, projectRef);
const projectServices = projectValues(serviceInstances, projectRef);
const withHealth = (subjectKind, value, subjectRef) => ({
...value,
health: latestPreviewHealth(projectRef, subjectKind, subjectRef),
});
return {
ontology: {
catalogHash: "229c61c02a790906",
packages: ["asset", "device", "infrastructure", "observation"],
},
assets: projectValues(assets, projectRef),
assetBindings: projectValues(assetBindings, projectRef),
hosts: projectHosts.map((host) => withHealth("host", host, host.hostRef)),
endpoints: projectValues(endpoints, projectRef),
deployments: projectValues(deployments, projectRef),
serviceInstances: projectServices.map((service) =>
withHealth("service-instance", service, service.serviceInstanceRef)),
policies: {
restrictedIdentifiers: "masked-only",
managementCredentials: "opaque-reference-only",
missingHealthEvidence: "unobserved-not-unhealthy",
arbitraryConsole: "disabled",
},
};
}
function latestPreviewHealth(projectRef, subjectKind, subjectRef) {
const values = projectValues(healthObservations, projectRef)
.filter((item) => item.subjectKind === subjectKind && item.subjectRef === subjectRef)
.sort((left, right) => right.observedAt.localeCompare(left.observedAt));
const latest = values[0];
if (!latest) return { state: "unobserved", freshness: "missing", observationRef: null };
const fresh = new Date(latest.expiresAt).valueOf() > Date.now();
return {
state: fresh ? latest.observedState : "unobserved",
freshness: fresh ? "fresh" : "stale",
lastObservedState: latest.observedState,
evidenceClass: latest.evidenceClass,
observedAt: latest.observedAt,
expiresAt: latest.expiresAt,
observationRef: latest.healthObservationRef,
};
}
@@ -608,6 +675,167 @@ export function createLocalPreviewDeviceCore({ fixture = null } = {}) {
if (command === "devices:claim") {
throw serviceError("device_discovery_not_found", 404);
}
if (command === "assets:ensure") {
if (!projects.has(input.projectRef)) throw serviceError("device_project_not_found", 404);
const existing = projectValues(assets, input.projectRef)
.find((item) => item.assetKey === input.assetKey);
const assetRef = existing?.assetRef || `asset:${randomUUID()}`;
const asset = {
assetRef,
projectRef: input.projectRef,
assetKey: input.assetKey,
displayName: input.displayName,
assetTypeRef: input.assetTypeRef,
lifecycleState: input.lifecycleState ?? "active",
ontology: previewOntology("asset.asset"),
};
assets.set(assetRef, asset);
audit(actor, input.projectRef, createdEvent(existing, "asset"));
return { replayed: false, result: { created: !existing, asset } };
}
if (command === "asset-bindings:ensure") {
const device = devices.get(input.deviceRef);
const asset = assets.get(input.assetRef);
if (!device || device.projectRef !== input.projectRef) {
throw serviceError("device_not_found", 404);
}
if (!asset || asset.projectRef !== input.projectRef) {
throw serviceError("device_asset_not_found", 404);
}
const existing = projectValues(assetBindings, input.projectRef)
.find((item) => item.bindingKey === input.bindingKey);
const assetBindingRef = existing?.assetBindingRef || `asset-binding:${randomUUID()}`;
const assetBinding = {
assetBindingRef,
projectRef: input.projectRef,
bindingKey: input.bindingKey,
deviceRef: input.deviceRef,
deviceName: device.displayName,
assetRef: input.assetRef,
assetName: asset.displayName,
bindingKind: input.bindingKind ?? "tracking",
validFrom: input.validFrom,
validTo: null,
provenanceRef: input.provenanceRef,
ontology: previewOntology("device.asset_binding"),
};
assetBindings.set(assetBindingRef, assetBinding);
audit(actor, input.projectRef, createdEvent(existing, "asset_binding"));
return { replayed: false, result: { created: !existing, assetBinding } };
}
if (command === "asset-bindings:close") {
const existing = assetBindings.get(input.assetBindingRef);
if (!existing || existing.projectRef !== input.projectRef || existing.validTo) {
throw serviceError("device_asset_binding_not_closable", 409);
}
const assetBinding = { ...existing, validTo: input.validTo };
assetBindings.set(existing.assetBindingRef, assetBinding);
audit(actor, input.projectRef, "asset_binding.closed");
return { replayed: false, result: { closed: true, assetBinding } };
}
if (command === "infrastructure-hosts:ensure") {
if (!projects.has(input.projectRef)) throw serviceError("device_project_not_found", 404);
const existing = projectValues(hosts, input.projectRef)
.find((item) => item.hostKey === input.hostKey);
const hostRef = existing?.hostRef || `host:${randomUUID()}`;
const host = {
hostRef,
projectRef: input.projectRef,
hostKey: input.hostKey,
displayName: input.displayName,
providerRef: input.providerRef ?? null,
externalRef: input.externalRef ?? null,
managementCredentialConfigured: Boolean(input.managementCredentialRef),
lifecycleState: input.lifecycleState ?? "provisioning",
ontology: previewOntology("infrastructure.host"),
};
hosts.set(hostRef, host);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_host"));
return { replayed: false, result: { created: !existing, host } };
}
if (command === "infrastructure-endpoints:ensure") {
const host = hosts.get(input.hostRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
const existing = projectValues(endpoints, input.projectRef)
.find((item) => item.hostRef === input.hostRef && item.endpointKey === input.endpointKey);
const endpointRef = existing?.endpointRef || `endpoint:${randomUUID()}`;
const endpoint = {
endpointRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
endpointKey: input.endpointKey,
purpose: input.purpose,
endpointUri: input.endpointUri,
lifecycleState: input.lifecycleState ?? "active",
ontology: previewOntology("infrastructure.endpoint"),
};
endpoints.set(endpointRef, endpoint);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_endpoint"));
return { replayed: false, result: { created: !existing, endpoint } };
}
if (command === "infrastructure-deployments:ensure") {
const host = hosts.get(input.hostRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
const existing = projectValues(deployments, input.projectRef)
.find((item) => item.deploymentKey === input.deploymentKey);
const deploymentRef = existing?.deploymentRef || `deployment:${randomUUID()}`;
const deployment = {
deploymentRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
deploymentKey: input.deploymentKey,
displayName: input.displayName,
artifactRef: input.artifactRef,
artifactDigest: input.artifactDigest,
lifecycleState: input.lifecycleState ?? "desired",
ontology: previewOntology("infrastructure.deployment"),
};
deployments.set(deploymentRef, deployment);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_deployment"));
return { replayed: false, result: { created: !existing, deployment } };
}
if (command === "infrastructure-service-instances:ensure") {
const host = hosts.get(input.hostRef);
const deployment = deployments.get(input.deploymentRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
if (!deployment || deployment.projectRef !== input.projectRef) {
throw serviceError("device_deployment_not_found", 404);
}
const existing = projectValues(serviceInstances, input.projectRef)
.find((item) => item.hostRef === input.hostRef && item.serviceKey === input.serviceKey);
const serviceInstanceRef = existing?.serviceInstanceRef || `service-instance:${randomUUID()}`;
const serviceInstance = {
serviceInstanceRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
deploymentRef: input.deploymentRef,
edgeRef: input.edgeRef ?? null,
serviceKey: input.serviceKey,
displayName: input.displayName,
serviceRole: input.serviceRole,
lifecycleState: input.lifecycleState ?? "provisioning",
ontology: previewOntology("infrastructure.service_instance"),
};
serviceInstances.set(serviceInstanceRef, serviceInstance);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_service_instance"));
return { replayed: false, result: { created: !existing, serviceInstance } };
}
if (command === "health-observations:record") {
const healthObservationRef = `health-observation:${randomUUID()}`;
const healthObservation = {
healthObservationRef,
projectRef: input.projectRef,
subjectKind: input.subjectKind,
subjectRef: input.subjectRef,
observedState: input.observedState,
evidenceClass: input.evidenceClass,
observedAt: input.observedAt,
expiresAt: input.expiresAt,
};
healthObservations.set(healthObservationRef, healthObservation);
audit(actor, input.projectRef, "health_observation.recorded");
return { replayed: false, result: { recorded: true, healthObservation } };
}
if (command === "devices:update") {
const device = devices.get(input.deviceRef);
if (!device || device.projectRef !== input.projectRef) {
@@ -658,6 +886,10 @@ function createdEvent(existing, resource) {
return `${resource}.${existing ? "updated" : "created"}`;
}
function previewOntology(entityId) {
return { entityId, catalogHash: "229c61c02a790906" };
}
function requirePlatformOwner(actor) {
if (actor?.hubRole !== "owner") {
throw serviceError("device_platform_catalog_access_denied", 403);
@@ -869,6 +1101,7 @@ const ownerCapabilities = Object.freeze([
"project.manage",
"access.manage",
"inventory.read",
"asset.manage",
"device.enroll",
"device.claim",
"device.transfer",
@@ -876,6 +1109,9 @@ const ownerCapabilities = Object.freeze([
"route.manage",
"binding.manage",
"telemetry.observe",
"observation.write",
"infrastructure.read",
"infrastructure.manage",
"configuration.read",
"configuration.manage",
"command.plan",
@@ -60,6 +60,36 @@ test("Device Core client accepts only canonical commands and entity refs", async
);
});
test("Device Core client merges workspace and ontology projections", async () => {
const calls = [];
const client = createDeviceCoreClient({
baseUrl: "http://127.0.0.1:3210",
token,
fetchImpl: async (url) => {
calls.push(String(url));
if (String(url).endsWith("/ontology")) {
return jsonResponse(200, {
ok: true,
projection: {
ontology: { catalogHash: "229c61c02a790906", packages: [] },
assets: [], assetBindings: [], hosts: [], endpoints: [],
deployments: [], serviceInstances: [], policies: {},
},
});
}
return jsonResponse(200, { ok: true, workspace: { project: { projectRef: "project:test" } } });
},
});
const workspace = await client.getWorkspace(
actor,
"project:11111111-1111-4111-8111-111111111111",
);
assert.equal(workspace.ontology.ontology.catalogHash, "229c61c02a790906");
assert.equal(calls.length, 2);
assert.ok(calls.some((value) => value.endsWith("/workspace")));
assert.ok(calls.some((value) => value.endsWith("/ontology")));
});
test("local preview is empty and creates resources only through canonical commands", async () => {
const client = createLocalPreviewDeviceCore();
assert.deepEqual(await client.listProjects(actor), []);
@@ -81,6 +111,47 @@ test("local preview is empty and creates resources only through canonical comman
assert.equal(created.result.created, true);
const projectRef = created.result.project.projectRef;
const host = await client.execute("infrastructure-hosts:ensure", actor, {
projectRef,
hostKey: "preview-vps",
displayName: "Preview VPS",
providerRef: "provider:preview",
externalRef: "provider-resource:preview-vps",
managementCredentialRef: "secret-ref:device-core/preview-vps",
lifecycleState: "active",
});
const deployment = await client.execute("infrastructure-deployments:ensure", actor, {
projectRef,
hostRef: host.result.host.hostRef,
deploymentKey: "preview-edge",
displayName: "Preview Edge deployment",
artifactRef: "artifact:device-edge/1.0.0",
artifactDigest: `sha256:${"c".repeat(64)}`,
lifecycleState: "active",
});
await client.execute("infrastructure-service-instances:ensure", actor, {
projectRef,
hostRef: host.result.host.hostRef,
deploymentRef: deployment.result.deployment.deploymentRef,
edgeRef: null,
serviceKey: "device-edge",
displayName: "Device Edge",
serviceRole: "device.edge",
lifecycleState: "active",
});
await client.execute("health-observations:record", actor, {
projectRef,
subjectKind: "host",
subjectRef: host.result.host.hostRef,
observedState: "reachable",
evidenceClass: "manual",
sourceRef: "test:preview",
schemaRef: "nodedc.health.test.v1",
evidence: {},
observedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 60_000).toISOString(),
});
await client.execute("collections:ensure", actor, {
projectRef,
collectionKey: "field-devices",
@@ -217,6 +288,10 @@ test("local preview is empty and creates resources only through canonical comman
assert.equal(workspace.grants.length, 2);
assert.ok(workspace.auditEvents.some((event) => event.eventType === "device_binding.created"));
assert.equal(workspace.policies.commandTransport, "disabled");
assert.equal(workspace.ontology.hosts[0].health.state, "reachable");
assert.equal(workspace.ontology.serviceInstances[0].serviceRole, "device.edge");
assert.equal(workspace.ontology.hosts[0].managementCredentialConfigured, true);
assert.equal(JSON.stringify(workspace).includes("secret-ref:device-core/preview-vps"), false);
assert.deepEqual(workspace.devices, []);
});
@@ -29,6 +29,14 @@ const mutationRoutes = new Map([
["/api/device-manager/enrollment-intents:ensure", "enrollment-intents:ensure"],
["/api/device-manager/devices:claim", "devices:claim"],
["/api/device-manager/devices:update", "devices:update"],
["/api/device-manager/assets:ensure", "assets:ensure"],
["/api/device-manager/asset-bindings:ensure", "asset-bindings:ensure"],
["/api/device-manager/asset-bindings:close", "asset-bindings:close"],
["/api/device-manager/infrastructure-hosts:ensure", "infrastructure-hosts:ensure"],
["/api/device-manager/infrastructure-endpoints:ensure", "infrastructure-endpoints:ensure"],
["/api/device-manager/infrastructure-deployments:ensure", "infrastructure-deployments:ensure"],
["/api/device-manager/infrastructure-service-instances:ensure", "infrastructure-service-instances:ensure"],
["/api/device-manager/health-observations:record", "health-observations:record"],
["/api/device-manager/device-bindings:ensure", "device-bindings:ensure"],
["/api/device-manager/device-bindings:revoke", "device-bindings:revoke"],
[
@@ -123,6 +123,34 @@ test("Device Manager BFF exposes an empty, mutation-driven project workspace", a
deploymentRef: "deployment:preview-edge",
lifecycleState: "provisioning",
});
const host = await postJson(`${baseUrl}/api/device-manager/infrastructure-hosts:ensure`, {
projectRef,
hostKey: "preview-vps",
displayName: "Preview VPS",
providerRef: "provider:preview",
externalRef: "provider-resource:preview-vps",
managementCredentialRef: "secret-ref:device-core/preview-vps",
lifecycleState: "active",
});
const deployment = await postJson(`${baseUrl}/api/device-manager/infrastructure-deployments:ensure`, {
projectRef,
hostRef: host.result.host.hostRef,
deploymentKey: "preview-edge",
displayName: "Preview Edge deployment",
artifactRef: "artifact:device-edge/1.0.0",
artifactDigest: `sha256:${"c".repeat(64)}`,
lifecycleState: "active",
});
await postJson(`${baseUrl}/api/device-manager/infrastructure-service-instances:ensure`, {
projectRef,
hostRef: host.result.host.hostRef,
deploymentRef: deployment.result.deployment.deploymentRef,
edgeRef: null,
serviceKey: "device-edge",
displayName: "Device Edge",
serviceRole: "device.edge",
lifecycleState: "active",
});
await postJson(`${baseUrl}/api/device-manager/project-grants:upsert`, {
projectRef,
principalKind: "group",
@@ -141,6 +169,9 @@ test("Device Manager BFF exposes an empty, mutation-driven project workspace", a
assert.equal(workspace.workspace.edges[0].edgeKey, "preview-edge");
assert.equal(workspace.workspace.grants.length, 2);
assert.equal(workspace.workspace.policies.commandTransport, "disabled");
assert.equal(workspace.workspace.ontology.hosts[0].hostKey, "preview-vps");
assert.equal(workspace.workspace.ontology.serviceInstances[0].serviceKey, "device-edge");
assert.equal(JSON.stringify(workspace).includes("secret-ref:device-core/preview-vps"), false);
assert.deepEqual(workspace.workspace.devices, []);
const missingKey = await fetch(`${baseUrl}/api/device-manager/projects:ensure`, {