task: metadata: install: pip install -e .
patch: diff --git a/synapse/cores/common.py b/synapse/cores/common.py
index f24e72ca4..02576a40e 100644
--- a/synapse/cores/common.py
+++ b/synapse/cores/common.py
@@ -1911,12 +1911,15 @@ class Cortex(EventBus, DataModel, Runtime, s_ingest.IngestApi):
         if ctor is not None:
             return ctor(prop, valu, **props)
 
+        tname = self.getPropTypeName(prop)
+        if tname is None and self.enforce:
+            raise s_common.NoSuchForm(name=prop)
+
         # special case for adding nodes with a guid primary property
         # if the value None is specified, generate a new guid and skip
         # deconfliction ( allows highly performant "event" ingest )
         deconf = True
         if valu is None:
-            tname = self.getPropTypeName(prop)
             if tname and self.isSubType(tname, 'guid'):
                 valu = s_common.guid()
                 deconf = False
diff --git a/synapse/datamodel.py b/synapse/datamodel.py
index f986b9f3d..cd5865777 100644
--- a/synapse/datamodel.py
+++ b/synapse/datamodel.py
@@ -122,6 +122,7 @@ class DataModel(s_types.TypeLib):
         self.addTufoProp('syn:form', 'doc', ptype='str', doc='basic form definition')
         self.addTufoProp('syn:form', 'ver', ptype='int', doc='form version within the model')
         self.addTufoProp('syn:form', 'model', ptype='str', doc='which model defines a given form')
+        self.addTufoProp('syn:form', 'ptype', ptype='syn:type', req=1, doc='Synapse type for this form')
 
         self.addTufoForm('syn:prop', ptype='syn:prop')
         self.addTufoProp('syn:prop', 'doc', ptype='str', req=1, doc='Description of the property definition')

problem_statement: Storm operators should validate form / prop types exist in the model
Storm operators (at minimum, addnode()) do not seem to validate that forms (and possibly properties?) exist before allowing node creation:
```
cli> ask addnode(inet:fqdn:hurrderp,umwut)
inet:fqdn:hurrderp = umwut
(1 results)

cli> ask --props inet:fqdn:hurrderp=umwut
inet:fqdn:hurrderp = umwut
(1 results)
```
repo: vertexproject/synapse
test_cmd: pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
test_patch: diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py
index 45e80bb0e..4b838f199 100644
--- a/synapse/tests/test_cortex.py
+++ b/synapse/tests/test_cortex.py
@@ -1298,10 +1298,20 @@ class CortexTest(SynTest):
             self.nn(cofo)
             self.false(core.enforce)
 
+            self.true(core.isSetPropOk('foo:bar:customprop'))
+            self.true(core.isSetPropOk('foo:baz:fqdn'))
+            self.true(core.isSetPropOk('foo:baz:haha'))
+            self.true(core.isSetPropOk('foo:baz:duck'))
+
             core.setConfOpt('enforce', True)
 
             self.true(core.enforce)
 
+            self.false(core.isSetPropOk('foo:bar:customprop'))
+            self.true(core.isSetPropOk('foo:baz:fqdn'))
+            self.true(core.isSetPropOk('foo:baz:haha'))
+            self.false(core.isSetPropOk('foo:baz:duck'))
+
             tufo0 = core.formTufoByProp('foo:bar', 'foo@bar.com', hehe=10, haha=20)
             tufo1 = core.formTufoByProp('foo:baz', 'foo@bar.com', hehe=10, haha=20)
 
@@ -1328,6 +1338,9 @@ class CortexTest(SynTest):
             self.eq(tufo1[1].get('foo:baz:haha'), 21)
             self.eq(tufo1[1].get('foo:baz:fqdn'), 'visi.com')
 
+            # Prevent the core from storing new types it does not know about
+            self.raises(NoSuchForm, core.formTufoByProp, 'foo:duck', 'something')
+
     def test_cortex_minmax(self):
 
         with s_cortex.openurl('ram://') as core: