@opensoftware/contracts 1.5.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoftware/contracts",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "The shared formats of the open software factory: JSON Schemas, TypeScript types and the osf-contracts CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "@typespec/openapi": "1.17.0",
45
45
  "@typespec/openapi3": "1.17.0",
46
46
  "openapi-typescript": "7.13.0",
47
- "@opensoftware/testkit": "0.12.0"
47
+ "@opensoftware/testkit": "0.23.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node scripts/build.ts",
@@ -0,0 +1,90 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/sesigl/open-software-factory/main/contracts/sandbox-spec/sandbox-spec.schema.json",
4
+ "title": "SandboxSpec",
5
+ "$comment": "Generated by `pnpm build` in contracts/ from typespec/. Do not edit.",
6
+ "type": "object",
7
+ "properties": {
8
+ "template": {
9
+ "type": "string",
10
+ "minLength": 1,
11
+ "description": "What the sandbox starts from: an image for the docker adapter, a SandboxTemplate for kubernetes, or a snapshot."
12
+ },
13
+ "cpu": {
14
+ "type": "number",
15
+ "minimum": 0.001,
16
+ "description": "CPU cores. The smallest unit is 0.001 (one millicore)."
17
+ },
18
+ "memory": {
19
+ "type": "string",
20
+ "pattern": "^[0-9]+(Ki|Mi|Gi|Ti)?$",
21
+ "description": "Kubernetes quantity, for example \"8Gi\"."
22
+ },
23
+ "egress": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "string",
27
+ "pattern": "^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$",
28
+ "description": "A hostname without scheme or port, for example \"api.linear.app\"."
29
+ },
30
+ "description": "Hostnames the sandbox may reach through its egress proxy. Everything else is refused."
31
+ },
32
+ "env": {
33
+ "type": "object",
34
+ "properties": {},
35
+ "unevaluatedProperties": {
36
+ "type": "string"
37
+ },
38
+ "description": "Variables of the sandbox's environment that hold no secret."
39
+ },
40
+ "secrets": {
41
+ "type": "object",
42
+ "properties": {},
43
+ "unevaluatedProperties": {
44
+ "type": "string",
45
+ "pattern": "^ref:[a-z][a-z0-9-]*/[A-Za-z0-9_./-]+$",
46
+ "description": "A reference to a secret, resolved at the egress proxy, never in the sandbox: \"ref:<provider>/<name>\"."
47
+ },
48
+ "description": "Secrets by the name of the variable that holds the placeholder in the sandbox. The egress proxy puts the real\nvalue in on the way out, for the hosts of `egress`."
49
+ },
50
+ "mcp": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "object",
54
+ "properties": {
55
+ "name": {
56
+ "type": "string",
57
+ "pattern": "^[a-z][a-z0-9-]{0,62}$",
58
+ "description": "The name in the egress log, for example \"wiki\"."
59
+ },
60
+ "url": {
61
+ "type": "string",
62
+ "pattern": "^https?://[^/]+",
63
+ "description": "The server's endpoint, for example \"https://wiki.example.com/mcp\"."
64
+ },
65
+ "tools": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ },
70
+ "minItems": 1,
71
+ "description": "The tools the sandbox may call. A call to any other tool is refused."
72
+ }
73
+ },
74
+ "required": [
75
+ "name",
76
+ "url",
77
+ "tools"
78
+ ],
79
+ "description": "An MCP server that a sandbox may call over Streamable HTTP.",
80
+ "additionalProperties": false
81
+ },
82
+ "description": "The MCP servers the sandbox may call. A call to any other MCP server is refused."
83
+ }
84
+ },
85
+ "required": [
86
+ "template"
87
+ ],
88
+ "description": "The input of a sandbox adapter's `create` (ADR 0054): what one sandbox runs, how much it may use, and what it may\nreach. Realizes SCN-FACTORY-RUNTIME-004, -006 and -007.",
89
+ "additionalProperties": false
90
+ }
@@ -39,6 +39,9 @@ model CloudEvent<Type extends string, Data> {
39
39
  /** intake: a work item was accepted. */
40
40
  model WorkReceived is CloudEvent<"work.received@v1", WorkItem>;
41
41
 
42
+ /** foreman: a run started (ADR 0067). */
43
+ model RunStarted is CloudEvent<"run.started@v1", RunStartedData>;
44
+
42
45
  /** foreman: a run ended. */
43
46
  model RunCompleted is CloudEvent<"run.completed@v1", RunCompletedData>;
44
47
 
@@ -60,6 +63,26 @@ model PrReviewed is CloudEvent<"pr.reviewed@v1", PrReviewedData>;
60
63
  /** forge adapters: a pull request was merged or closed without a merge. */
61
64
  model PrClosed is CloudEvent<"pr.closed@v1", PrClosedData>;
62
65
 
66
+ /** forge adapters: a label was added to an issue. */
67
+ model IssueLabeled is CloudEvent<"issue.labeled@v1", IssueLabeledData>;
68
+
69
+ model RunStartedData {
70
+ runId: Identifier;
71
+ workItemId: Identifier;
72
+
73
+ /** The name of the factory. */
74
+ factory: Name;
75
+
76
+ factoryVersion: CommitHash;
77
+
78
+ /** The stages of the run's workflow, in order. */
79
+ stages: Name[];
80
+
81
+ startedAt: utcDateTime;
82
+ requester: Actor;
83
+ source: Name;
84
+ }
85
+
63
86
  model RunCompletedData {
64
87
  runId: Identifier;
65
88
  workItemId: Identifier;
@@ -166,3 +189,27 @@ model PrClosedData {
166
189
 
167
190
  mergeCommit?: CommitHash;
168
191
  }
192
+
193
+ model IssueLabeledData {
194
+ /** The issue. */
195
+ url: url;
196
+
197
+ repository: url;
198
+
199
+ @minValue(1)
200
+ number: safeint;
201
+
202
+ @minLength(1)
203
+ title: string;
204
+
205
+ body?: string;
206
+
207
+ /** The person who opened the issue. */
208
+ author: Actor;
209
+
210
+ /** Every label of the issue after the change. */
211
+ labels: string[];
212
+
213
+ /** The label that was added, when the forge names it. */
214
+ label?: string;
215
+ }
@@ -0,0 +1,45 @@
1
+ // The HTTP API of `foreman serve` (ADR 0067). `pnpm build` compiles it with the OpenAPI 3 emitter and copies its paths
2
+ // into openapi/openapi.json, next to gatekeeper's and intake's; the models are the named models of the JSON Schema
3
+ // bundle.
4
+ import "@typespec/http";
5
+ import "./foreman.tsp";
6
+ import "./gatekeeper-api.tsp";
7
+
8
+ using TypeSpec.Http;
9
+
10
+ namespace Osf.Contracts;
11
+
12
+ namespace ForemanApi {
13
+ /** Start a run of the factory's workflow for a work item. The same work item again answers the run it started. */
14
+ @route("/v1/runs")
15
+ @post
16
+ op startRun(@body workItem: WorkItem):
17
+ | {
18
+ @statusCode statusCode: 201;
19
+ @body run: RunView;
20
+ }
21
+ | {
22
+ @statusCode statusCode: 200;
23
+ @body run: RunView;
24
+ }
25
+ | GatekeeperApi.Failure<400>
26
+ | GatekeeperApi.Failure<401>;
27
+
28
+ /** A run: its state and the state of each stage. */
29
+ @route("/v1/runs/{runId}")
30
+ @get
31
+ op getRun(@path runId: string): RunView | GatekeeperApi.Failure<401> | GatekeeperApi.Failure<404>;
32
+
33
+ /** Steer the running stage's agent, or cancel the run. */
34
+ @route("/v1/runs/{runId}/signals")
35
+ @post
36
+ op signalRun(@path runId: string, @body signal: RunSignal):
37
+ | {
38
+ @statusCode statusCode: 202;
39
+ @body run: RunView;
40
+ }
41
+ | GatekeeperApi.Failure<400>
42
+ | GatekeeperApi.Failure<401>
43
+ | GatekeeperApi.Failure<404>
44
+ | GatekeeperApi.Failure<409>;
45
+ }
@@ -0,0 +1,84 @@
1
+ import "@typespec/json-schema";
2
+ import "./common.tsp";
3
+ import "./work-item.tsp";
4
+
5
+ using TypeSpec.JsonSchema;
6
+
7
+ namespace Osf.Contracts;
8
+
9
+ /** foreman: where a run stands (vision scenario ORCH-008). */
10
+ enum RunState {
11
+ running,
12
+ blocked,
13
+ complete,
14
+ failed,
15
+ cancelled,
16
+ }
17
+
18
+ /** foreman: where a stage of a run stands. */
19
+ enum StageState {
20
+ pending,
21
+ blocked,
22
+ running,
23
+ succeeded,
24
+ failed,
25
+ rejected,
26
+ cancelled,
27
+ }
28
+
29
+ /** foreman: one stage of a run (ADR 0067). */
30
+ model StageView {
31
+ stage: Name;
32
+
33
+ /** The agent that runs the stage. */
34
+ agent: Name;
35
+
36
+ state: StageState;
37
+
38
+ /** The harness session of the stage's agent, once it started. */
39
+ sessionId?: string;
40
+
41
+ /** The gate that decides whether the stage starts, once it was requested. */
42
+ @pattern("^g[0-9a-f]{16}$")
43
+ gateId?: string;
44
+
45
+ startedAt?: utcDateTime;
46
+ endedAt?: utcDateTime;
47
+ }
48
+
49
+ /** foreman: a run of the factory's workflow for one work item, as the HTTP API shows it (ADR 0067). */
50
+ model RunView {
51
+ /** "run_" and 20 hex digits of the SHA-256 of the work item's ID. */
52
+ @pattern("^run_[0-9a-f]{20}$")
53
+ runId: string;
54
+
55
+ workItem: WorkItem;
56
+
57
+ /** The name of the factory. */
58
+ factory: Name;
59
+
60
+ /** The commit of the factory definition the run started with. */
61
+ factoryVersion: CommitHash;
62
+
63
+ state: RunState;
64
+ startedAt: utcDateTime;
65
+ endedAt?: utcDateTime;
66
+
67
+ /** The stages of the workflow, in order. */
68
+ stages: StageView[];
69
+ }
70
+
71
+ /** foreman: what a person tells a run. */
72
+ enum RunSignalType {
73
+ steer,
74
+ cancel,
75
+ }
76
+
77
+ /** foreman: a signal to a run: "steer" with a message for the running stage's agent, or "cancel". */
78
+ model RunSignal {
79
+ type: RunSignalType;
80
+
81
+ /** The steering message. Required with "steer". */
82
+ @minLength(1)
83
+ message?: string;
84
+ }
@@ -0,0 +1,40 @@
1
+ // The HTTP API of `intake serve` (ADR 0063). `pnpm build` compiles it with the OpenAPI 3 emitter and copies its paths
2
+ // into openapi/openapi.json, next to gatekeeper's; the models are the named models of the JSON Schema bundle.
3
+ import "@typespec/http";
4
+ import "./intake.tsp";
5
+ import "./gatekeeper-api.tsp";
6
+
7
+ using TypeSpec.Http;
8
+
9
+ namespace Osf.Contracts;
10
+
11
+ namespace IntakeApi {
12
+ /** Put work into the inbox. A submission with the dedupe key of a work item answers that work item. */
13
+ @route("/v1/work-items")
14
+ @post
15
+ op submitWork(@body submission: WorkSubmission):
16
+ | {
17
+ @statusCode statusCode: 201;
18
+ @body workItem: WorkItemView;
19
+ }
20
+ | {
21
+ @statusCode statusCode: 200;
22
+ @body workItem: WorkItemView;
23
+ }
24
+ | GatekeeperApi.Failure<400>
25
+ | GatekeeperApi.Failure<401>;
26
+
27
+ /** The work item with this dedupe key, such as an issue's link (ADR 0065). */
28
+ @route("/v1/work-items")
29
+ @get
30
+ op findWorkItem(@query dedupeKey: string):
31
+ | WorkItemView
32
+ | GatekeeperApi.Failure<400>
33
+ | GatekeeperApi.Failure<401>
34
+ | GatekeeperApi.Failure<404>;
35
+
36
+ /** A work item, its status and every request that asked for it. */
37
+ @route("/v1/work-items/{workItemId}")
38
+ @get
39
+ op getWorkItem(@path workItemId: string): WorkItemView | GatekeeperApi.Failure<401> | GatekeeperApi.Failure<404>;
40
+ }
@@ -0,0 +1,52 @@
1
+ import "@typespec/json-schema";
2
+ import "./common.tsp";
3
+ import "./work-item.tsp";
4
+
5
+ using TypeSpec.JsonSchema;
6
+
7
+ namespace Osf.Contracts;
8
+
9
+ /** intake: work to put into the inbox, a work item without its ID (ADR 0063). */
10
+ model WorkSubmission {
11
+ /** Default: task. */
12
+ type?: WorkItemType;
13
+
14
+ @minLength(1)
15
+ @maxLength(300)
16
+ title: string;
17
+
18
+ description?: string;
19
+ requester: Actor;
20
+ origin: Origin;
21
+
22
+ /** Requests with the same key become one work item. Default: the origin's url, else "<source>:<externalId>". */
23
+ @minLength(1)
24
+ dedupeKey?: string;
25
+
26
+ surface?: string;
27
+ links?: Link[];
28
+ }
29
+
30
+ /** Where a work item stands. */
31
+ enum WorkItemStatus {
32
+ received,
33
+ }
34
+
35
+ /** intake: one submission that asked for a work item. */
36
+ model WorkRequest {
37
+ requester: Actor;
38
+ origin: Origin;
39
+ receivedAt: utcDateTime;
40
+ }
41
+
42
+ /** intake: a work item as the HTTP API shows it (ADR 0063). */
43
+ model WorkItemView {
44
+ /** Its ID is "wi_" and 20 hex digits of the SHA-256 of its dedupe key. */
45
+ workItem: WorkItem;
46
+
47
+ status: WorkItemStatus;
48
+ receivedAt: utcDateTime;
49
+
50
+ /** Every submission that asked for the work item, oldest first; one per origin. */
51
+ requests: WorkRequest[];
52
+ }
package/typespec/main.tsp CHANGED
@@ -10,3 +10,8 @@ import "./score.tsp";
10
10
  import "./events.tsp";
11
11
  import "./gatekeeper.tsp";
12
12
  import "./gatekeeper-api.tsp";
13
+ import "./intake.tsp";
14
+ import "./intake-api.tsp";
15
+ import "./foreman.tsp";
16
+ import "./foreman-api.tsp";
17
+ import "./sandbox.tsp";
@@ -0,0 +1,63 @@
1
+ import "@typespec/json-schema";
2
+ import "./common.tsp";
3
+ import "./factory-definition.tsp";
4
+
5
+ using TypeSpec.JsonSchema;
6
+
7
+ namespace Osf.Contracts;
8
+
9
+ // The sandbox spec is written by a program (foreman) for a sandbox adapter, and a sandbox must never get more than
10
+ // it asked for: every object is closed, so an unknown field is an error and not a silent grant.
11
+
12
+ /**
13
+ * The input of a sandbox adapter's `create` (ADR 0054): what one sandbox runs, how much it may use, and what it may
14
+ * reach. Realizes SCN-FACTORY-RUNTIME-004, -006 and -007.
15
+ */
16
+ @extension("additionalProperties", Json<false>)
17
+ model SandboxSpec {
18
+ /** What the sandbox starts from: an image for the docker adapter, a SandboxTemplate for kubernetes, or a snapshot. */
19
+ @minLength(1)
20
+ template: string;
21
+
22
+ /** CPU cores. The smallest unit is 0.001 (one millicore). */
23
+ @minValue(0.001)
24
+ cpu?: float64;
25
+
26
+ /** Kubernetes quantity, for example "8Gi". */
27
+ @pattern("^[0-9]+(Ki|Mi|Gi|Ti)?$")
28
+ memory?: string;
29
+
30
+ /** Hostnames the sandbox may reach through its egress proxy. Everything else is refused. */
31
+ egress?: Hostname[];
32
+
33
+ /** Variables of the sandbox's environment that hold no secret. */
34
+ env?: Record<string>;
35
+
36
+ /**
37
+ * Secrets by the name of the variable that holds the placeholder in the sandbox. The egress proxy puts the real
38
+ * value in on the way out, for the hosts of `egress`.
39
+ */
40
+ secrets?: Record<SecretReference>;
41
+
42
+ /** The MCP servers the sandbox may call. A call to any other MCP server is refused. */
43
+ mcp?: SandboxMcpServer[];
44
+ }
45
+
46
+ /** A hostname without scheme or port, for example "api.linear.app". */
47
+ @pattern("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$")
48
+ scalar Hostname extends string;
49
+
50
+ /** An MCP server that a sandbox may call over Streamable HTTP. */
51
+ @extension("additionalProperties", Json<false>)
52
+ model SandboxMcpServer {
53
+ /** The name in the egress log, for example "wiki". */
54
+ name: Name;
55
+
56
+ /** The server's endpoint, for example "https://wiki.example.com/mcp". */
57
+ @pattern("^https?://[^/]+")
58
+ url: string;
59
+
60
+ /** The tools the sandbox may call. A call to any other tool is refused. */
61
+ @minItems(1)
62
+ tools: string[];
63
+ }