haiman-protocol 0.1.0__py3-none-any.whl

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.
Files changed (29) hide show
  1. haiman_protocol/__init__.py +99 -0
  2. haiman_protocol/envelope.py +70 -0
  3. haiman_protocol/errors.py +120 -0
  4. haiman_protocol/optypes.py +93 -0
  5. haiman_protocol/py.typed +0 -0
  6. haiman_protocol/schemas/common/envelope.schema.json +53 -0
  7. haiman_protocol/schemas/common/error.schema.json +43 -0
  8. haiman_protocol/schemas/common/tone.v1.schema.json +18 -0
  9. haiman_protocol/schemas/common/topic.v1.schema.json +13 -0
  10. haiman_protocol/schemas/handoffs/engagement-flag-to-sdr.v1.schema.json +35 -0
  11. haiman_protocol/schemas/handoffs/positive-reply-to-crm.v1.schema.json +50 -0
  12. haiman_protocol/schemas/handoffs/stalled-deal-reactivate.v1.schema.json +31 -0
  13. haiman_protocol/schemas/ops/accept.v1.schema.json +16 -0
  14. haiman_protocol/schemas/ops/contribute-signal.v1.schema.json +77 -0
  15. haiman_protocol/schemas/ops/edit.v1.schema.json +26 -0
  16. haiman_protocol/schemas/ops/handoff.v1.schema.json +23 -0
  17. haiman_protocol/schemas/ops/handshake.v1.schema.json +31 -0
  18. haiman_protocol/schemas/ops/notify.v1.schema.json +29 -0
  19. haiman_protocol/schemas/ops/propose.v1.schema.json +35 -0
  20. haiman_protocol/schemas/ops/query.v1.schema.json +19 -0
  21. haiman_protocol/schemas/ops/reject.v1.schema.json +31 -0
  22. haiman_protocol/store.py +108 -0
  23. haiman_protocol/validate.py +83 -0
  24. haiman_protocol/version.py +10 -0
  25. haiman_protocol-0.1.0.dist-info/METADATA +118 -0
  26. haiman_protocol-0.1.0.dist-info/RECORD +29 -0
  27. haiman_protocol-0.1.0.dist-info/WHEEL +4 -0
  28. haiman_protocol-0.1.0.dist-info/licenses/LICENSE +201 -0
  29. haiman_protocol-0.1.0.dist-info/licenses/LICENSE-SCHEMAS +16 -0
