@openwop/openwop-conformance 1.68.0 → 1.68.2

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": "@openwop/openwop-conformance",
3
- "version": "1.68.0",
3
+ "version": "1.68.2",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1773,6 +1773,66 @@
1773
1773
  }
1774
1774
  }
1775
1775
  },
1776
+ "artifactTypes": {
1777
+ "type": "object",
1778
+ "description": "RFC 0071, amended by RFC 0075; declared by RFC 0144. Host artifact-type surface — an advertisement (no `ctx.artifactTypes.*` method) that changes how the host treats the artifact references already on the wire (`nodes[].artifact.typeId`, `WorkflowNode.artifactType`, `artifact.created.artifactType`). Carries the corpus's only emission MUST: a host advertising `store: true` MUST persist registered artifacts and emit `artifact.created`. See `host-capabilities.md` §host.artifactTypes and `artifact-type-packs.md` §\"Host capability\". The discovery key is the PLAIN family name at the document root (RFC 0137 G16); `host.artifactTypes` is the capability IDENTIFIER used in pack `peerDependencies`, not this key.",
1779
+ "additionalProperties": false,
1780
+ "properties": {
1781
+ "supported": { "type": "boolean", "description": "Host honors registered artifact types — validates an artifact whose type is registered (pack-installed or host-native) against that type's schema before emitting `artifact.created`, setting `registered: true` + `registrationSource`. Unregistered types stay accepted unvalidated with `registered: false`." },
1782
+ "store": { "type": "boolean", "description": "`true` ⇒ the host persists artifacts of registered types AND emits `artifact.created`. Advertising this is a commitment to emit; RFC 0142 leg B is the witness." },
1783
+ "render": { "type": "boolean", "description": "Advisory — the spec defines no rendering surface. A host advertising `false` for a type it can `store` MUST still accept and store the artifact and MUST NOT fail the run for lack of a renderer (the store-without-render negotiation guarantee)." },
1784
+ "export": { "type": "array", "items": { "type": "string" }, "description": "Export-format identifiers the host can materialize, drawn from declared `exportFormats`. Advisory." },
1785
+ "types": {
1786
+ "type": "object",
1787
+ "description": "RFC 0075 per-type facets, keyed by `artifactTypeId`. Each entry overrides the global object for that type; the global object is the fallback for any type not listed (absent ⇒ host-global semantics; any facet absent ⇒ the global default — additive).",
1788
+ "additionalProperties": {
1789
+ "type": "object",
1790
+ "additionalProperties": false,
1791
+ "properties": {
1792
+ "validated": { "type": "boolean", "description": "The runtime validation guarantee — `true` ⇒ the host validates this type before emit, so emits `registered: true`. Decoupled from `schemaVersions`, which is a version DECLARATION only." },
1793
+ "validation": { "type": "string", "enum": ["open", "closed"], "description": "Mirrors `ArtifactType.validation`, surfacing schema strictness in discovery so a consumer needn't fetch the schema. Default `\"open\"` per `COMPATIBILITY.md` §2.1." },
1794
+ "schemaVersion": { "type": "integer", "minimum": 0 },
1795
+ "store": { "type": "boolean" },
1796
+ "render": { "type": "boolean" },
1797
+ "export": { "type": "array", "items": { "type": "string" } }
1798
+ }
1799
+ }
1800
+ }
1801
+ }
1802
+ },
1803
+ "forms": {
1804
+ "type": "object",
1805
+ "description": "RFC 0137; declared by RFC 0144. Host form-content-pack surface — like `artifactTypes` it adds no `ctx.forms.*` method; it advertises that the host resolves registered form templates from installed `kind: \"form-content\"` packs and instantiates them through its own normal create path. See `host-capabilities.md` §host.forms and `form-content-packs.md`. The discovery key is the PLAIN family name at the document root (RFC 0137 G16); a host may additionally carry a deprecated dotted `\"host.forms\"` mirror during migration — root `additionalProperties` stays `true` so that document remains valid.",
1806
+ "additionalProperties": false,
1807
+ "properties": {
1808
+ "contentPacks": { "type": "boolean", "description": "`true` ⇒ the host instantiates registered form templates. Normative consequences (`form-content-packs.md` §\"Instantiation\"): create through the SAME path a hand-authored form uses; degrade an unrecognized field type to a plain text input rather than failing; keep the instantiated form fully editable; execute nothing from the pack; sanitize/validate/authorize submissions exactly as hand-typed input. Pack-authored strings are UNTRUSTED and MUST propagate `meta.contentTrust: \"untrusted\"` into interpolated prompt segments (invariant `form-content-pack-string-trust-boundary`) — a signature proves authorship, not content safety." }
1809
+ }
1810
+ },
1811
+ "aiEnvelope": {
1812
+ "type": "object",
1813
+ "description": "Declared by RFC 0144. Host typed-envelope generation surface (`ctx.aiEnvelope.generate`), routing the call through the host's BYOK provider layer. Opinionated about envelope shape, unlike the lower-level `aiProviders` capability which returns raw model output. See `host-capabilities.md` §host.aiEnvelope and `ai-envelope.md`.",
1814
+ "additionalProperties": false,
1815
+ "properties": {
1816
+ "supported": { "type": "boolean", "description": "`true` ⇒ `ctx.aiEnvelope.generate` is exposed. Absent/`false` ⇒ packs binding this surface fail with `host_capability_missing`." },
1817
+ "await": { "type": "boolean", "description": "`true` ⇒ `ctx.aiEnvelope.await` is ALSO exposed. `generate` is required whenever the family is advertised; `await` is required only when this sub-flag is." }
1818
+ }
1819
+ },
1820
+ "promptLibrary": {
1821
+ "type": "object",
1822
+ "description": "Declared by RFC 0144. Host prompt-lookup surface (`ctx.promptLibrary.get`), returning a prompt pinned to a specific version so replay stays deterministic. Consumed by `core.ai.callPrompt`. See `host-capabilities.md` §host.promptLibrary.",
1823
+ "additionalProperties": false,
1824
+ "properties": {
1825
+ "supported": { "type": "boolean", "description": "`true` ⇒ `ctx.promptLibrary.get(promptId)` resolves a `{ promptId, systemPrompt, version, envelopeType? }` record. The returned `version` MUST pin the prompt text for replay." }
1826
+ }
1827
+ },
1828
+ "agentRuntime": {
1829
+ "type": "object",
1830
+ "description": "Declared by RFC 0144. The heavyweight swarm/consensus SUPERSET of the agent surface — spawn, delegate, consensus, message-send, skill-invoke, swarm-execute over RFC 0002/0003/0007 primitives. Advertising it IMPLIES `agents.manifestRuntime` (RFC 0070 §B), since `spawn({ manifestId })` instantiates a manifest agent; hosts needing only single-agent or crew dispatch advertise that floor and omit this family. Per-tool authorization, rate limiting, and the content-free tool-call audit trail live in the top-level `toolHooks` block, NOT under this family. See `host-capabilities.md` §host.agentRuntime.",
1831
+ "additionalProperties": false,
1832
+ "properties": {
1833
+ "supported": { "type": "boolean", "description": "`true` ⇒ `ctx.agentRuntime.*` is exposed AND `agents.manifestRuntime` is implied (RFC 0070 §B). A multi-tenant host additionally advertises `agents.manifestRuntime.installScope: 'tenant'` (RFC 0074) so `GET /v1/agents` scopes to the caller's owner triple." }
1834
+ }
1835
+ },
1776
1836
  "deadLetter": {
1777
1837
  "type": "object",
1778
1838
  "description": "RFC 0053 (`Draft`). Run-level dead-letter sink for terminally-failed runs/nodes. On retry exhaustion (RFC 0009), the run is routed to a durable, inspectable sink and a `run.dead_lettered` event is emitted; dead-lettered runs remain fork-eligible (RFC 0011) for the retention window. Distinct from `queueBus.deadLetterSupported`, which dead-letters transport *messages*, not *runs*.",
@@ -135,3 +135,26 @@ export function signPayload(
135
135
  algorithmHeader: 'v1',
136
136
  };
137
137
  }
