@passioncode-ai/passioncode 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +22 -5
- package/SECURITY.md +1 -1
- package/bin/passioncode.js +5 -5
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +20 -4
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +57 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +15 -2
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +267 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +19 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +77 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +177 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +215 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +470 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +14 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +118 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +9 -3
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
It is the worked example the skill points at, the target check_service.py tests
|
|
5
5
|
itself against, and the fixture Fabric Dashboards runs its end-to-end tests on.
|
|
6
|
+
Over MCP (fabric-interop/0.1) it serves two capabilities: `sample.echo` answers at
|
|
7
|
+
once, and `sample.draft` is a job that stops for a titled choice before it completes.
|
|
8
|
+
`register` also writes the service's manifest, which names this descriptor.
|
|
6
9
|
|
|
7
10
|
sample_service.py serve --port 47190 --data-dir DIR [--id sample] [--instance default]
|
|
8
11
|
sample_service.py register --port 47190 --data-dir DIR [--services-dir DIR]
|
|
@@ -11,6 +14,7 @@ itself against, and the fixture Fabric Dashboards runs its end-to-end tests on.
|
|
|
11
14
|
from __future__ import annotations
|
|
12
15
|
|
|
13
16
|
import argparse
|
|
17
|
+
import hashlib
|
|
14
18
|
import html
|
|
15
19
|
import json
|
|
16
20
|
import os
|
|
@@ -24,6 +28,7 @@ from urllib.parse import parse_qs, urlparse
|
|
|
24
28
|
|
|
25
29
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
30
|
import fabric_service as fs # noqa: E402
|
|
31
|
+
import fabric_interop as fi # noqa: E402
|
|
27
32
|
|
|
28
33
|
VERSION = "0.1.0"
|
|
29
34
|
PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
@@ -32,6 +37,24 @@ PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
|
32
37
|
</head><body><h1>{name}</h1><p>Status: {status}</p><ul>{rows}</ul></body></html>"""
|
|
33
38
|
|
|
34
39
|
|
|
40
|
+
def _schema(name: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
return {"$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:fabric:schema:%s" % name, **body}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
TEXT = {"type": "object", "additionalProperties": False, "properties": {"text": {"type": "string", "maxLength": 2000}}, "required": ["text"]}
|
|
45
|
+
CAPABILITIES = [
|
|
46
|
+
{"name": "sample.echo", "effect": "none", "idempotency": "required", "job": False,
|
|
47
|
+
"description": "Returns the text it was given.",
|
|
48
|
+
"input": _schema("sample.echo-input", TEXT), "output": _schema("sample.echo-output", TEXT)},
|
|
49
|
+
{"name": "sample.draft", "effect": "draft", "idempotency": "none", "job": True,
|
|
50
|
+
"description": "Drafts a short note on a topic; asks which title to use.",
|
|
51
|
+
"input": _schema("sample.draft-input", {"type": "object", "additionalProperties": False,
|
|
52
|
+
"properties": {"topic": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["topic"]}),
|
|
53
|
+
"output": _schema("sample.draft-output", {"type": "object", "additionalProperties": False,
|
|
54
|
+
"properties": {"title": {"type": "string"}, "body": {"type": "string"}}, "required": ["title", "body"]})},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
35
58
|
class Service:
|
|
36
59
|
def __init__(self, args: argparse.Namespace):
|
|
37
60
|
self.id = args.id
|
|
@@ -50,14 +73,59 @@ class Service:
|
|
|
50
73
|
self.token = fs.ensure_token(self.token_file)
|
|
51
74
|
self.log = fs.JsonlEventLog(self.data / "events.jsonl")
|
|
52
75
|
self.codes = fs.LoginCodes(self.data / "auth")
|
|
76
|
+
self.jobs = fi.JobStore(self.data / "jobs")
|
|
77
|
+
self.mcp = fi.McpToolServer(self.id, VERSION, jobs=self.jobs, on_job_started=self.draft_started, on_input=self.draft_answered)
|
|
78
|
+
for cap in CAPABILITIES:
|
|
79
|
+
handler = self.echo if cap["name"] == "sample.echo" else self.draft
|
|
80
|
+
self.mcp.add_tool(fi.tool_for_capability(cap, cap["input"], cap["output"]), handler)
|
|
53
81
|
self.log.append("service.started", "info", "%s started on build %s." % (self.name, self.build["commit"]))
|
|
54
82
|
|
|
83
|
+
# --- fabric-interop/0.1: the two capabilities ------------------------------------
|
|
84
|
+
def echo(self, args: Dict[str, Any], ctx: fi.CallContext) -> Dict[str, Any]:
|
|
85
|
+
if not isinstance(args.get("text"), str):
|
|
86
|
+
raise fi.InteropError("sample.echo needs text.")
|
|
87
|
+
return {"text": args["text"]}
|
|
88
|
+
|
|
89
|
+
def draft(self, args: Dict[str, Any], ctx: fi.CallContext) -> Dict[str, Any]:
|
|
90
|
+
if not isinstance(args.get("topic"), str) or not args["topic"].strip():
|
|
91
|
+
raise fi.InteropError("sample.draft needs a topic.")
|
|
92
|
+
return ctx.start_job(poll_interval_ms=1000)
|
|
93
|
+
|
|
94
|
+
def draft_started(self, job_id: str, ctx: fi.CallContext) -> None:
|
|
95
|
+
topic = str(ctx.arguments.get("topic"))
|
|
96
|
+
titles = [("plain", "About %s" % topic), ("question", "What is %s?" % topic)]
|
|
97
|
+
self.jobs.request_input(job_id, {"title_choice": fi.choice_request("Pick the title for the note.", "title", titles, "Title")},
|
|
98
|
+
"Two titles are ready.")
|
|
99
|
+
self.log.append("job.awaiting_choice", "notice", "A note about %s is waiting for you to pick its title." % topic,
|
|
100
|
+
notify=True, **fi.trace_ids(ctx.traceparent))
|
|
101
|
+
|
|
102
|
+
def draft_answered(self, job_id: str, answers: Dict[str, Any], ctx: fi.CallContext) -> None:
|
|
103
|
+
answer = answers.get("title_choice", {})
|
|
104
|
+
topic = str(ctx.arguments.get("topic"))
|
|
105
|
+
if answer.get("action") != "accept":
|
|
106
|
+
self.jobs.cancel(job_id)
|
|
107
|
+
self.log.append("job.cancelled", "info", "The note about %s was dropped: no title was chosen." % topic, **fi.trace_ids(ctx.traceparent))
|
|
108
|
+
return
|
|
109
|
+
choice = str((answer.get("content") or {}).get("title"))
|
|
110
|
+
title = "About %s" % topic if choice == "plain" else "What is %s?" % topic
|
|
111
|
+
body = "%s. This sample note was written by the sample service." % title
|
|
112
|
+
self.jobs.complete(job_id, fi.result_envelope(
|
|
113
|
+
outcome="partial", traceparent=ctx.traceparent,
|
|
114
|
+
producer={"id": "urn:fabric:provider:%s" % self.id, "revision": 1, "contentHash": "sha256:" + "0" * 64},
|
|
115
|
+
done=[{"claimId": "NOTE", "statement": "A note titled %s was drafted." % title}], proof=[],
|
|
116
|
+
scope={"project": "urn:fabric:project:sample", "run": "urn:fabric:run:%s" % job_id, "node": "urn:fabric:node:draft",
|
|
117
|
+
"binding": {"id": "urn:fabric:binding:sample.draft", "revision": 1, "contentHash": "sha256:" + "0" * 64}, "writeScopes": []},
|
|
118
|
+
not_verified=[{"claim": "NOTE", "reason": "no checker has read the note"}],
|
|
119
|
+
output={"title": title, "body": body}, usage={"inputTokens": 0, "outputTokens": 0, "wallMs": 1}))
|
|
120
|
+
self.log.append("job.completed", "info", "The note %s is drafted." % title, **fi.trace_ids(ctx.traceparent))
|
|
121
|
+
|
|
55
122
|
def well_known(self) -> Dict[str, Any]:
|
|
56
123
|
return fs.build_well_known(
|
|
57
124
|
service_id=self.id, instance=self.instance, name=self.name, version=VERSION, build=self.build,
|
|
58
125
|
started_at=self.started_at, status="ready", degraded=self.degraded,
|
|
59
126
|
summary=[{"label": "Events", "value": len(self.log.fetch(None, fs.EVENTS_MAX_LIMIT))}],
|
|
60
|
-
surfaces={"dashboard": {"path": "/", "login": True}, "events": {"path": "/fabric/v1/events"}
|
|
127
|
+
surfaces={"dashboard": {"path": "/", "login": True}, "events": {"path": "/fabric/v1/events"},
|
|
128
|
+
"mcp": {"path": "/mcp", "transport": "streamable-http", "capabilities": [c["name"] for c in CAPABILITIES]}},
|
|
61
129
|
)
|
|
62
130
|
|
|
63
131
|
|
|
@@ -137,6 +205,16 @@ def make_handler(svc: Service):
|
|
|
137
205
|
if not self._guard():
|
|
138
206
|
return
|
|
139
207
|
url = urlparse(self.path)
|
|
208
|
+
if url.path == "/mcp":
|
|
209
|
+
if not self._token_ok():
|
|
210
|
+
return
|
|
211
|
+
length = min(int(self.headers.get("Content-Length") or 0), 1048576)
|
|
212
|
+
try:
|
|
213
|
+
message = json.loads(self.rfile.read(length) or b"null")
|
|
214
|
+
except ValueError:
|
|
215
|
+
return self._send(400, {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error."}})
|
|
216
|
+
response = svc.mcp.handle(message)
|
|
217
|
+
return self._send(202) if response is None else self._send(200, response)
|
|
140
218
|
if url.path == "/fabric/v1/login-code":
|
|
141
219
|
if not self._token_ok():
|
|
142
220
|
return
|
|
@@ -180,8 +258,46 @@ def serve(args: argparse.Namespace) -> int:
|
|
|
180
258
|
return 0
|
|
181
259
|
|
|
182
260
|
|
|
261
|
+
def write_manifest(args: argparse.Namespace, data: Path) -> Path:
|
|
262
|
+
"""The provider manifest for this service, naming its descriptor (G-07), with its schemas beside it."""
|
|
263
|
+
schemas = data / "fabric" / "schemas"
|
|
264
|
+
for cap in CAPABILITIES:
|
|
265
|
+
for side in ("input", "output"):
|
|
266
|
+
fs.atomic_write(schemas / ("%s-%s.schema.json" % (cap["name"], side)),
|
|
267
|
+
(json.dumps(cap[side], indent=2) + "\n").encode(), 0o600)
|
|
268
|
+
origin = "http://127.0.0.1:%d" % args.port
|
|
269
|
+
capabilities = []
|
|
270
|
+
for cap in CAPABILITIES:
|
|
271
|
+
capabilities.append({
|
|
272
|
+
"id": "urn:fabric:capability:%s" % cap["name"], "name": cap["name"], "description": cap["description"],
|
|
273
|
+
"inputSchema": cap["input"]["$id"], "outputSchema": cap["output"]["$id"],
|
|
274
|
+
"effect": cap["effect"], "idempotency": cap["idempotency"], "dataClasses": ["public"],
|
|
275
|
+
"profile": {"kind": "mcp", "protocolRevision": fi.MCP_REVISION,
|
|
276
|
+
"connection": {"mode": "streamable-http", "url": origin + "/mcp"},
|
|
277
|
+
"requiredFeatures": ["tool:%s" % cap["name"]],
|
|
278
|
+
"probes": [{"id": "%s-shape" % cap["name"].replace(".", "-"), "inputFixture": "urn:fabric:fixture:%s" % cap["name"],
|
|
279
|
+
"outputSchema": cap["output"]["$id"], "timeoutMs": 5000, "sideEffectCeiling": "none",
|
|
280
|
+
"assertions": ["returns a value valid against its output schema"]}]},
|
|
281
|
+
"extensions": {fi.EXTENSION_KEY: {"job": cap["job"]}},
|
|
282
|
+
})
|
|
283
|
+
digest = hashlib.sha256(json.dumps(capabilities, sort_keys=True).encode()).hexdigest()
|
|
284
|
+
subject = "urn:fabric:provider:%s" % args.id
|
|
285
|
+
manifest = {
|
|
286
|
+
"contractVersion": "0.1.0",
|
|
287
|
+
"provider": {"id": subject, "revision": 1, "contentHash": "sha256:" + digest, "createdAt": fs.now_iso(),
|
|
288
|
+
"createdBy": "urn:fabric:adapter:sample-service", "name": args.name,
|
|
289
|
+
"identity": {"subject": subject, "method": "local-install"}, "supportedContractVersions": ["0.1.0"],
|
|
290
|
+
"extensions": {fs.EXTENSION_KEY: {"descriptor": "%s.%s" % (args.id, args.instance)}}},
|
|
291
|
+
"capabilities": capabilities,
|
|
292
|
+
}
|
|
293
|
+
path = data / "fabric-agent.json"
|
|
294
|
+
fs.atomic_write(path, (json.dumps(manifest, indent=2) + "\n").encode(), 0o600)
|
|
295
|
+
return path
|
|
296
|
+
|
|
297
|
+
|
|
183
298
|
def register(args: argparse.Namespace) -> int:
|
|
184
299
|
data = Path(args.data_dir)
|
|
300
|
+
manifest = write_manifest(args, data)
|
|
185
301
|
descriptor = {
|
|
186
302
|
"protocol": fs.PROTOCOL, "id": args.id, "instance": args.instance, "name": args.name,
|
|
187
303
|
"summary": "Sample fabric-service/0.1 service.",
|
|
@@ -190,6 +306,7 @@ def register(args: argparse.Namespace) -> int:
|
|
|
190
306
|
"lifecycle": {"manager": "launchd", "label": args.label, "plist": args.plist} if args.label
|
|
191
307
|
else {"manager": "none"},
|
|
192
308
|
"paths": {"data": str(data), "logs": []},
|
|
309
|
+
"fabricManifest": str(manifest),
|
|
193
310
|
"installedAt": fs.now_iso(), "installedBy": "sample_service.py register",
|
|
194
311
|
}
|
|
195
312
|
try:
|
|
@@ -5,9 +5,9 @@ license: PolyForm-Noncommercial-1.0.0 OR LicenseRef-PolyForm-Internal-Use-1.0.0
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned private fabric-agent-contract checkout. Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.5.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "9cd778eb6f14b977f9a5b4930826f62dc95c5619"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -93,7 +93,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
93
93
|
--schema-base <immutable-base-uri>
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Pin exactly contract `0.1.0` at commit `
|
|
96
|
+
Pin exactly contract `0.1.0` at commit `9cd778eb6f14b977f9a5b4930826f62dc95c5619` and
|
|
97
97
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
98
98
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
99
99
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|
|
@@ -102,6 +102,12 @@ contract's `docs/guides/connecting-compatible-agents.md` and mark the structural
|
|
|
102
102
|
The generated placeholders are deliberate blockers; a bundle still containing one is not
|
|
103
103
|
ready for admission.
|
|
104
104
|
|
|
105
|
+
A new agent is called the `fabric-interop/0.1` way from its first version: add `--job`
|
|
106
|
+
for a capability whose work may outlive one request, serve each capability as the MCP
|
|
107
|
+
tool of its name, and carry the caller's trace (the `building-fabric-services` interop
|
|
108
|
+
reference). An agent that is not a service is announced with a provider entry
|
|
109
|
+
(`adapting-projects-to-fabric`, `scripts/fabric_provider.py`).
|
|
110
|
+
|
|
105
111
|
## Step 4 — implement against the observables
|
|
106
112
|
|
|
107
113
|
If the agent keeps running on the operator's computer — it answers other agents, runs
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "passioncode",
|
|
4
4
|
"displayName": "PassionCode.ai",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.10",
|
|
6
6
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|