@@ -0,0 +1,99 @@
1
+ """Haiman protocol — reference SDK (Python).
2
+
3
+ Open reference implementation of the Haiman personal-AI substrate protocol.
4
+ Schemas (the wire format) are the open artifact; this package is the Python
5
+ binding that loads and validates against them.
6
+
7
+ Posture: open schema (CC-BY-4.0) + open reference implementation (Apache-2.0)
8
+ + paid conformance authority. See the Haiman platform model.
9
+
10
+ Public surface:
11
+
12
+ from haiman_protocol import (
13
+ __version__, PROTOCOL_VERSION,
14
+ validate_payload, validate_envelope, validate_message, validate_handoff_inner,
15
+ WireEnvelope,
16
+ ProtocolError, SchemaInvalid, error_response, ERROR_TYPES,
17
+ OP_TYPES, HANDOFF_TYPES, NOTIFY_EVENT_TYPES,
18
+ store,
19
+ )
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from . import optypes, store
25
+ from .envelope import WireEnvelope
26
+ from .errors import (
27
+ ERROR_TYPES,
28
+ IdempotencyConflict,
29
+ InternalError,
30
+ NotFound,
31
+ ProtocolError,
32
+ RateLimited,
33
+ SchemaInvalid,
34
+ ScopeViolation,
35
+ Unavailable,
36
+ Uncertified,
37
+ Ungranted,
38
+ error_response,
39
+ )
40
+ from .optypes import (
41
+ AUTONOMY_MODES,
42
+ CONTEXTS,
43
+ ENGAGEMENT_STRENGTH,
44
+ HANDOFF_TYPES,
45
+ KNOWN_CHANNELS,
46
+ NOTIFY_EVENT_TYPES,
47
+ OP_TYPES,
48
+ POSITIVE_REPLY_SIGNAL_STRENGTH,
49
+ REJECT_REASONS,
50
+ RETENTIONS,
51
+ SIGNAL_TYPES,
52
+ )
53
+ from .validate import (
54
+ validate_envelope,
55
+ validate_handoff_inner,
56
+ validate_message,
57
+ validate_payload,
58
+ )
59
+ from .version import PROTOCOL_VERSION, __version__
60
+
61
+ __all__ = [
62
+ "__version__",
63
+ "PROTOCOL_VERSION",
64
+ # validation
65
+ "validate_payload",
66
+ "validate_envelope",
67
+ "validate_message",
68
+ "validate_handoff_inner",
69
+ # envelope
70
+ "WireEnvelope",
71
+ # errors
72
+ "ProtocolError",
73
+ "Uncertified",
74
+ "Ungranted",
75
+ "ScopeViolation",
76
+ "SchemaInvalid",
77
+ "IdempotencyConflict",
78
+ "NotFound",
79
+ "RateLimited",
80
+ "Unavailable",
81
+ "InternalError",
82
+ "error_response",
83
+ "ERROR_TYPES",
84
+ # vocabularies
85
+ "OP_TYPES",
86
+ "HANDOFF_TYPES",
87
+ "NOTIFY_EVENT_TYPES",
88
+ "REJECT_REASONS",
89
+ "SIGNAL_TYPES",
90
+ "ENGAGEMENT_STRENGTH",
91
+ "POSITIVE_REPLY_SIGNAL_STRENGTH",
92
+ "KNOWN_CHANNELS",
93
+ "CONTEXTS",
94
+ "AUTONOMY_MODES",
95
+ "RETENTIONS",
96
+ # modules
97
+ "optypes",
98
+ "store",
99
+ ]
@@ -0,0 +1,70 @@
1
+ """The canonical cross-vendor wire envelope — haiman_protocol_schemas.md 1.3.
2
+
3
+ This is the W3C-native form services exchange off-platform: ``subject`` is a
4
+ W3C DID, ``service_certification`` and ``grant_credential`` are W3C VCs.
5
+
6
+ Haiman's own in-process services use a slim envelope (integer user_id, internal
7
+ service ids, no VCs) because they run inside one trust boundary; that slim form
8
+ stays in the monorepo. The SDK ships the canonical wire form, which is what a
9
+ third-party implementer needs.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import uuid
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
+
19
+
20
+ def _now_iso() -> str:
21
+ return datetime.now(timezone.utc).isoformat()
22
+
23
+
24
+ def _new_op_id() -> str:
25
+ # Production mints UUID v7 for time-ordering; the schema accepts any UUID.
26
+ return str(uuid.uuid4())
27
+
28
+
29
+ @dataclass
30
+ class WireEnvelope:
31
+ """A protocol message envelope in the cross-vendor wire form."""
32
+
33
+ op_type: str
34
+ subject: str
35
+ payload: dict
36
+ op_id: str = field(default_factory=_new_op_id)
37
+ timestamp: str = field(default_factory=_now_iso)
38
+ scope_ref: str | None = None
39
+ service_certification: str | None = None
40
+ grant_credential: str | None = None
41
+ status: str | None = None
42
+
43
+ def to_dict(self) -> dict[str, Any]:
44
+ """Serialise, omitting unset optional fields."""
45
+ out: dict[str, Any] = {
46
+ "op_id": self.op_id,
47
+ "op_type": self.op_type,
48
+ "timestamp": self.timestamp,
49
+ "subject": self.subject,
50
+ "payload": self.payload,
51
+ }
52
+ for key in ("scope_ref", "service_certification", "grant_credential", "status"):
53
+ value = getattr(self, key)
54
+ if value is not None:
55
+ out[key] = value
56
+ return out
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict[str, Any]) -> "WireEnvelope":
60
+ return cls(
61
+ op_type=data["op_type"],
62
+ subject=data["subject"],
63
+ payload=data.get("payload", {}),
64
+ op_id=data.get("op_id", _new_op_id()),
65
+ timestamp=data.get("timestamp", _now_iso()),
66
+ scope_ref=data.get("scope_ref"),
67
+ service_certification=data.get("service_certification"),
68
+ grant_credential=data.get("grant_credential"),
69
+ status=data.get("status"),
70
+ )
@@ -0,0 +1,120 @@
1
+ """Protocol error taxonomy — haiman_protocol_schemas.md section 1.5.
2
+
3
+ This is the canonical, dependency-free version of the error hierarchy that
4
+ ``app/protocol.py`` carries inline today. The monorepo refactor (step 1
5
+ follow-on) replaces its local ``ProtocolError`` with these so the wire-form
6
+ error model has one definition.
7
+
8
+ Each semantic ``type`` maps one-to-one to an HTTP code and a retry posture.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ # type -> (http_code, retryable). Mirrors the section 1.5 table.
16
+ ERROR_TYPES: dict[str, tuple[int, bool]] = {
17
+ "uncertified": (401, False),
18
+ "ungranted": (403, False),
19
+ "scope_violation": (403, False),
20
+ "schema_invalid": (400, False),
21
+ "idempotency_conflict": (409, False),
22
+ "not_found": (404, False),
23
+ "rate_limited": (429, True),
24
+ "unavailable": (503, True),
25
+ "internal_error": (500, True),
26
+ }
27
+
28
+
29
+ class ProtocolError(Exception):
30
+ """Base for protocol-layer errors.
31
+
32
+ Carries an ``error_type`` from the section 1.5 taxonomy, the matching
33
+ ``http_code``, and an optional ``retry_after`` (seconds).
34
+ """
35
+
36
+ error_type: str = "internal_error"
37
+ http_code: int = 500
38
+
39
+ def __init__(
40
+ self,
41
+ message: str,
42
+ *,
43
+ error_type: str | None = None,
44
+ http_code: int | None = None,
45
+ retry_after: int | None = None,
46
+ ):
47
+ super().__init__(message)
48
+ if error_type is not None:
49
+ self.error_type = error_type
50
+ if http_code is not None:
51
+ self.http_code = http_code
52
+ elif error_type is not None and error_type in ERROR_TYPES:
53
+ self.http_code = ERROR_TYPES[error_type][0]
54
+ self.retry_after = retry_after
55
+
56
+ @property
57
+ def retryable(self) -> bool:
58
+ return ERROR_TYPES.get(self.error_type, (self.http_code, False))[1]
59
+
60
+
61
+ class Uncertified(ProtocolError):
62
+ error_type = "uncertified"
63
+ http_code = 401
64
+
65
+
66
+ class Ungranted(ProtocolError):
67
+ error_type = "ungranted"
68
+ http_code = 403
69
+
70
+
71
+ class ScopeViolation(ProtocolError):
72
+ error_type = "scope_violation"
73
+ http_code = 403
74
+
75
+
76
+ class SchemaInvalid(ProtocolError):
77
+ error_type = "schema_invalid"
78
+ http_code = 400
79
+
80
+
81
+ class IdempotencyConflict(ProtocolError):
82
+ error_type = "idempotency_conflict"
83
+ http_code = 409
84
+
85
+
86
+ class NotFound(ProtocolError):
87
+ error_type = "not_found"
88
+ http_code = 404
89
+
90
+
91
+ class RateLimited(ProtocolError):
92
+ error_type = "rate_limited"
93
+ http_code = 429
94
+
95
+
96
+ class Unavailable(ProtocolError):
97
+ error_type = "unavailable"
98
+ http_code = 503
99
+
100
+
101
+ class InternalError(ProtocolError):
102
+ error_type = "internal_error"
103
+ http_code = 500
104
+
105
+
106
+ def error_response(op_id: str, op_type: str, err: ProtocolError) -> dict[str, Any]:
107
+ """Build the section 1.5 error envelope for an async/sync failure."""
108
+ error: dict[str, Any] = {
109
+ "code": err.http_code,
110
+ "type": err.error_type,
111
+ "message": str(err),
112
+ }
113
+ if err.retry_after is not None:
114
+ error["retry_after"] = err.retry_after
115
+ return {
116
+ "op_id": op_id,
117
+ "op_type": op_type,
118
+ "status": "error",
119
+ "error": error,
120
+ }
@@ -0,0 +1,93 @@
1
+ """Vocabularies — convenience constants for the v1 protocol.
2
+
3
+ These mirror enums encoded in the JSON Schemas and the scope/identity
4
+ vocabularies in haiman_protocol.md / haiman_protocol_schemas.md. The schemas
5
+ are the source of truth; ``tests/test_optypes_match_schemas.py`` asserts the
6
+ constants here cannot drift from the bundled schema files.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # The nine v1 operations (haiman_protocol_schemas.md section 2).
12
+ OP_TYPES: frozenset[str] = frozenset(
13
+ {
14
+ "handshake.v1",
15
+ "propose.v1",
16
+ "edit.v1",
17
+ "accept.v1",
18
+ "reject.v1",
19
+ "contribute-signal.v1",
20
+ "query.v1",
21
+ "handoff.v1",
22
+ "notify.v1",
23
+ }
24
+ )
25
+
26
+ # Typed handoffs in the v1 starter registry (section 3.2).
27
+ HANDOFF_TYPES: frozenset[str] = frozenset(
28
+ {
29
+ "engagement-flag-to-sdr.v1",
30
+ "positive-reply-to-crm.v1",
31
+ "stalled-deal-reactivate.v1",
32
+ }
33
+ )
34
+
35
+ # notify.v1 event vocabulary (section 3.3).
36
+ NOTIFY_EVENT_TYPES: frozenset[str] = frozenset(
37
+ {
38
+ "scope.revoked",
39
+ "scope.expiring",
40
+ "scope.autonomy_changed",
41
+ "profile.updated",
42
+ "signal_retention.changed",
43
+ "adapter.updated",
44
+ "op.failed",
45
+ "service.disconnected",
46
+ "handoff.received",
47
+ "schema.version_sunset",
48
+ }
49
+ )
50
+
51
+ # reject.v1 reason enum (section 2.5).
52
+ REJECT_REASONS: frozenset[str] = frozenset(
53
+ {
54
+ "off_voice",
55
+ "factually_wrong",
56
+ "wrong_tone",
57
+ "too_long",
58
+ "too_short",
59
+ "wrong_intent",
60
+ "user_changed_mind",
61
+ "other",
62
+ }
63
+ )
64
+
65
+ # contribute-signal.v1 signal types (section 2.6).
66
+ SIGNAL_TYPES: frozenset[str] = frozenset({"edit_pair", "accept", "reject", "comparative"})
67
+
68
+ # engagement-flag-to-sdr.v1 strength enum (section 3.2).
69
+ ENGAGEMENT_STRENGTH: frozenset[str] = frozenset({"warm", "hot", "cold"})
70
+
71
+ # positive-reply-to-crm.v1 signal_strength enum (section 3.2).
72
+ POSITIVE_REPLY_SIGNAL_STRENGTH: frozenset[str] = frozenset({"high", "medium", "low"})
73
+
74
+ # --- Scope / identity vocabularies (haiman_protocol.md section 2). These are
75
+ # not encoded in the op payload schemas; they govern the grant model. ---
76
+
77
+ # propose/edit channel set known at v1 (section 3.4). Extensible: receivers
78
+ # ignore unknown channels, so this is a convenience list, not a hard gate.
79
+ KNOWN_CHANNELS: frozenset[str] = frozenset(
80
+ {"email", "linkedin", "social_post", "internal_message"}
81
+ )
82
+
83
+ CONTEXTS: frozenset[str] = frozenset(
84
+ {"work", "personal", "admin", "sensitive", "custom"}
85
+ )
86
+
87
+ AUTONOMY_MODES: frozenset[str] = frozenset(
88
+ {"approval-first", "semi-auto", "full-auto"}
89
+ )
90
+
91
+ RETENTIONS: frozenset[str] = frozenset(
92
+ {"30d", "90d", "365d", "indefinite", "no-signal"}
93
+ )
File without changes
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/common/envelope.schema.json",
4
+ "title": "Haiman protocol common envelope (v1, cross-vendor wire form)",
5
+ "description": "Fixed envelope wrapping every protocol message. Identity is W3C-native: subject is a W3C DID; service_certification and grant_credential are W3C Verifiable Credentials. See the Haiman protocol specification section 1.3. Async result envelopes drop grant_credential and add a status field; the handshake op is the one op where scope_ref may be omitted. Unknown fields are ignored by receivers (forward-compatible), so additionalProperties stays permissive.",
6
+ "type": "object",
7
+ "required": ["op_id", "op_type", "timestamp", "subject", "payload"],
8
+ "properties": {
9
+ "op_id": {
10
+ "type": "string",
11
+ "description": "Idempotency key. UUID v7 in production; any UUID string accepted at the schema layer. Retries with the same op_id are no-ops at the receiver.",
12
+ "minLength": 1
13
+ },
14
+ "op_type": {
15
+ "type": "string",
16
+ "description": "op.vN format, e.g. propose.v1.",
17
+ "pattern": "^[a-z][a-z-]*\\.v[0-9]+$"
18
+ },
19
+ "timestamp": {
20
+ "type": "string",
21
+ "format": "date-time",
22
+ "description": "Sender's clock, ISO 8601. Receivers should not reject on skew alone."
23
+ },
24
+ "service_certification": {
25
+ "type": "string",
26
+ "description": "Compact reference (URN) to a W3C VC of type HaimanServiceCertificationCredential issued by Haiman as conformance authority. Required on requests; omitted on async results.",
27
+ "minLength": 1
28
+ },
29
+ "grant_credential": {
30
+ "type": "string",
31
+ "description": "User-issued, Haiman-signed W3C VC (JWT-VC compact form) proving this user authorised this service for these scopes. Present on requests; dropped on async result envelopes.",
32
+ "minLength": 1
33
+ },
34
+ "subject": {
35
+ "type": "string",
36
+ "description": "The user identifier as a W3C DID. v1 uses did:web; did:haiman reserved.",
37
+ "pattern": "^did:[a-z0-9]+:.+"
38
+ },
39
+ "scope_ref": {
40
+ "type": "string",
41
+ "description": "The specific granted scope this op operates within. Optional only for handshake."
42
+ },
43
+ "status": {
44
+ "type": "string",
45
+ "description": "Present on async result envelopes only.",
46
+ "enum": ["pending", "complete", "error"]
47
+ },
48
+ "payload": {
49
+ "type": "object",
50
+ "description": "Op-specific payload; validate against the matching ops/<op_type>.schema.json."
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/common/error.schema.json",
4
+ "title": "Haiman protocol error envelope (v1)",
5
+ "description": "Returned in place of a result when an op fails. See the Haiman protocol specification section 1.5. The error.type taxonomy maps one-to-one to an HTTP code and a retry posture.",
6
+ "type": "object",
7
+ "required": ["op_id", "op_type", "status", "error"],
8
+ "properties": {
9
+ "op_id": { "type": "string", "minLength": 1 },
10
+ "op_type": { "type": "string", "pattern": "^[a-z][a-z-]*\\.v[0-9]+$" },
11
+ "status": { "const": "error" },
12
+ "error": {
13
+ "type": "object",
14
+ "required": ["code", "type", "message"],
15
+ "properties": {
16
+ "code": {
17
+ "type": "integer",
18
+ "description": "HTTP status code for the semantic type."
19
+ },
20
+ "type": {
21
+ "type": "string",
22
+ "description": "Semantic error type. Drives retry behaviour.",
23
+ "enum": [
24
+ "uncertified",
25
+ "ungranted",
26
+ "scope_violation",
27
+ "schema_invalid",
28
+ "idempotency_conflict",
29
+ "not_found",
30
+ "rate_limited",
31
+ "unavailable",
32
+ "internal_error"
33
+ ]
34
+ },
35
+ "message": { "type": "string" },
36
+ "retry_after": {
37
+ "type": "integer",
38
+ "description": "Seconds; populated for rate_limited (and may be set for unavailable)."
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/common/tone.v1.schema.json",
4
+ "title": "tone.v1 value schema",
5
+ "description": "Common value schema for a tone result (e.g. query.v1 user.preferences.tone.default). See the Haiman protocol specification section 3.5. primary is drawn from a published vocabulary; modifiers are free-form short tags, unknown ones ignored gracefully.",
6
+ "type": "object",
7
+ "required": ["primary"],
8
+ "properties": {
9
+ "primary": {
10
+ "type": "string",
11
+ "description": "Enum from a published vocabulary (warm-direct, formal-precise, casual-friendly, ...). Not hard-enumed here: the vocabulary grows under minor revisions and receivers ignore unknown values."
12
+ },
13
+ "modifiers": {
14
+ "type": "array",
15
+ "items": { "type": "string" }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/common/topic.v1.schema.json",
4
+ "title": "topic.v1 value schema",
5
+ "description": "Common value schema for an engaged topic (e.g. query.v1 user.context.recent_topics). See the Haiman protocol specification section 3.5.",
6
+ "type": "object",
7
+ "required": ["label"],
8
+ "properties": {
9
+ "label": { "type": "string" },
10
+ "weight": { "type": "number", "minimum": 0, "maximum": 1 },
11
+ "last_engaged_at": { "type": "string", "format": "date-time" }
12
+ }
13
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/handoffs/engagement-flag-to-sdr.v1.schema.json",
4
+ "title": "engagement-flag-to-sdr.v1 inner_payload",
5
+ "description": "Social engagement -> SDR outreach handoff. See the Haiman protocol specification section 3.2. display_name and email_or_handle are required at the protocol floor even when the upstream can only supply URN values; role and suggested_outreach_angle are optional.",
6
+ "type": "object",
7
+ "required": ["engaged_contact", "engagement_type", "engagement_context", "engagement_strength"],
8
+ "properties": {
9
+ "engaged_contact": {
10
+ "type": "object",
11
+ "required": ["display_name", "email_or_handle", "platform"],
12
+ "properties": {
13
+ "display_name": { "type": "string", "minLength": 1 },
14
+ "role": { "type": "string" },
15
+ "email_or_handle": { "type": "string", "minLength": 1 },
16
+ "platform": { "type": "string", "minLength": 1 }
17
+ }
18
+ },
19
+ "engagement_type": { "type": "string", "minLength": 1 },
20
+ "engagement_context": {
21
+ "type": "object",
22
+ "required": ["post_topic", "engagement_text", "engagement_at"],
23
+ "properties": {
24
+ "post_topic": { "type": "string", "minLength": 1 },
25
+ "engagement_text": { "type": "string", "minLength": 1 },
26
+ "engagement_at": { "type": "string", "format": "date-time" }
27
+ }
28
+ },
29
+ "engagement_strength": {
30
+ "type": "string",
31
+ "enum": ["warm", "hot", "cold"]
32
+ },
33
+ "suggested_outreach_angle": { "type": "string" }
34
+ }
35
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/handoffs/positive-reply-to-crm.v1.schema.json",
4
+ "title": "positive-reply-to-crm.v1 inner_payload",
5
+ "description": "SDR positive reply -> CRM account creation handoff. See the Haiman protocol specification section 3.2. lead_contact.company.domain must be present but may be null (free-email senders). role is optional. source_lead_id / source_company_id are first-party row ids that let the receiver land the deal in the right tenant.",
6
+ "type": "object",
7
+ "required": ["lead_contact", "conversation_summary", "signal_strength", "suggested_account_name", "suggested_deal_stage", "outreach_thread_ref"],
8
+ "properties": {
9
+ "lead_contact": {
10
+ "type": "object",
11
+ "required": ["display_name", "email", "company"],
12
+ "properties": {
13
+ "display_name": { "type": "string", "minLength": 1 },
14
+ "role": { "type": "string" },
15
+ "email": { "type": "string", "minLength": 1 },
16
+ "company": {
17
+ "type": "object",
18
+ "required": ["name", "domain"],
19
+ "properties": {
20
+ "name": { "type": "string", "minLength": 1 },
21
+ "domain": {
22
+ "type": ["string", "null"],
23
+ "description": "Present-but-null is acceptable for free-email senders; absent entirely is not."
24
+ }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "conversation_summary": { "type": "string", "minLength": 1 },
30
+ "signal_strength": {
31
+ "type": "string",
32
+ "enum": ["high", "medium", "low"]
33
+ },
34
+ "suggested_account_name": { "type": "string", "minLength": 1 },
35
+ "suggested_deal_stage": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "description": "Non-empty string; the CRM service owns its own stage vocabulary, so no enum here."
39
+ },
40
+ "outreach_thread_ref": { "type": "string", "minLength": 1 },
41
+ "source_lead_id": {
42
+ "type": ["integer", "null"],
43
+ "description": "Originating SDR lead id. Optional authoritative linkage; absent or null when the sender cannot supply it (receiver falls back to email lookup)."
44
+ },
45
+ "source_company_id": {
46
+ "type": ["integer", "null"],
47
+ "description": "Originating tenant id. Optional; null when the lead carries no company_id."
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/handoffs/stalled-deal-reactivate.v1.schema.json",
4
+ "title": "stalled-deal-reactivate.v1 inner_payload",
5
+ "description": "CRM stalled deal -> SDR re-engagement handoff. See the Haiman protocol specification section 3.2. Floor is stalled_account with a name; the remaining fields are advisory context for the re-engagement angle. Spec-defined; not yet wired in the live intra-Haiman validator.",
6
+ "type": "object",
7
+ "required": ["stalled_account"],
8
+ "properties": {
9
+ "stalled_account": {
10
+ "type": "object",
11
+ "required": ["name"],
12
+ "properties": {
13
+ "name": { "type": "string", "minLength": 1 },
14
+ "domain": { "type": ["string", "null"] },
15
+ "primary_contact": {
16
+ "type": "object",
17
+ "properties": {
18
+ "display_name": { "type": "string" },
19
+ "role": { "type": "string" },
20
+ "email": { "type": "string" }
21
+ }
22
+ },
23
+ "deal_stage_at_stall": { "type": "string" }
24
+ }
25
+ },
26
+ "last_activity_at": { "type": "string", "format": "date-time" },
27
+ "weeks_stalled": { "type": "integer", "minimum": 0 },
28
+ "last_topic": { "type": "string" },
29
+ "suggested_reactivation_angle": { "type": "string" }
30
+ }
31
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://haiman.ai/protocol/schemas/ops/accept.v1.schema.json",
4
+ "title": "accept.v1 request payload",
5
+ "description": "Terminal user accept on a proposal; emits accept signal. See the Haiman protocol specification section 2.4. final_content may differ slightly from the proposal (last-second tweaks); AI treats a difference as a tiny implicit edit.",
6
+ "type": "object",
7
+ "required": ["proposal_id", "final_content", "user_action_at"],
8
+ "properties": {
9
+ "proposal_id": { "type": "string", "minLength": 1 },
10
+ "final_content": {
11
+ "type": "object",
12
+ "description": "Per-channel shape (section 3.4)."
13
+ },
14
+ "user_action_at": { "type": "string", "format": "date-time" }
15
+ }
16
+ }