@passioncode-ai/passioncode 0.1.9 → 0.1.11
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 +16 -0
- 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 +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +11 -7
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +34 -16
- 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 +13 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +44 -10
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +83 -15
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +5 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.11 - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fabric Agent Adapter 0.5.2 in the set (`family.json` pins `v0.5.2`, was `v0.5.1`): services built on its
|
|
8
|
+
kits answer the MCP `initialize` handshake, so a real client such as Claude Code connects (0.5.0
|
|
9
|
+
and 0.5.1 did not), and every tool's `outputSchema` has an object root (contract DEC-0018).
|
|
10
|
+
|
|
11
|
+
## 0.1.10 - 2026-09-30
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Fabric Agent Adapter 0.5.1 in the set (`family.json` pins `v0.5.1`, was `v0.5.0`): the kits follow the
|
|
16
|
+
contract rulings of DEC-0017 — full result envelope with its trace for jobs, a job tool's
|
|
17
|
+
`outputSchema` as the union of envelope and handle, `providers/` entries carrying `providerId`.
|
|
18
|
+
|
|
3
19
|
## 0.1.9 - 2026-09-30
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/family.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Every PassionCode.ai agent skill — Fabric Agent Adapter, Observatory Log and the organisation's working rules — for Claude Code and every other agent on the machine, updated as one set.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.5.
|
|
14
|
+
"version": "0.5.2",
|
|
15
15
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.1.
|
|
38
|
+
"version": "0.1.11",
|
|
39
39
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.5.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.5.
|
|
10
|
+
"ref": "v0.5.2",
|
|
11
|
+
"commit": "b7a6b8ea081894ea8c0d3765879721c17f6a3655",
|
|
12
|
+
"version": "0.5.2",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"legacyMarketplaces": [
|
|
23
23
|
"fabric-agent-adapter"
|
|
24
24
|
],
|
|
25
|
-
"contentHash": "sha256:
|
|
25
|
+
"contentHash": "sha256:8d2a7d2795b1ab1e0dd3a1fc9a073c70e6085f160fa125bd5ada5a5f3d8d4c0b",
|
|
26
26
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
27
27
|
"author": {
|
|
28
28
|
"name": "PassionCode.ai",
|
|
@@ -61,16 +61,16 @@
|
|
|
61
61
|
"name": "passioncode",
|
|
62
62
|
"displayName": "PassionCode.ai",
|
|
63
63
|
"repo": "passioncode-ai/passioncode",
|
|
64
|
-
"ref": "v0.1.
|
|
64
|
+
"ref": "v0.1.11",
|
|
65
65
|
"commit": null,
|
|
66
|
-
"version": "0.1.
|
|
66
|
+
"version": "0.1.11",
|
|
67
67
|
"via": "this repository",
|
|
68
68
|
"skills": [
|
|
69
69
|
"working-in-passioncode"
|
|
70
70
|
],
|
|
71
71
|
"legacyPluginIds": [],
|
|
72
72
|
"legacyMarketplaces": [],
|
|
73
|
-
"contentHash": "sha256:
|
|
73
|
+
"contentHash": "sha256:917c5c1429a60a70666f1f8c7b2ee73d2e50bb8db56f64104f418ab0b7a46816",
|
|
74
74
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
75
75
|
"author": {
|
|
76
76
|
"name": "PassionCode.ai",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
6
|
-
"version": "0.5.
|
|
6
|
+
"version": "0.5.2",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -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.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.5.
|
|
8
|
+
version: "0.5.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -70,7 +70,7 @@ Use exactly:
|
|
|
70
70
|
|
|
71
71
|
- contract version `0.1.0`;
|
|
72
72
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
73
|
-
- commit `
|
|
73
|
+
- commit `2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f`.
|
|
74
74
|
|
|
75
75
|
Read the pinned contract's guide
|
|
76
76
|
`docs/guides/connecting-compatible-agents.md`, the selected profile specification, and
|
|
@@ -128,7 +128,7 @@ Treat all provider output as untrusted until schemas and semantic assertions pas
|
|
|
128
128
|
An agent Fabric reaches as a CLI or a stdio MCP server gets a provider entry, written by
|
|
129
129
|
its installer with [`scripts/fabric_provider.py`](scripts/fabric_provider.py) and removed
|
|
130
130
|
by its uninstaller — argv arrays only, env values as `secret-ref:` references, never the
|
|
131
|
-
id of an existing service. Load [provider entries](references/provider-entry.md). An
|
|
131
|
+
id of an existing service, and a `providerId` equal to the manifest's `provider.id`. Load [provider entries](references/provider-entry.md). An
|
|
132
132
|
agent that runs as a service uses a descriptor instead (`building-fabric-services`).
|
|
133
133
|
|
|
134
134
|
### 5. Make probes safe and meaningful
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Provider entries: agents that are not services
|
|
2
2
|
|
|
3
3
|
Normative source: the Fabric Agent Contract's `docs/specification/provider.md`
|
|
4
|
-
(`fabric-provider/0.1`, DEC-0016) at the commit this plugin pins.
|
|
4
|
+
(`fabric-provider/0.1`, DEC-0016, rulings DEC-0017) at the commit this plugin pins.
|
|
5
5
|
|
|
6
6
|
An agent Fabric reaches as a CLI or a stdio MCP server — not a long-running service —
|
|
7
7
|
is announced by one entry, `providers/<id>.json`, in the same root as the services
|
|
@@ -22,7 +22,8 @@ service or a provider, never both.
|
|
|
22
22
|
and refuses what the contract refuses.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
python3 scripts/fabric_provider.py write --id example-agent --
|
|
25
|
+
python3 scripts/fabric_provider.py write --id example-agent --provider-id https://agents.example/providers/example-agent \
|
|
26
|
+
--name "Example Agent" \
|
|
26
27
|
--manifest ~/.local/share/example-agent/fabric-agent.json --installed-by "example-agent 1.0.0" \
|
|
27
28
|
--env EXAMPLE_API_KEY=secret-ref:example-agent/EXAMPLE_API_KEY \
|
|
28
29
|
--stdio ~/.local/bin/example-agent mcp
|
|
@@ -35,19 +36,22 @@ python3 scripts/fabric_provider.py remove example-agent # the uninstaller
|
|
|
35
36
|
|
|
36
37
|
It refuses, with one sentence that never quotes a value:
|
|
37
38
|
|
|
39
|
+
- a manifest that does not resolve, or whose `provider.id` is not the entry's
|
|
40
|
+
`providerId` — the two URIs are compared as URIs (FAC-SEM-014);
|
|
38
41
|
- an id that is already a service in `services/` (FAC-SEM-013);
|
|
39
42
|
- an env value that is not `secret-ref:<name>`, or a reference that has the shape of a
|
|
40
|
-
credential itself — a token prefix, a private key, a JWT (FAC-SEM-015
|
|
43
|
+
credential itself — a token prefix, a private key, a JWT (FAC-SEM-015: a value is
|
|
44
|
+
checked by its form; name patterns apply to names);
|
|
41
45
|
- a URL other than `http://127.0.0.1:<port>/mcp`, a manifest path that is not an
|
|
42
46
|
absolute or `~/` path to `fabric-agent.json`, a shell-string command, unknown fields.
|
|
43
47
|
|
|
48
|
+
`validate` also checks that the file is named `<id>.json` (FAC-SEM-014, slug half).
|
|
44
49
|
From Python: `write_provider_entry(entry)`, `remove_provider_entry(id)`,
|
|
45
|
-
`validate_provider_entry(entry)`, `providers_dir()`.
|
|
50
|
+
`validate_provider_entry(entry)`, `manifest_problems(entry)`, `providers_dir()`.
|
|
46
51
|
|
|
47
52
|
## What the entry does not do
|
|
48
53
|
|
|
49
54
|
It grants nothing: Fabric lists the agent, and admission (probes) and a project binding
|
|
50
55
|
still decide what it may do. The manifest it points at is the authority for the
|
|
51
|
-
agent's capabilities.
|
|
52
|
-
|
|
53
|
-
"the manifest resolves" is checked until the contract answers OQ-0001.
|
|
56
|
+
agent's capabilities. Equality is between like things (DEC-0017): the slug `id` with the
|
|
57
|
+
file name, the URI `providerId` with the manifest's URI `provider.id`.
|
|
@@ -16,7 +16,7 @@ from urllib.parse import urlparse
|
|
|
16
16
|
|
|
17
17
|
CONTRACT_VERSION = "0.1.0"
|
|
18
18
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
19
|
-
CONTRACT_COMMIT = "
|
|
19
|
+
CONTRACT_COMMIT = "2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f"
|
|
20
20
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
21
21
|
MCP_REVISION = "2026-07-28"
|
|
22
22
|
A2A_VERSION = "1.0"
|
|
@@ -6,13 +6,13 @@ the providers directory, beside the services directory. Only its installer write
|
|
|
6
6
|
entry and only its uninstaller removes it; the entry grants no Project access.
|
|
7
7
|
Standard library only; it does not import the service kit, so this skill stands alone.
|
|
8
8
|
|
|
9
|
-
fabric_provider.py write --id ID --name NAME --manifest PATH --installed-by TEXT
|
|
9
|
+
fabric_provider.py write --id ID --provider-id URI --name NAME --manifest PATH --installed-by TEXT
|
|
10
10
|
(--url http://127.0.0.1:PORT/mcp | --stdio EXECUTABLE [ARG ...])
|
|
11
11
|
[--env NAME=secret-ref:REF ...] [--summary TEXT] [--repository URL]
|
|
12
12
|
fabric_provider.py remove ID
|
|
13
13
|
fabric_provider.py validate PATH
|
|
14
14
|
|
|
15
|
-
Normative source: fabric-agent-contract docs/specification/provider.md (DEC-0016).
|
|
15
|
+
Normative source: fabric-agent-contract docs/specification/provider.md (DEC-0016, rulings DEC-0017).
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
# #region provider-writer — docs: plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md#the-writer
|
|
@@ -35,7 +35,8 @@ _ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
|
|
35
35
|
_SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})$")
|
|
36
36
|
_LOCAL_PATH = re.compile(r"^(~/|/)[^\x00]*$")
|
|
37
37
|
_URL = re.compile(r"^http://127\.0\.0\.1:([0-9]{1,5})/mcp$")
|
|
38
|
-
|
|
38
|
+
_URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[^\s]+$")
|
|
39
|
+
FIELDS = {"protocol", "id", "providerId", "name", "summary", "manifest", "run", "source", "installedAt", "installedBy", "extensions"}
|
|
39
40
|
# FAC-SEM-015 reads a credential by its SHAPE: a reference named after a key is fine.
|
|
40
41
|
CREDENTIAL_SHAPES = [re.compile(p) for p in (
|
|
41
42
|
r"^(sk|pk|rk)[-_](live|test|proj|or|ant)?[-_]?[A-Za-z0-9_-]{16,}",
|
|
@@ -82,7 +83,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
82
83
|
if not isinstance(entry, dict):
|
|
83
84
|
return ["an entry is a JSON object"]
|
|
84
85
|
problems: List[str] = []
|
|
85
|
-
for key in ("protocol", "id", "name", "manifest", "run", "installedAt", "installedBy"):
|
|
86
|
+
for key in ("protocol", "id", "providerId", "name", "manifest", "run", "installedAt", "installedBy"):
|
|
86
87
|
if key not in entry:
|
|
87
88
|
problems.append("missing %s" % key)
|
|
88
89
|
for key in sorted(set(entry) - FIELDS):
|
|
@@ -93,6 +94,8 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
93
94
|
problems.append("protocol must be %s" % PROTOCOL)
|
|
94
95
|
if not _ID.match(str(entry["id"])):
|
|
95
96
|
problems.append("id must match %s" % _ID.pattern)
|
|
97
|
+
if not _URI.match(str(entry["providerId"])):
|
|
98
|
+
problems.append("providerId is the absolute URI of the manifest's provider.id")
|
|
96
99
|
if not isinstance(entry["name"], str) or not 1 <= len(entry["name"]) <= 80:
|
|
97
100
|
problems.append("name is 1 to 80 characters")
|
|
98
101
|
if "summary" in entry and (not isinstance(entry["summary"], str) or len(entry["summary"]) > 200):
|
|
@@ -130,6 +133,20 @@ def _run_problems(run: Any) -> List[str]:
|
|
|
130
133
|
return problems
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
def manifest_problems(entry: Dict[str, Any]) -> List[str]:
|
|
137
|
+
"""FAC-SEM-014 (DEC-0017), URI half: the manifest resolves and its provider.id equals providerId."""
|
|
138
|
+
try:
|
|
139
|
+
manifest = json.loads(Path(os.path.expanduser(str(entry["manifest"]))).read_text(encoding="utf-8"))
|
|
140
|
+
except (OSError, ValueError):
|
|
141
|
+
return ["the manifest %s does not resolve (FAC-SEM-014)" % entry["manifest"]]
|
|
142
|
+
provider = manifest.get("provider") if isinstance(manifest, dict) else None
|
|
143
|
+
if not isinstance(provider, dict) or not isinstance(manifest.get("capabilities"), list):
|
|
144
|
+
return ["%s is not a provider manifest (FAC-SEM-014)" % entry["manifest"]]
|
|
145
|
+
if provider.get("id") != entry["providerId"]:
|
|
146
|
+
return ["providerId %s is not the manifest's provider.id %s (FAC-SEM-014)" % (entry["providerId"], provider.get("id"))]
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
|
|
133
150
|
def _atomic_write(path: Path, data: bytes) -> None:
|
|
134
151
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
152
|
os.chmod(path.parent, 0o700)
|
|
@@ -163,6 +180,8 @@ def _service_ids(directory: Path) -> Dict[str, str]:
|
|
|
163
180
|
def write_provider_entry(entry: Dict[str, Any], directory: Optional[Path] = None, services: Optional[Path] = None) -> Path:
|
|
164
181
|
"""Installer-only. Refuses an invalid entry and an id that is already a service (FAC-SEM-013)."""
|
|
165
182
|
problems = validate_provider_entry(entry)
|
|
183
|
+
if not problems:
|
|
184
|
+
problems = manifest_problems(entry)
|
|
166
185
|
if problems:
|
|
167
186
|
raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
|
|
168
187
|
clash = _service_ids(services or services_dir()).get(entry["id"])
|
|
@@ -193,6 +212,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
193
212
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
194
213
|
write = sub.add_parser("write")
|
|
195
214
|
write.add_argument("--id", required=True)
|
|
215
|
+
write.add_argument("--provider-id", required=True, help="the manifest's provider.id (a URI)")
|
|
196
216
|
write.add_argument("--name", required=True)
|
|
197
217
|
write.add_argument("--summary")
|
|
198
218
|
write.add_argument("--manifest", required=True)
|
|
@@ -212,7 +232,12 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
212
232
|
print("removed" if remove_provider_entry(args.id) else "no entry for %s" % args.id)
|
|
213
233
|
return 0
|
|
214
234
|
if args.command == "validate":
|
|
215
|
-
|
|
235
|
+
entry = json.loads(args.path.read_text(encoding="utf-8"))
|
|
236
|
+
problems = validate_provider_entry(entry)
|
|
237
|
+
if isinstance(entry, dict) and entry.get("id") != args.path.stem:
|
|
238
|
+
problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
|
|
239
|
+
if not problems:
|
|
240
|
+
problems = manifest_problems(entry)
|
|
216
241
|
print("; ".join(problems) if problems else "valid")
|
|
217
242
|
return 1 if problems else 0
|
|
218
243
|
env: Dict[str, str] = {}
|
|
@@ -224,7 +249,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
224
249
|
run: Dict[str, Any] = {"mcp": {"url": args.url}} if args.url else {"mcp": {"stdio": {"command": args.stdio}}}
|
|
225
250
|
if env and args.stdio:
|
|
226
251
|
run["mcp"]["stdio"]["env"] = env
|
|
227
|
-
entry: Dict[str, Any] = {"protocol": PROTOCOL, "id": args.id, "name": args.name, "manifest": args.manifest,
|
|
252
|
+
entry: Dict[str, Any] = {"protocol": PROTOCOL, "id": args.id, "providerId": args.provider_id, "name": args.name, "manifest": args.manifest,
|
|
228
253
|
"run": run, "installedAt": _now(), "installedBy": args.installed_by}
|
|
229
254
|
if args.summary:
|
|
230
255
|
entry["summary"] = args.summary
|
|
@@ -15,10 +15,10 @@ license: PolyForm-Noncommercial-1.0.0 OR LicenseRef-PolyForm-Internal-Use-1.0.0
|
|
|
15
15
|
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). No network or package install; the contract checkout is optional.
|
|
16
16
|
metadata:
|
|
17
17
|
author: PassionCode.ai
|
|
18
|
-
version: "0.5.
|
|
18
|
+
version: "0.5.2"
|
|
19
19
|
contract-version: "0.1.0"
|
|
20
20
|
extension: "fabric-service/0.1"
|
|
21
|
-
extension-commit: "
|
|
21
|
+
extension-commit: "2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f"
|
|
22
22
|
---
|
|
23
23
|
|
|
24
24
|
# Building Fabric services
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Being called: `fabric-interop/0.1`
|
|
2
2
|
|
|
3
|
-
Normative source: the Fabric Agent Contract's `docs/specification/interop.md` (DEC-0016)
|
|
3
|
+
Normative source: the Fabric Agent Contract's `docs/specification/interop.md` (DEC-0016, rulings DEC-0017 and DEC-0018)
|
|
4
4
|
at the commit this plugin pins. This page says how the kit implements it; where the two
|
|
5
5
|
differ, the contract is right.
|
|
6
6
|
|
|
@@ -9,11 +9,11 @@ A service that other agents — and Fabric — call does five things. Each has o
|
|
|
9
9
|
|
|
10
10
|
| Rule | What the service does | Helper |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's
|
|
12
|
+
| C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's input schema unchanged, its output schema unchanged — or, for a job, wrapped as `oneOf[result envelope, job handle]` — and annotations from the effect | `tool_for_capability` / `toolForCapability`, `job_tool_output_schema`, `expected_annotations` |
|
|
13
13
|
| C3.2 long work = job | a capability whose work can outlive one request declares `"job": true` in its interop block, returns `{"job": {"id", "status": "working"}}`, and serves `fabric.job.get` and `fabric.job.cancel` | `JobStore`, `McpToolServer` (Python), `unknown_job_result` |
|
|
14
|
-
| C3.2 result | a completed job carries the result envelope: `done`, `proof`, `scope`, `notVerified`, `output`, `usage` | `result_envelope` / `resultEnvelope` |
|
|
14
|
+
| C3.2 result | a completed job carries the full result envelope — the shape a synchronous call returns: `id`, `contractVersion`, `outcome`, `done`, `proof`, `scope`, `notVerified`, `artifacts`, `createdAt`, `producer`, `output`, `usage`, and its `trace` | `result_envelope` / `resultEnvelope` |
|
|
15
15
|
| C3.3 a person decides | the job goes `input_required` with an elicitation: a titled single-select in form mode, and URL mode for anything secret | `choice_request`, `form_request`, `url_request` |
|
|
16
|
-
| C3.4 trace | every call runs as a child span of the caller's `_meta.traceparent`; outgoing calls carry the same trace; events about
|
|
16
|
+
| C3.4 trace | every call runs as a child span of the caller's `_meta.traceparent`; outgoing calls carry the same trace; the result envelope records it; events about traced work carry `traceId` and `spanId`, events about untraced work none | `child_traceparent`, `trace_ids`, `make_event(..., trace_id=, span_id=)` |
|
|
17
17
|
|
|
18
18
|
The interop block sits in the manifest under the capability's `extensions`:
|
|
19
19
|
|
|
@@ -40,25 +40,42 @@ response = server.handle(json_rpc_message) # POST /mcp body in, JSON re
|
|
|
40
40
|
- `jobs.request_input(job_id, {"title_choice": fi.choice_request(...)}, "Two titles are ready.")`
|
|
41
41
|
stops for a person. Fabric answers through `fabric.job.get` with `inputResponses`; the
|
|
42
42
|
server calls `on_input(job_id, answers, ctx)` with the answers that matched.
|
|
43
|
-
- `jobs.complete(job_id, fi.result_envelope(...))`,
|
|
44
|
-
`jobs.cancel(job_id)`. A terminal job never changes
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
- `jobs.complete(job_id, fi.result_envelope(outcome=..., producer=..., ...))`,
|
|
44
|
+
`jobs.fail(job_id, code, message)`, `jobs.cancel(job_id)`. A terminal job never changes
|
|
45
|
+
again. A result with unverified claims is `partial`, never `succeeded`.
|
|
46
|
+
- The envelope's `trace` is authoritative for a stored result (DEC-0017): `complete` fills
|
|
47
|
+
it with the span of the call that started the job and refuses one naming another span,
|
|
48
|
+
and `fabric.job.get` answers with that same traceparent in `_meta` (FAC-SEM-022). The
|
|
49
|
+
contract's FAC-SEM-019 records a result without a trace as an incomplete span.
|
|
48
50
|
- Log events about the work with `**fi.trace_ids(ctx.traceparent)` so the pair lands on
|
|
49
51
|
the event.
|
|
50
52
|
|
|
51
|
-
`McpToolServer` answers `
|
|
52
|
-
|
|
53
|
+
`McpToolServer` answers `initialize` and `ping` (clients such as Claude Code 2.1.285 still
|
|
54
|
+
open with a handshake, and a server that refuses it is marked failed), `server/discover`,
|
|
55
|
+
`tools/list` and `tools/call` with JSON responses. Every tool's `outputSchema` must be
|
|
56
|
+
rooted at `type: object` (DEC-0018): `add_tool` refuses any other root, because a client may
|
|
57
|
+
drop the whole tool list over one bad schema — an SDK-level test will not show it, so check
|
|
58
|
+
with the real client (below). A service already on an MCP SDK keeps
|
|
53
59
|
the SDK and uses only the helpers. The Node kit has the helpers and the same job file
|
|
54
60
|
format, without the dispatcher. `scripts/sample_service.py` is the worked example:
|
|
55
61
|
`sample.echo` answers at once, `sample.draft` is a job that stops for a title choice.
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
The contract owner's rulings the kit follows: a job tool's `outputSchema` is the
|
|
64
|
+
self-contained `{type: object, oneOf: [result envelope, job handle]}` (`job_tool_output_schema`,
|
|
65
|
+
DEC-0017 amended by DEC-0018), so its `structuredContent` always conforms; an event about
|
|
66
|
+
untraced work carries no trace pair.
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
68
|
+
**Check with a real client**, never only an SDK: `FABRIC_REAL_CLIENT=1 python3 -m unittest
|
|
69
|
+
test.test_real_client` in the adapter repository, or by hand — a throwaway directory, a
|
|
70
|
+
temporary `--mcp-config` (mode 600, the token in a header) and `--strict-mcp-config`, so
|
|
71
|
+
the operator's own configuration is not touched:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
claude -p "Reply with the single word OK." --strict-mcp-config --mcp-config "$TMP/mcp.json" \
|
|
75
|
+
--output-format stream-json --verbose --max-turns 1
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The first `system/init` event lists `mcp_servers` (`connected`) and the `mcp__<server>__*` tools.
|
|
62
79
|
|
|
63
80
|
## What the probe checks
|
|
64
81
|
|
|
@@ -66,9 +83,10 @@ Two readings the contract leaves open, and what the kit does meanwhile:
|
|
|
66
83
|
|
|
67
84
|
| Rule | PASS when | NOT_RUN when |
|
|
68
85
|
|---|---|---|
|
|
86
|
+
| `interop.output-schema-object` | every listed tool's `outputSchema`, when present, has root `type: object` (FAC-SEM-023) | no MCP surface |
|
|
69
87
|
| `interop.manifest-link` | the manifest `fabricManifest` names carries the service key with this `<id>.<instance>` (G-07) | the descriptor names no manifest |
|
|
70
88
|
| `interop.well-known-capabilities` | every name in `surfaces.mcp.capabilities` is a manifest capability | the surface lists none |
|
|
71
|
-
| `interop.tools-match` | every `mcp` capability is served as its tool, schemas equal to the files beside the manifest (matched by `$id
|
|
89
|
+
| `interop.tools-match` | every `mcp` capability is served as its tool, schemas equal to the files beside the manifest (matched by `$id`; a job's output wrapped in the DEC-0017 union), annotations derived | no manifest, no MCP surface, or a schema is not beside the manifest |
|
|
72
90
|
| `interop.job-tools` | a `job: true` capability comes with `fabric.job.get` and `fabric.job.cancel` | no capability is a job |
|
|
73
91
|
| `interop.unknown-job` | `fabric.job.get` for a made-up id answers `isError` with `unknown-job` | `fabric.job.get` is not served |
|
|
74
92
|
| `interop.trace-propagation` | the answer's `_meta.traceparent` has the probe's trace id and a new span | the answer carries no traceparent |
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fabric-service/0.1 — wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f`
|
|
4
4
|
(`docs/specification/service.md`, DEC-0015). The contract's schemas are normative;
|
|
5
5
|
this page is the working summary. Extension key:
|
|
6
6
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
@@ -287,7 +287,7 @@ class Probe:
|
|
|
287
287
|
self.add("interop.well-known-capabilities", "FAIL" if extra else "PASS",
|
|
288
288
|
("listed but not in the manifest: " + ", ".join(extra)) if extra else "%d listed, all in the manifest" % len(listed))
|
|
289
289
|
if not mcp_surface or not self.token:
|
|
290
|
-
for rule in ("interop.tools-match", "interop.job-tools", "interop.unknown-job", "interop.trace-propagation"):
|
|
290
|
+
for rule in ("interop.output-schema-object", "interop.tools-match", "interop.job-tools", "interop.unknown-job", "interop.trace-propagation"):
|
|
291
291
|
self.add(rule, "NOT_RUN", "no MCP surface" if not mcp_surface else "no readable token")
|
|
292
292
|
else:
|
|
293
293
|
try:
|
|
@@ -298,12 +298,21 @@ class Probe:
|
|
|
298
298
|
if listing is not None:
|
|
299
299
|
sent = self.sent_traceparent
|
|
300
300
|
tools = {t.get("name"): t for t in (listing.get("result") or {}).get("tools", []) if isinstance(t, dict)}
|
|
301
|
+
self.object_root_rule(tools)
|
|
301
302
|
self.tools_match_rule(capabilities, manifest_path, tools)
|
|
302
303
|
self.job_tools_rule(capabilities, tools)
|
|
303
304
|
self.unknown_job_rule(list(tools))
|
|
304
305
|
self.trace_rule(listing, sent)
|
|
305
306
|
self.events_trace_rule(self.events)
|
|
306
307
|
|
|
308
|
+
def object_root_rule(self, tools: Dict[str, Any]) -> None:
|
|
309
|
+
"""FAC-SEM-023 (DEC-0018): every listed tool's outputSchema, when present, has root type object —
|
|
310
|
+
a client may refuse the whole tools/list otherwise, whatever the SDK in the tests accepted."""
|
|
311
|
+
bad = sorted(n for n, t in tools.items() if "outputSchema" in t and (t.get("outputSchema") or {}).get("type") != "object")
|
|
312
|
+
with_schema = sum(1 for t in tools.values() if "outputSchema" in t)
|
|
313
|
+
self.add("interop.output-schema-object", "FAIL" if bad else "PASS",
|
|
314
|
+
("outputSchema root is not type object: " + ", ".join(bad)) if bad else "%d outputSchemas, every root type object" % with_schema)
|
|
315
|
+
|
|
307
316
|
def tools_match_rule(self, capabilities: List[Dict[str, Any]], manifest_path: Optional[Path], tools: Dict[str, Any]) -> None:
|
|
308
317
|
"""FAC-SEM-017: each mcp capability is served as the tool of its name, with its schemas and derived annotations."""
|
|
309
318
|
served = [c for c in capabilities if (c.get("profile") or {}).get("kind") == "mcp"]
|
|
@@ -321,6 +330,9 @@ class Probe:
|
|
|
321
330
|
schema = self.resolve_schema(manifest_path, str(cap.get(side)))
|
|
322
331
|
if schema is None:
|
|
323
332
|
unresolved.append("%s %s" % (name, cap.get(side)))
|
|
333
|
+
elif side == "outputSchema" and fi.is_job_capability(cap):
|
|
334
|
+
if tool.get(side) != fi.job_tool_output_schema(schema):
|
|
335
|
+
problems.append("%s is a job: its outputSchema must be oneOf[result envelope, job handle] around %s (DEC-0017)" % (name, cap.get(side)))
|
|
324
336
|
elif tool.get(side) != schema:
|
|
325
337
|
problems.append("%s serves an %s that differs from %s" % (name, side, cap.get(side)))
|
|
326
338
|
annotations = tool.get("annotations") or {}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The Node twin of fabric_interop.py: the same rules and the same job file format, so a
|
|
3
3
|
// job written by one kit is read by the other. The minimal MCP dispatcher is Python-only;
|
|
4
4
|
// a Node service uses the official MCP SDK for the wire and these helpers for the rules.
|
|
5
|
-
// Normative source: fabric-agent-contract docs/specification/interop.md (DEC-0016).
|
|
5
|
+
// Normative source: fabric-agent-contract docs/specification/interop.md (DEC-0016, rulings DEC-0017).
|
|
6
6
|
// #region interop-kit-node — docs: plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md#the-kit
|
|
7
7
|
|
|
8
8
|
import crypto from 'node:crypto';
|
|
@@ -14,6 +14,15 @@ export const PROTOCOL = 'fabric-interop/0.1';
|
|
|
14
14
|
export const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1';
|
|
15
15
|
export const MCP_REVISION = '2026-07-28';
|
|
16
16
|
export const TERMINAL = ['completed', 'failed', 'cancelled'];
|
|
17
|
+
export const CONTRACT_VERSION = '0.1.0';
|
|
18
|
+
const OUTCOMES = ['succeeded', 'partial', 'failed', 'cancelled', 'blocked'];
|
|
19
|
+
const ENVELOPE_REQUIRED = ['id', 'contractVersion', 'outcome', 'done', 'proof', 'scope', 'notVerified', 'artifacts', 'createdAt', 'producer', 'output', 'usage'];
|
|
20
|
+
// The job handle inline, as a job tool's outputSchema carries it (contract interop-job-handle.schema.json).
|
|
21
|
+
export const JOB_HANDLE_SCHEMA = {
|
|
22
|
+
type: 'object', required: ['job'], additionalProperties: false,
|
|
23
|
+
properties: { job: { type: 'object', required: ['id', 'status'], additionalProperties: false,
|
|
24
|
+
properties: { id: { type: 'string', minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$' }, status: { const: 'working' } } } },
|
|
25
|
+
};
|
|
17
26
|
|
|
18
27
|
const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
19
28
|
const JOB_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
@@ -56,9 +65,20 @@ export function expectedAnnotations(effect, idempotency) {
|
|
|
56
65
|
return hints;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
//
|
|
68
|
+
// DEC-0017: a job-backed tool serves oneOf[result envelope, job handle]; the manifest keeps the pure output schema.
|
|
69
|
+
export const jobToolOutputSchema = (outputSchema) => ({ type: 'object', oneOf: [{ type: 'object', required: [...ENVELOPE_REQUIRED], properties: { output: outputSchema } }, JOB_HANDLE_SCHEMA] });
|
|
70
|
+
export const isJobCapability = (capability) => capability.job === true || capability.extensions?.[EXTENSION_KEY]?.job === true;
|
|
71
|
+
|
|
72
|
+
// DEC-0018 (FAC-SEM-023): an outputSchema is rooted at type object; a client may reject the whole
|
|
73
|
+
// tools/list when one tool's outputSchema is, say, a bare oneOf.
|
|
74
|
+
export function requireObjectRoot(name, outputSchema) {
|
|
75
|
+
if (!outputSchema || typeof outputSchema !== 'object' || outputSchema.type !== 'object') throw new InteropError(`Tool ${name}: an outputSchema must have root type "object".`);
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
export function toolForCapability(capability, inputSchema, outputSchema, title) {
|
|
61
|
-
|
|
79
|
+
requireObjectRoot(capability.name, outputSchema);
|
|
80
|
+
const served = isJobCapability(capability) ? jobToolOutputSchema(outputSchema) : outputSchema;
|
|
81
|
+
const tool = { name: capability.name, inputSchema, outputSchema: served, annotations: expectedAnnotations(capability.effect, capability.idempotency) };
|
|
62
82
|
if (capability.description) tool.description = capability.description;
|
|
63
83
|
if (title) tool.title = title;
|
|
64
84
|
return tool;
|
|
@@ -85,12 +105,21 @@ function checkUsage(usage) {
|
|
|
85
105
|
return { ...usage };
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
// The full result envelope (contract result.schema.json, DEC-0017): the shape a synchronous call returns.
|
|
109
|
+
export function resultEnvelope({ outcome, done, proof, scope, notVerified, output, usage, producer, artifacts = [], traceparent, id, createdAt }) {
|
|
110
|
+
if (!OUTCOMES.includes(outcome)) throw new InteropError(`outcome must be one of ${OUTCOMES.join(', ')}.`);
|
|
111
|
+
for (const [label, value] of [['done', done], ['proof', proof], ['notVerified', notVerified], ['artifacts', artifacts]]) {
|
|
90
112
|
if (!Array.isArray(value)) throw new InteropError(`${label} must be a list, even when empty.`);
|
|
91
113
|
}
|
|
92
|
-
if (
|
|
93
|
-
|
|
114
|
+
if (outcome === 'succeeded' && notVerified.length) throw new InteropError('A succeeded result cannot keep unverified claims (FAC-SEM-001); report partial.');
|
|
115
|
+
if (!scope || typeof scope !== 'object' || !producer || typeof producer !== 'object') throw new InteropError('scope and producer must be objects.');
|
|
116
|
+
const envelope = {
|
|
117
|
+
id: id ?? `urn:fabric:result:${crypto.randomBytes(12).toString('hex')}`, contractVersion: CONTRACT_VERSION, outcome,
|
|
118
|
+
done: [...done], proof: [...proof], scope: { ...scope }, notVerified: [...notVerified], artifacts: [...artifacts],
|
|
119
|
+
createdAt: createdAt ?? nowIso(), producer: { ...producer }, output, usage: checkUsage(usage),
|
|
120
|
+
};
|
|
121
|
+
if (parseTraceparent(traceparent)) envelope.trace = { traceparent };
|
|
122
|
+
return envelope;
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
// --- C3.3 awaiting a choice ------------------------------------------------------------------
|
|
@@ -176,10 +205,15 @@ export class JobStore {
|
|
|
176
205
|
return matched;
|
|
177
206
|
}
|
|
178
207
|
|
|
208
|
+
// The envelope carries the job's trace, authoritative for the stored result (DEC-0017, FAC-SEM-022).
|
|
179
209
|
complete(jobId, envelope) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
if (!envelope || !ENVELOPE_REQUIRED.every((k) => k in envelope)) throw new InteropError('A completed job carries the full result envelope; build it with resultEnvelope.');
|
|
211
|
+
const stored = this.traceparent(jobId);
|
|
212
|
+
const given = envelope.trace?.traceparent;
|
|
213
|
+
const ids = (v) => { const p = parseTraceparent(v); return p ? `${p.traceId}-${p.spanId}` : null; };
|
|
214
|
+
if (stored && given && ids(given) !== ids(stored)) throw new InteropError(`Job ${jobId} ran in span ${stored}; its result names another trace.`);
|
|
215
|
+
const result = stored && !given ? { ...envelope, trace: { traceparent: stored } } : envelope;
|
|
216
|
+
return this.transition(jobId, 'completed', { result });
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
fail(jobId, code, message) { return this.transition(jobId, 'failed', { error: { code, message } }); }
|
|
@@ -10,7 +10,7 @@ it, so a service that calls these functions inherits the rule:
|
|
|
10
10
|
- C3.3 a question for a person is an elicitation (form mode, never a secret; URL mode for those);
|
|
11
11
|
- C3.4 every answer is a child span of the caller's traceparent, and so is every event.
|
|
12
12
|
|
|
13
|
-
Normative source: fabric-agent-contract docs/specification/interop.md (DEC-0016).
|
|
13
|
+
Normative source: fabric-agent-contract docs/specification/interop.md (DEC-0016, rulings DEC-0017).
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
# #region interop-kit — docs: plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md#the-kit
|
|
@@ -30,7 +30,22 @@ import fabric_service as _fs # noqa: E402
|
|
|
30
30
|
PROTOCOL = "fabric-interop/0.1"
|
|
31
31
|
EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
32
32
|
MCP_REVISION = "2026-07-28"
|
|
33
|
+
# Revisions a client may name in `initialize`. MCP 2026-07-28 needs no handshake, but clients
|
|
34
|
+
# built on earlier revisions (Claude Code 2.1.285 among them) still open with one, and a server
|
|
35
|
+
# that refuses it is marked failed before tools/list is ever called.
|
|
36
|
+
HANDSHAKE_REVISIONS = ("2025-03-26", "2025-06-18", "2025-11-25", "2026-07-28")
|
|
37
|
+
CONTRACT_VERSION = "0.1.0"
|
|
33
38
|
JOB_STATES = ("working", "input_required", "completed", "failed", "cancelled")
|
|
39
|
+
OUTCOMES = ("succeeded", "partial", "failed", "cancelled", "blocked")
|
|
40
|
+
ENVELOPE_REQUIRED = ["id", "contractVersion", "outcome", "done", "proof", "scope", "notVerified", "artifacts",
|
|
41
|
+
"createdAt", "producer", "output", "usage"]
|
|
42
|
+
# The job handle inline, as a job tool's outputSchema carries it (contract interop-job-handle.schema.json).
|
|
43
|
+
JOB_HANDLE_SCHEMA: Dict[str, Any] = {
|
|
44
|
+
"type": "object", "required": ["job"], "additionalProperties": False,
|
|
45
|
+
"properties": {"job": {"type": "object", "required": ["id", "status"], "additionalProperties": False,
|
|
46
|
+
"properties": {"id": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$"},
|
|
47
|
+
"status": {"const": "working"}}}},
|
|
48
|
+
}
|
|
34
49
|
TERMINAL = ("completed", "failed", "cancelled")
|
|
35
50
|
|
|
36
51
|
_TRACEPARENT = re.compile(r"^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
|
|
@@ -98,16 +113,35 @@ def expected_annotations(effect: str, idempotency: str) -> Dict[str, bool]:
|
|
|
98
113
|
return hints
|
|
99
114
|
|
|
100
115
|
|
|
116
|
+
def job_tool_output_schema(output_schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
117
|
+
"""DEC-0017: a job-backed tool's outputSchema is oneOf[result envelope, job handle], self-contained,
|
|
118
|
+
so structuredContent always conforms; the manifest's capability keeps the pure output schema."""
|
|
119
|
+
return {"type": "object",
|
|
120
|
+
"oneOf": [{"type": "object", "required": list(ENVELOPE_REQUIRED), "properties": {"output": output_schema}},
|
|
121
|
+
JOB_HANDLE_SCHEMA]}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def require_object_root(name: str, output_schema: Any) -> None:
|
|
125
|
+
"""DEC-0018 (FAC-SEM-023): an outputSchema is rooted at type object. MCP requires it, and a client
|
|
126
|
+
may reject the WHOLE tools/list when one tool's outputSchema is, say, a bare oneOf."""
|
|
127
|
+
if not isinstance(output_schema, dict) or output_schema.get("type") != "object":
|
|
128
|
+
raise InteropError("Tool %s: an outputSchema must have root type \"object\"." % name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def is_job_capability(capability: Dict[str, Any]) -> bool:
|
|
132
|
+
block = (capability.get("extensions") or {}).get(EXTENSION_KEY) or {}
|
|
133
|
+
return capability.get("job") is True or block.get("job") is True
|
|
134
|
+
|
|
135
|
+
|
|
101
136
|
def tool_for_capability(capability: Dict[str, Any], input_schema: Dict[str, Any], output_schema: Dict[str, Any],
|
|
102
137
|
title: Optional[str] = None) -> Dict[str, Any]:
|
|
103
|
-
"""The MCP tool for one manifest capability: its name, its
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
structuredContent is the job handle (contract OQ-0006)."""
|
|
138
|
+
"""The MCP tool for one manifest capability: its name, its input schema as it is, derived annotations,
|
|
139
|
+
and its output schema — wrapped in the job union when the capability is a job (DEC-0017)."""
|
|
140
|
+
require_object_root(capability["name"], output_schema)
|
|
107
141
|
tool: Dict[str, Any] = {
|
|
108
142
|
"name": capability["name"],
|
|
109
143
|
"inputSchema": input_schema,
|
|
110
|
-
"outputSchema": output_schema,
|
|
144
|
+
"outputSchema": job_tool_output_schema(output_schema) if is_job_capability(capability) else output_schema,
|
|
111
145
|
"annotations": expected_annotations(capability.get("effect", ""), capability.get("idempotency", "")),
|
|
112
146
|
}
|
|
113
147
|
if capability.get("description"):
|
|
@@ -153,16 +187,30 @@ def _usage(usage: Any) -> Dict[str, Any]:
|
|
|
153
187
|
return dict(usage)
|
|
154
188
|
|
|
155
189
|
|
|
156
|
-
def result_envelope(*, done: List[Dict[str, Any]], proof: List[Dict[str, Any]], scope: Dict[str, Any],
|
|
157
|
-
not_verified: List[Dict[str, Any]], output: Any, usage: Dict[str, Any]
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
def result_envelope(*, outcome: str, done: List[Dict[str, Any]], proof: List[Dict[str, Any]], scope: Dict[str, Any],
|
|
191
|
+
not_verified: List[Dict[str, Any]], output: Any, usage: Dict[str, Any], producer: Dict[str, Any],
|
|
192
|
+
artifacts: Optional[List[Dict[str, Any]]] = None, traceparent: Optional[str] = None,
|
|
193
|
+
result_id: Optional[str] = None, created_at: Optional[str] = None) -> Dict[str, Any]:
|
|
194
|
+
"""The full result envelope (contract result.schema.json, DEC-0011, DEC-0017): the same shape a
|
|
195
|
+
synchronous call returns, with output, usage and — when the work was traced — its trace."""
|
|
196
|
+
if outcome not in OUTCOMES:
|
|
197
|
+
raise InteropError("outcome must be one of %s." % ", ".join(OUTCOMES))
|
|
198
|
+
for label, value in (("done", done), ("proof", proof), ("notVerified", not_verified), ("artifacts", artifacts or [])):
|
|
160
199
|
if not isinstance(value, list):
|
|
161
200
|
raise InteropError("%s must be a list, even when empty." % label)
|
|
162
|
-
if
|
|
163
|
-
raise InteropError("
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
if outcome == "succeeded" and not_verified:
|
|
202
|
+
raise InteropError("A succeeded result cannot keep unverified claims (FAC-SEM-001); report partial.")
|
|
203
|
+
if not isinstance(scope, dict) or not isinstance(producer, dict):
|
|
204
|
+
raise InteropError("scope and producer must be objects.")
|
|
205
|
+
envelope: Dict[str, Any] = {
|
|
206
|
+
"id": result_id or "urn:fabric:result:" + secrets.token_hex(12), "contractVersion": CONTRACT_VERSION,
|
|
207
|
+
"outcome": outcome, "done": list(done), "proof": list(proof), "scope": dict(scope),
|
|
208
|
+
"notVerified": list(not_verified), "artifacts": list(artifacts or []), "createdAt": created_at or _fs.now_iso(),
|
|
209
|
+
"producer": dict(producer), "output": output, "usage": _usage(usage),
|
|
210
|
+
}
|
|
211
|
+
if parse_traceparent(traceparent):
|
|
212
|
+
envelope["trace"] = {"traceparent": traceparent}
|
|
213
|
+
return envelope
|
|
166
214
|
|
|
167
215
|
|
|
168
216
|
# --- C3.3 awaiting a choice ------------------------------------------------------------
|
|
@@ -287,8 +335,17 @@ class JobStore:
|
|
|
287
335
|
return matched
|
|
288
336
|
|
|
289
337
|
def complete(self, job_id: str, envelope: Dict[str, Any]) -> Dict[str, Any]:
|
|
290
|
-
|
|
338
|
+
"""The envelope carries the job's trace, authoritative for the stored result (DEC-0017); one that names
|
|
339
|
+
another span is refused, so fabric.job.get's _meta and the envelope always agree (FAC-SEM-022)."""
|
|
340
|
+
if not isinstance(envelope, dict) or not set(ENVELOPE_REQUIRED) <= set(envelope):
|
|
291
341
|
raise InteropError("A completed job carries the full result envelope; build it with result_envelope.")
|
|
342
|
+
stored = self.traceparent(job_id)
|
|
343
|
+
given = (envelope.get("trace") or {}).get("traceparent")
|
|
344
|
+
ids = lambda value: ((parse_traceparent(value) or {}).get("trace_id"), (parse_traceparent(value) or {}).get("span_id"))
|
|
345
|
+
if stored and given and ids(given) != ids(stored):
|
|
346
|
+
raise InteropError("Job %s ran in span %s; its result names another trace." % (job_id, stored))
|
|
347
|
+
if stored and not given:
|
|
348
|
+
envelope = dict(envelope, trace={"traceparent": stored})
|
|
292
349
|
return self._transition(job_id, "completed", result=envelope)
|
|
293
350
|
|
|
294
351
|
def fail(self, job_id: str, code: Any, message: str) -> Dict[str, Any]:
|
|
@@ -347,6 +404,8 @@ class McpToolServer:
|
|
|
347
404
|
def add_tool(self, tool: Dict[str, Any], handler: Handler) -> None:
|
|
348
405
|
if tool["name"] in self.handlers:
|
|
349
406
|
raise InteropError("Tool %s is served twice." % tool["name"])
|
|
407
|
+
if "outputSchema" in tool:
|
|
408
|
+
require_object_root(tool["name"], tool["outputSchema"])
|
|
350
409
|
self.tools.append(tool)
|
|
351
410
|
self.handlers[tool["name"]] = handler
|
|
352
411
|
|
|
@@ -379,6 +438,13 @@ class McpToolServer:
|
|
|
379
438
|
meta = params.get("_meta") or {}
|
|
380
439
|
span = child_traceparent(meta.get("traceparent"))
|
|
381
440
|
method = message["method"]
|
|
441
|
+
if method == "initialize":
|
|
442
|
+
asked = params.get("protocolVersion")
|
|
443
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {
|
|
444
|
+
"protocolVersion": asked if asked in HANDSHAKE_REVISIONS else MCP_REVISION,
|
|
445
|
+
"capabilities": {"tools": {"listChanged": False}}, "serverInfo": self.info}}
|
|
446
|
+
if method == "ping":
|
|
447
|
+
return {"jsonrpc": "2.0", "id": rid, "result": {}}
|
|
382
448
|
if method == "server/discover":
|
|
383
449
|
return {"jsonrpc": "2.0", "id": rid, "result": {"resultType": "complete", "capabilities": {"tools": {}},
|
|
384
450
|
"serverInfo": self.info, "_meta": {"traceparent": span}}}
|
|
@@ -404,6 +470,8 @@ class McpToolServer:
|
|
|
404
470
|
job_id = str(arguments.get("id", ""))
|
|
405
471
|
try:
|
|
406
472
|
job_span = self.jobs.traceparent(job_id) or span
|
|
473
|
+
state = self.jobs.get(job_id)
|
|
474
|
+
job_span = ((state.get("result") or {}).get("trace") or {}).get("traceparent") or job_span
|
|
407
475
|
if name == "fabric.job.cancel":
|
|
408
476
|
state = self.jobs.get(job_id)
|
|
409
477
|
if state["status"] not in TERMINAL:
|
|
@@ -110,6 +110,8 @@ class Service:
|
|
|
110
110
|
title = "About %s" % topic if choice == "plain" else "What is %s?" % topic
|
|
111
111
|
body = "%s. This sample note was written by the sample service." % title
|
|
112
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},
|
|
113
115
|
done=[{"claimId": "NOTE", "statement": "A note titled %s was drafted." % title}], proof=[],
|
|
114
116
|
scope={"project": "urn:fabric:project:sample", "run": "urn:fabric:run:%s" % job_id, "node": "urn:fabric:node:draft",
|
|
115
117
|
"binding": {"id": "urn:fabric:binding:sample.draft", "revision": 1, "contentHash": "sha256:" + "0" * 64}, "writeScopes": []},
|
|
@@ -185,6 +187,9 @@ def make_handler(svc: Service):
|
|
|
185
187
|
except fs.ServiceError as exc:
|
|
186
188
|
return self._send(400, {"error": str(exc)})
|
|
187
189
|
return self._send(200, page)
|
|
190
|
+
if url.path == "/mcp":
|
|
191
|
+
# No server-to-client stream here: Streamable HTTP says 405 for a GET.
|
|
192
|
+
return self._send(405, {"error": "POST JSON-RPC to /mcp."}, {"Allow": "POST"})
|
|
188
193
|
if url.path == "/fabric/v1/login":
|
|
189
194
|
cookie = svc.codes.redeem((query.get("code") or [None])[0])
|
|
190
195
|
if not cookie:
|
|
@@ -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.5.
|
|
8
|
+
version: "0.5.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f"
|
|
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 `2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f` 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
|
|
@@ -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.11",
|
|
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",
|