138
+
139
+ /**
140
+ * Discover a tenant the calling bearer provably OWNS, by creating a probe run
141
+ * and reading `owner.tenantId` off its snapshot (RFC 0048). A host that scopes
142
+ * webhook subscriptions by tenant membership (RFC 0093) 403s a `tenantId` the
143
+ * bearer is not a member of — so a hard-coded tenant is wrong; the only portable
144
+ * owned tenant is the one on a run the bearer just created. Returns `undefined`
145
+ * for a single-tenant host (which omits `owner.tenantId`) or when no probe
146
+ * fixture is available — callers then omit `tenantId`, which single-tenant hosts
147
+ * accept. (RFC 0093 / webhooks.md §Register; suite defect fixed 2026-08-09.)
148
+ */
149
+ export async function discoverOwnedTenant(
150
+ driver: { post: (p: string, b: unknown) => Promise<{ status: number; json: unknown }>; get: (p: string) => Promise<{ status: number; json: unknown }> },
151
+ probeWorkflowId = 'conformance-noop',
152
+ ): Promise<string | undefined> {
153
+ const create = await driver.post('/v1/runs', { workflowId: probeWorkflowId });
154
+ if (create.status !== 201) return undefined;
155
+ const runId = (create.json as { runId?: string } | null)?.runId;
156
+ if (typeof runId !== 'string') return undefined;
157
+ const snap = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
158
+ const owner = (snap.json as { owner?: { tenantId?: unknown } } | null)?.owner;
159
+ return typeof owner?.tenantId === 'string' ? owner.tenantId : undefined;
160
+ }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
+ import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
25
26
 
