@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.
Files changed (24) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -5
  3. package/SECURITY.md +1 -1
  4. package/bin/passioncode.js +5 -5
  5. package/family.json +1 -1
  6. package/package.json +1 -1
  7. package/payload/.claude-plugin/marketplace.json +2 -2
  8. package/payload/manifest.json +8 -8
  9. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  10. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +20 -4
  11. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +57 -0
  12. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +15 -2
  13. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +267 -0
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +19 -3
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +77 -0
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +177 -0
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +215 -0
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -1
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +470 -0
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +14 -2
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +118 -1
  23. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +9 -3
  24. 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.4.3"
8
+ version: "0.5.1"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "a5a27092ba0dcc5facfbeae8b359146dfb403e9a"
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 `a5a27092ba0dcc5facfbeae8b359146dfb403e9a` and
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.8",
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",