@openship/protocol 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/package-meta.json +2 -2
- package/dist/skill/SKILL.md +1 -1
- package/dist/skill/references/examples/invalid/discovery-mcp.json +6 -0
- package/dist/skill/references/examples/invalid/discovery.json +6 -1
- package/dist/skill/references/examples/invalid/systems-ownership.json +29 -7
- package/dist/skill/references/examples/invalid/systems.json +63 -12
- package/dist/skill/references/examples/valid/discovery.json +8 -1
- package/dist/skill/references/examples/valid/systems-layered.json +332 -0
- package/dist/skill/references/examples/valid/systems.json +140 -25
- package/dist/skill/references/openship-mcp.md +1 -0
- package/dist/skill/references/openship-systems.md +70 -69
- package/dist/skill/references/openship.md +39 -5
- package/dist/skill/references/schemas/discovery.schema.json +21 -6
- package/dist/skill/references/schemas/systems.schema.json +519 -57
- package/package.json +1 -1
- package/src/index.d.ts +13 -5
- package/src/index.js +148 -48
package/src/index.js
CHANGED
|
@@ -179,24 +179,43 @@ export function validateSources(manifestValue, bundleValue, options = {}) {
|
|
|
179
179
|
|
|
180
180
|
export function validateDiscovery(value) {
|
|
181
181
|
const discovery = envelope(value, "discovery");
|
|
182
|
+
const absoluteUrl = (value, path) => {
|
|
183
|
+
const raw = string(value, path);
|
|
184
|
+
let url;
|
|
185
|
+
try { url = new URL(raw); } catch { fail(path, "must be an absolute HTTPS URL"); }
|
|
186
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
187
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) fail(path, "must be an absolute HTTPS URL outside local development");
|
|
188
|
+
return raw;
|
|
189
|
+
};
|
|
182
190
|
const project = object(discovery.project, "$.project");
|
|
183
191
|
string(project.name, "$.project.name");
|
|
184
192
|
string(project.description, "$.project.description");
|
|
193
|
+
const agent = object(discovery.agent, "$.agent");
|
|
194
|
+
string(agent.summary, "$.agent.summary");
|
|
195
|
+
string(agent.instructions, "$.agent.instructions");
|
|
196
|
+
absoluteUrl(agent.skill, "$.agent.skill");
|
|
197
|
+
if (discovery.page !== undefined) absoluteUrl(discovery.page, "$.page");
|
|
185
198
|
const capabilities = object(discovery.capabilities, "$.capabilities");
|
|
186
199
|
const sources = object(capabilities.sources, "$.capabilities.sources");
|
|
187
|
-
|
|
188
|
-
const url = string(value, path);
|
|
189
|
-
try { new URL(url); } catch { fail(path, "must be an absolute URL"); }
|
|
190
|
-
return url;
|
|
191
|
-
};
|
|
200
|
+
string(sources.description, "$.capabilities.sources.description");
|
|
192
201
|
for (const key of ["manifest", "bundle"]) {
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
absoluteUrl(sources[key], `$.capabilities.sources.${key}`);
|
|
203
|
+
}
|
|
204
|
+
for (const key of ["mcp", "archive", "instructions"]) {
|
|
205
|
+
if (sources[key] !== undefined) absoluteUrl(sources[key], `$.capabilities.sources.${key}`);
|
|
206
|
+
}
|
|
207
|
+
if (sources.file !== undefined) {
|
|
208
|
+
absoluteUrl(sources.file, "$.capabilities.sources.file");
|
|
209
|
+
if (!String(sources.file).includes("{path}")) fail("$.capabilities.sources.file", "must contain {path}");
|
|
210
|
+
}
|
|
211
|
+
if (capabilities.systems) {
|
|
212
|
+
const systems = object(capabilities.systems, "$.capabilities.systems");
|
|
213
|
+
string(systems.description, "$.capabilities.systems.description");
|
|
214
|
+
absoluteUrl(systems.document, "$.capabilities.systems.document");
|
|
195
215
|
}
|
|
196
|
-
if (sources.mcp !== undefined) absoluteUrl(sources.mcp, "$.capabilities.sources.mcp");
|
|
197
|
-
if (capabilities.systems) absoluteUrl(object(capabilities.systems, "$.capabilities.systems").document, "$.capabilities.systems.document");
|
|
198
216
|
if (capabilities.changes) {
|
|
199
217
|
const changes = object(capabilities.changes, "$.capabilities.changes");
|
|
218
|
+
string(changes.description, "$.capabilities.changes.description");
|
|
200
219
|
for (const key of ["policy", "submit", "status"]) absoluteUrl(changes[key], `$.capabilities.changes.${key}`);
|
|
201
220
|
if (!String(changes.status).includes("{changeId}")) fail("$.capabilities.changes.status", "must contain {changeId}");
|
|
202
221
|
}
|
|
@@ -231,54 +250,132 @@ export function validateSystems(value, options = {}) {
|
|
|
231
250
|
const system = object(payload.system, "$.system");
|
|
232
251
|
string(system.id, "$.system.id");
|
|
233
252
|
string(system.name, "$.system.name");
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (![
|
|
246
|
-
|
|
253
|
+
if (payload.systemsVersion !== "2.0") fail("$.systemsVersion", "requires layered Systems 2.0; legacy Systems is unsupported", "unsupported_version");
|
|
254
|
+
for (const key of ["nodes", "edges", "rootNodeId"]) if (key in system) fail(`$.system.${key}`, "legacy graph members are unsupported");
|
|
255
|
+
const layers = array(system.layers, "$.system.layers");
|
|
256
|
+
if (!layers.length) fail("$.system.layers", "requires at least one layer");
|
|
257
|
+
const identifier = (value, path) => {
|
|
258
|
+
if (!idPattern.test(string(value, path))) fail(path, "has an invalid identifier");
|
|
259
|
+
return value;
|
|
260
|
+
};
|
|
261
|
+
identifier(system.id, "$.system.id");
|
|
262
|
+
const jsonValue = (value, path, ancestors = new Set()) => {
|
|
263
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) return;
|
|
264
|
+
if (typeof value !== "object" || ancestors.has(value) || (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) fail(path, "must be a JSON value");
|
|
265
|
+
ancestors.add(value);
|
|
266
|
+
for (const item of Array.isArray(value) ? value : Object.values(value)) jsonValue(item, path, ancestors);
|
|
267
|
+
ancestors.delete(value);
|
|
268
|
+
};
|
|
269
|
+
const configuration = (entries, path) => {
|
|
270
|
+
if (entries === undefined) return;
|
|
271
|
+
const names = [];
|
|
272
|
+
for (const [index, raw] of array(entries, path).entries()) {
|
|
273
|
+
const at = `${path}[${index}]`, entry = object(raw, at);
|
|
274
|
+
names.push(string(entry.name, `${at}.name`));
|
|
275
|
+
string(entry.description, `${at}.description`);
|
|
276
|
+
if (typeof entry.required !== "boolean") fail(`${at}.required`, "must be boolean");
|
|
277
|
+
if (entry.sensitive !== undefined && typeof entry.sensitive !== "boolean") fail(`${at}.sensitive`, "must be boolean");
|
|
278
|
+
if ("value" in entry) jsonValue(entry.value, `${at}.value`);
|
|
279
|
+
if ("value" in entry && (entry.sensitive === true || entry.secretRef !== undefined)) fail(at, "secret configuration cannot contain a literal value");
|
|
280
|
+
if (entry.secretRef !== undefined) {
|
|
281
|
+
const ref = object(entry.secretRef, `${at}.secretRef`);
|
|
282
|
+
identifier(ref.nodeId, `${at}.secretRef.nodeId`);
|
|
283
|
+
string(ref.key, `${at}.secretRef.key`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
unique(names, path);
|
|
287
|
+
};
|
|
288
|
+
const nodeById = new Map(), layerByNode = new Map(), layerById = new Map();
|
|
289
|
+
for (const [index, raw] of layers.entries()) {
|
|
290
|
+
const at = `$.system.layers[${index}]`, layer = object(raw, at);
|
|
291
|
+
identifier(layer.id, `${at}.id`);
|
|
292
|
+
if (layerById.has(layer.id)) fail(`${at}.id`, "must be unique");
|
|
293
|
+
layerById.set(layer.id, layer);
|
|
294
|
+
string(layer.name, `${at}.name`);
|
|
295
|
+
if (!["logical", "technical", "provider", "custom"].includes(layer.role)) fail(`${at}.role`, "invalid layer role");
|
|
296
|
+
const rootNodeId = identifier(layer.rootNodeId, `${at}.rootNodeId`);
|
|
297
|
+
const nodes = array(layer.nodes, `${at}.nodes`), local = new Map();
|
|
298
|
+
for (const [i, rawNode] of nodes.entries()) {
|
|
299
|
+
const path = `${at}.nodes[${i}]`, node = object(rawNode, path);
|
|
300
|
+
identifier(node.id, `${path}.id`);
|
|
301
|
+
if (nodeById.has(node.id)) fail(`${path}.id`, "must be globally unique");
|
|
302
|
+
if (!["Root", "Block", "Store", "Host", "Container", "Process", "Library"].includes(node.kind)) fail(`${path}.kind`, "invalid node kind");
|
|
303
|
+
string(node.name, `${path}.name`);
|
|
304
|
+
const metadata = object(node.metadata, `${path}.metadata`);
|
|
305
|
+
if (!["first_party", "third_party"].includes(metadata.ownership)) fail(`${path}.metadata.ownership`, "must be first_party or third_party");
|
|
306
|
+
configuration(node.configuration, `${path}.configuration`);
|
|
307
|
+
if (node.sourceSelectors !== undefined) {
|
|
308
|
+
unique(array(node.sourceSelectors, `${path}.sourceSelectors`), `${path}.sourceSelectors`);
|
|
309
|
+
for (const selector of node.sourceSelectors) {
|
|
310
|
+
string(selector, `${path}.sourceSelectors`);
|
|
311
|
+
if (!source.manifest.files.some((file) => matchOpenShipPattern(selector, file.path))) fail(`${path}.sourceSelectors`, `${selector} matches no source path`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
nodeById.set(node.id, node); local.set(node.id, node); layerByNode.set(node.id, index);
|
|
315
|
+
}
|
|
316
|
+
const roots = nodes.filter((node) => node.kind === "Root");
|
|
317
|
+
if (roots.length !== 1 || roots[0].id !== rootNodeId || roots[0].parentId !== undefined) fail(`${at}.rootNodeId`, "must identify the one parentless Root");
|
|
318
|
+
for (const node of nodes) if (node.id !== rootNodeId && !local.has(node.parentId)) fail(`${at}.nodes.${node.id}.parentId`, "must reference a parent in this layer");
|
|
319
|
+
assertAcyclic([...local.keys()], nodes.filter((node) => node.parentId).map((node) => [node.parentId, node.id]), `${at}.nodes`);
|
|
320
|
+
const edges = array(layer.edges, `${at}.edges`);
|
|
321
|
+
unique(edges.map((edge) => identifier(object(edge, `${at}.edges`).id, `${at}.edges.id`)), `${at}.edges`);
|
|
322
|
+
for (const edge of edges) {
|
|
323
|
+
const from = local.get(edge.fromNodeId), to = local.get(edge.toNodeId);
|
|
324
|
+
if (!from || !to || from.kind === "Root" || to.kind === "Root") fail(`${at}.edges.${edge.id}`, "endpoints must be non-root nodes in this layer");
|
|
325
|
+
if (!["Runtime", "Dataflow", "Dependency"].includes(edge.type)) fail(`${at}.edges.${edge.id}.type`, "invalid edge type");
|
|
326
|
+
}
|
|
327
|
+
for (const type of ["Dataflow", "Dependency"]) assertAcyclic([...local.keys()], edges.filter((edge) => edge.type === type).map((edge) => [edge.fromNodeId, edge.toNodeId]), `${at}.edges[${type}]`);
|
|
328
|
+
}
|
|
329
|
+
const refinements = array(system.refinements, "$.system.refinements");
|
|
330
|
+
unique(refinements.map((ref) => identifier(object(ref, "$.system.refinements").id, "$.system.refinements.id")), "$.system.refinements");
|
|
331
|
+
for (const ref of refinements) {
|
|
332
|
+
if (!nodeById.has(ref.fromNodeId) || !nodeById.has(ref.toNodeId) || layerByNode.get(ref.fromNodeId) <= layerByNode.get(ref.toNodeId)) fail(`$.system.refinements.${ref.id}`, "must connect a concrete node to a node in an earlier layer");
|
|
247
333
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
for (const
|
|
258
|
-
|
|
334
|
+
const instances = array(system.instances ?? [], "$.system.instances");
|
|
335
|
+
unique(instances.map((instance) => identifier(object(instance, "$.system.instances").id, "$.system.instances.id")), "$.system.instances");
|
|
336
|
+
for (const instance of instances) {
|
|
337
|
+
const at = `$.system.instances.${instance.id}`;
|
|
338
|
+
string(instance.name, `${at}.name`); string(instance.environment, `${at}.environment`);
|
|
339
|
+
const layer = layerById.get(instance.layerId);
|
|
340
|
+
if (!layer) fail(`${at}.layerId`, "must reference a layer");
|
|
341
|
+
const bindings = array(instance.bindings, `${at}.bindings`);
|
|
342
|
+
unique(bindings.map((binding) => object(binding, `${at}.bindings`).nodeId), `${at}.bindings`);
|
|
343
|
+
for (const binding of bindings) {
|
|
344
|
+
const node = nodeById.get(binding.nodeId);
|
|
345
|
+
if (!node || !layer.nodes.includes(node) || node.kind === "Root") fail(`${at}.bindings`, "must bind non-root nodes in the instance layer");
|
|
346
|
+
if (binding.resourceId !== undefined) string(binding.resourceId, `${at}.resourceId`);
|
|
347
|
+
configuration(binding.configuration, `${at}.configuration`);
|
|
348
|
+
if (binding.state !== undefined) {
|
|
349
|
+
if (node.kind !== "Store") fail(`${at}.state`, "database state requires a Store node");
|
|
350
|
+
const state = object(binding.state, `${at}.state`);
|
|
351
|
+
if (state.appliedMigration !== undefined) string(state.appliedMigration, `${at}.state.appliedMigration`);
|
|
352
|
+
if (state.snapshot !== undefined) {
|
|
353
|
+
const snapshot = object(state.snapshot, `${at}.state.snapshot`);
|
|
354
|
+
string(snapshot.ref, `${at}.state.snapshot.ref`);
|
|
355
|
+
if (typeof snapshot.capturedAt !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(snapshot.capturedAt) || !Number.isFinite(Date.parse(snapshot.capturedAt))) fail(`${at}.state.snapshot.capturedAt`, "must be an RFC3339 timestamp");
|
|
356
|
+
const [year, month, day] = snapshot.capturedAt.slice(0, 10).split("-").map(Number);
|
|
357
|
+
if (day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate()) fail(`${at}.state.snapshot.capturedAt`, "must be a valid calendar date");
|
|
358
|
+
if (snapshot.digest !== undefined && !digestPattern.test(snapshot.digest)) fail(`${at}.state.snapshot.digest`, "must be a sha256 digest");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
259
361
|
}
|
|
260
362
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
unique(edges.map((edge) => edge.id), "$.system.edges[].id");
|
|
264
|
-
for (const edge of edges) {
|
|
265
|
-
const from = nodeById.get(edge.fromNodeId);
|
|
266
|
-
const to = nodeById.get(edge.toNodeId);
|
|
267
|
-
if (!from || !to) fail(`$.system.edges.${edge.id}`, "references a missing node");
|
|
268
|
-
if (from.kind !== "Process") fail(`$.system.edges.${edge.id}.fromNodeId`, "must reference a Process");
|
|
269
|
-
if (edge.type === "Dependency" && to.kind !== "Library") fail(`$.system.edges.${edge.id}.toNodeId`, "Dependency must target a Library");
|
|
270
|
-
if ((edge.type === "Runtime" || edge.type === "Dataflow") && to.kind !== "Process" && to.kind !== "Container") fail(`$.system.edges.${edge.id}.toNodeId`, "must target a Process or Container");
|
|
271
|
-
if (!["Runtime", "Dataflow", "Dependency"].includes(edge.type)) fail(`$.system.edges.${edge.id}.type`, "is not a v1 edge type");
|
|
363
|
+
for (const entries of [...nodeById.values()].map((node) => node.configuration).concat(instances.flatMap((instance) => instance.bindings.map((binding) => binding.configuration)))) {
|
|
364
|
+
for (const entry of entries ?? []) if (entry.secretRef && (!nodeById.has(entry.secretRef.nodeId) || nodeById.get(entry.secretRef.nodeId).kind === "Root")) fail("$.system.configuration.secretRef.nodeId", "must reference a component");
|
|
272
365
|
}
|
|
273
|
-
const ids = nodes.map((node) => node.id);
|
|
274
|
-
assertAcyclic(ids, edges.filter((edge) => edge.type === "Dataflow").map((edge) => [edge.fromNodeId, edge.toNodeId]), "$.system.edges[Dataflow]");
|
|
275
|
-
assertAcyclic(ids, edges.filter((edge) => edge.type === "Dependency").map((edge) => [edge.fromNodeId, edge.toNodeId]), "$.system.edges[Dependency]");
|
|
276
366
|
const context = system.context;
|
|
277
367
|
if (!context) return payload;
|
|
278
368
|
object(context, "$.system.context");
|
|
369
|
+
for (const key of ["concerns", "documents", "matrix", "artifacts", "systemPromptRefs"]) if (context[key] !== undefined) array(context[key], `$.system.context.${key}`);
|
|
370
|
+
unique(context.concerns ?? [], "$.system.context.concerns");
|
|
371
|
+
for (const concern of context.concerns ?? []) string(concern, "$.system.context.concerns");
|
|
279
372
|
const concerns = new Set(context.concerns ?? []);
|
|
280
373
|
const documents = new Map();
|
|
281
374
|
for (const document of context.documents ?? []) {
|
|
375
|
+
object(document, "$.system.context.documents");
|
|
376
|
+
string(document.title, "$.system.context.documents.title");
|
|
377
|
+
string(document.language, "$.system.context.documents.language");
|
|
378
|
+
if (typeof document.text !== "string") fail("$.system.context.documents.text", "must be a string");
|
|
282
379
|
if (!["Document", "Skill", "Prompt"].includes(document.kind)) fail("$.system.context.documents", "contains an invalid document kind");
|
|
283
380
|
const expected = `sha256:${sha256Hex(`${document.kind}\n${document.title}\n${document.language}\n${document.text}`)}`;
|
|
284
381
|
if (document.hash !== expected) fail(`$.system.context.documents.${document.hash}`, "hash does not match canonical document content");
|
|
@@ -287,6 +384,8 @@ export function validateSystems(value, options = {}) {
|
|
|
287
384
|
}
|
|
288
385
|
assertAcyclic([...documents.keys()], [...documents.values()].filter((doc) => doc.supersedes && documents.has(doc.supersedes)).map((doc) => [doc.hash, doc.supersedes]), "$.system.context.documents[].supersedes");
|
|
289
386
|
for (const cell of context.matrix ?? []) {
|
|
387
|
+
object(cell, "$.system.context.matrix");
|
|
388
|
+
for (const key of ["documentRefs", "skillRefs"]) if (cell[key] !== undefined) unique(array(cell[key], `$.system.context.matrix.${key}`), `$.system.context.matrix.${key}`);
|
|
290
389
|
if (!nodeById.has(cell.nodeId)) fail("$.system.context.matrix", `references missing node ${cell.nodeId}`);
|
|
291
390
|
if (!concerns.has(cell.concern)) fail("$.system.context.matrix", `references undeclared concern ${cell.concern}`);
|
|
292
391
|
for (const hash of cell.documentRefs ?? []) if (documents.get(hash)?.kind !== "Document") fail("$.system.context.matrix", `Document reference ${hash} is missing or has the wrong kind`);
|
|
@@ -296,7 +395,8 @@ export function validateSystems(value, options = {}) {
|
|
|
296
395
|
const artifactIds = [];
|
|
297
396
|
const sourcePaths = new Set(source.manifest.files.map((file) => file.path));
|
|
298
397
|
for (const artifact of context.artifacts ?? []) {
|
|
299
|
-
|
|
398
|
+
object(artifact, "$.system.context.artifacts");
|
|
399
|
+
artifactIds.push(identifier(artifact.id, "$.system.context.artifacts.id"));
|
|
300
400
|
if (!nodeById.has(artifact.nodeId)) fail("$.system.context.artifacts", `references missing node ${artifact.nodeId}`);
|
|
301
401
|
if (!concerns.has(artifact.concern)) fail("$.system.context.artifacts", `references undeclared concern ${artifact.concern}`);
|
|
302
402
|
if (artifact.type === "Code") {
|