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.
- haiman_protocol/__init__.py +99 -0
- haiman_protocol/envelope.py +70 -0
- haiman_protocol/errors.py +120 -0
- haiman_protocol/optypes.py +93 -0
- haiman_protocol/py.typed +0 -0
- haiman_protocol/schemas/common/envelope.schema.json +53 -0
- haiman_protocol/schemas/common/error.schema.json +43 -0
- haiman_protocol/schemas/common/tone.v1.schema.json +18 -0
- haiman_protocol/schemas/common/topic.v1.schema.json +13 -0
- haiman_protocol/schemas/handoffs/engagement-flag-to-sdr.v1.schema.json +35 -0
- haiman_protocol/schemas/handoffs/positive-reply-to-crm.v1.schema.json +50 -0
- haiman_protocol/schemas/handoffs/stalled-deal-reactivate.v1.schema.json +31 -0
- haiman_protocol/schemas/ops/accept.v1.schema.json +16 -0
- haiman_protocol/schemas/ops/contribute-signal.v1.schema.json +77 -0
- haiman_protocol/schemas/ops/edit.v1.schema.json +26 -0
- haiman_protocol/schemas/ops/handoff.v1.schema.json +23 -0
- haiman_protocol/schemas/ops/handshake.v1.schema.json +31 -0
- haiman_protocol/schemas/ops/notify.v1.schema.json +29 -0
- haiman_protocol/schemas/ops/propose.v1.schema.json +35 -0
- haiman_protocol/schemas/ops/query.v1.schema.json +19 -0
- haiman_protocol/schemas/ops/reject.v1.schema.json +31 -0
- haiman_protocol/store.py +108 -0
- haiman_protocol/validate.py +83 -0
- haiman_protocol/version.py +10 -0
- haiman_protocol-0.1.0.dist-info/METADATA +118 -0
- haiman_protocol-0.1.0.dist-info/RECORD +29 -0
- haiman_protocol-0.1.0.dist-info/WHEEL +4 -0
- haiman_protocol-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
)
|
haiman_protocol/py.typed
ADDED
|
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
|
+
}
|