eclipse-gig-core 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.
bazaar-manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"bench":{"bench_verdict":"CERTIFIED(local)","certified":true,"certified_scope":"local","checks":["exact-address brief refused (ZIP-tier only)","approval binds the exact brief hash","post-approval edit invalidates approval","UNEXPOSED-fee quote never hireable under a ceiling","owner exception: all-in computed, status stays UNEXPOSED","mandate ALLOW without person token is not hireable","platform-done vs owner-not_done opens an exception; states never merge","review draft carries no quality claims the record lacks","leverage language refused at draft time","capability denylist: exactly the 10 G0 tools","receipt chain valid; tamper detected","auth enforced","schema valid","no network surface (local-only build)","no secret leak","sane errors","process health","audit completeness","bench isolation"],"not_tested":["Live platform calls of any kind — G0 contains no live marketplace code path; briefs, quotes, mandates, and verifications are local records evaluated against shipped test data.","Adapter variant approval binding against a live adapter — the Compass fixture adapter is the first consumer; cross-product bench is a later run.","Photo EXIF stripping tooling itself — the core records the strip status per photo as supplied; the stripping tool is adapter/build scope [verify] per spec 5.1."],"scorecard_ref":"test-bench/results/eclipse-gig-core-mcp-2026-10-25.json","status":"CERTIFIED(local) — Eclipse Bazaar Test Bench 2026-10-25, 18/19 checks pass, 1 na (U1 auth: stdio process boundary). Run by W-003 (Verity), suites/gig_core.py."},"category":"Gig / Task Marketplace Infrastructure","created":"2026-10-25","description":"The shared core of the Eclipse gig adapter family: one canonical, owner-approved Task Brief (versioned, SHA-256 hash-bound; any post-approval edit invalidates approval) becomes a native request on each task marketplace through its adapter. The core normalizes quotes from every platform model (lead, stated-budget, fixed-price, hourly) to one record with face price and itemized fees forever separate and a single ALL-IN total; a quote whose fees are UNEXPOSED cannot be hired under a ceiling absent a written per-quote owner exception. The hire gate evaluates a live Eclipse Procure / Agent Wallet mandate (cap, category + platform allowlists, approval threshold, expiry) and returns receipted ALLOW/DENY decisions; in v1, selecting a person requires an owner-confirm token even under a mandate. Completion verification keeps platform state and owner attestation in separate fields (platform-done is not verified), and review drafts are generated only from the verification record. G0 executes nothing and calls nothing live.","icon":"icon-512.png","name":"Eclipse Gig Core MCP","powered_by":"Cyan Eclipse Technologies","price_model":"Free local v0.1; family pricing TBD per spec","repo":null,"slug":"eclipse-gig-core-mcp","status":"In development — v0.1.0 G0 local build (Task Brief core + quote normalization + hire gate evaluation; fixtures only; CERTIFIED(local) — Eclipse Bazaar Test Bench 2026-10-25)","tagline":"One approved Task Brief, every marketplace: hash-bound briefs, all-in quotes, and a hire that runs only inside a mandate.","tools":["brief_create","brief_update","brief_approve","brief_read","quote_normalize","quote_read","hire_check","completion_verify","review_draft","receipt_list"],"transport":"MCP over stdio (HTTP transport on roadmap)","vendor":"Cyan Eclipse Technologies","x402_ready":false}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: eclipse-gig-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Eclipse Gig Core MCP — the shared core of the Eclipse gig adapter family: one canonical, owner-approved, hash-bound Task Brief; quote normalization to face price + itemized fees + one all-in total; and the mandate hire gate. Fixtures and evaluation only: no live platform call, no hire executed, no payment moved.
|
|
5
|
+
Project-URL: Homepage, https://cetech.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/CYAN-ECLIPSE-TECHNOLOGIES-LLC/eclipse-gig-core-mcp
|
|
7
|
+
Project-URL: Vendor, https://cetech.ai
|
|
8
|
+
Author: Cyan Eclipse Technologies LLC
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: gig-economy,mcp,model-context-protocol,procurement,task-marketplace
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# eclipse-gig-core
|
|
21
|
+
|
|
22
|
+
mcp-name: io.github.CYAN-ECLIPSE-TECHNOLOGIES-LLC/gig-core
|
|
23
|
+
|
|
24
|
+
**What this is:** the MCP server for **Eclipse Gig Core** — the shared
|
|
25
|
+
core of Cyan Eclipse Technologies' Eclipse gig adapter family. One
|
|
26
|
+
canonical, owner-approved Task Brief becomes a native request, offer
|
|
27
|
+
round, booking, or prep pack on each task marketplace, according to
|
|
28
|
+
what that platform actually sanctions.
|
|
29
|
+
|
|
30
|
+
**What this is not:** phase G0 is fixtures and evaluation only. **No
|
|
31
|
+
live platform call exists in this code, no hire is executed, no review
|
|
32
|
+
is submitted, and no payment moves.** Hiring happens only inside an
|
|
33
|
+
Eclipse Procure / Agent Wallet mandate, evaluated — never executed —
|
|
34
|
+
by this core.
|
|
35
|
+
|
|
36
|
+
## Run it
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
$ pip install eclipse-gig-core
|
|
40
|
+
$ eclipse-gig-core # MCP over stdio, Python stdlib only
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
MCP client config:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{ "mcpServers": { "eclipse-gig-core": { "command": "uvx", "args": ["eclipse-gig-core"] } } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Tools (10)
|
|
50
|
+
|
|
51
|
+
`brief_create` · `brief_update` · `brief_approve` · `brief_read` ·
|
|
52
|
+
`quote_normalize` · `quote_read` · `hire_check` · `completion_verify` ·
|
|
53
|
+
`review_draft` · `receipt_list`
|
|
54
|
+
|
|
55
|
+
## Honest scope
|
|
56
|
+
|
|
57
|
+
- The Task Brief is versioned and SHA-256 hash-bound; any
|
|
58
|
+
post-approval edit invalidates the approval. Quotes normalize every
|
|
59
|
+
platform model (lead, stated-budget, fixed-price, hourly) to face
|
|
60
|
+
price and itemized fees — forever separate — plus one all-in total;
|
|
61
|
+
a quote whose fees are unexposed cannot be hireable under a ceiling.
|
|
62
|
+
- The hire gate returns ALLOW / DENY with named reasons, receipted
|
|
63
|
+
either way; in v1, selecting a person requires an owner-confirm
|
|
64
|
+
token even when a mandate allows the money.
|
|
65
|
+
- State persists in `data/state.json` beside the server (override with
|
|
66
|
+
`ECLIPSE_GIG_CORE_STATE`).
|
|
67
|
+
- Certified (local) by the Eclipse Test Bench at v0.1.0; the listing
|
|
68
|
+
status in the Eclipse Bazaar catalog is in-development.
|
|
69
|
+
|
|
70
|
+
Powered by Cyan Eclipse Technologies LLC (cetech.ai) — a Veteran-Owned
|
|
71
|
+
Small Business. MIT licensed.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
bazaar-manifest.json,sha256=ZHINaFHszZXpx4JIZzhj3h9_jyHQ7YpR2Z1tgBOejs8,3474
|
|
2
|
+
server.py,sha256=0Lnks3qs7IswwDrFTMjYU4tYUoC8ltAn9z6OoEsoypg,37125
|
|
3
|
+
eclipse_gig_core-0.1.0.dist-info/METADATA,sha256=dceSYh9v_j974HtUfcufvZSfOKoMZAu6xLJUrB5IjGA,3025
|
|
4
|
+
eclipse_gig_core-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
5
|
+
eclipse_gig_core-0.1.0.dist-info/entry_points.txt,sha256=95dyQ5aC-s4cw1kFnOlxPOZjJDAQ1QHwdPNQpbnDeXc,49
|
|
6
|
+
eclipse_gig_core-0.1.0.dist-info/licenses/LICENSE,sha256=J64raxD33SRnnCn8p1F-lRl0rnesurM9Jlkd3auDZvA,1086
|
|
7
|
+
eclipse_gig_core-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cyan Eclipse Technologies LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
server.py
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Eclipse Gig Core MCP
|
|
3
|
+
# Copyright © 2026 Cyan Eclipse Technologies LLC · cetech.ai
|
|
4
|
+
|
|
5
|
+
"""Eclipse Gig Core MCP — v0.1.0, phase G0 (shared Task Brief core).
|
|
6
|
+
|
|
7
|
+
Powered by Cyan Eclipse Technologies (cetech.ai).
|
|
8
|
+
|
|
9
|
+
The Eclipse gig adapter family — one canonical, owner-approved Task
|
|
10
|
+
Brief becomes a native request, offer round, booking, or prep pack on
|
|
11
|
+
each task marketplace, according to what that platform actually
|
|
12
|
+
sanctions. Hiring happens only inside an Eclipse Procure / Agent
|
|
13
|
+
Wallet mandate. It never diverts a hire off-platform, never invents
|
|
14
|
+
a sanctioned surface that does not exist, and never posts a brief
|
|
15
|
+
the owner has not approved.
|
|
16
|
+
|
|
17
|
+
This server is the SHARED CORE (spec eclipse-gig-family-spec-20261025
|
|
18
|
+
sections 5-7). It owns:
|
|
19
|
+
* the canonical Task Brief object — versioned, SHA-256 hash-bound,
|
|
20
|
+
owner-approved; any post-approval edit invalidates approval;
|
|
21
|
+
* the quote normalization engine — every platform model (lead,
|
|
22
|
+
stated-budget, fixed-price, hourly) normalizes to one record with
|
|
23
|
+
face price and itemized fees FOREVER separate and one ALL-IN
|
|
24
|
+
total; a quote whose fees are UNEXPOSED cannot be hireable under
|
|
25
|
+
a ceiling;
|
|
26
|
+
* the hire gate — a mandate evaluation (cap, category + platform
|
|
27
|
+
allowlists, approval threshold, expiry) returning ALLOW/DENY,
|
|
28
|
+
receipted either way. In v1, selecting a PERSON requires an
|
|
29
|
+
owner-confirm token even when a mandate allows the money;
|
|
30
|
+
* completion verification records — platform state and owner
|
|
31
|
+
verification are separate fields and are never merged;
|
|
32
|
+
* review drafts generated ONLY from a verification record.
|
|
33
|
+
|
|
34
|
+
G0 SCOPE: fixtures and evaluation only. NO live platform call exists
|
|
35
|
+
in this file, no hire is executed, no review is submitted, and no
|
|
36
|
+
payment moves. The platform is merchant of record on every real hire
|
|
37
|
+
(spec section 6); this core only evaluates and records.
|
|
38
|
+
|
|
39
|
+
Transport: MCP over stdio (newline-delimited JSON-RPC 2.0), stdlib only.
|
|
40
|
+
State: $ECLIPSE_GIG_CORE_STATE or ./data/state.json beside this file.
|
|
41
|
+
"""
|
|
42
|
+
import hashlib
|
|
43
|
+
import json
|
|
44
|
+
import os
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
import time
|
|
48
|
+
from datetime import datetime, timezone
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
|
|
51
|
+
HERE = Path(__file__).parent
|
|
52
|
+
STATE_PATH = Path(os.environ.get(
|
|
53
|
+
"ECLIPSE_GIG_CORE_STATE", HERE / "data" / "state.json"))
|
|
54
|
+
|
|
55
|
+
BRIEF_CONTENT_FIELDS = [
|
|
56
|
+
"category", "service", "scope", "location", "timing",
|
|
57
|
+
"ceiling_all_in", "tip_policy", "photos", "license_required",
|
|
58
|
+
"safety_access", "intended_platforms", "prohibited_platforms",
|
|
59
|
+
"details",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
QUOTE_MODELS = {"lead_model", "stated_budget", "fixed_price", "hourly"}
|
|
63
|
+
|
|
64
|
+
# Words a review draft may only contain when the verification record
|
|
65
|
+
# itself carries quality notes (spec 7.3: no sentiment not in record).
|
|
66
|
+
QUALITY_CLAIM_WORDS = ["excellent", "great", "superb", "outstanding",
|
|
67
|
+
"fantastic", "quality", "wonderful", "amazing"]
|
|
68
|
+
|
|
69
|
+
# Prohibited leverage language, filtered at draft time (spec 7.3).
|
|
70
|
+
LEVERAGE_PATTERNS = ["refund or", "or i will post", "or i'll post",
|
|
71
|
+
"take this down", "or else i", "one star unless",
|
|
72
|
+
"1 star unless", "negative review unless"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ToolError(Exception):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def now_iso() -> str:
|
|
80
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_time(value):
|
|
84
|
+
if value is None:
|
|
85
|
+
return None
|
|
86
|
+
text = str(value).strip()
|
|
87
|
+
if len(text) == 10:
|
|
88
|
+
text += "T00:00:00+00:00"
|
|
89
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sha(text: str) -> str:
|
|
93
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def canon(obj) -> str:
|
|
97
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def money(value) -> float:
|
|
101
|
+
return round(float(value), 2)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --------------------------------------------------------------------- state
|
|
105
|
+
def empty_state() -> dict:
|
|
106
|
+
return {"briefs": {}, "quotes": {}, "verifications": {},
|
|
107
|
+
"receipts": []}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_state() -> dict:
|
|
111
|
+
if STATE_PATH.exists():
|
|
112
|
+
return json.loads(STATE_PATH.read_text())
|
|
113
|
+
return empty_state()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def save_state(state: dict) -> None:
|
|
117
|
+
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
STATE_PATH.write_text(json.dumps(state, indent=1))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ receipts
|
|
122
|
+
def emit_receipt(state: dict, rtype: str, detail: dict) -> dict:
|
|
123
|
+
"""Append-only, hash-chained (house pattern)."""
|
|
124
|
+
prev = state["receipts"][-1]["hash"] if state["receipts"] else "GENESIS"
|
|
125
|
+
entry = {"seq": len(state["receipts"]) + 1, "ts": int(time.time()),
|
|
126
|
+
"type": rtype, "prev_hash": prev, "detail": detail}
|
|
127
|
+
entry["hash"] = sha(prev + canon(
|
|
128
|
+
{k: v for k, v in entry.items() if k != "hash"}))
|
|
129
|
+
state["receipts"].append(entry)
|
|
130
|
+
return entry
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def verify_receipt_chain(receipts) -> bool:
|
|
134
|
+
prev = "GENESIS"
|
|
135
|
+
for entry in receipts:
|
|
136
|
+
if entry.get("prev_hash") != prev:
|
|
137
|
+
return False
|
|
138
|
+
expected = sha(prev + canon(
|
|
139
|
+
{k: v for k, v in entry.items() if k != "hash"}))
|
|
140
|
+
if entry.get("hash") != expected:
|
|
141
|
+
return False
|
|
142
|
+
prev = entry["hash"]
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --------------------------------------------------------------------- briefs
|
|
147
|
+
def brief_content(brief: dict) -> dict:
|
|
148
|
+
return {k: brief.get(k) for k in BRIEF_CONTENT_FIELDS}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def brief_hash(brief: dict) -> str:
|
|
152
|
+
return sha(canon(brief_content(brief)))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def validate_brief_content(content: dict) -> None:
|
|
156
|
+
if not str(content.get("category") or "").strip():
|
|
157
|
+
raise ToolError("brief requires a category")
|
|
158
|
+
if not str(content.get("service") or "").strip():
|
|
159
|
+
raise ToolError("brief requires a service")
|
|
160
|
+
scope = content.get("scope") or {}
|
|
161
|
+
if not str(scope.get("statement") or "").strip():
|
|
162
|
+
raise ToolError("brief requires scope.statement "
|
|
163
|
+
"(what done looks like)")
|
|
164
|
+
location = content.get("location") or {}
|
|
165
|
+
precision = str(location.get("precision") or "zip").lower()
|
|
166
|
+
if precision != "zip":
|
|
167
|
+
raise ToolError(
|
|
168
|
+
"LOCATION_PRECISION: briefs carry ZIP-tier location only; "
|
|
169
|
+
"the exact address is released only inside a platform's "
|
|
170
|
+
"own post-hire flow (spec 5.1/8.4)")
|
|
171
|
+
if not str(location.get("zip") or "").strip():
|
|
172
|
+
raise ToolError("brief requires location.zip (ZIP-tier)")
|
|
173
|
+
timing = content.get("timing") or {}
|
|
174
|
+
if not timing.get("window_start") or not timing.get("window_end"):
|
|
175
|
+
raise ToolError("brief requires timing.window_start/window_end")
|
|
176
|
+
ceiling = content.get("ceiling_all_in")
|
|
177
|
+
if ceiling is None or float(ceiling) <= 0:
|
|
178
|
+
raise ToolError("brief requires a positive ceiling_all_in "
|
|
179
|
+
"(one ALL-IN number the owner authorizes)")
|
|
180
|
+
license_required = content.get("license_required")
|
|
181
|
+
if license_required not in ("unknown", "no") and not (
|
|
182
|
+
isinstance(license_required, str)
|
|
183
|
+
and license_required.startswith("yes:")):
|
|
184
|
+
raise ToolError("license_required must be 'unknown', 'no', or "
|
|
185
|
+
"'yes:<type>' (spec 5.1)")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def public_brief(brief: dict) -> dict:
|
|
189
|
+
out = json.loads(json.dumps(brief))
|
|
190
|
+
out["approval_valid"] = bool(
|
|
191
|
+
brief.get("approval")
|
|
192
|
+
and brief["approval"].get("brief_hash") == brief["hash"])
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def t_brief_create(state, args):
|
|
197
|
+
content = {k: args.get(k) for k in BRIEF_CONTENT_FIELDS}
|
|
198
|
+
content["photos"] = content.get("photos") or []
|
|
199
|
+
content["intended_platforms"] = content.get("intended_platforms") or []
|
|
200
|
+
content["prohibited_platforms"] = \
|
|
201
|
+
content.get("prohibited_platforms") or []
|
|
202
|
+
content["details"] = content.get("details") or {}
|
|
203
|
+
validate_brief_content(content)
|
|
204
|
+
photos = []
|
|
205
|
+
for photo in content["photos"]:
|
|
206
|
+
photos.append({
|
|
207
|
+
"ref": str(photo.get("ref", "")),
|
|
208
|
+
# EXIF/geolocation stripping is recorded on the photo
|
|
209
|
+
# record itself; unstripped photos are flagged, and an
|
|
210
|
+
# adapter must not use them (spec 5.1).
|
|
211
|
+
"exif_stripped": bool(photo.get("exif_stripped", False)),
|
|
212
|
+
"exif_note": ("EXIF/geolocation stripped before adapter use"
|
|
213
|
+
if photo.get("exif_stripped")
|
|
214
|
+
else "NOT stripped — adapter use blocked"),
|
|
215
|
+
})
|
|
216
|
+
content["photos"] = photos
|
|
217
|
+
brief_id = str(args.get("brief_id") or "")
|
|
218
|
+
if not brief_id:
|
|
219
|
+
brief_id = "brief_" + sha(canon(content) + now_iso())[:12]
|
|
220
|
+
if brief_id in state["briefs"]:
|
|
221
|
+
raise ToolError(f"brief already exists: {brief_id}")
|
|
222
|
+
brief = dict(content)
|
|
223
|
+
brief.update({"brief_id": brief_id, "version": 1,
|
|
224
|
+
"status": "draft", "approval": None})
|
|
225
|
+
brief["hash"] = brief_hash(brief)
|
|
226
|
+
state["briefs"][brief_id] = brief
|
|
227
|
+
receipt = emit_receipt(state, "brief_created", {
|
|
228
|
+
"brief_id": brief_id, "version": 1, "hash": brief["hash"],
|
|
229
|
+
"category": brief["category"],
|
|
230
|
+
"ceiling_all_in": money(brief["ceiling_all_in"])})
|
|
231
|
+
return {"brief": public_brief(brief), "receipt_seq": receipt["seq"]}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def t_brief_update(state, args):
|
|
235
|
+
brief_id = str(args.get("brief_id") or "")
|
|
236
|
+
brief = state["briefs"].get(brief_id)
|
|
237
|
+
if not brief:
|
|
238
|
+
raise ToolError(f"unknown brief: {brief_id}")
|
|
239
|
+
changes = args.get("changes") or {}
|
|
240
|
+
unknown = set(changes) - set(BRIEF_CONTENT_FIELDS)
|
|
241
|
+
if unknown:
|
|
242
|
+
raise ToolError(f"not brief content fields: {sorted(unknown)}")
|
|
243
|
+
was_approved = bool(brief.get("approval"))
|
|
244
|
+
old_hash = brief["hash"]
|
|
245
|
+
for key, value in changes.items():
|
|
246
|
+
brief[key] = value
|
|
247
|
+
validate_brief_content(brief_content(brief))
|
|
248
|
+
brief["version"] += 1
|
|
249
|
+
brief["hash"] = brief_hash(brief)
|
|
250
|
+
# Any post-approval edit invalidates the approval for the changed
|
|
251
|
+
# brief (spec 5.1: approval binds hashes, not vibes).
|
|
252
|
+
brief["approval"] = None
|
|
253
|
+
brief["status"] = "draft"
|
|
254
|
+
receipt = emit_receipt(state, "brief_version_created", {
|
|
255
|
+
"brief_id": brief_id, "version": brief["version"],
|
|
256
|
+
"old_hash": old_hash, "new_hash": brief["hash"],
|
|
257
|
+
"changed_fields": sorted(changes),
|
|
258
|
+
"approval_invalidated": was_approved})
|
|
259
|
+
return {"brief": public_brief(brief), "receipt_seq": receipt["seq"],
|
|
260
|
+
"approval_invalidated": was_approved}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def t_brief_approve(state, args):
|
|
264
|
+
brief_id = str(args.get("brief_id") or "")
|
|
265
|
+
brief = state["briefs"].get(brief_id)
|
|
266
|
+
if not brief:
|
|
267
|
+
raise ToolError(f"unknown brief: {brief_id}")
|
|
268
|
+
claimed = str(args.get("brief_hash") or "")
|
|
269
|
+
if claimed != brief["hash"]:
|
|
270
|
+
raise ToolError(
|
|
271
|
+
"approval binds the exact brief hash: supplied "
|
|
272
|
+
f"{claimed or '(none)'} != current {brief['hash']}")
|
|
273
|
+
approval = {
|
|
274
|
+
"approver": str(args.get("approver") or "owner"),
|
|
275
|
+
"approved_at": str(args.get("approved_at") or now_iso()),
|
|
276
|
+
"brief_hash": brief["hash"],
|
|
277
|
+
"variant_hashes": args.get("variant_hashes") or {},
|
|
278
|
+
}
|
|
279
|
+
brief["approval"] = approval
|
|
280
|
+
brief["status"] = "approved"
|
|
281
|
+
receipt = emit_receipt(state, "brief_approved", {
|
|
282
|
+
"brief_id": brief_id, "version": brief["version"],
|
|
283
|
+
"brief_hash": brief["hash"], "approver": approval["approver"],
|
|
284
|
+
"variant_hashes": approval["variant_hashes"]})
|
|
285
|
+
return {"brief": public_brief(brief), "receipt_seq": receipt["seq"]}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def t_brief_read(state, args):
|
|
289
|
+
brief = state["briefs"].get(str(args.get("brief_id") or ""))
|
|
290
|
+
if not brief:
|
|
291
|
+
raise ToolError(f"unknown brief: {args.get('brief_id')}")
|
|
292
|
+
return {"brief": public_brief(brief)}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --------------------------------------------------------------------- quotes
|
|
296
|
+
def normalize_quote(raw: dict) -> dict:
|
|
297
|
+
"""Normalize any platform model to one quote record (spec 5.3).
|
|
298
|
+
|
|
299
|
+
Face price and itemized fees stay separate forever. The ALL-IN
|
|
300
|
+
total counts the face plus buyer-borne fees plus platform-stated
|
|
301
|
+
tax. Pro-borne fees are itemized (they explain quote levels) but
|
|
302
|
+
are not charged to the buyer. If the platform does not expose its
|
|
303
|
+
fees, the quote is UNEXPOSED and carries no all-in total at all.
|
|
304
|
+
"""
|
|
305
|
+
model = str(raw.get("model") or "")
|
|
306
|
+
if model not in QUOTE_MODELS:
|
|
307
|
+
raise ToolError(f"unknown quote model: {model or '(none)'}; "
|
|
308
|
+
f"expected one of {sorted(QUOTE_MODELS)}")
|
|
309
|
+
pro = raw.get("pro") or {}
|
|
310
|
+
if not pro.get("platform_id"):
|
|
311
|
+
raise ToolError("quote requires pro.platform_id")
|
|
312
|
+
hourly = None
|
|
313
|
+
if model == "hourly":
|
|
314
|
+
rate = raw.get("hourly_rate")
|
|
315
|
+
cap = raw.get("hour_cap")
|
|
316
|
+
if rate is None or cap is None:
|
|
317
|
+
raise ToolError("hourly quotes require hourly_rate and the "
|
|
318
|
+
"owner-approved hour_cap (spec 5.3)")
|
|
319
|
+
face = money(float(rate) * float(cap))
|
|
320
|
+
hourly = {"rate": money(rate), "hour_cap": float(cap),
|
|
321
|
+
"basis": "rate x owner-approved hour cap; overruns "
|
|
322
|
+
"return to review, never auto-extend"}
|
|
323
|
+
else:
|
|
324
|
+
if raw.get("job_price") is None:
|
|
325
|
+
raise ToolError(f"model {model} requires job_price")
|
|
326
|
+
face = money(raw["job_price"])
|
|
327
|
+
fees = []
|
|
328
|
+
for fee in raw.get("fees") or []:
|
|
329
|
+
bearer = str(fee.get("bearer") or "")
|
|
330
|
+
if bearer not in ("buyer", "pro", "split"):
|
|
331
|
+
raise ToolError("each fee requires bearer: buyer|pro|split")
|
|
332
|
+
fees.append({"name": str(fee.get("name") or "fee"),
|
|
333
|
+
"amount": money(fee["amount"]), "bearer": bearer})
|
|
334
|
+
bearers = {f["bearer"] for f in fees}
|
|
335
|
+
fee_bearer = ("split" if len(bearers) > 1
|
|
336
|
+
else (bearers.pop() if bearers else "none"))
|
|
337
|
+
fees_exposed = bool(raw.get("fees_exposed", False))
|
|
338
|
+
tax = money(raw.get("tax") or 0)
|
|
339
|
+
buyer_fees = money(sum(f["amount"] for f in fees
|
|
340
|
+
if f["bearer"] in ("buyer", "split")))
|
|
341
|
+
exception = raw.get("owner_fee_exception") or None
|
|
342
|
+
if fees_exposed:
|
|
343
|
+
all_in = money(face + buyer_fees + tax)
|
|
344
|
+
fees_status = "EXPOSED"
|
|
345
|
+
elif exception and fees:
|
|
346
|
+
# Written per-quote owner exception (spec 5.3): the platform
|
|
347
|
+
# does not expose its fees, but the owner supplied the figures
|
|
348
|
+
# (e.g. confirmed on the platform's own hire screen) in the
|
|
349
|
+
# approval pack. The status stays UNEXPOSED — the platform
|
|
350
|
+
# never exposed them — and the all-in is labeled as computed
|
|
351
|
+
# from the owner-supplied itemization under exception.
|
|
352
|
+
all_in = money(face + buyer_fees + tax)
|
|
353
|
+
fees_status = "UNEXPOSED"
|
|
354
|
+
else:
|
|
355
|
+
# No silent incompletes: an unexposed-fee quote with nothing
|
|
356
|
+
# to total has no all-in and cannot be hired under a ceiling.
|
|
357
|
+
all_in = None
|
|
358
|
+
fees_status = "UNEXPOSED"
|
|
359
|
+
quote = {
|
|
360
|
+
"platform": str(raw.get("platform") or ""),
|
|
361
|
+
"pro": {
|
|
362
|
+
"platform_id": str(pro["platform_id"]),
|
|
363
|
+
"display_name": pro.get("display_name"),
|
|
364
|
+
"rating": pro.get("rating"),
|
|
365
|
+
"hires": pro.get("hires"),
|
|
366
|
+
# License claims are surfaced verbatim with provenance;
|
|
367
|
+
# the core never asserts a claim is valid (spec 8.3).
|
|
368
|
+
"license_claims": pro.get("license_claims") or [],
|
|
369
|
+
"badges": pro.get("badges") or [],
|
|
370
|
+
},
|
|
371
|
+
"model": model,
|
|
372
|
+
"job_price_face": face,
|
|
373
|
+
"hourly": hourly,
|
|
374
|
+
"fees": fees,
|
|
375
|
+
"fee_bearer": fee_bearer,
|
|
376
|
+
"buyer_fees_total": buyer_fees,
|
|
377
|
+
"tax": tax,
|
|
378
|
+
"tip": raw.get("tip") or {"mode": "n/a"},
|
|
379
|
+
"all_in_total": all_in,
|
|
380
|
+
"fees_status": fees_status,
|
|
381
|
+
"owner_fee_exception": exception,
|
|
382
|
+
"currency": str(raw.get("currency") or "USD"),
|
|
383
|
+
"valid_until": raw.get("valid_until"),
|
|
384
|
+
"raw_quote_ref": raw.get("raw_quote_ref"),
|
|
385
|
+
}
|
|
386
|
+
quote["quote_id"] = "quote_" + sha(canon(quote))[:12]
|
|
387
|
+
return quote
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def ceiling_test(quote: dict, ceiling) -> dict:
|
|
391
|
+
ceiling = money(ceiling)
|
|
392
|
+
if quote["fees_status"] == "UNEXPOSED" and \
|
|
393
|
+
not quote.get("owner_fee_exception"):
|
|
394
|
+
return {"ceiling_all_in": ceiling, "hireable_under_ceiling": False,
|
|
395
|
+
"reason": "fees UNEXPOSED: a quote whose fees the "
|
|
396
|
+
"platform does not expose cannot be hired "
|
|
397
|
+
"under a ceiling (spec 5.3); only a written "
|
|
398
|
+
"per-quote owner exception in the approval "
|
|
399
|
+
"pack can lift this"}
|
|
400
|
+
if quote["all_in_total"] is None:
|
|
401
|
+
return {"ceiling_all_in": ceiling, "hireable_under_ceiling": False,
|
|
402
|
+
"reason": "no all-in total could be computed (fees "
|
|
403
|
+
"UNEXPOSED and no owner-supplied itemization "
|
|
404
|
+
"under a written exception)"}
|
|
405
|
+
ok = quote["all_in_total"] <= ceiling
|
|
406
|
+
reason = ("all-in total is within the ceiling"
|
|
407
|
+
if ok else "all-in total exceeds the ceiling")
|
|
408
|
+
if quote["fees_status"] == "UNEXPOSED":
|
|
409
|
+
reason += " (owner fee exception on record)"
|
|
410
|
+
return {"ceiling_all_in": ceiling, "hireable_under_ceiling": ok,
|
|
411
|
+
"reason": reason}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def t_quote_normalize(state, args):
|
|
415
|
+
quote = normalize_quote(args)
|
|
416
|
+
if quote["quote_id"] not in state["quotes"]:
|
|
417
|
+
state["quotes"][quote["quote_id"]] = quote
|
|
418
|
+
emit_receipt(state, "quote_normalized", {
|
|
419
|
+
"quote_id": quote["quote_id"], "platform": quote["platform"],
|
|
420
|
+
"pro_id": quote["pro"]["platform_id"], "model": quote["model"],
|
|
421
|
+
"all_in_total": quote["all_in_total"],
|
|
422
|
+
"fees_status": quote["fees_status"]})
|
|
423
|
+
out = {"quote": quote}
|
|
424
|
+
if args.get("ceiling_all_in") is not None:
|
|
425
|
+
out["ceiling_test"] = ceiling_test(quote, args["ceiling_all_in"])
|
|
426
|
+
return out
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def t_quote_read(state, args):
|
|
430
|
+
quote = state["quotes"].get(str(args.get("quote_id") or ""))
|
|
431
|
+
if not quote:
|
|
432
|
+
raise ToolError(f"unknown quote: {args.get('quote_id')}")
|
|
433
|
+
out = {"quote": quote}
|
|
434
|
+
if args.get("ceiling_all_in") is not None:
|
|
435
|
+
out["ceiling_test"] = ceiling_test(quote, args["ceiling_all_in"])
|
|
436
|
+
return out
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ------------------------------------------------------------------ hire gate
|
|
440
|
+
def expected_quote_token(quote_id: str) -> str:
|
|
441
|
+
return f"approve-quote-{quote_id}"
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def expected_person_token(quote_id: str) -> str:
|
|
445
|
+
return f"confirm-person-{quote_id}"
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def t_hire_check(state, args):
|
|
449
|
+
"""The hire gate (spec 6 + 7.4): pure mandate evaluation.
|
|
450
|
+
|
|
451
|
+
Executes nothing. Returns a decision and whether the quote is
|
|
452
|
+
hireable RIGHT NOW. In v1 the selection of a person requires an
|
|
453
|
+
owner-confirm token even when every mandate check passes.
|
|
454
|
+
"""
|
|
455
|
+
brief = state["briefs"].get(str(args.get("brief_id") or ""))
|
|
456
|
+
quote = state["quotes"].get(str(args.get("quote_id") or ""))
|
|
457
|
+
as_of = parse_time(str(args.get("as_of") or now_iso()))
|
|
458
|
+
reasons, notes = [], []
|
|
459
|
+
|
|
460
|
+
if not brief:
|
|
461
|
+
reasons.append(f"unknown brief: {args.get('brief_id')}")
|
|
462
|
+
if not quote:
|
|
463
|
+
reasons.append(f"unknown quote: {args.get('quote_id')}")
|
|
464
|
+
|
|
465
|
+
mandate = args.get("mandate")
|
|
466
|
+
approval_ok = False
|
|
467
|
+
person_ok = False
|
|
468
|
+
if brief and quote:
|
|
469
|
+
approval = brief.get("approval")
|
|
470
|
+
if not approval or approval.get("brief_hash") != brief["hash"]:
|
|
471
|
+
reasons.append("brief is not approved at its current hash "
|
|
472
|
+
"(approval binds hashes; spec 5.1)")
|
|
473
|
+
if not mandate:
|
|
474
|
+
reasons.append("NO_MANDATE: no mandate, no hire (spec 6.1)")
|
|
475
|
+
else:
|
|
476
|
+
expires = parse_time(mandate.get("expires_at"))
|
|
477
|
+
if expires is None or expires <= as_of:
|
|
478
|
+
reasons.append("mandate expired or has no expiry "
|
|
479
|
+
"(spec 6.1)")
|
|
480
|
+
if quote["platform"] not in \
|
|
481
|
+
(mandate.get("platforms") or []):
|
|
482
|
+
reasons.append(
|
|
483
|
+
f"platform {quote['platform']!r} not in mandate "
|
|
484
|
+
"platform allowlist")
|
|
485
|
+
if brief["category"] not in \
|
|
486
|
+
(mandate.get("categories") or []):
|
|
487
|
+
reasons.append(
|
|
488
|
+
f"category {brief['category']!r} not in mandate "
|
|
489
|
+
"category allowlist")
|
|
490
|
+
cet = ceiling_test(quote, mandate.get("cap", 0)) \
|
|
491
|
+
if mandate.get("cap") is not None else None
|
|
492
|
+
brief_cet = ceiling_test(quote, brief["ceiling_all_in"])
|
|
493
|
+
if cet and not cet["hireable_under_ceiling"]:
|
|
494
|
+
reasons.append("mandate cap: " + cet["reason"])
|
|
495
|
+
if not brief_cet["hireable_under_ceiling"]:
|
|
496
|
+
reasons.append("brief ceiling: " + brief_cet["reason"])
|
|
497
|
+
threshold = mandate.get("approval_threshold")
|
|
498
|
+
if threshold is not None and quote["all_in_total"] is not None \
|
|
499
|
+
and quote["all_in_total"] > float(threshold):
|
|
500
|
+
if args.get("quote_approval_token") == \
|
|
501
|
+
expected_quote_token(quote["quote_id"]):
|
|
502
|
+
approval_ok = True
|
|
503
|
+
notes.append("above-threshold quote approved by "
|
|
504
|
+
"owner token for THIS quote (spec 6.2)")
|
|
505
|
+
else:
|
|
506
|
+
reasons.append(
|
|
507
|
+
"all-in total exceeds the mandate approval "
|
|
508
|
+
"threshold; an owner approval token for this "
|
|
509
|
+
"exact quote is required (spec 6.2)")
|
|
510
|
+
valid_until = parse_time(quote.get("valid_until"))
|
|
511
|
+
if valid_until is not None and valid_until <= as_of:
|
|
512
|
+
reasons.append("quote has expired (valid_until passed)")
|
|
513
|
+
# Person-selection rule (spec 7.4): v1 default is
|
|
514
|
+
# owner-confirmed, mandate or not.
|
|
515
|
+
if args.get("person_confirm_token") == \
|
|
516
|
+
expected_person_token(quote["quote_id"]):
|
|
517
|
+
person_ok = True
|
|
518
|
+
else:
|
|
519
|
+
notes.append("PERSON_CONFIRM_REQUIRED: in v1 the selection "
|
|
520
|
+
"of a person is owner-confirmed even under a "
|
|
521
|
+
"mandate (spec 7.4)")
|
|
522
|
+
|
|
523
|
+
if reasons:
|
|
524
|
+
decision, hireable = "DENY", False
|
|
525
|
+
elif not person_ok:
|
|
526
|
+
decision, hireable = "MANDATE_ALLOW_PERSON_PENDING", False
|
|
527
|
+
else:
|
|
528
|
+
decision, hireable = "ALLOW", True
|
|
529
|
+
detail = {"brief_id": args.get("brief_id"),
|
|
530
|
+
"quote_id": args.get("quote_id"), "decision": decision,
|
|
531
|
+
"hireable": hireable, "reasons": reasons, "notes": notes,
|
|
532
|
+
"mandate_id": (mandate or {}).get("mandate_id"),
|
|
533
|
+
"executes": "nothing — evaluation only (G0)"}
|
|
534
|
+
emit_receipt(state, "hire_check", detail)
|
|
535
|
+
return detail
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# ------------------------------------------------------- completion + reviews
|
|
539
|
+
def t_completion_verify(state, args):
|
|
540
|
+
brief = state["briefs"].get(str(args.get("brief_id") or ""))
|
|
541
|
+
if not brief:
|
|
542
|
+
raise ToolError(f"unknown brief: {args.get('brief_id')}")
|
|
543
|
+
scope_items = (brief.get("scope") or {}).get("items") or []
|
|
544
|
+
results = args.get("scope_results") or []
|
|
545
|
+
by_item = {r.get("item"): r for r in results}
|
|
546
|
+
checked = []
|
|
547
|
+
for item in scope_items:
|
|
548
|
+
result = by_item.get(item)
|
|
549
|
+
if result is None:
|
|
550
|
+
checked.append({"item": item, "status": "unattested",
|
|
551
|
+
"attestation": None})
|
|
552
|
+
else:
|
|
553
|
+
status = str(result.get("status") or "")
|
|
554
|
+
if status not in ("done", "partial", "not_done"):
|
|
555
|
+
raise ToolError("scope result status must be done | "
|
|
556
|
+
"partial | not_done")
|
|
557
|
+
checked.append({"item": item, "status": status,
|
|
558
|
+
"attestation": result.get("attestation")})
|
|
559
|
+
platform_state = str(args.get("platform_state") or "unknown")
|
|
560
|
+
incomplete = [c for c in checked if c["status"] != "done"]
|
|
561
|
+
# Platform state and owner verification are separate fields and
|
|
562
|
+
# are never merged (spec 7.2). A platform that claims completion
|
|
563
|
+
# the owner does not attest opens an exception, not an edit.
|
|
564
|
+
mismatch = bool(incomplete) and \
|
|
565
|
+
platform_state.strip().lower() in ("completed", "complete",
|
|
566
|
+
"done", "paid", "released")
|
|
567
|
+
if mismatch:
|
|
568
|
+
verification_status = "exception"
|
|
569
|
+
elif incomplete:
|
|
570
|
+
verification_status = "partial"
|
|
571
|
+
else:
|
|
572
|
+
verification_status = "owner_verified"
|
|
573
|
+
record = {
|
|
574
|
+
"brief_id": brief["brief_id"],
|
|
575
|
+
"brief_hash": brief["hash"],
|
|
576
|
+
"quote_id": args.get("quote_id"),
|
|
577
|
+
"platform": args.get("platform"),
|
|
578
|
+
"pro_display_name": args.get("pro_display_name"),
|
|
579
|
+
"platform_state": platform_state,
|
|
580
|
+
"platform_state_basis": "read back verbatim; never smoothed "
|
|
581
|
+
"(spec 7.2)",
|
|
582
|
+
"scope_check": checked,
|
|
583
|
+
"verification_status": verification_status,
|
|
584
|
+
"platform_owner_mismatch": mismatch,
|
|
585
|
+
"timeliness": args.get("timeliness"),
|
|
586
|
+
"communication": args.get("communication"),
|
|
587
|
+
"quality_notes": args.get("quality_notes"),
|
|
588
|
+
"verified_at": str(args.get("verified_at") or now_iso()),
|
|
589
|
+
}
|
|
590
|
+
record["verification_id"] = \
|
|
591
|
+
"verify_" + sha(canon(record))[:12]
|
|
592
|
+
state["verifications"][record["verification_id"]] = record
|
|
593
|
+
receipt = emit_receipt(state, "completion_verified", {
|
|
594
|
+
"verification_id": record["verification_id"],
|
|
595
|
+
"brief_id": brief["brief_id"],
|
|
596
|
+
"verification_status": verification_status,
|
|
597
|
+
"platform_owner_mismatch": mismatch})
|
|
598
|
+
return {"verification": record, "receipt_seq": receipt["seq"]}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def t_review_draft(state, args):
|
|
602
|
+
record = state["verifications"].get(
|
|
603
|
+
str(args.get("verification_id") or ""))
|
|
604
|
+
if not record:
|
|
605
|
+
raise ToolError(
|
|
606
|
+
f"unknown verification: {args.get('verification_id')}")
|
|
607
|
+
notes_blob = " ".join(str(record.get(k) or "") for k in
|
|
608
|
+
("quality_notes", "timeliness",
|
|
609
|
+
"communication")).lower()
|
|
610
|
+
for pattern in LEVERAGE_PATTERNS:
|
|
611
|
+
if pattern in notes_blob:
|
|
612
|
+
raise ToolError(
|
|
613
|
+
"prohibited leverage language in the verification "
|
|
614
|
+
f"record ({pattern!r}); review drafts are factual and "
|
|
615
|
+
"record-bound only (spec 7.3)")
|
|
616
|
+
brief = state["briefs"].get(record["brief_id"]) or {}
|
|
617
|
+
done = sum(1 for c in record["scope_check"] if c["status"] == "done")
|
|
618
|
+
total = len(record["scope_check"])
|
|
619
|
+
lines = [
|
|
620
|
+
f"Hired for {brief.get('category', 'a task')} "
|
|
621
|
+
f"({brief.get('service', 'service')}) via "
|
|
622
|
+
f"{record.get('platform') or 'the platform'}.",
|
|
623
|
+
f"Completion verified against the brief: {done} of {total} "
|
|
624
|
+
"scope items done.",
|
|
625
|
+
]
|
|
626
|
+
if record["verification_status"] != "owner_verified":
|
|
627
|
+
lines.append(
|
|
628
|
+
f"Verification status: {record['verification_status']}.")
|
|
629
|
+
if record.get("timeliness"):
|
|
630
|
+
lines.append(f"Timeliness: {record['timeliness']}.")
|
|
631
|
+
if record.get("communication"):
|
|
632
|
+
lines.append(f"Communication: {record['communication']}.")
|
|
633
|
+
# Sentiment enters ONLY from the record's own quality notes.
|
|
634
|
+
if record.get("quality_notes"):
|
|
635
|
+
lines.append(f"Quality: {record['quality_notes']}.")
|
|
636
|
+
text = " ".join(lines)
|
|
637
|
+
draft = {
|
|
638
|
+
"verification_id": record["verification_id"],
|
|
639
|
+
"brief_id": record["brief_id"],
|
|
640
|
+
"pro_display_name": record.get("pro_display_name"),
|
|
641
|
+
"text": text,
|
|
642
|
+
"basis": "verification record only (spec 7.3)",
|
|
643
|
+
"status": "DRAFT — owner approves the exact text before any "
|
|
644
|
+
"submission; drafts never submit themselves",
|
|
645
|
+
}
|
|
646
|
+
emit_receipt(state, "review_drafted", {
|
|
647
|
+
"verification_id": record["verification_id"],
|
|
648
|
+
"draft_hash": sha(text)})
|
|
649
|
+
return {"review_draft": draft}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def t_receipt_list(state, args):
|
|
653
|
+
limit = args.get("limit")
|
|
654
|
+
receipts = state["receipts"]
|
|
655
|
+
if limit is not None:
|
|
656
|
+
receipts = receipts[-int(limit):]
|
|
657
|
+
return {"receipts": receipts, "count": len(state["receipts"]),
|
|
658
|
+
"chain_valid": verify_receipt_chain(state["receipts"])}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
TOOLS = {
|
|
662
|
+
"brief_create": (
|
|
663
|
+
t_brief_create,
|
|
664
|
+
"Create a canonical Task Brief: category, service, scope "
|
|
665
|
+
"{statement, items[]}, ZIP-tier location, timing window, "
|
|
666
|
+
"ALL-IN ceiling, tip policy, photos (EXIF-strip status "
|
|
667
|
+
"recorded per photo), license_required (unknown|no|yes:<type>), "
|
|
668
|
+
"safety/access block, intended/prohibited platforms, and a "
|
|
669
|
+
"details bag adapters draw native form answers from. Issues "
|
|
670
|
+
"the version-1 hash an approval binds to.",
|
|
671
|
+
{"brief_id": {"type": "string"}, "category": {"type": "string"},
|
|
672
|
+
"service": {"type": "string"}, "scope": {"type": "object"},
|
|
673
|
+
"location": {"type": "object"}, "timing": {"type": "object"},
|
|
674
|
+
"ceiling_all_in": {"type": "number"},
|
|
675
|
+
"tip_policy": {"type": "object"},
|
|
676
|
+
"photos": {"type": "array", "items": {"type": "object"}},
|
|
677
|
+
"license_required": {"type": "string"},
|
|
678
|
+
"safety_access": {"type": "object"},
|
|
679
|
+
"intended_platforms": {"type": "array",
|
|
680
|
+
"items": {"type": "string"}},
|
|
681
|
+
"prohibited_platforms": {"type": "array",
|
|
682
|
+
"items": {"type": "string"}},
|
|
683
|
+
"details": {"type": "object"}},
|
|
684
|
+
["category", "service", "scope", "location", "timing",
|
|
685
|
+
"ceiling_all_in", "license_required"]),
|
|
686
|
+
"brief_update": (
|
|
687
|
+
t_brief_update,
|
|
688
|
+
"Create a new brief version from content-field changes. Any "
|
|
689
|
+
"post-approval edit invalidates the approval and returns the "
|
|
690
|
+
"brief to draft (approval binds hashes, spec 5.1).",
|
|
691
|
+
{"brief_id": {"type": "string"}, "changes": {"type": "object"}},
|
|
692
|
+
["brief_id", "changes"]),
|
|
693
|
+
"brief_approve": (
|
|
694
|
+
t_brief_approve,
|
|
695
|
+
"Record the owner's approval of the EXACT current brief hash, "
|
|
696
|
+
"plus the per-platform variant hashes approved with it. A "
|
|
697
|
+
"hash mismatch is refused.",
|
|
698
|
+
{"brief_id": {"type": "string"}, "brief_hash": {"type": "string"},
|
|
699
|
+
"approver": {"type": "string"},
|
|
700
|
+
"approved_at": {"type": "string"},
|
|
701
|
+
"variant_hashes": {"type": "object"}},
|
|
702
|
+
["brief_id", "brief_hash"]),
|
|
703
|
+
"brief_read": (
|
|
704
|
+
t_brief_read,
|
|
705
|
+
"Read a brief with its version, hash, and approval validity.",
|
|
706
|
+
{"brief_id": {"type": "string"}}, ["brief_id"]),
|
|
707
|
+
"quote_normalize": (
|
|
708
|
+
t_quote_normalize,
|
|
709
|
+
"Normalize a quote from any platform model (lead_model, "
|
|
710
|
+
"stated_budget, fixed_price, hourly) into one record: face "
|
|
711
|
+
"price, itemized fees with bearers, tax, and ALL-IN total "
|
|
712
|
+
"kept separate forever. Fees the platform does not expose "
|
|
713
|
+
"mark the quote UNEXPOSED with no all-in. Optional "
|
|
714
|
+
"ceiling_all_in runs the ceiling test.",
|
|
715
|
+
{"platform": {"type": "string"}, "pro": {"type": "object"},
|
|
716
|
+
"model": {"type": "string"}, "job_price": {"type": "number"},
|
|
717
|
+
"hourly_rate": {"type": "number"},
|
|
718
|
+
"hour_cap": {"type": "number"},
|
|
719
|
+
"fees": {"type": "array", "items": {"type": "object"}},
|
|
720
|
+
"fees_exposed": {"type": "boolean"},
|
|
721
|
+
"tax": {"type": "number"}, "tip": {"type": "object"},
|
|
722
|
+
"currency": {"type": "string"},
|
|
723
|
+
"valid_until": {"type": "string"},
|
|
724
|
+
"raw_quote_ref": {"type": "string"},
|
|
725
|
+
"owner_fee_exception": {"type": "object"},
|
|
726
|
+
"ceiling_all_in": {"type": "number"}},
|
|
727
|
+
["platform", "pro", "model"]),
|
|
728
|
+
"quote_read": (
|
|
729
|
+
t_quote_read,
|
|
730
|
+
"Read a normalized quote; optional ceiling_all_in re-runs the "
|
|
731
|
+
"ceiling test.",
|
|
732
|
+
{"quote_id": {"type": "string"},
|
|
733
|
+
"ceiling_all_in": {"type": "number"}}, ["quote_id"]),
|
|
734
|
+
"hire_check": (
|
|
735
|
+
t_hire_check,
|
|
736
|
+
"Evaluate a hire against a live Procure/Agent Wallet mandate: "
|
|
737
|
+
"approved brief hash, mandate presence + expiry, cap vs "
|
|
738
|
+
"all-in, brief ceiling, category + platform allowlists, "
|
|
739
|
+
"approval threshold (above = owner token for THIS quote), "
|
|
740
|
+
"quote expiry, fee exposure. v1 person-selection always "
|
|
741
|
+
"needs an owner-confirm token. Returns ALLOW / "
|
|
742
|
+
"MANDATE_ALLOW_PERSON_PENDING / DENY with named reasons. "
|
|
743
|
+
"Executes nothing; receipted either way.",
|
|
744
|
+
{"brief_id": {"type": "string"}, "quote_id": {"type": "string"},
|
|
745
|
+
"mandate": {"type": "object"},
|
|
746
|
+
"quote_approval_token": {"type": "string"},
|
|
747
|
+
"person_confirm_token": {"type": "string"},
|
|
748
|
+
"as_of": {"type": "string"}}, ["brief_id", "quote_id"]),
|
|
749
|
+
"completion_verify": (
|
|
750
|
+
t_completion_verify,
|
|
751
|
+
"Verify completion against the brief's scope statement: each "
|
|
752
|
+
"scope item done/partial/not_done with owner attestation. "
|
|
753
|
+
"Platform state is stored verbatim and separately; a "
|
|
754
|
+
"platform-completed claim the owner does not attest opens "
|
|
755
|
+
"an exception record (spec 7.2).",
|
|
756
|
+
{"brief_id": {"type": "string"}, "quote_id": {"type": "string"},
|
|
757
|
+
"platform": {"type": "string"},
|
|
758
|
+
"pro_display_name": {"type": "string"},
|
|
759
|
+
"platform_state": {"type": "string"},
|
|
760
|
+
"scope_results": {"type": "array",
|
|
761
|
+
"items": {"type": "object"}},
|
|
762
|
+
"timeliness": {"type": "string"},
|
|
763
|
+
"communication": {"type": "string"},
|
|
764
|
+
"quality_notes": {"type": "string"},
|
|
765
|
+
"verified_at": {"type": "string"}}, ["brief_id"]),
|
|
766
|
+
"review_draft": (
|
|
767
|
+
t_review_draft,
|
|
768
|
+
"Draft review text from a verification record ONLY: what was "
|
|
769
|
+
"hired, what was verified, and timeliness/communication/"
|
|
770
|
+
"quality strictly as recorded. No sentiment the record does "
|
|
771
|
+
"not carry; leverage language is refused at draft time. "
|
|
772
|
+
"The draft never submits itself.",
|
|
773
|
+
{"verification_id": {"type": "string"}}, ["verification_id"]),
|
|
774
|
+
"receipt_list": (
|
|
775
|
+
t_receipt_list,
|
|
776
|
+
"List the append-only, hash-chained gig-core receipts and "
|
|
777
|
+
"verify the chain.",
|
|
778
|
+
{"limit": {"type": "integer"}}, []),
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
# Capability denylist, stated in code: G0 evaluates and records only.
|
|
782
|
+
# Live submission, live hiring, payment movement, and review
|
|
783
|
+
# submission are adapter/G1 surfaces behind owner gates — and
|
|
784
|
+
# session/scraping routes are prohibited at any tier (spec 8.6).
|
|
785
|
+
FORBIDDEN_TOOL_NAMES = {
|
|
786
|
+
"request_submit", "hire_execute", "payment_execute",
|
|
787
|
+
"review_submit", "pro_contact_export", "session_login",
|
|
788
|
+
"cookie_reuse", "scrape_quotes", "scrape_profiles",
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def tool_list():
|
|
793
|
+
return [{"name": name, "description": description,
|
|
794
|
+
"inputSchema": {"type": "object", "properties": props,
|
|
795
|
+
"required": required}}
|
|
796
|
+
for name, (_, description, props, required) in TOOLS.items()]
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def handle(req: dict):
|
|
800
|
+
method, rid = req.get("method", ""), req.get("id")
|
|
801
|
+
if method == "initialize":
|
|
802
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {
|
|
803
|
+
"protocolVersion": req.get("params", {}).get(
|
|
804
|
+
"protocolVersion", "2025-03-26"),
|
|
805
|
+
"capabilities": {"tools": {}},
|
|
806
|
+
"serverInfo": {"name": "eclipse-gig-core", "version": "0.1.0"}}}
|
|
807
|
+
if method == "ping":
|
|
808
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {}}
|
|
809
|
+
if method == "tools/list":
|
|
810
|
+
return {"jsonrpc": "2.0", "id": rid,
|
|
811
|
+
"result": {"tools": tool_list()}}
|
|
812
|
+
if method == "tools/call":
|
|
813
|
+
name = req["params"]["name"]
|
|
814
|
+
args = req["params"].get("arguments", {})
|
|
815
|
+
if name in FORBIDDEN_TOOL_NAMES or name not in TOOLS:
|
|
816
|
+
return {"jsonrpc": "2.0", "id": rid, "error": {
|
|
817
|
+
"code": -32602,
|
|
818
|
+
"message": f"unknown or G0-forbidden tool {name}"}}
|
|
819
|
+
state = load_state()
|
|
820
|
+
try:
|
|
821
|
+
out = TOOLS[name][0](state, args)
|
|
822
|
+
save_state(state)
|
|
823
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {
|
|
824
|
+
"content": [{"type": "text",
|
|
825
|
+
"text": json.dumps(out, indent=1)}]}}
|
|
826
|
+
except ToolError as exc:
|
|
827
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {
|
|
828
|
+
"content": [{"type": "text",
|
|
829
|
+
"text": json.dumps({"error": str(exc)})}],
|
|
830
|
+
"isError": True}}
|
|
831
|
+
if rid is not None:
|
|
832
|
+
return {"jsonrpc": "2.0", "id": rid, "error": {
|
|
833
|
+
"code": -32601, "message": f"method not found: {method}"}}
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def main():
|
|
838
|
+
for line in sys.stdin:
|
|
839
|
+
line = line.strip()
|
|
840
|
+
if not line:
|
|
841
|
+
continue
|
|
842
|
+
try:
|
|
843
|
+
response = handle(json.loads(line))
|
|
844
|
+
except Exception as exc: # never crash the transport
|
|
845
|
+
response = {"jsonrpc": "2.0", "id": None, "error": {
|
|
846
|
+
"code": -32603, "message": str(exc)}}
|
|
847
|
+
if response is not None:
|
|
848
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
849
|
+
sys.stdout.flush()
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
if __name__ == "__main__":
|
|
853
|
+
main()
|
|
854
|
+
|
|
855
|
+
# Eclipse Gig Core MCP · © 2026 Cyan Eclipse Technologies LLC · cetech.ai
|