@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.
Files changed (19) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +4 -4
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +11 -7
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +1 -1
  10. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -6
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +34 -16
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +13 -1
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +44 -10
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +83 -15
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +5 -0
  18. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  19. 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
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.5.0",
11
+ "ref": "v0.5.2",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.1.9",
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.0",
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.9",
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",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.1.9",
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.0",
11
- "commit": "f31c2b2792f77153b1a5adda4bec6c622fc8af97",
12
- "version": "0.5.0",
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:0f275fc3d94e42c1e8f387c409d25b675496d384a8c3eb418959d5a87d3cd64b",
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.9",
64
+ "ref": "v0.1.11",
65
65
  "commit": null,
66
- "version": "0.1.9",
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:c810e4afb3cfccaa5a367933ce258b97a7d608be6f3bc791cd5c821893fd4554",
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.0",
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.0"
8
+ version: "0.5.2"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "a22dea359ba04b8fe549abe81a5131552cb90eff"
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 `a22dea359ba04b8fe549abe81a5131552cb90eff`.
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 --name "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. The contract's rule that the manifest's `provider.id` "equals" the
52
- entry's id cannot hold as written — `provider.id` is a URI, the entry id a slug — so only
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 = "a22dea359ba04b8fe549abe81a5131552cb90eff"
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
- FIELDS = {"protocol", "id", "name", "summary", "manifest", "run", "source", "installedAt", "installedBy", "extensions"}
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
- problems = validate_provider_entry(json.loads(args.path.read_text(encoding="utf-8")))
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.0"
18
+ version: "0.5.2"
19
19
  contract-version: "0.1.0"
20
20
  extension: "fabric-service/0.1"
21
- extension-commit: "a22dea359ba04b8fe549abe81a5131552cb90eff"
21
+ extension-commit: "2ea54f70c161c8b3bf7ec3c567735c7a2d4ce12f"
22
22
  ---
23
23
 
24
24
  # Building Fabric services
@@ -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 two schemas unchanged and annotations from the effect | `tool_for_capability` / `toolForCapability`, `expected_annotations` |
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 the work carry `traceId` and `spanId` | `child_traceparent`, `trace_ids`, `make_event(..., trace_id=, span_id=)` |
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(...))`, `jobs.fail(job_id, code, message)`,
44
- `jobs.cancel(job_id)`. A terminal job never changes again.
45
- - Every result carries `_meta.traceparent`. For a job it is the span of the call that
46
- started it, so the result is traced (the contract's FAC-SEM-019 records an untraced
47
- result as an incomplete span).
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 `server/discover`, `tools/list` and `tools/call` with JSON
52
- responses — enough for Fabric and for the probe. A service already on an MCP SDK keeps
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
- Two readings the contract leaves open, and what the kit does meanwhile:
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
- - a job tool serves the capability's `outputSchema` although its `structuredContent` is
60
- the handle (contract OQ-0006);
61
- - events that belong to no traced work carry no trace pair (contract OQ-0007).
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`), annotations derived | no manifest, no MCP surface, or a schema is not beside the manifest |
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 |
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1 — wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `a22dea359ba04b8fe549abe81a5131552cb90eff`
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
- // A job-capable tool still serves the capability's outputSchema (contract OQ-0006).
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
- const tool = { name: capability.name, inputSchema, outputSchema, annotations: expectedAnnotations(capability.effect, capability.idempotency) };
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
- export function resultEnvelope({ done, proof, scope, notVerified, output, usage }) {
89
- for (const [label, value] of [['done', done], ['proof', proof], ['notVerified', notVerified]]) {
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 (!scope || typeof scope !== 'object') throw new InteropError('scope must be an object.');
93
- return { done: [...done], proof: [...proof], scope: { ...scope }, notVerified: [...notVerified], output, usage: checkUsage(usage) };
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
- const needed = ['done', 'proof', 'scope', 'notVerified', 'output', 'usage'];
181
- if (!envelope || !needed.every((k) => k in envelope)) throw new InteropError('A completed job carries the full result envelope; build it with resultEnvelope.');
182
- return this.transition(jobId, 'completed', { result: envelope });
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 two schemas as they are, derived annotations.
104
-
105
- A job-capable tool still serves the capability's outputSchema, though its
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]) -> Dict[str, Any]:
158
- """DONE / PROOF / SCOPE / NOT VERIFIED (DEC-0011) plus the output and usage; the four collections are always present."""
159
- for label, value in (("done", done), ("proof", proof), ("notVerified", not_verified)):
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 not isinstance(scope, dict):
163
- raise InteropError("scope must be an object.")
164
- return {"done": list(done), "proof": list(proof), "scope": dict(scope), "notVerified": list(not_verified),
165
- "output": output, "usage": _usage(usage)}
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
- if not isinstance(envelope, dict) or not {"done", "proof", "scope", "notVerified", "output", "usage"} <= set(envelope):
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.0"
8
+ version: "0.5.2"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "a22dea359ba04b8fe549abe81a5131552cb90eff"
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 `a22dea359ba04b8fe549abe81a5131552cb90eff` and
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.9",
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",