@openship/protocol 0.0.1

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.
Files changed (35) hide show
  1. package/README.md +12 -0
  2. package/bin/openship.mjs +96 -0
  3. package/dist/package-meta.json +7 -0
  4. package/dist/skill/SKILL.md +18 -0
  5. package/dist/skill/references/examples/invalid/changes-submission.json +9 -0
  6. package/dist/skill/references/examples/invalid/changes-violation.json +7 -0
  7. package/dist/skill/references/examples/invalid/discovery.json +13 -0
  8. package/dist/skill/references/examples/invalid/sources-manifest.json +11 -0
  9. package/dist/skill/references/examples/invalid/systems-ownership.json +29 -0
  10. package/dist/skill/references/examples/invalid/systems.json +34 -0
  11. package/dist/skill/references/examples/valid/changes-accepted.json +13 -0
  12. package/dist/skill/references/examples/valid/changes-policy.json +12 -0
  13. package/dist/skill/references/examples/valid/changes-status.json +12 -0
  14. package/dist/skill/references/examples/valid/changes-submission.json +11 -0
  15. package/dist/skill/references/examples/valid/changes-violation.json +16 -0
  16. package/dist/skill/references/examples/valid/discovery.json +20 -0
  17. package/dist/skill/references/examples/valid/sources-bundle.json +10 -0
  18. package/dist/skill/references/examples/valid/sources-manifest.json +27 -0
  19. package/dist/skill/references/examples/valid/systems.json +55 -0
  20. package/dist/skill/references/openship-changes.md +135 -0
  21. package/dist/skill/references/openship-sources.md +143 -0
  22. package/dist/skill/references/openship-systems.md +165 -0
  23. package/dist/skill/references/openship.md +125 -0
  24. package/dist/skill/references/schemas/changes-accepted.schema.json +17 -0
  25. package/dist/skill/references/schemas/changes-policy.schema.json +44 -0
  26. package/dist/skill/references/schemas/changes-status.schema.json +22 -0
  27. package/dist/skill/references/schemas/changes-submission.schema.json +34 -0
  28. package/dist/skill/references/schemas/changes-violation.schema.json +32 -0
  29. package/dist/skill/references/schemas/discovery.schema.json +61 -0
  30. package/dist/skill/references/schemas/sources-bundle.schema.json +27 -0
  31. package/dist/skill/references/schemas/sources-manifest.schema.json +79 -0
  32. package/dist/skill/references/schemas/systems.schema.json +121 -0
  33. package/package.json +52 -0
  34. package/src/index.d.ts +36 -0
  35. package/src/index.js +454 -0
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @openship/protocol
2
+
3
+ Canonical OpenShip 1.0 types, validators, source digest/diff helpers, selector matching, safe path/base64 utilities, browser discovery retrieval, and skill synchronization.
4
+
5
+ ```js
6
+ import { fetchOpenShip, validateSystems } from "@openship/protocol";
7
+
8
+ const imported = await fetchOpenShip("https://example.com");
9
+ if (imported.snapshot.kind === "systems") validateSystems(imported.snapshot.document);
10
+ ```
11
+
12
+ The package contains the exact canonical `skills/openship` schemas, examples, and references. See the repository root README for the generated-skill workflow.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { createHash } from "node:crypto";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ validateChangesAccepted,
9
+ validateChangesDocument,
10
+ validateChangesPolicy,
11
+ validateChangesStatus,
12
+ validateChangesSubmission,
13
+ validateChangesViolation,
14
+ validateDiscovery,
15
+ validateSources,
16
+ validateSystems,
17
+ } from "../src/index.js";
18
+
19
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const meta = JSON.parse(await readFile(join(packageRoot, "dist", "package-meta.json"), "utf8"));
21
+ const skillSource = join(packageRoot, "dist", "skill");
22
+
23
+ async function digestDirectory(root, excludes = new Set()) {
24
+ const files = [];
25
+ async function walk(directory) {
26
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
27
+ const absolute = join(directory, entry.name);
28
+ if (entry.isDirectory()) await walk(absolute);
29
+ else {
30
+ const path = relative(root, absolute).split("\\").join("/");
31
+ if (!excludes.has(path)) files.push(path);
32
+ }
33
+ }
34
+ }
35
+ await walk(root);
36
+ files.sort();
37
+ const records = [];
38
+ for (const path of files) {
39
+ const hash = createHash("sha256").update(await readFile(join(root, path))).digest("hex");
40
+ records.push(`${path}\0${hash}\n`);
41
+ }
42
+ return { files, digest: `sha256:${createHash("sha256").update(records.join("")).digest("hex")}` };
43
+ }
44
+
45
+ const [command, rawDestination, ...rest] = process.argv.slice(2);
46
+ if (command === "sync-skill") {
47
+ if (!rawDestination) throw new Error("Usage: openship sync-skill <destination>");
48
+ const destination = resolve(rawDestination);
49
+ await rm(destination, { recursive: true, force: true });
50
+ await mkdir(dirname(destination), { recursive: true });
51
+ await cp(skillSource, destination, { recursive: true });
52
+ const result = await digestDirectory(destination);
53
+ await writeFile(join(destination, "UPSTREAM.json"), `${JSON.stringify({
54
+ ...meta,
55
+ packageDigest: result.digest,
56
+ digestExcludes: ["UPSTREAM.json"],
57
+ }, null, 2)}\n`);
58
+ console.log(`OpenShip skill synchronized (${result.files.length} files, ${result.digest}).`);
59
+ } else if (command === "verify-skill") {
60
+ if (!rawDestination) throw new Error("Usage: openship verify-skill <destination>");
61
+ const destination = resolve(rawDestination);
62
+ const provenance = JSON.parse(await readFile(join(destination, "UPSTREAM.json"), "utf8"));
63
+ const result = await digestDirectory(destination, new Set(provenance.digestExcludes ?? []));
64
+ const canonical = await digestDirectory(skillSource);
65
+ if (
66
+ provenance.package !== meta.package
67
+ || provenance.packageVersion !== meta.packageVersion
68
+ || provenance.sourceCommit !== meta.sourceCommit
69
+ || provenance.packageDigest !== canonical.digest
70
+ || result.digest !== canonical.digest
71
+ || JSON.stringify(result.files) !== JSON.stringify(canonical.files)
72
+ ) {
73
+ throw new Error(`OpenShip skill mismatch. Expected ${provenance.packageDigest}; got ${result.digest}.`);
74
+ }
75
+ console.log(`OpenShip skill verified (${result.files.length} files, ${result.digest}).`);
76
+ } else if (command === "validate") {
77
+ const paths = [rawDestination, ...rest].filter(Boolean);
78
+ if (paths.length === 0) throw new Error("Usage: openship validate <document.json> [bundle.json]");
79
+ const values = await Promise.all(paths.map(async (path) => JSON.parse(await readFile(resolve(path), "utf8"))));
80
+ const value = values[0];
81
+ if (value.capability === "discovery") validateDiscovery(value);
82
+ else if (value.capability === "systems") validateSystems(value);
83
+ else if (value.capability === "sources") validateSources(value, values[1]);
84
+ else if (value.capability === "changes") {
85
+ if (value.files && value.title) validateChangesSubmission(value);
86
+ else if (value.writable) validateChangesPolicy(value);
87
+ else if (value.violations) validateChangesViolation(value);
88
+ else if (value.statusUrl) validateChangesAccepted(value);
89
+ else if (value.status) validateChangesStatus(value);
90
+ else validateChangesDocument(value);
91
+ }
92
+ else throw new Error(`Unsupported capability ${String(value.capability)}.`);
93
+ console.log("OpenShip document is valid.");
94
+ } else {
95
+ throw new Error("Usage: openship <sync-skill|verify-skill|validate> ...");
96
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "openship": "1.0",
3
+ "package": "@openship/protocol",
4
+ "packageVersion": "0.0.1",
5
+ "source": "https://github.com/openshipdev/openship/tree/main/skills/openship",
6
+ "sourceCommit": "40e94cc1a5c7eb06787d22257df63785052c9124"
7
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: openship
3
+ description: Work with OpenShip v1 discovery, public source snapshots, candidate code changes, and self-contained system descriptions. Use when implementing, consuming, validating, or explaining an OpenShip capability; do not use for unrelated repository or deployment work.
4
+ ---
5
+
6
+ # OpenShip
7
+
8
+ OpenShip lets a running project publish the source that produced it, optionally accept changes as isolated candidate versions, and optionally describe the complete software system around that source.
9
+
10
+ Read only the references needed for the task:
11
+
12
+ - For the protocol overview, discovery, shared conventions, or capability selection, read [references/openship.md](references/openship.md).
13
+ - For publishing, retrieving, or validating a source snapshot, read [references/openship-sources.md](references/openship-sources.md).
14
+ - For proposing or serving candidate code versions, read both [references/openship-sources.md](references/openship-sources.md) and [references/openship-changes.md](references/openship-changes.md).
15
+ - For authoring or consuming a self-contained architecture and source payload, read both [references/openship-sources.md](references/openship-sources.md) and [references/openship-systems.md](references/openship-systems.md).
16
+
17
+ Machine-readable schemas and conformance examples are under [references/schemas](references/schemas) and [references/examples](references/examples). Treat the Markdown specifications as normative when a constraint cannot be expressed by JSON Schema.
18
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "base": "not-a-digest",
5
+ "title": "",
6
+ "intent": "",
7
+ "files": {}
8
+ }
9
+
@@ -0,0 +1,7 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "error": "policy_violation",
5
+ "message": "The response forgot to identify any violation.",
6
+ "violations": []
7
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "discovery",
4
+ "project": { "name": "Broken", "description": "Sources is missing." },
5
+ "capabilities": {
6
+ "changes": {
7
+ "policy": "https://example.com/openship/policy.json",
8
+ "submit": "https://example.com/openship/changes",
9
+ "status": "https://example.com/openship/changes/{changeId}"
10
+ }
11
+ }
12
+ }
13
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "sources",
4
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
5
+ "project": { "name": "Broken", "description": "Contains an unsafe path." },
6
+ "totals": { "files": 1, "bytes": 1 },
7
+ "files": [
8
+ { "path": "../secret", "size": 1, "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "encoding": "utf-8", "mediaType": "text/plain", "type": "file" }
9
+ ]
10
+ }
11
+
@@ -0,0 +1,29 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "systems",
4
+ "source": {
5
+ "manifest": {
6
+ "openship": "1.0",
7
+ "capability": "sources",
8
+ "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
9
+ "project": { "name": "Broken ownership", "description": "A Systems node with non-conformant ownership." },
10
+ "totals": { "files": 0, "bytes": 0 },
11
+ "files": []
12
+ },
13
+ "bundle": {
14
+ "openship": "1.0",
15
+ "capability": "sources",
16
+ "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
17
+ "files": {}
18
+ }
19
+ },
20
+ "system": {
21
+ "id": "broken-ownership",
22
+ "name": "Broken ownership",
23
+ "rootNodeId": "s.root",
24
+ "nodes": [
25
+ { "id": "s.root", "kind": "Root", "name": "Broken ownership", "metadata": { "ownership": "partner" } }
26
+ ],
27
+ "edges": []
28
+ }
29
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "systems",
4
+ "source": {
5
+ "manifest": {
6
+ "openship": "1.0",
7
+ "capability": "sources",
8
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
9
+ "project": { "name": "Broken", "description": "The runtime edge targets a Library." },
10
+ "totals": { "files": 0, "bytes": 0 },
11
+ "files": []
12
+ },
13
+ "bundle": {
14
+ "openship": "1.0",
15
+ "capability": "sources",
16
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
17
+ "files": {}
18
+ }
19
+ },
20
+ "system": {
21
+ "id": "broken",
22
+ "name": "Broken",
23
+ "rootNodeId": "s.root",
24
+ "nodes": [
25
+ { "id": "s.root", "kind": "Root", "name": "Broken", "metadata": { "ownership": "first_party" } },
26
+ { "id": "h.runtime", "kind": "Host", "name": "Runtime", "parentId": "s.root", "metadata": { "ownership": "first_party" } },
27
+ { "id": "p.web", "kind": "Process", "name": "Web", "parentId": "h.runtime", "metadata": { "ownership": "first_party" } },
28
+ { "id": "l.config", "kind": "Library", "name": "Config", "metadata": { "ownership": "third_party" } }
29
+ ],
30
+ "edges": [
31
+ { "id": "e.invalid", "type": "Runtime", "fromNodeId": "p.web", "toNodeId": "l.config" }
32
+ ]
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "changeId": "5d7621f4-7ad5-49f6-8168-da99620ff1cf",
5
+ "base": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
6
+ "digest": "sha256:b0eea548e74112b0be4dcff867f405086ef4456508089ee007ce38959a78cb08",
7
+ "status": "pending",
8
+ "phase": "queued",
9
+ "candidateOrigin": "https://b0eea548e741.example-builds.net",
10
+ "statusUrl": "https://example.com/openship/changes/5d7621f4-7ad5-49f6-8168-da99620ff1cf",
11
+ "buildId": "b0eea548e741"
12
+ }
13
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "writable": ["app/**", "public/**"],
5
+ "protected": ["app/api/**", "skills/openship/**"],
6
+ "limits": { "filesPerChange": 40, "bytesPerFile": 262144, "bytesPerChange": 1048576 },
7
+ "contentRules": [
8
+ { "id": "dynamic-eval", "rule": "Dynamic evaluation", "message": "Use static imports." }
9
+ ],
10
+ "document": "https://example.com/openship/file/skills/openship/references/openship-changes.md"
11
+ }
12
+
@@ -0,0 +1,12 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "changeId": "5d7621f4-7ad5-49f6-8168-da99620ff1cf",
5
+ "base": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
6
+ "digest": "sha256:b0eea548e74112b0be4dcff867f405086ef4456508089ee007ce38959a78cb08",
7
+ "status": "ready",
8
+ "phase": "deployed",
9
+ "candidateOrigin": "https://b0eea548e741.example-builds.net",
10
+ "reason": null
11
+ }
12
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "base": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
5
+ "title": "Update the example page",
6
+ "intent": "Make the example visibly identify its second candidate version.",
7
+ "files": {
8
+ "app/page.js": { "encoding": "utf-8", "content": "export default function Page(){return \"v2\"}\n" }
9
+ }
10
+ }
11
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "changes",
4
+ "error": "policy_violation",
5
+ "message": "1 rule rejected this change.",
6
+ "base": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
7
+ "policy": "https://example.com/openship/policy.json",
8
+ "violations": [
9
+ {
10
+ "gate": "path",
11
+ "rule": "protected",
12
+ "path": "app/api/session/route.ts",
13
+ "message": "This path is protected."
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "discovery",
4
+ "project": { "name": "Example", "description": "An example OpenShip project." },
5
+ "skill": "https://example.com/openship/file/skills/openship/SKILL.md",
6
+ "capabilities": {
7
+ "sources": {
8
+ "manifest": "https://example.com/openship/manifest.json",
9
+ "bundle": "https://example.com/openship/bundle.json",
10
+ "file": "https://example.com/openship/file/{path}",
11
+ "archive": "https://example.com/openship/source.tar.gz"
12
+ },
13
+ "changes": {
14
+ "policy": "https://example.com/openship/policy.json",
15
+ "submit": "https://example.com/openship/changes",
16
+ "status": "https://example.com/openship/changes/{changeId}"
17
+ }
18
+ }
19
+ }
20
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "sources",
4
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
5
+ "files": {
6
+ "app/page.js": { "encoding": "utf-8", "content": "export default function Page() {}\n" },
7
+ "package.json": { "encoding": "utf-8", "content": "{\"name\":\"example\"}\n" }
8
+ }
9
+ }
10
+
@@ -0,0 +1,27 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "sources",
4
+ "generatedAt": "2026-08-21T12:00:00.000Z",
5
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
6
+ "project": { "name": "Example", "description": "An example OpenShip project." },
7
+ "totals": { "files": 2, "bytes": 53 },
8
+ "files": [
9
+ {
10
+ "path": "app/page.js",
11
+ "size": 34,
12
+ "sha256": "6ce070542590adb63fd9621ff65be91446f8bd9b65465cda5e4ec43e5d86dc5e",
13
+ "encoding": "utf-8",
14
+ "mediaType": "text/plain; charset=utf-8",
15
+ "type": "file"
16
+ },
17
+ {
18
+ "path": "package.json",
19
+ "size": 19,
20
+ "sha256": "9579cf52285b23b618b615d704b19d04ff591ae513f6ae34ef37e2b9a067cb12",
21
+ "encoding": "utf-8",
22
+ "mediaType": "application/json; charset=utf-8",
23
+ "type": "file"
24
+ }
25
+ ]
26
+ }
27
+
@@ -0,0 +1,55 @@
1
+ {
2
+ "openship": "1.0",
3
+ "capability": "systems",
4
+ "source": {
5
+ "manifest": {
6
+ "openship": "1.0",
7
+ "capability": "sources",
8
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
9
+ "project": { "name": "Example", "description": "An example OpenShip project." },
10
+ "totals": { "files": 2, "bytes": 53 },
11
+ "files": [
12
+ { "path": "app/page.js", "size": 34, "sha256": "6ce070542590adb63fd9621ff65be91446f8bd9b65465cda5e4ec43e5d86dc5e", "encoding": "utf-8", "mediaType": "text/plain; charset=utf-8", "type": "file" },
13
+ { "path": "package.json", "size": 19, "sha256": "9579cf52285b23b618b615d704b19d04ff591ae513f6ae34ef37e2b9a067cb12", "encoding": "utf-8", "mediaType": "application/json; charset=utf-8", "type": "file" }
14
+ ]
15
+ },
16
+ "bundle": {
17
+ "openship": "1.0",
18
+ "capability": "sources",
19
+ "digest": "sha256:1d322539e651a3c2d7c51eb8b33160627395b9601bff909e599771b806d9e565",
20
+ "files": {
21
+ "app/page.js": { "encoding": "utf-8", "content": "export default function Page() {}\n" },
22
+ "package.json": { "encoding": "utf-8", "content": "{\"name\":\"example\"}\n" }
23
+ }
24
+ }
25
+ },
26
+ "system": {
27
+ "id": "example-system",
28
+ "name": "Example system",
29
+ "rootNodeId": "s.root",
30
+ "nodes": [
31
+ { "id": "s.root", "kind": "Root", "name": "Example", "metadata": { "ownership": "first_party" } },
32
+ { "id": "h.runtime", "kind": "Host", "name": "Application runtime", "parentId": "s.root", "metadata": { "ownership": "first_party" } },
33
+ { "id": "p.web", "kind": "Process", "name": "Web process", "parentId": "h.runtime", "sourceSelectors": ["app/**"], "metadata": { "ownership": "first_party" } },
34
+ { "id": "l.config", "kind": "Library", "name": "Project configuration", "sourceSelectors": ["package.json"], "metadata": { "ownership": "third_party" } }
35
+ ],
36
+ "edges": [
37
+ { "id": "e.web.config", "type": "Dependency", "fromNodeId": "p.web", "toNodeId": "l.config" }
38
+ ],
39
+ "context": {
40
+ "concerns": ["Interfaces", "Implementation"],
41
+ "documents": [
42
+ { "kind": "Document", "hash": "sha256:77760f8cb9fdaaadd3c22612d97fba630f78730c8d750c6cce02754b689b11da", "title": "API contract", "language": "en", "text": "The API returns JSON." },
43
+ { "kind": "Skill", "hash": "sha256:bae5f866c2994c1cec740965e45935920376a2df1fb933e798c3e2c3b8c0e391", "title": "Node implementation", "language": "en", "text": "Use Node.js." },
44
+ { "kind": "Prompt", "hash": "sha256:416a3efe045d89a00fa0cca4516b8bdd442a8f65a42e35a5c102067f3ecb429d", "title": "System prompt", "language": "en", "text": "Keep changes minimal." }
45
+ ],
46
+ "matrix": [
47
+ { "nodeId": "p.web", "concern": "Interfaces", "documentRefs": ["sha256:77760f8cb9fdaaadd3c22612d97fba630f78730c8d750c6cce02754b689b11da"], "skillRefs": [] }
48
+ ],
49
+ "systemPromptRefs": ["sha256:416a3efe045d89a00fa0cca4516b8bdd442a8f65a42e35a5c102067f3ecb429d"],
50
+ "artifacts": [
51
+ { "id": "a.web.code", "nodeId": "p.web", "concern": "Implementation", "type": "Code", "sourcePaths": ["app/page.js"] }
52
+ ]
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,135 @@
1
+ # OpenShip Changes v1
2
+
3
+ OpenShip Changes lets a project accept a replacement patch against a known Sources digest and, after validation, expose the resulting code at an isolated candidate origin.
4
+
5
+ Changes depends on [OpenShip Sources](openship-sources.md). It does not promote code to production.
6
+
7
+ ## Discovery and methods
8
+
9
+ Discovery advertises:
10
+
11
+ - `policy`: public `GET` describing server-specific rules.
12
+ - `submit`: `POST` accepting a JSON change.
13
+ - `status`: public `GET` URI template containing `{changeId}`.
14
+
15
+ Policy and status reads follow OpenShip's public CORS rules. Submission MUST support CORS preflight for JSON and any advertised authorization or payment headers.
16
+
17
+ ## Policy
18
+
19
+ ```json
20
+ {
21
+ "openship": "1.0",
22
+ "capability": "changes",
23
+ "writable": ["app/**", "components/**", "public/**"],
24
+ "protected": ["app/api/**", "skills/openship/**"],
25
+ "limits": {
26
+ "filesPerChange": 40,
27
+ "bytesPerFile": 262144,
28
+ "bytesPerChange": 1048576
29
+ },
30
+ "contentRules": [
31
+ { "id": "dynamic-eval", "rule": "Dynamic evaluation", "message": "Use static imports." }
32
+ ],
33
+ "document": "https://example.com/openship/file/skills/openship/references/openship-changes.md"
34
+ }
35
+ ```
36
+
37
+ `writable` is an allowlist. `protected` takes precedence. Patterns use one shared grammar: an exact path matches itself; a value ending in `/**` matches that directory and all descendants. No other wildcard syntax is defined in v1.
38
+
39
+ A producer MAY add authorization, payment, media, build, review, or deployment policy. It MUST NOT describe a filter as a security boundary.
40
+
41
+ See [schemas/changes-policy.schema.json](schemas/changes-policy.schema.json).
42
+
43
+ ## Submission
44
+
45
+ ```json
46
+ {
47
+ "openship": "1.0",
48
+ "capability": "changes",
49
+ "base": "sha256:1c413f...",
50
+ "title": "Improve the project page",
51
+ "intent": "Explain what the change does and why.",
52
+ "files": {
53
+ "app/page.tsx": { "encoding": "utf-8", "content": "..." },
54
+ "public/old-logo.png": null
55
+ }
56
+ }
57
+ ```
58
+
59
+ `base` MUST name a Sources digest the server currently accepts. A file value replaces or creates that path. `null` deletes it. An absent path is unchanged. Paths and encodings follow Sources.
60
+
61
+ The producer MUST validate the envelope, base, paths, sizes, content rules, and resulting file tree before charging or queueing expensive work. A stale base returns `409`. Deterministic policy violations return `422` with precise violations.
62
+
63
+ See [schemas/changes-submission.schema.json](schemas/changes-submission.schema.json).
64
+
65
+ ## Accepted response
66
+
67
+ ```json
68
+ {
69
+ "openship": "1.0",
70
+ "capability": "changes",
71
+ "changeId": "5d7621f4-7ad5-49f6-8168-da99620ff1cf",
72
+ "base": "sha256:1c413f...",
73
+ "digest": "sha256:9f2c1a...",
74
+ "status": "pending",
75
+ "phase": "queued",
76
+ "candidateOrigin": "https://9f2c1a7b3e04.example-builds.net",
77
+ "statusUrl": "https://example.com/openship/changes/5d7621f4-7ad5-49f6-8168-da99620ff1cf",
78
+ "buildId": "9f2c1a7b3e04"
79
+ }
80
+ ```
81
+
82
+ The server computes the resulting Manifest and digest before acceptance. `candidateOrigin` MUST be returned for an accepted change, even if it is not live yet. `buildId` and `phase` are optional provider metadata.
83
+
84
+ The same resulting digest MAY return an existing record with `200`. A newly queued change returns `202`.
85
+
86
+ See [schemas/changes-accepted.schema.json](schemas/changes-accepted.schema.json).
87
+
88
+ ## Status
89
+
90
+ The normative lifecycle is:
91
+
92
+ | Status | Meaning |
93
+ |---|---|
94
+ | `pending` | Accepted but processing has not started. |
95
+ | `processing` | Validation, building, review, or publication is in progress. |
96
+ | `ready` | Candidate origin is live and verified. |
97
+ | `rejected` | Policy or review rejected the candidate. |
98
+ | `failed` | Infrastructure failed to produce a candidate. |
99
+
100
+ An implementation maps internal states such as `queued`, `building`, `reviewing`, and `deployed` into the core status and MAY expose the internal value as `phase`.
101
+
102
+ Before reporting `ready`, the producer MUST fetch or otherwise verify the candidate origin's advertised Sources Manifest and confirm its digest equals the accepted resulting digest.
103
+
104
+ Status responses MUST use `Cache-Control: no-store`. See [schemas/changes-status.schema.json](schemas/changes-status.schema.json).
105
+
106
+ ## Error responses
107
+
108
+ Errors contain `openship`, `capability`, `error`, and a human-readable `message`. Relevant status codes include:
109
+
110
+ - `400` malformed JSON or envelope.
111
+ - `401` or `403` authorization failure.
112
+ - `402` advertised payment required.
113
+ - `409` stale or unknown base digest.
114
+ - `413` transport or decoded size limit.
115
+ - `422` deterministic policy violations, with `violations`.
116
+ - `501` Changes is installed but disabled on this deployment.
117
+
118
+ Synchronous stale-base and policy-violation responses follow
119
+ [schemas/changes-violation.schema.json](schemas/changes-violation.schema.json).
120
+
121
+ ## Candidate isolation
122
+
123
+ A candidate executes code supplied by an untrusted author. Therefore:
124
+
125
+ - Its origin MUST use a different registrable domain from production, not merely a subdomain.
126
+ - Its build and runtime MUST contain no production secret or production credential.
127
+ - Submitted code MUST never receive a deployment credential.
128
+ - Build execution SHOULD have no network after dependency installation and SHOULD use explicit resource limits.
129
+ - Production promotion is a separate maintainer decision outside OpenShip.
130
+
131
+ Path filters, content scans, tests, and model review are useful filters. Isolation is the security boundary.
132
+
133
+ ## Identity and lineage
134
+
135
+ The resulting Sources digest is the candidate's normative identity. A hostname or `buildId` derived from a digest is a convenience that clients verify by retrieving Sources from the candidate origin. Candidate Sources SHOULD include optional `parent` metadata naming the base digest.