@intentius/chant 0.89.0 → 0.91.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/dist/cli/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +14 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/serve.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +10 -5
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +13 -5
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +55 -0
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -0
- package/dist/cli/registry.d.ts +31 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/index.d.ts +9 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +19 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +10 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +29 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +9 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward.d.ts +140 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +51 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +71 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +1 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +10 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +42 -2
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +184 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +51 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +172 -0
- package/dist/workspace/declared-kinds.d.ts +12 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +29 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +12 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +35 -3
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +12 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/source-block.d.ts +85 -0
- package/dist/workspace/source-block.d.ts.map +1 -0
- package/dist/workspace/status-stewards.d.ts +121 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +52 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +78 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/operator.ts +107 -2
- package/src/cli/handlers/run.test.ts +19 -0
- package/src/cli/handlers/run.ts +53 -1
- package/src/cli/handlers/serve.ts +2 -1
- package/src/cli/main.test.ts +40 -0
- package/src/cli/main.ts +78 -2
- package/src/cli/mcp/docs-parity.test.ts +20 -2
- package/src/cli/mcp/server.ts +23 -6
- package/src/cli/mcp/types.ts +15 -2
- package/src/cli/mcp/workspace-tools.test.ts +211 -0
- package/src/cli/mcp/workspace-tools.ts +449 -0
- package/src/cli/registry.ts +31 -0
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/op/builders.ts +5 -0
- package/src/op/discover.ts +71 -0
- package/src/op/index.ts +16 -3
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +216 -18
- package/src/op/local-output.ts +13 -0
- package/src/op/op-ir.ts +14 -0
- package/src/op/op-verb-class.ts +6 -0
- package/src/op/operator.ts +75 -4
- package/src/op/runtime.ts +6 -0
- package/src/op/runtimes/local.ts +3 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward.test.ts +212 -0
- package/src/op/steward.ts +253 -0
- package/src/op/types.ts +53 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +326 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/check-contract.test.ts +3 -1
- package/src/workspace/check.schema.json +15 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +197 -0
- package/src/workspace/checks/boxes.ts +307 -0
- package/src/workspace/checks/records.ts +25 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +19 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +100 -6
- package/src/workspace/conformance/index.mjs +3 -0
- package/src/workspace/conformance/index.ts +185 -7
- package/src/workspace/conformance/vitest.ts +17 -8
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +576 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +172 -0
- package/src/workspace/declaration.ts +137 -0
- package/src/workspace/declared-kinds.ts +25 -2
- package/src/workspace/intent.schema.json +4 -1
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +273 -0
- package/src/workspace/points-write.schema.json +489 -0
- package/src/workspace/points.schema.json +710 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +14 -1
- package/src/workspace/reason-codes.ts +36 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -1
- package/src/workspace/records-cli.ts +15 -2
- package/src/workspace/records-close.schema.json +2 -1
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +4 -1
- package/src/workspace/records-review.schema.json +2 -1
- package/src/workspace/records-write.ts +85 -7
- package/src/workspace/records.schema.json +18 -0
- package/src/workspace/records.ts +61 -3
- package/src/workspace/source-block.test.ts +167 -0
- package/src/workspace/source-block.ts +129 -0
- package/src/workspace/status-contract.test.ts +178 -0
- package/src/workspace/status-stewards.ts +225 -0
- package/src/workspace/status.schema.json +230 -4
- package/src/workspace/status.ts +106 -5
- package/src/workspace/work-cli.test.ts +180 -0
- package/src/workspace/work-cli.ts +246 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/workspace/decision-points/v1/decision-points.schema.json",
|
|
4
|
+
"title": "Decision points",
|
|
5
|
+
"description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. Taken from chud's decision-points.schema.json; `boolean` is read as `noul`. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["points"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"points": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"minProperties": 1,
|
|
13
|
+
"propertyNames": { "pattern": "^[a-z][a-z0-9-]*$" },
|
|
14
|
+
"additionalProperties": { "$ref": "#/$defs/point" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"patternProperties": { "^x-": true },
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"$defs": {
|
|
20
|
+
"text": { "type": "string", "minLength": 1 },
|
|
21
|
+
"point": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["title", "question", "inputs", "deciders"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"title": { "$ref": "#/$defs/text" },
|
|
26
|
+
"question": { "$ref": "#/$defs/question" },
|
|
27
|
+
"inputs": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"description": "The state the deciders see, input name to description. A name is a read-contract output (record, decision, work-item, finding, region, commit, member, gate, release, environment, component), optionally followed by dotted field names, such as work-item.fits_small: a field of that output, or a value the caller derives from it. The caller supplies the values; a table row can only test these names.",
|
|
30
|
+
"minProperties": 1,
|
|
31
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_-]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$" },
|
|
32
|
+
"additionalProperties": { "type": "string" }
|
|
33
|
+
},
|
|
34
|
+
"deciders": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"minItems": 1,
|
|
37
|
+
"items": { "$ref": "#/$defs/decider" }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"patternProperties": { "^x-": true },
|
|
41
|
+
"additionalProperties": false
|
|
42
|
+
},
|
|
43
|
+
"question": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"required": ["type", "instructions", "criteria"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"type": { "enum": ["noul", "boolean", "choice", "score"], "description": "boolean is chud's name for noul, read as noul." },
|
|
48
|
+
"instructions": { "$ref": "#/$defs/text" },
|
|
49
|
+
"criteria": {
|
|
50
|
+
"description": "The candidates. noul: {true, false} descriptions. choice: option to description, 2 to 255 options. score: 2 to 10 ordered levels.",
|
|
51
|
+
"type": ["object", "array"]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"allOf": [
|
|
56
|
+
{
|
|
57
|
+
"if": { "properties": { "type": { "enum": ["noul", "boolean"] } } },
|
|
58
|
+
"then": {
|
|
59
|
+
"properties": {
|
|
60
|
+
"criteria": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["true", "false"],
|
|
63
|
+
"properties": { "true": { "type": "string" }, "false": { "type": "string" } },
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"if": { "properties": { "type": { "const": "choice" } } },
|
|
71
|
+
"then": { "properties": { "criteria": { "type": "object", "minProperties": 2, "maxProperties": 255, "additionalProperties": { "type": "string" } } } }
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"if": { "properties": { "type": { "const": "score" } } },
|
|
75
|
+
"then": { "properties": { "criteria": { "type": "array", "minItems": 2, "maxItems": 10, "uniqueItems": true, "items": { "$ref": "#/$defs/text" } } } }
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"scalar": { "type": ["string", "number", "boolean", "null"] },
|
|
80
|
+
"condition": {
|
|
81
|
+
"description": "A value the input must equal, or one comparison.",
|
|
82
|
+
"oneOf": [
|
|
83
|
+
{ "$ref": "#/$defs/scalar" },
|
|
84
|
+
{
|
|
85
|
+
"type": "object",
|
|
86
|
+
"minProperties": 1,
|
|
87
|
+
"maxProperties": 1,
|
|
88
|
+
"properties": {
|
|
89
|
+
"eq": { "$ref": "#/$defs/scalar" },
|
|
90
|
+
"ne": { "$ref": "#/$defs/scalar" },
|
|
91
|
+
"lt": { "type": "number" },
|
|
92
|
+
"lte": { "type": "number" },
|
|
93
|
+
"gt": { "type": "number" },
|
|
94
|
+
"gte": { "type": "number" },
|
|
95
|
+
"in": { "type": "array", "items": { "$ref": "#/$defs/scalar" } }
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"row": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"required": ["when", "answer"],
|
|
104
|
+
"properties": {
|
|
105
|
+
"when": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"description": "Every named input must meet its condition. An empty object matches every state.",
|
|
108
|
+
"additionalProperties": { "$ref": "#/$defs/condition" }
|
|
109
|
+
},
|
|
110
|
+
"answer": { "type": ["string", "boolean"] },
|
|
111
|
+
"note": { "type": "string" }
|
|
112
|
+
},
|
|
113
|
+
"additionalProperties": false
|
|
114
|
+
},
|
|
115
|
+
"decider": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"required": ["kind"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"kind": { "enum": ["table", "model", "quorum"] },
|
|
120
|
+
"rows": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/row" } },
|
|
121
|
+
"backend": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "model: which backend answers, such as systemone (any server with the POST /v1/systemone wire format). chant never calls it: the decide Op activity or a runtime's decider does." },
|
|
122
|
+
"model": { "type": "string", "minLength": 1, "description": "model: the pinned model id the backend is asked for, such as jev-1.13.0. An alias such as jev-latest is refused." },
|
|
123
|
+
"threshold": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "description": "model: the least confidence that makes the answer a proposal. Below it the answer is not observed and the next decider is asked." },
|
|
124
|
+
"unreachable": { "enum": ["escalate", "fail"], "description": "model: what happens when the backend can't be reached or gives no usable answer: escalate to the next decider (the default), or fail the ask and write nothing." },
|
|
125
|
+
"count": { "type": "integer", "minimum": 1, "description": "quorum: how many distinct people must answer." },
|
|
126
|
+
"roles": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/text" }, "description": "quorum: the trust policy roles whose holders count." },
|
|
127
|
+
"note": { "type": "string" }
|
|
128
|
+
},
|
|
129
|
+
"additionalProperties": false,
|
|
130
|
+
"allOf": [
|
|
131
|
+
{ "if": { "properties": { "kind": { "const": "table" } } }, "then": { "required": ["rows"] } },
|
|
132
|
+
{ "if": { "properties": { "kind": { "const": "model" } } }, "then": { "required": ["backend", "model", "threshold"] } },
|
|
133
|
+
{ "if": { "properties": { "kind": { "const": "quorum" } } }, "then": { "required": ["count"] } }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -56,6 +56,13 @@
|
|
|
56
56
|
"$ref": "#/$defs/recordKind"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
|
+
"hosts": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"description": "The hosts boxes run on (#2727). A host is one machine reached under one hostname: boxes on it share its ports, its state root and, in a browser, its cookie jar, so chant workspace check compares the boxes on one host and never boxes on different hosts. A member's box block names its host. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
62
|
+
"items": {
|
|
63
|
+
"$ref": "#/$defs/host"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
59
66
|
"checks": {
|
|
60
67
|
"type": "object",
|
|
61
68
|
"description": "Severity for the declaration checks that allow it, keyed by WSP id (#2535, ws-028): error, warning, info or off. chant workspace check refuses an id it does not know, and one whose severity is fixed.",
|
|
@@ -269,6 +276,9 @@
|
|
|
269
276
|
"$ref": "#/$defs/recordKind"
|
|
270
277
|
}
|
|
271
278
|
},
|
|
279
|
+
"box": {
|
|
280
|
+
"$ref": "#/$defs/box"
|
|
281
|
+
},
|
|
272
282
|
"upstream": {
|
|
273
283
|
"type": "string",
|
|
274
284
|
"minLength": 1,
|
|
@@ -354,6 +364,114 @@
|
|
|
354
364
|
"description": "A file or directory path with / separators: no leading /, no . or .. segments and no trailing /.",
|
|
355
365
|
"pattern": "^(?!\\.\\.?(/|$))[^/\\\\]+(/(?!\\.\\.?(/|$))[^/\\\\]+)*$"
|
|
356
366
|
},
|
|
367
|
+
"box": {
|
|
368
|
+
"type": "object",
|
|
369
|
+
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
370
|
+
"properties": {
|
|
371
|
+
"capabilities": {
|
|
372
|
+
"type": "array",
|
|
373
|
+
"description": "Each capability the box reaches outside itself, named once. None when omitted.",
|
|
374
|
+
"items": {
|
|
375
|
+
"$ref": "#/$defs/capability"
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"host": {
|
|
379
|
+
"$ref": "#/$defs/name",
|
|
380
|
+
"description": "The host the box runs on, one of the top-level hosts (#2727). The box's name is the member's name."
|
|
381
|
+
},
|
|
382
|
+
"slot": {
|
|
383
|
+
"type": "integer",
|
|
384
|
+
"minimum": 0,
|
|
385
|
+
"description": "The box's number on its host (#2727). Its ports come from the slot's block of the host's range."
|
|
386
|
+
},
|
|
387
|
+
"ports": {
|
|
388
|
+
"type": "object",
|
|
389
|
+
"description": "The ports the box listens on, each a name and its offset in the box's block (#2727). The port is the block's first port plus the offset.",
|
|
390
|
+
"propertyNames": {
|
|
391
|
+
"$ref": "#/$defs/kindName"
|
|
392
|
+
},
|
|
393
|
+
"additionalProperties": {
|
|
394
|
+
"type": "integer",
|
|
395
|
+
"minimum": 0,
|
|
396
|
+
"maximum": 65534
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
"state": {
|
|
400
|
+
"type": "object",
|
|
401
|
+
"description": "State the box keeps outside its checkout, such as hud's identity and database, each a name and a path relative to the box's state directory (#2727). The name is usually the environment variable the runtime sets, such as HUD_IDENTITY_PATH. A path that is absolute, starts with ~ or an environment reference, or has a .. segment is a WSP124 finding.",
|
|
402
|
+
"propertyNames": {
|
|
403
|
+
"type": "string",
|
|
404
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$"
|
|
405
|
+
},
|
|
406
|
+
"additionalProperties": {
|
|
407
|
+
"type": "string",
|
|
408
|
+
"minLength": 1
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
"cookies": {
|
|
412
|
+
"type": "array",
|
|
413
|
+
"description": "Session cookie names the box's runtimes set, such as hud_session (#2727). A browser scopes a cookie to a host, not a port, so each resolves to <cookie>_<member name>.",
|
|
414
|
+
"uniqueItems": true,
|
|
415
|
+
"items": {
|
|
416
|
+
"type": "string",
|
|
417
|
+
"pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
"dependentRequired": {
|
|
422
|
+
"host": [
|
|
423
|
+
"slot"
|
|
424
|
+
],
|
|
425
|
+
"slot": [
|
|
426
|
+
"host"
|
|
427
|
+
],
|
|
428
|
+
"ports": [
|
|
429
|
+
"host"
|
|
430
|
+
],
|
|
431
|
+
"state": [
|
|
432
|
+
"host"
|
|
433
|
+
],
|
|
434
|
+
"cookies": [
|
|
435
|
+
"host"
|
|
436
|
+
]
|
|
437
|
+
},
|
|
438
|
+
"patternProperties": {
|
|
439
|
+
"^x-": true
|
|
440
|
+
},
|
|
441
|
+
"additionalProperties": false
|
|
442
|
+
},
|
|
443
|
+
"capability": {
|
|
444
|
+
"type": "object",
|
|
445
|
+
"description": "A capability a box needs and does not hold the credential for, such as inference, fountain or a third-party API (#2726).",
|
|
446
|
+
"required": [
|
|
447
|
+
"name"
|
|
448
|
+
],
|
|
449
|
+
"properties": {
|
|
450
|
+
"name": {
|
|
451
|
+
"$ref": "#/$defs/kindName",
|
|
452
|
+
"description": "The capability, such as inference or fountain. Unique within the box."
|
|
453
|
+
},
|
|
454
|
+
"broker": {
|
|
455
|
+
"type": "string",
|
|
456
|
+
"minLength": 1,
|
|
457
|
+
"description": "What brokers the capability: the runtime that holds the credential and swaps the box's own token for it, such as lobby or door. It may name a member of this workspace, or a runtime outside it. Without it the capability is unbrokered, and chant workspace check fails with WSP122."
|
|
458
|
+
},
|
|
459
|
+
"scope": {
|
|
460
|
+
"type": "array",
|
|
461
|
+
"uniqueItems": true,
|
|
462
|
+
"description": "What of the box's own the broker lets it reach, which the broker enforces. For Fountain: agent (start conversations with its own agent), vault (attach its own vault), conversations and sandboxes (its own). The words are the broker's; chant records them.",
|
|
463
|
+
"items": {
|
|
464
|
+
"type": "string",
|
|
465
|
+
"minLength": 1,
|
|
466
|
+
"pattern": "^[^\\u0000-\\u001f]+$"
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
"patternProperties": {
|
|
471
|
+
"^x-": true
|
|
472
|
+
},
|
|
473
|
+
"additionalProperties": false
|
|
474
|
+
},
|
|
357
475
|
"recordKind": {
|
|
358
476
|
"type": "object",
|
|
359
477
|
"description": "A record kind the workspace declares (#2680): a kind file that exports recordKind. chant workspace check fails (WSP115) when the file is missing or does not load as a record kind.",
|
|
@@ -469,6 +587,60 @@
|
|
|
469
587
|
"additionalProperties": false
|
|
470
588
|
}
|
|
471
589
|
]
|
|
590
|
+
},
|
|
591
|
+
"host": {
|
|
592
|
+
"type": "object",
|
|
593
|
+
"description": "A host boxes run on (#2727).",
|
|
594
|
+
"required": [
|
|
595
|
+
"name",
|
|
596
|
+
"ports"
|
|
597
|
+
],
|
|
598
|
+
"properties": {
|
|
599
|
+
"name": {
|
|
600
|
+
"$ref": "#/$defs/name",
|
|
601
|
+
"description": "The host's name, unique among hosts. A box block names it in host."
|
|
602
|
+
},
|
|
603
|
+
"ports": {
|
|
604
|
+
"type": "object",
|
|
605
|
+
"description": "The TCP ports boxes on this host take theirs from. Slot n owns the block from + n * perBox to from + (n + 1) * perBox - 1, which must end at or before to.",
|
|
606
|
+
"required": [
|
|
607
|
+
"from",
|
|
608
|
+
"to",
|
|
609
|
+
"perBox"
|
|
610
|
+
],
|
|
611
|
+
"properties": {
|
|
612
|
+
"from": {
|
|
613
|
+
"$ref": "#/$defs/port",
|
|
614
|
+
"description": "The first port of the range."
|
|
615
|
+
},
|
|
616
|
+
"to": {
|
|
617
|
+
"$ref": "#/$defs/port",
|
|
618
|
+
"description": "The last port of the range, inclusive."
|
|
619
|
+
},
|
|
620
|
+
"perBox": {
|
|
621
|
+
"type": "integer",
|
|
622
|
+
"minimum": 1,
|
|
623
|
+
"maximum": 65535,
|
|
624
|
+
"description": "How many ports each box's block holds. A box's port offsets are below it."
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
"additionalProperties": false
|
|
628
|
+
},
|
|
629
|
+
"stateRoot": {
|
|
630
|
+
"type": "string",
|
|
631
|
+
"minLength": 1,
|
|
632
|
+
"description": "Where boxes on this host keep state, as an environment reference such as ${XDG_STATE_HOME}/chant/boxes, which the runtime expands on the machine. A box's state directory is <stateRoot>/<member name>. A literal machine path, such as /Users/... or anything under $HOME, is a WSP124 finding. Default ${XDG_STATE_HOME}/chant/boxes."
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
"patternProperties": {
|
|
636
|
+
"^x-": true
|
|
637
|
+
},
|
|
638
|
+
"additionalProperties": false
|
|
639
|
+
},
|
|
640
|
+
"port": {
|
|
641
|
+
"type": "integer",
|
|
642
|
+
"minimum": 1,
|
|
643
|
+
"maximum": 65535
|
|
472
644
|
}
|
|
473
645
|
}
|
|
474
646
|
}
|
|
@@ -145,6 +145,54 @@ export interface RecordKindDeclaration {
|
|
|
145
145
|
pointer: string;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* A capability a box reaches through a broker (#2726): inference, Fountain,
|
|
150
|
+
* a third-party API. The broker is runtime (a lobby, a door, a studio) and
|
|
151
|
+
* holds the credential; the declaration names it and the scope it enforces.
|
|
152
|
+
*/
|
|
153
|
+
export interface BoxCapability {
|
|
154
|
+
name: string;
|
|
155
|
+
/** What brokers it, such as `lobby`, or null when the entry names no broker (WSP122). */
|
|
156
|
+
broker: string | null;
|
|
157
|
+
/** What of the box's own the broker lets it reach, such as `agent`, `vault`, `conversations`, `sandboxes`. */
|
|
158
|
+
scope: string[];
|
|
159
|
+
/** The entry's JSON Pointer in the file, for messages. */
|
|
160
|
+
pointer: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A member that is a box, or a box's declarations (#2726). */
|
|
164
|
+
export interface BoxDeclaration {
|
|
165
|
+
capabilities: BoxCapability[];
|
|
166
|
+
/** The host and slot of the box's isolation, or null when the block declares none (#2727). The values are derived in `boxes.ts`. */
|
|
167
|
+
isolation: BoxIsolationDeclaration | null;
|
|
168
|
+
/** The block's JSON Pointer in the file, for messages. */
|
|
169
|
+
pointer: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What a box declares about its isolation (#2727): its identity on a host, and the names of what it needs kept apart. */
|
|
173
|
+
export interface BoxIsolationDeclaration {
|
|
174
|
+
host: string;
|
|
175
|
+
slot: number;
|
|
176
|
+
/** Port name to offset in the box's block, in file order. */
|
|
177
|
+
ports: Record<string, number>;
|
|
178
|
+
/** State name to a path relative to the box's state directory, in file order. */
|
|
179
|
+
state: Record<string, string>;
|
|
180
|
+
cookies: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A host boxes run on (#2727): one machine under one hostname. */
|
|
184
|
+
export interface Host {
|
|
185
|
+
name: string;
|
|
186
|
+
ports: { from: number; to: number; perBox: number };
|
|
187
|
+
/** As written, or null for the default ({@link DEFAULT_STATE_ROOT}). */
|
|
188
|
+
stateRoot: string | null;
|
|
189
|
+
/** The entry's JSON Pointer in the file, for messages. */
|
|
190
|
+
pointer: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Where boxes keep state when their host names no stateRoot (#2727). */
|
|
194
|
+
export const DEFAULT_STATE_ROOT = "${XDG_STATE_HOME}/chant/boxes";
|
|
195
|
+
|
|
148
196
|
export interface Member {
|
|
149
197
|
type: "member";
|
|
150
198
|
name: string;
|
|
@@ -160,6 +208,8 @@ export interface Member {
|
|
|
160
208
|
links: LinkDeclaration[];
|
|
161
209
|
/** The record kinds this member declares, in file order (#2680). */
|
|
162
210
|
records: RecordKindDeclaration[];
|
|
211
|
+
/** The member's box block, or null when it declares none (#2726). */
|
|
212
|
+
box: BoxDeclaration | null;
|
|
163
213
|
upstream: string | null;
|
|
164
214
|
because: string | null;
|
|
165
215
|
suppress: Suppression[];
|
|
@@ -202,6 +252,8 @@ export interface Declaration {
|
|
|
202
252
|
quorum: number | null;
|
|
203
253
|
/** The workspace's own record kinds, from the top-level `records`, in file order (#2680). A member's are on the member. */
|
|
204
254
|
records: RecordKindDeclaration[];
|
|
255
|
+
/** The hosts boxes run on, in file order (#2727). */
|
|
256
|
+
hosts: Host[];
|
|
205
257
|
/** The file, relative to the workspace root's tree (`chant.workspace.json` or `.jsonc`). */
|
|
206
258
|
file: string;
|
|
207
259
|
}
|
|
@@ -414,6 +466,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
414
466
|
pointer: `${pointer}/links/${j}`,
|
|
415
467
|
}));
|
|
416
468
|
const records = recordKindsOf(e.records, e.dir as string, e.name as string, `${pointer}/records`);
|
|
469
|
+
const box = boxOf(e.box, `${pointer}/box`);
|
|
417
470
|
return {
|
|
418
471
|
type: "member",
|
|
419
472
|
name: e.name as string,
|
|
@@ -424,6 +477,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
424
477
|
outputs: e.outputs === undefined ? null : [...(e.outputs as string[])],
|
|
425
478
|
links,
|
|
426
479
|
records,
|
|
480
|
+
box,
|
|
427
481
|
upstream: (e.upstream as string | undefined) ?? null,
|
|
428
482
|
because: (e.because as string | undefined) ?? null,
|
|
429
483
|
suppress,
|
|
@@ -508,6 +562,18 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
508
562
|
byName.set(r.name, r);
|
|
509
563
|
}
|
|
510
564
|
|
|
565
|
+
// A box names each capability once (#2726): a broker reads its scope by name.
|
|
566
|
+
for (const m of members) {
|
|
567
|
+
const seen = new Map<string, BoxCapability>();
|
|
568
|
+
for (const c of m.box?.capabilities ?? []) {
|
|
569
|
+
const first = seen.get(c.name);
|
|
570
|
+
if (first) throw new WorkspaceReadError("declaration-invalid", `member ${m.name}'s box lists the capability ${c.name} twice; the first is at ${first.pointer}`, at(`${c.pointer}/name`));
|
|
571
|
+
seen.set(c.name, c);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const hosts = hostsOf(obj, members, at);
|
|
576
|
+
|
|
511
577
|
const pins = ((obj.pins as Record<string, string>[] | undefined) ?? []).map((p) => ({
|
|
512
578
|
package: p.package ?? null,
|
|
513
579
|
version: p.version ?? null,
|
|
@@ -526,6 +592,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
526
592
|
checks: { ...((obj.checks as Record<string, CheckSeverity> | undefined) ?? {}) },
|
|
527
593
|
quorum: typeof obj.quorum === "number" ? obj.quorum : null,
|
|
528
594
|
records: ownRecords,
|
|
595
|
+
hosts,
|
|
529
596
|
file,
|
|
530
597
|
};
|
|
531
598
|
}
|
|
@@ -541,6 +608,76 @@ function recordKindsOf(raw: unknown, dir: string, member: string | null, pointer
|
|
|
541
608
|
}));
|
|
542
609
|
}
|
|
543
610
|
|
|
611
|
+
/** The `box` block at `pointer`, already validated, or null when there is none (#2726, #2727). */
|
|
612
|
+
function boxOf(raw: unknown, pointer: string): BoxDeclaration | null {
|
|
613
|
+
if (raw === undefined) return null;
|
|
614
|
+
const b = raw as {
|
|
615
|
+
capabilities?: { name: string; broker?: string; scope?: string[] }[];
|
|
616
|
+
host?: string;
|
|
617
|
+
slot?: number;
|
|
618
|
+
ports?: Record<string, number>;
|
|
619
|
+
state?: Record<string, string>;
|
|
620
|
+
cookies?: string[];
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
capabilities: (b.capabilities ?? []).map((c, i) => ({ name: c.name, broker: c.broker ?? null, scope: [...(c.scope ?? [])], pointer: `${pointer}/capabilities/${i}` })),
|
|
624
|
+
// The schema requires host and slot together, and host for ports, state and cookies.
|
|
625
|
+
isolation:
|
|
626
|
+
b.host === undefined
|
|
627
|
+
? null
|
|
628
|
+
: { host: b.host, slot: b.slot!, ports: { ...(b.ports ?? {}) }, state: { ...(b.state ?? {}) }, cookies: [...(b.cookies ?? [])] },
|
|
629
|
+
pointer,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The hosts, already validated, with the rules the schema can't say (#2727):
|
|
635
|
+
* host names are unique, a box's host is declared, its slot's block fits the
|
|
636
|
+
* host's range and its offsets fit the block. What two boxes resolve to is
|
|
637
|
+
* `chant workspace check`'s question (WSP123), not a read error.
|
|
638
|
+
*/
|
|
639
|
+
function hostsOf(obj: Record<string, unknown>, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): Host[] {
|
|
640
|
+
const hosts = ((obj.hosts as Record<string, unknown>[] | undefined) ?? []).map((h, i): Host => ({
|
|
641
|
+
name: h.name as string,
|
|
642
|
+
ports: { ...(h.ports as Host["ports"]) },
|
|
643
|
+
stateRoot: (h.stateRoot as string | undefined) ?? null,
|
|
644
|
+
pointer: `/hosts/${i}`,
|
|
645
|
+
}));
|
|
646
|
+
const invalid = (message: string, pointer: string) => new WorkspaceReadError("declaration-invalid", message, at(pointer));
|
|
647
|
+
const byName = new Map<string, Host>();
|
|
648
|
+
for (const h of hosts) {
|
|
649
|
+
const first = byName.get(h.name);
|
|
650
|
+
if (first) throw invalid(`the host name ${JSON.stringify(h.name)} is already used by the host at ${first.pointer}`, `${h.pointer}/name`);
|
|
651
|
+
byName.set(h.name, h);
|
|
652
|
+
if (h.ports.from > h.ports.to) throw invalid(`host ${h.name}'s port range starts at ${h.ports.from}, after its end ${h.ports.to}`, `${h.pointer}/ports`);
|
|
653
|
+
}
|
|
654
|
+
for (const m of members) {
|
|
655
|
+
const iso = m.box?.isolation;
|
|
656
|
+
if (!iso) continue;
|
|
657
|
+
const box = m.box!.pointer;
|
|
658
|
+
const host = byName.get(iso.host);
|
|
659
|
+
if (!host) {
|
|
660
|
+
const known = hosts.map((h) => h.name).join(", ") || "none are declared";
|
|
661
|
+
throw invalid(`member ${m.name}'s box names the host ${JSON.stringify(iso.host)}, which hosts does not declare; declared hosts: ${known}`, `${box}/host`);
|
|
662
|
+
}
|
|
663
|
+
const { from, to, perBox } = host.ports;
|
|
664
|
+
const last = from + (iso.slot + 1) * perBox - 1;
|
|
665
|
+
if (last > to) {
|
|
666
|
+
const slots = Math.floor((to - from + 1) / perBox);
|
|
667
|
+
throw invalid(
|
|
668
|
+
`member ${m.name}'s box has slot ${iso.slot} on host ${host.name}, which needs ports ${from + iso.slot * perBox} to ${last}, past the end of the host's range (${to}); the range holds ${slots} slot${slots === 1 ? "" : "s"}`,
|
|
669
|
+
`${box}/slot`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
for (const [port, offset] of Object.entries(iso.ports)) {
|
|
673
|
+
if (offset >= perBox) {
|
|
674
|
+
throw invalid(`member ${m.name}'s box gives port ${port} offset ${offset}, and host ${host.name} gives each box ${perBox} port${perBox === 1 ? "" : "s"} (offsets 0 to ${perBox - 1})`, `${box}/ports/${pointerToken(port)}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return hosts;
|
|
679
|
+
}
|
|
680
|
+
|
|
544
681
|
/**
|
|
545
682
|
* Every record kind the declaration names (#2680), in the order readers use
|
|
546
683
|
* them: the workspace's own first, then each member's, members in file order
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* kind file.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { join } from "node:path";
|
|
16
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
17
17
|
import { declaredRecordKinds, type Declaration, type RecordKindDeclaration } from "./declaration";
|
|
18
18
|
import type { ReasonCode } from "./reason-codes";
|
|
19
19
|
import type { WorkspaceTree } from "./tree";
|
|
@@ -30,6 +30,12 @@ export interface DeclaredKind {
|
|
|
30
30
|
kind: string | null;
|
|
31
31
|
/** Why it can't be loaded, or null. */
|
|
32
32
|
reason: { code: DeclaredKindReasonCode; message: string } | null;
|
|
33
|
+
/**
|
|
34
|
+
* For an answer kind (ws-058, #2738): its points file from the workspace
|
|
35
|
+
* root, and what is wrong with it, empty when nothing is. Absent for any
|
|
36
|
+
* other kind, and when the kind wasn't loaded.
|
|
37
|
+
*/
|
|
38
|
+
points?: { file: string; problems: { field: string | null; message: string }[] };
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/** The kind file of `declared` on disk, under the workspace root `rootOnDisk`. */
|
|
@@ -65,7 +71,9 @@ export async function loadDeclaredKinds(
|
|
|
65
71
|
}
|
|
66
72
|
try {
|
|
67
73
|
const loaded = await records.loadRecordKind(file);
|
|
68
|
-
|
|
74
|
+
const entry: DeclaredKind = { declared: d, file, kind: loaded.kind.name, reason: null };
|
|
75
|
+
if (loaded.kind.answers) entry.points = await checkPoints(resolve(dirname(file), loaded.kind.answers.points), rootOnDisk, tree);
|
|
76
|
+
out.push(entry);
|
|
69
77
|
} catch (err) {
|
|
70
78
|
if (!(err instanceof records.RecordReadError)) throw err;
|
|
71
79
|
// loadRecordKind names the file as it was given, here an absolute path: name it from the workspace root instead.
|
|
@@ -74,3 +82,18 @@ export async function loadDeclaredKinds(
|
|
|
74
82
|
}
|
|
75
83
|
return out;
|
|
76
84
|
}
|
|
85
|
+
|
|
86
|
+
/** An answer kind's points file (ws-058), read from `tree` and validated as `points ask` reads it. */
|
|
87
|
+
async function checkPoints(abs: string, rootOnDisk: string, tree: WorkspaceTree): Promise<NonNullable<DeclaredKind["points"]>> {
|
|
88
|
+
const file = relative(rootOnDisk, abs).split(sep).join("/");
|
|
89
|
+
if (file.startsWith("../")) return { file, problems: [{ field: null, message: "is outside the workspace" }] };
|
|
90
|
+
if (tree.stat(file) !== "file") return { file, problems: [{ field: null, message: `does not exist${tree.label}` }] };
|
|
91
|
+
const { parsePoints, PointsError } = await import("./points");
|
|
92
|
+
try {
|
|
93
|
+
parsePoints(tree.read(file), file);
|
|
94
|
+
return { file, problems: [] };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof PointsError) return { file, problems: err.problems };
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -940,7 +940,10 @@
|
|
|
940
940
|
"work-implements-undecided",
|
|
941
941
|
"work-done-unpinned",
|
|
942
942
|
"work-closed-without-date",
|
|
943
|
-
"work-done-gap-open"
|
|
943
|
+
"work-done-gap-open",
|
|
944
|
+
"answer-points-unreadable",
|
|
945
|
+
"answer-point-unknown",
|
|
946
|
+
"answer-point-changed"
|
|
944
947
|
]
|
|
945
948
|
},
|
|
946
949
|
"message": {
|