26
27
  async function isWebhookSupported(): Promise<boolean> {
27
28
  const disco = await driver.get('/.well-known/openwop');
@@ -40,10 +41,11 @@ describe('webhook-negative: SSRF guard rejects private destinations', () => {
40
41
  // Spec-complete except for the private URL, so validation passes and the
41
42
  // request reaches the SSRF guard. `{ url }` alone 400s `validation_error`
42
43
  // (missing `events`) before the guard runs. (Suite defect, fixed 2026-08-09.)
44
+ const ownedTenant = await discoverOwnedTenant(driver);
43
45
  const reg = await driver.post('/v1/webhooks', {
44
46
  url: 'http://127.0.0.1:65535/',
45
47
  events: ['run.completed'],
46
- tenantId: 'conformance-tenant',
48
+ ...(ownedTenant ? { tenantId: ownedTenant } : {}),
47
49
  });
48
50
  if (reg.status === 201) {
49
51
  // Host accepted — SSRF guard not implemented or bypassed.
@@ -29,6 +29,7 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
29
29
  import { driver } from '../lib/driver.js';
30
30
  import { pollUntilTerminal } from '../lib/polling.js';
31
31
  import { isFixtureAdvertised } from '../lib/fixtures.js';
32
+ import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
32
33
 
33
34
  interface DeliveredRequest {
34
35
  readonly headers: Record<string, string>;
@@ -93,10 +94,14 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
93
94
  // The pre-fix `{ url }`-only body 400s on validation before delivery can occur.
94
95
  // (Suite defect, fixed 2026-08-09.) The delivered run's tenant must match this
95
96
  // subscription's tenantId — verified end-to-end against the openwop-app host.
97
+ // Derive a tenant the bearer OWNS from a probe run's snapshot — a hard-coded
98
+ // tenantId is 403'd by a host that scopes subscriptions by membership
99
+ // (RFC 0093). Single-tenant hosts return undefined ⇒ omit tenantId.
100
+ const ownedTenant = await discoverOwnedTenant(driver);
96
101
  const reg = await driver.post('/v1/webhooks', {
97
102
  url: receiver.url,
98
103
  events: ['run.completed'],
99
- tenantId: 'conformance-tenant',
104
+ ...(ownedTenant ? { tenantId: ownedTenant } : {}),
100
105
  });
101
106
 
102
107
  // SSRF guard skip: if the host rejects loopback destinations,
@@ -158,20 +163,35 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
158
163
  'webhooks.md §"Signature algorithm versioning"',
159
164
  'every delivery MUST set X-openwop-Signature-Algorithm: v1',
160
165
  )).toBe('v1');
161
- expect(first.headers['x-openwop-subscription-id']).toBe(sub.subscriptionId);
162
-
163
- const timestamp = first.headers['x-openwop-signature-timestamp'];
164
- expect(typeof timestamp).toBe('string');
165
- expect((timestamp ?? '').length).toBeGreaterThan(0);
166
-
167
- const signature = first.headers['x-openwop-signature'];
166
+ // Header names + signature format per webhooks.md v1.1 §"Delivery headers"
167
+ // (the SSoT). The pre-fix scenario asserted `x-openwop-subscription-id`,
168
+ // `x-openwop-signature-timestamp`, and a BARE-HEX `x-openwop-signature` —
169
+ // three names/shapes that appear in NO spec file or RFC. webhooks.md
170
+ // specifies `X-openwop-Webhook-Id`, `X-openwop-Timestamp`, and
171
+ // `X-openwop-Signature: sha256={hex}`. (Suite↔spec divergence fixed 2026-08-09.)
172
+ expect(
173
+ first.headers['x-openwop-webhook-id'],
174
+ driver.describe('webhooks.md §"Delivery headers"', 'X-openwop-Webhook-Id MUST carry the subscription id'),
175
+ ).toBe(sub.subscriptionId);
176
+
177
+ const timestamp = first.headers['x-openwop-timestamp'];
178
+ expect(
179
+ typeof timestamp === 'string' && timestamp.length > 0,
180
+ driver.describe('webhooks.md §"Delivery headers"', 'X-openwop-Timestamp MUST be a Unix-seconds integer string'),
181
+ ).toBe(true);
182
+
183
+ const signature = first.headers['x-openwop-signature'] ?? '';
184
+ expect(
185
+ signature.startsWith('sha256='),
186
+ driver.describe('webhooks.md §"Delivery headers"', 'X-openwop-Signature MUST carry the `sha256=` prefix'),
187
+ ).toBe(true);
168
188
  const expected = createHmac('sha256', sub.secret)
169
189
  .update(`${timestamp}.${first.body}`, 'utf8')
170
190
  .digest('hex');
171
- expect(signature, driver.describe(
172
- 'webhooks.md §"Signature scheme"',
173
- 'X-openwop-Signature MUST be HMAC-SHA256(secret, `${timestamp}.${rawBody}`) hex',
174
- )).toBe(expected);
191
+ expect(
192
+ signature.replace('sha256=', ''),
193
+ driver.describe('webhooks.md §"Delivery headers"', 'X-openwop-Signature MUST be sha256=HMAC-SHA256(secret, `${X-openwop-Timestamp}.${rawBody}`)'),
194
+ ).toBe(expected);
175
195
 
176
196
  // Body should parse as JSON with a run event shape.
177
197
  const event = JSON.parse(first.body) as { type?: unknown; runId?: unknown };