@openwop/openwop-conformance 1.0.0 → 1.1.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 +17 -0
- package/README.md +31 -6
- package/api/grpc/openwop.proto +251 -0
- package/api/openapi.yaml +109 -3
- package/coverage.md +48 -9
- package/fixtures/conformance-configurable-schema.json +39 -0
- package/fixtures/conformance-subworkflow-parent.json +1 -1
- package/fixtures/conformance-wasm-pack-memory-cap-breach.json +23 -0
- package/fixtures/openwop-smoke-byok-roundtrip.json +25 -0
- package/fixtures.md +21 -0
- package/package.json +3 -1
- package/schemas/README.md +4 -0
- package/schemas/audit-verify-result.schema.json +90 -0
- package/schemas/capabilities.schema.json +293 -1
- package/schemas/node-pack-manifest.schema.json +4 -4
- package/schemas/pack-lockfile.schema.json +92 -0
- package/schemas/registry-version-manifest.schema.json +145 -0
- package/schemas/run-event-payloads.schema.json +2 -2
- package/schemas/security-advisory.schema.json +109 -0
- package/src/lib/a2a-fake-peer.ts +143 -56
- package/src/lib/behavior-gate.ts +68 -0
- package/src/lib/env.ts +10 -0
- package/src/lib/grpc-framing.test.ts +96 -0
- package/src/lib/grpc-framing.ts +76 -0
- package/src/lib/oidc-issuer.test.ts +328 -0
- package/src/lib/oidc-issuer.ts +241 -0
- package/src/lib/otel-collector-grpc.test.ts +191 -0
- package/src/lib/otel-collector.test.ts +303 -0
- package/src/lib/otel-collector.ts +318 -14
- package/src/lib/otlp-protobuf.test.ts +461 -0
- package/src/lib/otlp-protobuf.ts +529 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +147 -28
- package/src/scenarios/agentConfidenceEscalation.test.ts +1 -0
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +1 -0
- package/src/scenarios/agentMemoryRedactionContract.test.ts +1 -0
- package/src/scenarios/agentMemoryRoundTrip.test.ts +1 -0
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +1 -0
- package/src/scenarios/agentMessageReducer.test.ts +1 -0
- package/src/scenarios/agentMetadata.test.ts +1 -0
- package/src/scenarios/agentPackExport.test.ts +1 -0
- package/src/scenarios/agentPackInstall.test.ts +1 -0
- package/src/scenarios/agentPackProvenance.test.ts +1 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -6
- package/src/scenarios/auth-api-key-rotation.test.ts +182 -0
- package/src/scenarios/auth-mtls.test.ts +274 -0
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +259 -0
- package/src/scenarios/auth-oidc-user-bearer.test.ts +361 -0
- package/src/scenarios/bulk-cancel.test.ts +111 -0
- package/src/scenarios/configurable-schema.test.ts +48 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +1 -0
- package/src/scenarios/conversationLifecycle.test.ts +1 -0
- package/src/scenarios/conversationReplayDeterminism.test.ts +1 -0
- package/src/scenarios/conversationVsLegacySuspend.test.ts +1 -0
- package/src/scenarios/debug-bundle-truncation.test.ts +95 -0
- package/src/scenarios/discovery.test.ts +183 -0
- package/src/scenarios/http-client-ssrf.test.ts +71 -0
- package/src/scenarios/idempotency.test.ts +6 -0
- package/src/scenarios/idempotencyRetry.test.ts +3 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +198 -34
- package/src/scenarios/mcp-toolcall-redaction.test.ts +66 -0
- package/src/scenarios/metric-emission.test.ts +113 -0
- package/src/scenarios/orchestratorConservativePath.test.ts +1 -0
- package/src/scenarios/orchestratorDispatch.test.ts +1 -0
- package/src/scenarios/orchestratorTermination.test.ts +1 -0
- package/src/scenarios/otel-emission-grpc.test.ts +98 -0
- package/src/scenarios/pause-resume.test.ts +119 -0
- package/src/scenarios/production-backpressure.test.ts +342 -0
- package/src/scenarios/production-retention-expiry.test.ts +164 -0
- package/src/scenarios/registry-public.test.ts +131 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +35 -0
- package/src/scenarios/replay-retention-expiry.test.ts +178 -0
- package/src/scenarios/restart-during-run.test.ts +177 -0
- package/src/scenarios/spec-corpus-validity.test.ts +54 -26
- package/src/scenarios/staleClaim.test.ts +3 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +67 -10
- package/src/scenarios/wasm-pack-memory-cap.test.ts +64 -9
- package/src/scenarios/webhook-negative.test.ts +90 -0
- package/src/scenarios/webhook-signed-delivery.test.ts +178 -0
- package/src/setup.ts +25 -1
- package/vitest.config.ts +5 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/pack-lockfile.schema.json",
|
|
4
|
+
"title": "PackLockfile",
|
|
5
|
+
"description": "Reproducible-build lockfile pinning resolved versions of every pack a workspace depends on. Per spec/v1/node-packs.md §\"Dependency resolution + lockfile\". A workspace records its lockfile alongside its workflow definitions; on subsequent installs the registry resolver MUST honor the lockfile's exact versions rather than re-running semver resolution. This guarantees that a workflow that ran against `vendor.openwop.rust-hello@1.0.0` will still run against `1.0.0` even after `1.0.1` is published.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["lockfileVersion", "generatedAt", "packs"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"lockfileVersion": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"minimum": 1,
|
|
12
|
+
"description": "Lockfile schema version. Bumped when the lockfile structure changes incompatibly. v1.0 ships at lockfileVersion 1."
|
|
13
|
+
},
|
|
14
|
+
"generatedAt": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "date-time",
|
|
17
|
+
"description": "ISO 8601 timestamp the lockfile was written. Used for audit + cache-bust diagnostics; resolvers MUST NOT use this for resolution decisions."
|
|
18
|
+
},
|
|
19
|
+
"registry": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"format": "uri",
|
|
22
|
+
"description": "Optional source-of-truth registry URL the resolved versions were fetched from (e.g., `https://packs.openwop.dev`). Multi-registry workspaces include this for audit. Single-registry workspaces MAY omit."
|
|
23
|
+
},
|
|
24
|
+
"packs": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"description": "Resolved pack records, one per pack referenced (transitively) by the workspace's workflow definitions. Order is informational only; resolvers MUST NOT rely on order. Empty arrays are allowed (a workspace with no packs).",
|
|
27
|
+
"items": { "$ref": "#/$defs/ResolvedPack" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"$defs": {
|
|
32
|
+
"ResolvedPack": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"required": ["name", "version", "integrity"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"name": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 3,
|
|
39
|
+
"maxLength": 256,
|
|
40
|
+
"pattern": "^(core|vendor|community|private|local)\\.[a-z0-9][a-z0-9.-]*$",
|
|
41
|
+
"description": "Pack name per node-packs.md §\"Manifest format\" `name`. Reverse-DNS-style; namespace prefix is one of the canonical tiers."
|
|
42
|
+
},
|
|
43
|
+
"version": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1,
|
|
46
|
+
"maxLength": 64,
|
|
47
|
+
"description": "Resolved exact version (no ranges). Semver MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]. The resolver MUST pin to this exact version on subsequent installs."
|
|
48
|
+
},
|
|
49
|
+
"resolved": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"format": "uri",
|
|
52
|
+
"description": "Optional canonical URL the tarball was fetched from. Audit aid; resolvers MUST verify the `integrity` field even if this URL is present."
|
|
53
|
+
},
|
|
54
|
+
"integrity": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^sha256-[A-Za-z0-9+/]{43}=$",
|
|
57
|
+
"description": "Subresource-integrity-style hash of the tarball: literal `sha256-` followed by 43-char base64-encoded SHA-256 digest with trailing `=` padding. Resolvers MUST verify the downloaded tarball's SHA-256 matches before extraction. Hash mismatch MUST fail the install with `pack_integrity_mismatch`."
|
|
58
|
+
},
|
|
59
|
+
"signature": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"description": "Optional pack signature material (Ed25519 per node-packs.md §Signing). When present, resolvers MUST verify the signature against the publisher's advertised public key.",
|
|
62
|
+
"required": ["algorithm", "publicKey", "value"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"algorithm": { "type": "string", "enum": ["ed25519"] },
|
|
65
|
+
"publicKey": { "type": "string", "minLength": 1, "description": "Base64-encoded Ed25519 public key bytes." },
|
|
66
|
+
"value": { "type": "string", "minLength": 1, "description": "Base64-encoded Ed25519 signature over the tarball." }
|
|
67
|
+
},
|
|
68
|
+
"additionalProperties": false
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"minLength": 1,
|
|
75
|
+
"maxLength": 64
|
|
76
|
+
},
|
|
77
|
+
"description": "Pinned versions for this pack's `dependencies` (per its manifest). Each key is a pack name; each value is the resolved exact version. Pinned recursively — a transitively-installed pack appears once in the top-level `packs[]` array AND as a value in each parent's `dependencies` map."
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"minLength": 1,
|
|
84
|
+
"maxLength": 64
|
|
85
|
+
},
|
|
86
|
+
"description": "Echo of this pack's manifest `peerDependencies` for audit. Resolvers MUST verify that the host's `/.well-known/openwop` advertises every key listed here (per host-capabilities.md §\"Capability negotiation\"). Lockfile records the host-capability values the workspace committed to at lockfile-write time; mismatches at install time MAY warn (host advertises richer set) but MUST fail if a peer-dep is missing."
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"additionalProperties": false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/registry-version-manifest.schema.json",
|
|
4
|
+
"title": "RegistryVersionManifest",
|
|
5
|
+
"description": "Registry-augmented version manifest served at `/v1/packs/{name}/-/{version}.json`. Extends the bare pack manifest (`node-pack-manifest.schema.json`) with registry-side metadata (integrity hash, publishedAt timestamp, signed-blob URLs, lifecycle flags). This is the shape clients receive when they GET a specific pack version; the bare manifest is what publishers commit inside the tarball.\n\n**Why two schemas?** The bare manifest is the authoring contract — what a vendor writes in their pack source tree. The registry-augmented version is the SERVING contract — what `packs.openwop.dev` produces after `build-index.mjs` computes integrity hashes + adds URL templates. Validating both surfaces independently catches drift between authored content and served metadata.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["name", "version", "engines", "runtime", "integrity"],
|
|
8
|
+
"anyOf": [
|
|
9
|
+
{ "properties": { "nodes": { "type": "array", "minItems": 1 } }, "required": ["nodes"] },
|
|
10
|
+
{ "properties": { "agents": { "type": "array", "minItems": 1 } }, "required": ["agents"] }
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"name": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Reverse-DNS pack name. Mirrors `node-pack-manifest.schema.json` §name.",
|
|
16
|
+
"pattern": "^(core|vendor|community|private)\\.[a-z][a-z0-9_-]*(\\.[a-z][a-zA-Z0-9_-]*)+$",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"maxLength": 256
|
|
19
|
+
},
|
|
20
|
+
"version": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Pack version per SemVer 2.0.0. Must match the URL path the manifest is served at.",
|
|
23
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
|
|
24
|
+
},
|
|
25
|
+
"description": { "type": "string", "maxLength": 1024 },
|
|
26
|
+
"author": { "type": "string" },
|
|
27
|
+
"license": { "type": "string" },
|
|
28
|
+
"homepage": { "type": "string", "format": "uri" },
|
|
29
|
+
"repository": { "type": "string", "format": "uri" },
|
|
30
|
+
"keywords": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "type": "string", "maxLength": 64 },
|
|
33
|
+
"maxItems": 50
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["openwop"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"openwop": { "type": "string", "minLength": 1 }
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": { "type": "string" }
|
|
42
|
+
},
|
|
43
|
+
"runtime": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"required": ["language"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"language": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["javascript", "typescript", "wasm", "remote"],
|
|
50
|
+
"description": "Pack runtime. `javascript`/`typescript` for in-process Node executors; `wasm` for sandboxed WASM (RFC 0008 ABI); `remote` for agent-only packs that dispatch via remote LLM providers + don't ship local executor bytecode."
|
|
51
|
+
},
|
|
52
|
+
"entry": { "type": "string" },
|
|
53
|
+
"abiVersion": { "type": "string" }
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": true
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"description": "Host capabilities the pack consumes. Each key is a host-capability identifier (e.g., `host.canvas`); each value declares the support level the pack expects.",
|
|
60
|
+
"additionalProperties": { "type": "string" }
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Pack dependencies (other packs). Wire-locked at install time per `pack-lockfile.schema.json`.",
|
|
65
|
+
"additionalProperties": { "type": "string" }
|
|
66
|
+
},
|
|
67
|
+
"signing": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"description": "Embedded signing metadata. The pack's signing key MUST be one of the keys registered in `.well-known/openwop-registry.json` `signingKeys[]` AND MUST be authorized for the pack's namespace via `permittedNamespaces`.\n\nTwo identifier forms in the wild:\n - `keyId`: canonical (newer packs). Matches an entry in registry's `signingKeys[].keyId`.\n - `publicKeyRef`: legacy alias of `keyId` (used by `build-pack-tarball.mjs --key-id <id>`). Equivalent semantics.\n\nAt least one of `keyId` / `publicKeyRef` MUST be present; if both, they MUST be equal. The schema's `oneOf` block enforces this.",
|
|
70
|
+
"required": ["method"],
|
|
71
|
+
"oneOf": [
|
|
72
|
+
{ "required": ["keyId"] },
|
|
73
|
+
{ "required": ["publicKeyRef"] }
|
|
74
|
+
],
|
|
75
|
+
"properties": {
|
|
76
|
+
"method": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["manual", "ed25519", "sigstore"],
|
|
79
|
+
"description": "Signing method. `manual` and `ed25519` both denote a detached Ed25519 signature over canonical-JSON pack.json (synonymous in practice; both forms appear in published packs). `sigstore` reserved for the SLSA-friendly OIDC path."
|
|
80
|
+
},
|
|
81
|
+
"keyId": { "type": "string", "minLength": 1 },
|
|
82
|
+
"publicKeyRef": { "type": "string", "minLength": 1 },
|
|
83
|
+
"publicKeyUrl": { "type": "string", "format": "uri-reference" },
|
|
84
|
+
"signatureRef": { "type": "string" }
|
|
85
|
+
},
|
|
86
|
+
"additionalProperties": true
|
|
87
|
+
},
|
|
88
|
+
"nodes": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["typeId", "version", "category", "role"],
|
|
93
|
+
"additionalProperties": true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"agents": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"items": { "type": "object", "additionalProperties": true }
|
|
99
|
+
},
|
|
100
|
+
"integrity": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "SRI-style integrity hash of the SIGNED tarball (`sha256-<base64(sha256)>`). Computed by `registry/scripts/build-index.mjs` from the on-disk `.tgz`. Consumers MUST verify the fetched tarball's hash matches this field before unpacking.",
|
|
103
|
+
"pattern": "^sha256-[A-Za-z0-9+/=]+$"
|
|
104
|
+
},
|
|
105
|
+
"publishedAt": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "ISO-8601 UTC timestamp of first publish. Immutable per spec — set at first publish, never re-issued on subsequent index rebuilds.",
|
|
108
|
+
"format": "date-time"
|
|
109
|
+
},
|
|
110
|
+
"tarballUrl": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Registry-relative URL for the signed tarball. Always `/v1/packs/{name}/-/{version}.tgz`.",
|
|
113
|
+
"pattern": "^/v1/packs/[^/]+/-/[^/]+\\.tgz$"
|
|
114
|
+
},
|
|
115
|
+
"signatureUrl": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Registry-relative URL for the detached Ed25519 signature. Always `/v1/packs/{name}/-/{version}.sig`.",
|
|
118
|
+
"pattern": "^/v1/packs/[^/]+/-/[^/]+\\.sig$"
|
|
119
|
+
},
|
|
120
|
+
"deprecated": {
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"description": "Advisory: this version is deprecated. Consumers MAY refuse to install but registry continues to serve."
|
|
123
|
+
},
|
|
124
|
+
"yanked": {
|
|
125
|
+
"type": "boolean",
|
|
126
|
+
"description": "Hard refusal: this version was published in error. Registry MUST refuse to serve the tarball; consumers MUST refuse to dispatch nodes from yanked versions."
|
|
127
|
+
},
|
|
128
|
+
"yankedReason": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"maxLength": 512,
|
|
131
|
+
"description": "Optional one-line rationale shown by clients when a yanked install is attempted. Conventionally references the advisory id (`OPENWOP-YYYY-NNNN`) when one was issued."
|
|
132
|
+
},
|
|
133
|
+
"deprecationReason": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"maxLength": 512,
|
|
136
|
+
"description": "Optional one-line rationale shown by clients when an install of a deprecated version is attempted. Typically names the replacement version + migration link."
|
|
137
|
+
},
|
|
138
|
+
"advisoryUrl": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"format": "uri",
|
|
141
|
+
"description": "Optional URL with deprecation / yank rationale + remediation."
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"additionalProperties": false
|
|
145
|
+
}
|
|
@@ -501,10 +501,10 @@
|
|
|
501
501
|
|
|
502
502
|
"capBreached": {
|
|
503
503
|
"type": "object",
|
|
504
|
-
"description": "Emitted when a CapabilityLimit is exceeded (clarificationRounds / schemaRounds / envelopesPerTurn / maxNodeExecutions).",
|
|
504
|
+
"description": "Emitted when a CapabilityLimit is exceeded. Protocol-level limits use the four engine kinds (clarificationRounds / schemaRounds / envelopesPerTurn / maxNodeExecutions). RFC 0008 §K WASM-runtime caps use the wasm-* kinds (memory ceiling, fuel exhaustion, execution-time wall-clock).",
|
|
505
505
|
"required": ["kind", "limit", "observed"],
|
|
506
506
|
"properties": {
|
|
507
|
-
"kind": { "type": "string", "enum": ["clarification", "schema", "envelopes", "node-executions"] },
|
|
507
|
+
"kind": { "type": "string", "enum": ["clarification", "schema", "envelopes", "node-executions", "wasm-memory", "wasm-fuel", "wasm-execution-time"] },
|
|
508
508
|
"limit": { "type": "integer", "minimum": 0 },
|
|
509
509
|
"observed": { "type": "integer", "minimum": 0 },
|
|
510
510
|
"nodeId": { "type": "string" }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/security-advisory.schema.json",
|
|
4
|
+
"title": "SecurityAdvisory",
|
|
5
|
+
"description": "Registry-owned security advisory for a published OpenWOP pack version. Sourced from registry/security/advisories.json. Every entry MUST be reviewed + merged via maintainer PR; the CI check-advisories gate refuses to deploy if any active advisory targets a pack version that is not already yanked.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "publishedAt", "severity", "summary", "affected"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Stable advisory identifier. Format: `OPENWOP-YYYY-NNNN` (sequential, assigned by maintainers at PR merge time). MAY also include an aliased CVE ID via `cveIds[]`.",
|
|
12
|
+
"pattern": "^OPENWOP-[0-9]{4}-[0-9]{4}$"
|
|
13
|
+
},
|
|
14
|
+
"cveIds": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"description": "Aliased CVE identifiers if a CVE was reserved through Mitre or a CNA. Listed in cve.mitre.org format.",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^CVE-[0-9]{4}-[0-9]+$"
|
|
20
|
+
},
|
|
21
|
+
"uniqueItems": true
|
|
22
|
+
},
|
|
23
|
+
"publishedAt": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"format": "date",
|
|
26
|
+
"description": "Date the advisory was first published (ISO 8601 yyyy-mm-dd). Frozen at merge time; do not update on revision."
|
|
27
|
+
},
|
|
28
|
+
"updatedAt": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"format": "date",
|
|
31
|
+
"description": "Date of last advisory update. Bump when severity, affected, or fixedIn changes."
|
|
32
|
+
},
|
|
33
|
+
"severity": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": ["critical", "high", "medium", "low"],
|
|
36
|
+
"description": "Severity per the rubric in docs/runbooks/INCIDENT-RESPONSE.md. critical=S0 (yank immediately), high=S1, medium=S2, low=S3."
|
|
37
|
+
},
|
|
38
|
+
"cvssV4Score": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"minimum": 0,
|
|
41
|
+
"maximum": 10,
|
|
42
|
+
"description": "Optional CVSS 4.0 base score. Informational; severity field is the authoritative classification."
|
|
43
|
+
},
|
|
44
|
+
"summary": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "One-line human-readable summary of the vulnerability. Surfaces in tooling output.",
|
|
47
|
+
"minLength": 1,
|
|
48
|
+
"maxLength": 200
|
|
49
|
+
},
|
|
50
|
+
"details": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Long-form description: vulnerability mechanism, attack vector, impact, recommended consumer action. May reference external URLs via advisoryUrl."
|
|
53
|
+
},
|
|
54
|
+
"advisoryUrl": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"format": "uri",
|
|
57
|
+
"description": "External advisory link (e.g., GitHub Security Advisory, vendor security page, CVE detail page)."
|
|
58
|
+
},
|
|
59
|
+
"affected": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"description": "Pack versions impacted by this advisory. Each entry pins a pack name + a SemVer range. The check-advisories CI gate cross-references this list against every published version and fails if any non-yanked version matches.",
|
|
62
|
+
"minItems": 1,
|
|
63
|
+
"items": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["packName", "versions"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"packName": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "Pack name (e.g., `vendor.myndhyve.ai`). MUST exist under registry/v1/packs/.",
|
|
70
|
+
"pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
|
|
71
|
+
},
|
|
72
|
+
"versions": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "SemVer range using node-semver syntax (`<1.0.0`, `>=1.0.0 <1.0.3`, `1.0.0 || 1.0.1`). Matched against published version numbers via the check-advisories gate."
|
|
75
|
+
},
|
|
76
|
+
"fixedIn": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Earliest version that contains the fix. Consumers MUST upgrade to >= this version. Absent if no fix is available yet (defense-only).",
|
|
79
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
|
|
80
|
+
},
|
|
81
|
+
"fixedInRef": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "PR or commit reference where the fix landed (e.g., `openwop/openwop#42`). Helps consumers audit the patch."
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"additionalProperties": false
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"credits": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"description": "Acknowledged reporters or researchers.",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": ["name"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"name": { "type": "string", "minLength": 1 },
|
|
97
|
+
"url": { "type": "string", "format": "uri" },
|
|
98
|
+
"role": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"enum": ["finder", "reporter", "analyst", "coordinator", "remediation-developer"],
|
|
101
|
+
"description": "Per CSAF role vocabulary. Defaults to `finder` when not specified."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"additionalProperties": false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"additionalProperties": false
|
|
109
|
+
}
|
package/src/lib/a2a-fake-peer.ts
CHANGED
|
@@ -3,24 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The A2A protocol (https://a2a-protocol.org/) defines an `AgentCard` for
|
|
5
5
|
* discovery plus a Task lifecycle whose `TaskState` enum drives most of
|
|
6
|
-
* the conformance burden. We expose just enough of
|
|
6
|
+
* the conformance burden. We expose just enough of A2A v0.3 JSON-RPC
|
|
7
7
|
* transport to let conformance scenarios drive the host through the
|
|
8
8
|
* four documented drift points from `spec/v1/a2a-integration.md`
|
|
9
9
|
* §"State projection".
|
|
10
10
|
*
|
|
11
|
-
* Endpoints (
|
|
12
|
-
* GET /agent.json
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* POST /
|
|
11
|
+
* Endpoints (A2A v0.3 wire shape):
|
|
12
|
+
* GET /.well-known/agent-card.json — AgentCard at the spec-mandated
|
|
13
|
+
* well-known path (per
|
|
14
|
+
* @a2a-js/sdk@0.3.13 `AGENT_CARD_PATH`)
|
|
15
|
+
* POST /a2a/jsonrpc — JSON-RPC dispatch:
|
|
16
|
+
* - method `message/send` →
|
|
17
|
+
* creates a Task, returns Task obj
|
|
18
|
+
* - method `tasks/get` → fetches Task
|
|
16
19
|
*
|
|
17
20
|
* Test fixtures set the *next* state transition via `setNextState(...)`
|
|
18
21
|
* so a single scenario can walk the peer through SUBMITTED → WORKING →
|
|
19
22
|
* INPUT_REQUIRED → COMPLETED (or AUTH_REQUIRED, or REJECTED) without
|
|
20
|
-
* implementing a real agent.
|
|
23
|
+
* implementing a real agent. The internal API uses the UPPERCASE enum
|
|
24
|
+
* from openwop's a2a-integration.md (which references the gRPC enum);
|
|
25
|
+
* wire responses use the v0.3 JSON-RPC lowercase-hyphen form.
|
|
21
26
|
*
|
|
22
27
|
* @see spec/v1/a2a-integration.md §"State projection"
|
|
23
|
-
* @see https://a2a-protocol.org/
|
|
28
|
+
* @see https://a2a-protocol.org/v0.3.0/specification
|
|
24
29
|
*/
|
|
25
30
|
|
|
26
31
|
import { createServer, type Server } from 'node:http';
|
|
@@ -37,16 +42,32 @@ export type A2ATaskState =
|
|
|
37
42
|
| 'CANCELED'
|
|
38
43
|
| 'REJECTED';
|
|
39
44
|
|
|
45
|
+
/** Translate internal UPPERCASE state to A2A v0.3 wire form (lowercase + hyphen). */
|
|
46
|
+
function wireState(s: A2ATaskState): string {
|
|
47
|
+
switch (s) {
|
|
48
|
+
case 'UNSPECIFIED': return 'unknown';
|
|
49
|
+
case 'SUBMITTED': return 'submitted';
|
|
50
|
+
case 'WORKING': return 'working';
|
|
51
|
+
case 'INPUT_REQUIRED': return 'input-required';
|
|
52
|
+
case 'AUTH_REQUIRED': return 'auth-required';
|
|
53
|
+
case 'COMPLETED': return 'completed';
|
|
54
|
+
case 'FAILED': return 'failed';
|
|
55
|
+
case 'CANCELED': return 'canceled';
|
|
56
|
+
case 'REJECTED': return 'rejected';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
interface A2ATask {
|
|
41
|
-
|
|
61
|
+
id: string;
|
|
62
|
+
contextId: string;
|
|
42
63
|
state: A2ATaskState;
|
|
43
|
-
|
|
44
|
-
metadata?: Record<string, unknown>;
|
|
64
|
+
history: Array<{ role: 'user' | 'agent'; parts: unknown[] }>;
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
export interface A2APeerInvocation {
|
|
48
68
|
readonly method: string;
|
|
49
69
|
readonly path: string;
|
|
70
|
+
readonly rpcMethod: string | null;
|
|
50
71
|
readonly body: unknown;
|
|
51
72
|
readonly timestamp: number;
|
|
52
73
|
}
|
|
@@ -61,7 +82,14 @@ export class A2AFakePeer {
|
|
|
61
82
|
|
|
62
83
|
async start(port: number = 0): Promise<void> {
|
|
63
84
|
return new Promise((resolve, reject) => {
|
|
64
|
-
const server = createServer((req, res) =>
|
|
85
|
+
const server = createServer((req, res) => {
|
|
86
|
+
this._handle(req, res).catch((err) => {
|
|
87
|
+
if (!res.headersSent) {
|
|
88
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
65
93
|
server.on('error', reject);
|
|
66
94
|
server.listen(port, '127.0.0.1', () => {
|
|
67
95
|
const addr = server.address() as AddressInfo;
|
|
@@ -121,6 +149,21 @@ export class A2AFakePeer {
|
|
|
121
149
|
return this._tasks.get(taskId);
|
|
122
150
|
}
|
|
123
151
|
|
|
152
|
+
private _taskToWire(t: A2ATask): Record<string, unknown> {
|
|
153
|
+
return {
|
|
154
|
+
id: t.id,
|
|
155
|
+
kind: 'task',
|
|
156
|
+
contextId: t.contextId,
|
|
157
|
+
status: { state: wireState(t.state) },
|
|
158
|
+
history: t.history.map((m) => ({
|
|
159
|
+
kind: 'message',
|
|
160
|
+
role: m.role,
|
|
161
|
+
parts: m.parts,
|
|
162
|
+
messageId: `msg-${t.id}-${t.history.indexOf(m)}`,
|
|
163
|
+
})),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
124
167
|
private async _handle(
|
|
125
168
|
req: import('node:http').IncomingMessage,
|
|
126
169
|
res: import('node:http').ServerResponse,
|
|
@@ -138,82 +181,126 @@ export class A2AFakePeer {
|
|
|
138
181
|
}
|
|
139
182
|
}
|
|
140
183
|
|
|
184
|
+
const rpcMethod =
|
|
185
|
+
body && typeof body === 'object' && body !== null && 'method' in body
|
|
186
|
+
? String((body as { method: unknown }).method)
|
|
187
|
+
: null;
|
|
188
|
+
|
|
141
189
|
this._invocations.push({
|
|
142
190
|
method: req.method ?? 'GET',
|
|
143
191
|
path: url,
|
|
192
|
+
rpcMethod,
|
|
144
193
|
body,
|
|
145
194
|
timestamp: Date.now(),
|
|
146
195
|
});
|
|
147
196
|
|
|
148
|
-
// GET /agent.json
|
|
149
|
-
if (req.method === 'GET' && url.startsWith('/agent.json')) {
|
|
197
|
+
// GET /.well-known/agent-card.json — A2A v0.3 well-known path
|
|
198
|
+
if (req.method === 'GET' && url.startsWith('/.well-known/agent-card.json')) {
|
|
150
199
|
const card = {
|
|
151
200
|
protocolVersion: '0.3.0',
|
|
152
201
|
name: 'openwop-conformance-fake-a2a',
|
|
153
202
|
description: 'Synthetic A2A peer for openwop conformance suite',
|
|
154
|
-
url: this.endpoint()
|
|
203
|
+
url: `${this.endpoint()}/a2a/jsonrpc`,
|
|
155
204
|
version: '1.0.0',
|
|
156
|
-
capabilities: { streaming: false },
|
|
205
|
+
capabilities: { streaming: false, pushNotifications: false },
|
|
157
206
|
skills: [
|
|
158
207
|
{
|
|
159
208
|
id: 'echo',
|
|
160
209
|
name: 'echo',
|
|
161
210
|
description: 'Returns input verbatim',
|
|
211
|
+
tags: ['echo'],
|
|
162
212
|
},
|
|
163
213
|
],
|
|
214
|
+
defaultInputModes: ['text'],
|
|
215
|
+
defaultOutputModes: ['text'],
|
|
216
|
+
additionalInterfaces: [
|
|
217
|
+
{ url: `${this.endpoint()}/a2a/jsonrpc`, transport: 'JSONRPC' },
|
|
218
|
+
],
|
|
164
219
|
};
|
|
165
220
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
166
221
|
res.end(JSON.stringify(card));
|
|
167
222
|
return;
|
|
168
223
|
}
|
|
169
224
|
|
|
170
|
-
// POST /
|
|
171
|
-
if (req.method === 'POST' && url === '/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
// POST /a2a/jsonrpc — A2A v0.3 JSON-RPC transport
|
|
226
|
+
if (req.method === 'POST' && url === '/a2a/jsonrpc') {
|
|
227
|
+
if (
|
|
228
|
+
!body ||
|
|
229
|
+
typeof body !== 'object' ||
|
|
230
|
+
(body as { jsonrpc?: unknown }).jsonrpc !== '2.0'
|
|
231
|
+
) {
|
|
232
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
233
|
+
res.end(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
jsonrpc: '2.0',
|
|
236
|
+
id: null,
|
|
237
|
+
error: { code: -32600, message: 'Invalid JSON-RPC envelope' },
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const rpc = body as { id?: string | number; method?: string; params?: unknown };
|
|
185
243
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
if (rpc.method === 'message/send') {
|
|
245
|
+
const taskId = `task-${++this._taskIdCounter}`;
|
|
246
|
+
const contextId = `ctx-${taskId}`;
|
|
247
|
+
const initial: A2ATaskState = this._nextStateOverride ?? 'SUBMITTED';
|
|
248
|
+
this._nextStateOverride = null;
|
|
249
|
+
const userMessage = (rpc.params as { message?: { parts?: unknown[] } } | undefined)
|
|
250
|
+
?.message;
|
|
251
|
+
const task: A2ATask = {
|
|
252
|
+
id: taskId,
|
|
253
|
+
contextId,
|
|
254
|
+
state: initial,
|
|
255
|
+
history: userMessage
|
|
256
|
+
? [{ role: 'user', parts: userMessage.parts ?? [] }]
|
|
257
|
+
: [],
|
|
258
|
+
};
|
|
259
|
+
this._tasks.set(taskId, task);
|
|
260
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
261
|
+
res.end(
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
jsonrpc: '2.0',
|
|
264
|
+
id: rpc.id ?? null,
|
|
265
|
+
result: this._taskToWire(task),
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
193
268
|
return;
|
|
194
269
|
}
|
|
195
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
196
|
-
res.end(JSON.stringify(task));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
270
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
271
|
+
if (rpc.method === 'tasks/get') {
|
|
272
|
+
const params = (rpc.params ?? {}) as { id?: string };
|
|
273
|
+
const task = params.id ? this._tasks.get(params.id) : undefined;
|
|
274
|
+
if (!task) {
|
|
275
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
276
|
+
res.end(
|
|
277
|
+
JSON.stringify({
|
|
278
|
+
jsonrpc: '2.0',
|
|
279
|
+
id: rpc.id ?? null,
|
|
280
|
+
error: { code: -32001, message: 'Task not found' },
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
286
|
+
res.end(
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
id: rpc.id ?? null,
|
|
290
|
+
result: this._taskToWire(task),
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
207
293
|
return;
|
|
208
294
|
}
|
|
209
|
-
|
|
210
|
-
// Move from INPUT_REQUIRED back to WORKING then to COMPLETED for the
|
|
211
|
-
// simple roundtrip. Tests that need a different next-state set it
|
|
212
|
-
// via setNextState() before posting the message.
|
|
213
|
-
task.state = this._nextStateOverride ?? 'COMPLETED';
|
|
214
|
-
this._nextStateOverride = null;
|
|
295
|
+
|
|
215
296
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
216
|
-
res.end(
|
|
297
|
+
res.end(
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
jsonrpc: '2.0',
|
|
300
|
+
id: rpc.id ?? null,
|
|
301
|
+
error: { code: -32601, message: `Method not found: ${rpc.method}` },
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
217
304
|
return;
|
|
218
305
|
}
|
|
219
306
|
|