@openwop/openwop-conformance 1.25.0 → 1.28.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/CHANGELOG.md +34 -0
- package/README.md +2 -2
- package/api/openapi.yaml +215 -0
- package/coverage.md +11 -0
- package/package.json +1 -1
- package/schemas/README.md +5 -0
- package/schemas/capabilities.schema.json +49 -0
- package/schemas/envelopes/ui.a2ui-surface.schema.json +154 -0
- package/schemas/localized-content-language-settings.schema.json +26 -0
- package/schemas/localized-content-page-response.schema.json +60 -0
- package/schemas/localized-content-page.schema.json +62 -0
- package/schemas/localized-content-section.schema.json +51 -0
- package/schemas/suspend-request.schema.json +20 -0
- package/src/scenarios/a2ui-surface-degrades.test.ts +54 -0
- package/src/scenarios/a2ui-surface-replay.test.ts +84 -0
- package/src/scenarios/a2ui-surface-shape.test.ts +162 -0
- package/src/scenarios/a2ui-surface-version-refusal.test.ts +77 -0
- package/src/scenarios/a2ui-untrusted-blocks-approval.test.ts +68 -0
- package/src/scenarios/i18n-negotiation.test.ts +30 -3
- package/src/scenarios/interrupt-approver-routing.test.ts +166 -0
- package/src/scenarios/localized-content-delivery.test.ts +221 -0
- package/src/scenarios/spec-corpus-validity.test.ts +1 -0
|
@@ -2052,6 +2052,32 @@
|
|
|
2052
2052
|
},
|
|
2053
2053
|
"additionalProperties": false
|
|
2054
2054
|
},
|
|
2055
|
+
"content": {
|
|
2056
|
+
"type": "object",
|
|
2057
|
+
"description": "RFC 0103 (spec/v1/localized-content.md). Localized authored content (pages → sections) advertisement. Reuses the i18n annex's Accept-Language/Content-Language negotiation; it does NOT redeclare negotiation. Requires `i18n.supported: true`. `baseLocale` MUST equal `capabilities.i18n.defaultLocale`; `({baseLocale} ∪ supportedLocales)` MUST be a subset of `capabilities.i18n.supportedLocales`; `baseLocale` MUST NOT appear in `supportedLocales`. Hosts that omit this block serve no content surface; the conformance scenarios skip cleanly.",
|
|
2058
|
+
"additionalProperties": false,
|
|
2059
|
+
"required": ["supported", "baseLocale", "supportedLocales"],
|
|
2060
|
+
"properties": {
|
|
2061
|
+
"supported": {
|
|
2062
|
+
"type": "boolean",
|
|
2063
|
+
"description": "Host serves the localized-content surface (`GET /v1/content/pages/{slug}` + tenant-scoped admin CRUD). Requires `i18n.supported: true`."
|
|
2064
|
+
},
|
|
2065
|
+
"baseLocale": {
|
|
2066
|
+
"type": "string",
|
|
2067
|
+
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8}){0,3}$",
|
|
2068
|
+
"description": "The locale section `data` is authored in. MUST equal `capabilities.i18n.defaultLocale`."
|
|
2069
|
+
},
|
|
2070
|
+
"supportedLocales": {
|
|
2071
|
+
"type": "array",
|
|
2072
|
+
"uniqueItems": true,
|
|
2073
|
+
"items": {
|
|
2074
|
+
"type": "string",
|
|
2075
|
+
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8}){0,3}$"
|
|
2076
|
+
},
|
|
2077
|
+
"description": "BCP 47 tags the host has authored content translations for. MUST NOT contain `baseLocale`; `({baseLocale} ∪ supportedLocales)` MUST be a subset of `i18n.supportedLocales`."
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2055
2081
|
"portability": {
|
|
2056
2082
|
"type": "object",
|
|
2057
2083
|
"description": "RFC 0098 (`Active`). Export/import of a tenant's reusable estate (agents 0070, packs 0003/0013, prompt templates 0027, connection *refs* 0045/0095, schedules 0052, roster/org-chart 0086/0087). An export bundle carries NO credential values — only refs to be re-bound at the destination (RFC 0046/0079). Import maps the estate onto the destination's RFC 0048 identity, MUST offer a no-write dry-run plan, MUST be idempotent, and is gated by an RFC 0049 scope. A host advertising this serves `/v1/host/sample/{export,import}` (promotable to `/v1/{export,import}`) and emits the content-free `import.applied` event. Hosts that omit this block neither export nor import; the conformance scenarios skip cleanly.",
|
|
@@ -2081,6 +2107,29 @@
|
|
|
2081
2107
|
},
|
|
2082
2108
|
"if": { "properties": { "import": { "const": true } }, "required": ["import"] },
|
|
2083
2109
|
"then": { "properties": { "dryRun": { "const": true } }, "required": ["dryRun"] }
|
|
2110
|
+
},
|
|
2111
|
+
"interrupt": {
|
|
2112
|
+
"type": "object",
|
|
2113
|
+
"additionalProperties": false,
|
|
2114
|
+
"description": "RFC 0104 (`Active`). Interrupt-surface capabilities. Currently advertises portable HITL approver routing on the `kind: \"approval\"` InterruptPayload.",
|
|
2115
|
+
"properties": {
|
|
2116
|
+
"approverRouting": {
|
|
2117
|
+
"type": "object",
|
|
2118
|
+
"additionalProperties": false,
|
|
2119
|
+
"required": ["supported"],
|
|
2120
|
+
"description": "RFC 0104. When `supported: true`, the host surfaces the OPTIONAL, ADVISORY `approverGroupRefs` / `approverRoleRefs` / `audience` fields on the approval InterruptPayload unchanged, resolves the ref kinds it advertises against its own identity/RBAC, ENFORCES eligibility at resolve time (the refs stay advisory metadata for clients; enforcement is host-side), and SHOULD route notifications to the resolved union. Refs are opaque to the engine and snapshotted at decision time for deterministic replay. Hosts that omit this block (or set `supported: false`) ignore the fields and remain conformant. Advertise only what the host actually resolves (`refKinds`) and honors (`audience`).",
|
|
2121
|
+
"properties": {
|
|
2122
|
+
"supported": { "type": "boolean", "description": "Host honors the RFC 0104 approver-routing fields." },
|
|
2123
|
+
"refKinds": {
|
|
2124
|
+
"type": "array",
|
|
2125
|
+
"items": { "type": "string", "enum": ["group", "role"] },
|
|
2126
|
+
"uniqueItems": true,
|
|
2127
|
+
"description": "Which approver ref kinds the host actually resolves. `group` ⇒ honors `approverGroupRefs`; `role` ⇒ honors `approverRoleRefs`. Absent ⇒ the host resolves neither (advisory-only passthrough). Capability honesty: advertise only the kinds the host's resolver supports."
|
|
2128
|
+
},
|
|
2129
|
+
"audience": { "type": "boolean", "default": false, "description": "Host honors the `audience` notification-targeting override. Absent/`false` ⇒ the host notifies the resolved eligible union and ignores `audience`." }
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2084
2133
|
}
|
|
2085
2134
|
},
|
|
2086
2135
|
"additionalProperties": true
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/envelopes/ui.a2ui-surface.schema.json",
|
|
4
|
+
"title": "UiA2uiSurfacePayload",
|
|
5
|
+
"description": "Payload for the OPTIONAL, advertised `ui.a2ui-surface` AI Envelope kind (RFC 0102). Carries a declarative A2UI interface surface — a closed component tree the consumer renders with native widgets, routing user actions back to the producing agent WITHOUT executing any agent-supplied code. `ui.*` is a core, un-namespaced content-primitive family beside `media.*` (ai-envelope.md §\"A2UI surfaces\" / §\"Vendor-namespaced kinds\"). Not one of the four MUST-recognize universal kinds; an unrecognizing consumer falls back to store-without-render.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["catalogVersion", "surface"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"reasoning": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "OPTIONAL per RFC 0030 §A — the model's reasoning, conventionally the first property of a kind whose payload benefits from multi-step composition."
|
|
13
|
+
},
|
|
14
|
+
"catalogVersion": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["0.9.1"],
|
|
17
|
+
"description": "REQUIRED — the A2UI catalog version the surface targets. This enum IS the host's supported-version set (ai-envelope.md §\"A2UI surfaces\"); it is never a free string the producer invents. A consumer MUST refuse an unknown/higher version with `unknown_schema_version`. Pinning the version in the payload keeps `:fork`/replay deterministic after the external A2UI standard ships a breaking version."
|
|
18
|
+
},
|
|
19
|
+
"surface": {
|
|
20
|
+
"$ref": "#/$defs/surface",
|
|
21
|
+
"description": "REQUIRED — the A2UI surface document: a closed component tree (ai-envelope.md §\"A2UI surfaces\"). Self-contained: renderable from the payload alone, never a live reference into an external catalog."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"surface": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["components"],
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"properties": {
|
|
30
|
+
"title": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "OPTIONAL display title for the surface."
|
|
33
|
+
},
|
|
34
|
+
"components": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"description": "The flat, ordered component list. Each element is one of the host's day-1 catalog components, discriminated by the single-string-enum `component` field. `anyOf` (not `oneOf`) per ai-envelope.md §\"Schema discipline\".",
|
|
37
|
+
"items": { "$ref": "#/$defs/component" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"component": {
|
|
42
|
+
"description": "A single A2UI component. Closed set (day-1 catalog 0.9.1); the `component` discriminator is a single-string-enum per branch.",
|
|
43
|
+
"anyOf": [
|
|
44
|
+
{ "$ref": "#/$defs/heading" },
|
|
45
|
+
{ "$ref": "#/$defs/text" },
|
|
46
|
+
{ "$ref": "#/$defs/fieldText" },
|
|
47
|
+
{ "$ref": "#/$defs/fieldDate" },
|
|
48
|
+
{ "$ref": "#/$defs/fieldSelect" },
|
|
49
|
+
{ "$ref": "#/$defs/fieldCheckbox" },
|
|
50
|
+
{ "$ref": "#/$defs/actionButton" }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"heading": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"required": ["component", "text"],
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"properties": {
|
|
58
|
+
"component": { "type": "string", "enum": ["heading"] },
|
|
59
|
+
"text": { "type": "string" },
|
|
60
|
+
"level": { "type": "integer", "minimum": 1, "maximum": 6 }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"text": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["component", "text"],
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"component": { "type": "string", "enum": ["text"] },
|
|
69
|
+
"text": { "type": "string" }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"fieldText": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"required": ["component", "id", "label"],
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"component": { "type": "string", "enum": ["field.text"] },
|
|
78
|
+
"id": { "type": "string", "description": "Binding key; the collected value is keyed by `id` in the resume value." },
|
|
79
|
+
"label": { "type": "string" },
|
|
80
|
+
"placeholder": { "type": "string" },
|
|
81
|
+
"required": { "type": "boolean" }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"fieldDate": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"required": ["component", "id", "label"],
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": {
|
|
89
|
+
"component": { "type": "string", "enum": ["field.date"] },
|
|
90
|
+
"id": { "type": "string" },
|
|
91
|
+
"label": { "type": "string" },
|
|
92
|
+
"required": { "type": "boolean" }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"fieldSelect": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"required": ["component", "id", "label", "options"],
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"properties": {
|
|
100
|
+
"component": { "type": "string", "enum": ["field.select"] },
|
|
101
|
+
"id": { "type": "string" },
|
|
102
|
+
"label": { "type": "string" },
|
|
103
|
+
"required": { "type": "boolean" },
|
|
104
|
+
"options": {
|
|
105
|
+
"type": "array",
|
|
106
|
+
"minItems": 1,
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"required": ["value", "label"],
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"properties": {
|
|
112
|
+
"value": { "type": "string" },
|
|
113
|
+
"label": { "type": "string" }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"fieldCheckbox": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"required": ["component", "id", "label"],
|
|
122
|
+
"additionalProperties": false,
|
|
123
|
+
"properties": {
|
|
124
|
+
"component": { "type": "string", "enum": ["field.checkbox"] },
|
|
125
|
+
"id": { "type": "string" },
|
|
126
|
+
"label": { "type": "string" },
|
|
127
|
+
"default": { "type": "boolean" }
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"actionButton": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"required": ["component", "id", "label", "action"],
|
|
133
|
+
"additionalProperties": false,
|
|
134
|
+
"properties": {
|
|
135
|
+
"component": { "type": "string", "enum": ["action.button"] },
|
|
136
|
+
"id": { "type": "string" },
|
|
137
|
+
"label": { "type": "string" },
|
|
138
|
+
"action": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"required": ["target"],
|
|
141
|
+
"additionalProperties": false,
|
|
142
|
+
"description": "Confined action (ai-envelope.md §\"A2UI surfaces\" rule 2). `target` resolves to exactly one host-allowlisted destination — a run interrupt resume or a conversation exchange — never an arbitrary URL, endpoint, or network egress. Enforced by the `a2ui-action-confinement` + `a2ui-surface-no-network-egress` SECURITY invariants.",
|
|
143
|
+
"properties": {
|
|
144
|
+
"target": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"enum": ["resume", "exchange"],
|
|
147
|
+
"description": "`resume` → the collected field values become the interrupt `resumeValue` (interrupt.md). `exchange` → the values become a conversation exchange message (RFC 0005)."
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/localized-content-language-settings.schema.json",
|
|
4
|
+
"title": "LocalizedContentLanguageSettings",
|
|
5
|
+
"description": "RFC 0103 (spec/v1/localized-content.md §B). Per-tenant authoring configuration for the content surface. Invariant: `baseLocale` MUST NOT appear in `supportedLocales` (the base locale is carried by section `data`, supported locales by `localizations`). This object is the source of truth for the host's advertised `content` capability block; the advertisement MUST reflect it (`baseLocale` equal, advertised `supportedLocales` equal or a subset).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["baseLocale", "supportedLocales", "autoTranslateOnPublish"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"baseLocale": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$",
|
|
13
|
+
"description": "The locale section `data` is authored in. MUST equal `capabilities.i18n.defaultLocale`."
|
|
14
|
+
},
|
|
15
|
+
"supportedLocales": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"description": "BCP-47 tags the tenant authors content translations for. MUST NOT contain `baseLocale`.",
|
|
18
|
+
"items": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
|
19
|
+
"uniqueItems": true
|
|
20
|
+
},
|
|
21
|
+
"autoTranslateOnPublish": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"description": "Whether the host auto-translates missing locale fields on publish (host-extension behavior; informational at the protocol layer)."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/localized-content-page-response.schema.json",
|
|
4
|
+
"title": "LocalizedContentPageResponse",
|
|
5
|
+
"description": "RFC 0103 (spec/v1/localized-content.md §D). The resolved delivery response for `GET /v1/content/pages/{slug}`. `locale` is the negotiated locale (equals the `Content-Language` response header). `sections[]` carry the ALREADY-MERGED section bodies — resolution (the per-section field merge §C) happened server-side, so no `localizations` map appears in the response. Only `published`, `enabled` sections are present, in render order.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "generatedAt", "locale", "slug", "page", "sections"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Response schema version marker (e.g. `\"1\"`)."
|
|
13
|
+
},
|
|
14
|
+
"generatedAt": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "date-time",
|
|
17
|
+
"description": "When the resolved response was generated."
|
|
18
|
+
},
|
|
19
|
+
"locale": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8}){0,3}$",
|
|
22
|
+
"description": "The negotiated locale actually used (BCP-47). Equals the `Content-Language` response header."
|
|
23
|
+
},
|
|
24
|
+
"slug": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
27
|
+
"description": "The requested page slug."
|
|
28
|
+
},
|
|
29
|
+
"page": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["pageId", "slug", "name"],
|
|
33
|
+
"description": "Resolved page metadata (status omitted — only published pages are delivered).",
|
|
34
|
+
"properties": {
|
|
35
|
+
"pageId": { "type": "string", "minLength": 1 },
|
|
36
|
+
"slug": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
37
|
+
"name": { "type": "string", "minLength": 1 },
|
|
38
|
+
"seo": { "type": "object", "additionalProperties": true }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"sections": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"description": "Resolved sections in render order; each body is the merged result, not the raw record.",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"required": ["sectionId", "sectionType", "data"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"sectionId": { "type": "string", "minLength": 1 },
|
|
50
|
+
"sectionType": { "type": "string", "minLength": 1 },
|
|
51
|
+
"data": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": true,
|
|
54
|
+
"description": "The merged section body for the negotiated locale (exact → family → base shallow overlay)."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/localized-content-page.schema.json",
|
|
4
|
+
"title": "LocalizedContentPage",
|
|
5
|
+
"description": "RFC 0103 (spec/v1/localized-content.md §B). An authored content page: an ordered composition of sections addressed by a URL `slug`. Publication status is atomic across locales for v1. SEO carries hreflang + og:locale alternates so a renderer can emit per-locale link relations.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["pageId", "slug", "name", "status", "sectionOrder"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"pageId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Stable identifier of the page within its tenant."
|
|
14
|
+
},
|
|
15
|
+
"slug": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
18
|
+
"description": "URL slug for public delivery (`GET /v1/content/pages/{slug}`). Lowercase, hyphen-separated."
|
|
19
|
+
},
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Human-facing page name (admin/authoring label)."
|
|
24
|
+
},
|
|
25
|
+
"status": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["draft", "published"],
|
|
28
|
+
"description": "Publication status, atomic across locales for v1. A `draft` page MUST NOT be served on the public delivery path."
|
|
29
|
+
},
|
|
30
|
+
"sectionOrder": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "type": "string", "minLength": 1 },
|
|
33
|
+
"description": "Ordered list of `sectionId`s defining the page's render order."
|
|
34
|
+
},
|
|
35
|
+
"seo": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"description": "Optional SEO metadata for per-locale link relations.",
|
|
39
|
+
"properties": {
|
|
40
|
+
"hreflang": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"description": "hreflang alternates: per-locale canonical URLs.",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["locale", "href"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"locale": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
|
49
|
+
"href": { "type": "string", "format": "uri" }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"ogLocaleAlternates": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"description": "og:locale:alternate values.",
|
|
56
|
+
"items": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
|
57
|
+
"uniqueItems": true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/localized-content-section.schema.json",
|
|
4
|
+
"title": "LocalizedContentSection",
|
|
5
|
+
"description": "RFC 0103 (spec/v1/localized-content.md §B). One section record of an authored content page. A section is ONE record carrying a base `data` payload (authored in the host's `content.baseLocale`) plus a sparse `localizations` map of partial per-locale field overrides — never one record per locale. The negotiated locale's resolved body is computed by the normative per-section field merge (exact-locale → language-family → base `data`, shallow overlay; §C). Body field shapes inside `data`/`localizations` are open and host/section-type-defined; this schema closes the envelope, not the body.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["sectionId", "sectionType", "data", "localizations", "status", "enabled", "order"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"sectionId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Stable identifier of the section within its page."
|
|
14
|
+
},
|
|
15
|
+
"sectionType": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "The section-type discriminator (e.g. `hero`, `features`). The host/section-type defines the body field shape; the protocol does not."
|
|
19
|
+
},
|
|
20
|
+
"data": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": true,
|
|
23
|
+
"description": "The base/default-locale fields, authored in `content.baseLocale`. Open object — body shapes are the host/section-type's concern."
|
|
24
|
+
},
|
|
25
|
+
"localizations": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "Sparse map of per-locale partial field overrides. MAY be empty (`{}`). Each key is a BCP-47 tag matching `^[a-z]{2}(-[A-Z]{2})?$` and MUST NOT equal `content.baseLocale` (the base locale is carried by `data`). Each value is a partial overlay of `data` fields; missing fields fall through to `data` (per-section merge §C).",
|
|
28
|
+
"propertyNames": {
|
|
29
|
+
"pattern": "^[a-z]{2}(-[A-Z]{2})?$"
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": true,
|
|
34
|
+
"description": "Partial field overrides for one locale; shallow-overlaid onto `data`."
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"status": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["draft", "published"],
|
|
40
|
+
"description": "Publication status. Atomic across all locales for v1 (no per-locale publish). The public delivery path serves `published` only."
|
|
41
|
+
},
|
|
42
|
+
"enabled": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"description": "Whether the section participates in delivery when its page is rendered."
|
|
45
|
+
},
|
|
46
|
+
"order": {
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"description": "Render order of the section within its page (lower first). `sectionOrder` on the page is authoritative; `order` is the per-section tiebreak/sort hint."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -81,6 +81,26 @@
|
|
|
81
81
|
"type": "boolean",
|
|
82
82
|
"default": false,
|
|
83
83
|
"description": "RFC 0093 §D2 (pins RFC 0051 UQ 2). When `true`, a configured override principal (per `interrupt-profiles.md` §approval-gate `override.requiredRole`) MAY bypass the quorum (`requiredApprovals`) and release the gate alone. Default `false`: an override principal's approval counts as ONE quorum vote; quorum still applies. Optional, additive."
|
|
84
|
+
},
|
|
85
|
+
"approverGroupRefs": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string" },
|
|
88
|
+
"description": "RFC 0104 — OPTIONAL, ADVISORY. Opaque group refs whose resolved members are eligible to approve. Like `approversList`, advisory: the host resolves refs against its own identity/RBAC and ENFORCES eligibility at resolve time; the engine treats refs as opaque (no membership resolution in the engine). Capability-gated via `interrupt.approverRouting` (`refKinds` MUST include `group`). Hosts that don't advertise it ignore this field. Membership is resolved at decision time and snapshotted for deterministic replay."
|
|
89
|
+
},
|
|
90
|
+
"approverRoleRefs": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "string" },
|
|
93
|
+
"description": "RFC 0104 — OPTIONAL, ADVISORY. Opaque role refs whose holders are eligible to approve. Same advisory + host-resolved + opaque-to-engine + decision-time-snapshot semantics as `approverGroupRefs`. Capability-gated via `interrupt.approverRouting` (`refKinds` MUST include `role`)."
|
|
94
|
+
},
|
|
95
|
+
"audience": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"additionalProperties": false,
|
|
98
|
+
"description": "RFC 0104 — OPTIONAL, ADVISORY notification-targeting hint: who should be TOLD about this gate, distinct from who MAY approve (the eligibility refs). When OMITTED, the host SHOULD notify the resolved union of the eligibility refs (`approversList` ∪ `approverGroupRefs` ∪ `approverRoleRefs`). When PRESENT, it OVERRIDES that default. Advisory; the host remains the routing authority. Capability-gated via `interrupt.approverRouting.audience`.",
|
|
99
|
+
"properties": {
|
|
100
|
+
"subjects": { "type": "array", "items": { "type": "string" }, "description": "Opaque subject refs to notify." },
|
|
101
|
+
"groups": { "type": "array", "items": { "type": "string" }, "description": "Opaque group refs to notify." },
|
|
102
|
+
"roles": { "type": "array", "items": { "type": "string" }, "description": "Opaque role refs to notify." }
|
|
103
|
+
}
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a2ui-surface-degrades — RFC 0102 §A point: graceful degradation.
|
|
3
|
+
*
|
|
4
|
+
* A consumer that does NOT advertise `ui.a2ui-surface` and receives one
|
|
5
|
+
* MUST fall back to store-without-render and MUST NOT fail the run (N6;
|
|
6
|
+
* `ui.a2ui-surface` is an OPTIONAL advertised kind, not a MUST-recognize
|
|
7
|
+
* universal kind — precedent `artifact-type-store-without-render`).
|
|
8
|
+
*
|
|
9
|
+
* Always-on (server-free): `ui.a2ui-surface` is NOT one of the four
|
|
10
|
+
* universal kinds, so a non-advertising consumer is entitled to degrade.
|
|
11
|
+
* Capability-gated (HTTP): posting the kind to a host that does not list it
|
|
12
|
+
* in `supportedEnvelopes` is gated (run not failed), not accepted.
|
|
13
|
+
*
|
|
14
|
+
* @see RFCS/0102-a2ui-agent-authored-interface-surfaces.md §A
|
|
15
|
+
* @see spec/v1/ai-envelope.md §"A2UI surfaces"
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { driver } from '../lib/driver.js';
|
|
20
|
+
|
|
21
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
22
|
+
const UNIVERSAL_KINDS = ['clarification.request', 'schema.request', 'schema.response', 'error'];
|
|
23
|
+
|
|
24
|
+
describe('a2ui-surface-degrades: optional-advertised, not universal (RFC 0102 §A)', () => {
|
|
25
|
+
it('ui.a2ui-surface is NOT a MUST-recognize universal kind', () => {
|
|
26
|
+
expect(
|
|
27
|
+
UNIVERSAL_KINDS.includes('ui.a2ui-surface'),
|
|
28
|
+
'ai-envelope.md §"A2UI surfaces": ui.a2ui-surface MUST be optional/advertised so an unrecognizing consumer may store-without-render',
|
|
29
|
+
).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe.skipIf(HTTP_SKIP)('a2ui-surface-degrades: unadvertised kind is gated, run survives (RFC 0102 §A)', () => {
|
|
34
|
+
it('posting ui.a2ui-surface to a host that does not advertise it → gated (not failed)', async () => {
|
|
35
|
+
const res = await driver.post('/v1/host/sample/envelope/accept', {
|
|
36
|
+
envelope: {
|
|
37
|
+
type: 'ui.a2ui-surface',
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
envelopeId: 'env-a2ui-degrade-1',
|
|
40
|
+
correlationId: 'run-a2ui:node-1:turn-0:deg',
|
|
41
|
+
payload: { catalogVersion: '0.9.1', surface: { components: [] } },
|
|
42
|
+
meta: { source: 'ai-generation', ts: '2026-06-15T10:00:00Z' },
|
|
43
|
+
},
|
|
44
|
+
hostSupportedEnvelopes: ['clarification.request'], // does not advertise ui.a2ui-surface
|
|
45
|
+
});
|
|
46
|
+
if (res.status === 404) return; // seam absent — soft-skip
|
|
47
|
+
expect(res.status).toBe(200);
|
|
48
|
+
const body = res.json as { status?: string };
|
|
49
|
+
expect(
|
|
50
|
+
body.status,
|
|
51
|
+
driver.describe('RFC 0102 §A (N6)', 'an unadvertised ui.a2ui-surface MUST be gated, never crash the run'),
|
|
52
|
+
).toBe('gated');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a2ui-surface-replay — RFC 0102 §A.6: replay determinism by correlationId.
|
|
3
|
+
*
|
|
4
|
+
* The surface envelope replays by `correlationId` (ai-envelope.md §"Replay
|
|
5
|
+
* determinism"); on recovery/`:fork` the cached outcome is returned and the
|
|
6
|
+
* surface is NEVER regenerated. Re-emission with the same correlationId but a
|
|
7
|
+
* divergent `type` refuses with `envelope_correlation_conflict`. Durable
|
|
8
|
+
* state is exactly `(surface envelope, submitted resume value)`.
|
|
9
|
+
*
|
|
10
|
+
* Always-on (server-free): the surface payload is self-contained (no external
|
|
11
|
+
* `$ref`), the precondition that makes a stored surface replay deterministically
|
|
12
|
+
* after the external A2UI catalog ships a breaking version.
|
|
13
|
+
* Capability-gated (HTTP): same correlationId + different type → conflict.
|
|
14
|
+
*
|
|
15
|
+
* @see RFCS/0102-a2ui-agent-authored-interface-surfaces.md §A.6
|
|
16
|
+
* @see spec/v1/ai-envelope.md §"Replay determinism"
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
24
|
+
|
|
25
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
26
|
+
const schema = JSON.parse(
|
|
27
|
+
readFileSync(join(SCHEMAS_DIR, 'envelopes/ui.a2ui-surface.schema.json'), 'utf8'),
|
|
28
|
+
) as Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
function refStrings(node: unknown, out: string[]): void {
|
|
31
|
+
if (!node || typeof node !== 'object') return;
|
|
32
|
+
if (Array.isArray(node)) {
|
|
33
|
+
node.forEach((n) => refStrings(n, out));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const obj = node as Record<string, unknown>;
|
|
37
|
+
if (typeof obj['$ref'] === 'string') out.push(obj['$ref'] as string);
|
|
38
|
+
Object.values(obj).forEach((v) => refStrings(v, out));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('a2ui-surface-replay: self-contained surface (RFC 0102 §A.6)', () => {
|
|
42
|
+
it('all $refs are internal (#/...) — surface renders from the payload alone on replay', () => {
|
|
43
|
+
const refs: string[] = [];
|
|
44
|
+
refStrings(schema, refs);
|
|
45
|
+
const external = refs.filter((r) => !r.startsWith('#'));
|
|
46
|
+
expect(
|
|
47
|
+
external,
|
|
48
|
+
'RFC 0102 §A.6: a stored surface MUST be self-contained for deterministic :fork/replay (no live external-catalog ref)',
|
|
49
|
+
).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe.skipIf(HTTP_SKIP)('a2ui-surface-replay: correlationId conflict on type divergence (RFC 0102 §A.6)', () => {
|
|
54
|
+
const base = {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
correlationId: 'run-a2ui:node-1:turn-0:rep',
|
|
57
|
+
payload: { catalogVersion: '0.9.1', surface: { components: [] } },
|
|
58
|
+
meta: { source: 'ai-generation', ts: '2026-06-15T10:00:00Z' },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
it('re-emission with same correlationId + different type → envelope_correlation_conflict', async () => {
|
|
62
|
+
const first = await driver.post('/v1/host/sample/envelope/accept', {
|
|
63
|
+
envelope: { ...base, type: 'ui.a2ui-surface', envelopeId: 'env-a2ui-rep-1' },
|
|
64
|
+
hostSupportedEnvelopes: ['ui.a2ui-surface', 'clarification.request'],
|
|
65
|
+
});
|
|
66
|
+
if (first.status === 404) return; // seam absent — soft-skip
|
|
67
|
+
|
|
68
|
+
const conflict = await driver.post('/v1/host/sample/envelope/accept', {
|
|
69
|
+
envelope: {
|
|
70
|
+
...base,
|
|
71
|
+
type: 'clarification.request',
|
|
72
|
+
envelopeId: 'env-a2ui-rep-2',
|
|
73
|
+
payload: { questions: [{ id: 'q', question: 'x' }] },
|
|
74
|
+
},
|
|
75
|
+
hostSupportedEnvelopes: ['ui.a2ui-surface', 'clarification.request'],
|
|
76
|
+
});
|
|
77
|
+
if (conflict.status === 404) return;
|
|
78
|
+
const body = conflict.json as { status?: string; reason?: string };
|
|
79
|
+
expect(
|
|
80
|
+
body.reason ?? '',
|
|
81
|
+
driver.describe('ai-envelope.md §"Replay determinism"', 'same correlationId + divergent type MUST conflict'),
|
|
82
|
+
).toContain('envelope_correlation_conflict');
|
|
83
|
+
});
|
|
84
|
+
});
|