@passioncode-ai/passioncode 0.1.4
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 +73 -0
- package/LICENSE +22 -0
- package/README.md +69 -0
- package/SECURITY.md +42 -0
- package/bin/passioncode.js +65 -0
- package/family.json +45 -0
- package/lib/launcher.js +361 -0
- package/package.json +48 -0
- package/payload/.claude-plugin/marketplace.json +47 -0
- package/payload/manifest.json +77 -0
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +24 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +175 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/profile-selection.md +71 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-bundle.md +62 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +55 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +537 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +126 -0
- package/payload/plugins/observatory-log/.claude-plugin/plugin.json +19 -0
- package/payload/plugins/observatory-log/hooks/ask-why.py +98 -0
- package/payload/plugins/observatory-log/hooks/hooks.json +31 -0
- package/payload/plugins/observatory-log/hooks/record-turn.sh +81 -0
- package/payload/plugins/observatory-log/hooks/session-start.sh +27 -0
- package/payload/plugins/observatory-log/skills/explaining-changes/SKILL.md +111 -0
- package/payload/plugins/observatory-log/skills/handling-secrets/SKILL.md +115 -0
- package/payload/plugins/observatory-log/skills/tracking-resources/SKILL.md +87 -0
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +13 -0
- package/payload/plugins/passioncode/hooks/hooks.json +10 -0
- package/payload/plugins/passioncode/hooks/probe.js +23 -0
- package/payload/plugins/passioncode/hooks/session-start.js +45 -0
- package/payload/plugins/passioncode/hooks/update-check.js +126 -0
- package/payload/plugins/passioncode/trust.json +6 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inspect, scaffold, and structurally check a Fabric provider project."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CONTRACT_VERSION = "0.1.0"
|
|
18
|
+
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
19
|
+
CONTRACT_COMMIT = "20a818e648a4c09a60df0126d11626922e8b9094"
|
|
20
|
+
MCP_REVISION = "2026-07-28"
|
|
21
|
+
A2A_VERSION = "1.0"
|
|
22
|
+
LOCAL_VERSION = "fabric-local-runner/0.1"
|
|
23
|
+
SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", ".venv", "venv", "dist", "build"}
|
|
24
|
+
TEXT_SUFFIXES = {".md", ".json", ".toml", ".yaml", ".yml", ".py", ".js", ".ts", ".go", ".rs"}
|
|
25
|
+
GENERATED_PATHS = (
|
|
26
|
+
"fabric-agent.json",
|
|
27
|
+
"fabric-contract.lock.json",
|
|
28
|
+
"fabric/schemas/capability-input.schema.json",
|
|
29
|
+
"fabric/schemas/capability-output.schema.json",
|
|
30
|
+
"fabric/fixtures/admission-input.json",
|
|
31
|
+
"fabric/probes/assertions.md",
|
|
32
|
+
"fabric/FABRIC-CONFORMANCE.md",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdaptationError(Exception):
|
|
37
|
+
"""Expected user-facing adaptation error."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_text(path: Path, limit: int = 262_144) -> str:
|
|
41
|
+
try:
|
|
42
|
+
if path.stat().st_size > limit:
|
|
43
|
+
return ""
|
|
44
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
45
|
+
except (OSError, UnicodeError):
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _files(root: Path) -> Iterable[Path]:
|
|
50
|
+
count = 0
|
|
51
|
+
for current, dirs, names in os.walk(root):
|
|
52
|
+
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
|
53
|
+
for name in sorted(names):
|
|
54
|
+
count += 1
|
|
55
|
+
if count > 5000:
|
|
56
|
+
return
|
|
57
|
+
yield Path(current) / name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _relative(path: Path, root: Path) -> str:
|
|
61
|
+
return path.relative_to(root).as_posix()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def inspect_project(root: Path) -> Dict[str, Any]:
|
|
65
|
+
if not root.is_dir():
|
|
66
|
+
raise AdaptationError("project path is not a directory: %s" % root)
|
|
67
|
+
|
|
68
|
+
evidence: Dict[str, List[str]] = {"mcp": [], "a2a": [], "local-runner": [], "autonomous": []}
|
|
69
|
+
sampled = 0
|
|
70
|
+
file_count = 0
|
|
71
|
+
for path in _files(root):
|
|
72
|
+
file_count += 1
|
|
73
|
+
rel = _relative(path, root)
|
|
74
|
+
lower_rel = rel.lower()
|
|
75
|
+
if lower_rel.endswith("agent-card.json") or ".well-known/agent-card" in lower_rel:
|
|
76
|
+
evidence["a2a"].append("file:%s" % rel)
|
|
77
|
+
if "mcp" in Path(lower_rel).name:
|
|
78
|
+
evidence["mcp"].append("file:%s" % rel)
|
|
79
|
+
if lower_rel.startswith("bin/") or lower_rel in {"cli.py", "cli.js", "cli.ts"}:
|
|
80
|
+
evidence["local-runner"].append("file:%s" % rel)
|
|
81
|
+
|
|
82
|
+
if path.suffix.lower() not in TEXT_SUFFIXES or sampled >= 80:
|
|
83
|
+
continue
|
|
84
|
+
text = _read_text(path)
|
|
85
|
+
if not text:
|
|
86
|
+
continue
|
|
87
|
+
sampled += 1
|
|
88
|
+
lowered = text.lower()
|
|
89
|
+
if "@modelcontextprotocol/sdk" in lowered or "fastmcp" in lowered or "model context protocol" in lowered:
|
|
90
|
+
evidence["mcp"].append("content:%s" % rel)
|
|
91
|
+
if '"a2a"' in lowered or "agent card" in lowered or "/.well-known/agent-card.json" in lowered:
|
|
92
|
+
evidence["a2a"].append("content:%s" % rel)
|
|
93
|
+
if "[project.scripts]" in lowered or ('"bin"' in lowered and path.name == "package.json"):
|
|
94
|
+
evidence["local-runner"].append("content:%s" % rel)
|
|
95
|
+
lifecycle_terms = sum(term in lowered for term in ("start job", "job status", "cancel job", "task lifecycle", "stream progress"))
|
|
96
|
+
if lifecycle_terms >= 2:
|
|
97
|
+
evidence["autonomous"].append("content:%s" % rel)
|
|
98
|
+
|
|
99
|
+
for key in evidence:
|
|
100
|
+
evidence[key] = sorted(set(evidence[key]))[:20]
|
|
101
|
+
|
|
102
|
+
candidates: List[str] = []
|
|
103
|
+
if evidence["a2a"] or evidence["autonomous"]:
|
|
104
|
+
candidates.append("a2a")
|
|
105
|
+
if evidence["mcp"]:
|
|
106
|
+
candidates.append("mcp")
|
|
107
|
+
if evidence["local-runner"]:
|
|
108
|
+
candidates.append("local-runner")
|
|
109
|
+
candidates = sorted(set(candidates))
|
|
110
|
+
|
|
111
|
+
if len(candidates) == 1:
|
|
112
|
+
recommendation = candidates[0]
|
|
113
|
+
question = None
|
|
114
|
+
elif not candidates:
|
|
115
|
+
recommendation = "undetermined"
|
|
116
|
+
question = "Which stable API, MCP/A2A endpoint, or installed CLI will Fabric invoke?"
|
|
117
|
+
else:
|
|
118
|
+
recommendation = "undetermined"
|
|
119
|
+
question = "Which capability is being adapted, and who owns its task lifecycle?"
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"project": str(root.resolve()),
|
|
123
|
+
"filesInspected": file_count,
|
|
124
|
+
"textFilesSampled": sampled,
|
|
125
|
+
"recommendedProfile": recommendation,
|
|
126
|
+
"candidateProfiles": candidates,
|
|
127
|
+
"evidence": evidence,
|
|
128
|
+
"openQuestion": question,
|
|
129
|
+
"warning": "Static inspection does not execute or trust the target project.",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _require_absolute_uri(value: str, label: str) -> None:
|
|
134
|
+
parsed = urlparse(value)
|
|
135
|
+
if not parsed.scheme:
|
|
136
|
+
raise AdaptationError("%s must be an absolute URI: %s" % (label, value))
|
|
137
|
+
if parsed.scheme in {"http", "https"} and not parsed.netloc:
|
|
138
|
+
raise AdaptationError("%s must include a host: %s" % (label, value))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _schema_uri(base: str, name: str) -> str:
|
|
142
|
+
return base.rstrip("/") + "/" + name
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _profile(args: argparse.Namespace, schema_base: str) -> Dict[str, Any]:
|
|
146
|
+
profile = args.profile
|
|
147
|
+
fixture_uri = _schema_uri(schema_base, "fixtures/admission-input.json")
|
|
148
|
+
output_uri = _schema_uri(schema_base, "schemas/capability-output.schema.json")
|
|
149
|
+
probe = {
|
|
150
|
+
"id": "safe-shape-probe",
|
|
151
|
+
"inputFixture": fixture_uri,
|
|
152
|
+
"outputSchema": output_uri,
|
|
153
|
+
"timeoutMs": 30000,
|
|
154
|
+
"sideEffectCeiling": "none",
|
|
155
|
+
"assertions": ["returns a typed result without external effects"],
|
|
156
|
+
}
|
|
157
|
+
if profile == "mcp":
|
|
158
|
+
if args.mcp_mode == "stdio":
|
|
159
|
+
connection = {
|
|
160
|
+
"mode": "stdio",
|
|
161
|
+
"executableRef": args.executable_ref,
|
|
162
|
+
"args": args.runtime_arg,
|
|
163
|
+
}
|
|
164
|
+
else:
|
|
165
|
+
connection = {"mode": "streamable-http", "url": args.mcp_url}
|
|
166
|
+
return {
|
|
167
|
+
"kind": "mcp",
|
|
168
|
+
"protocolRevision": MCP_REVISION,
|
|
169
|
+
"connection": connection,
|
|
170
|
+
"requiredFeatures": ["tool:replace-me"],
|
|
171
|
+
"probes": [probe],
|
|
172
|
+
}
|
|
173
|
+
if profile == "a2a":
|
|
174
|
+
return {
|
|
175
|
+
"kind": "a2a",
|
|
176
|
+
"protocolVersion": A2A_VERSION,
|
|
177
|
+
"agentCardUrl": args.agent_card_url,
|
|
178
|
+
"skillIds": ["replace-me"],
|
|
179
|
+
"binding": "REST",
|
|
180
|
+
"streaming": False,
|
|
181
|
+
"pushNotifications": False,
|
|
182
|
+
"probes": [probe],
|
|
183
|
+
}
|
|
184
|
+
if profile == "local-runner":
|
|
185
|
+
return {
|
|
186
|
+
"kind": "local-runner",
|
|
187
|
+
"profileVersion": LOCAL_VERSION,
|
|
188
|
+
"runnerKind": args.runner_kind,
|
|
189
|
+
"executableRef": args.executable_ref,
|
|
190
|
+
"args": args.runtime_arg or ["--input", "{inputFile}"],
|
|
191
|
+
"inputMode": "file-json",
|
|
192
|
+
"resultUriTemplate": "file:{worktree}/.fabric/result.json",
|
|
193
|
+
"cancellation": "signal",
|
|
194
|
+
"heartbeatSeconds": 30,
|
|
195
|
+
"probes": [probe],
|
|
196
|
+
}
|
|
197
|
+
raise AdaptationError("unsupported profile: %s" % profile)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _json_text(value: Any) -> str:
|
|
201
|
+
return json.dumps(value, indent=2, ensure_ascii=False) + "\n"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _generated_files(args: argparse.Namespace) -> Dict[str, str]:
|
|
205
|
+
for value, label in (
|
|
206
|
+
(args.provider_id, "provider ID"),
|
|
207
|
+
(args.capability_id, "capability ID"),
|
|
208
|
+
(args.schema_base, "schema base"),
|
|
209
|
+
):
|
|
210
|
+
_require_absolute_uri(value, label)
|
|
211
|
+
if "?" in args.schema_base or "#" in args.schema_base:
|
|
212
|
+
raise AdaptationError("schema base must not contain a query or fragment")
|
|
213
|
+
_require_absolute_uri(args.executable_ref, "executable reference")
|
|
214
|
+
_require_absolute_uri(args.mcp_url, "MCP URL")
|
|
215
|
+
_require_absolute_uri(args.agent_card_url, "Agent Card URL")
|
|
216
|
+
if urlparse(args.agent_card_url).scheme != "https":
|
|
217
|
+
raise AdaptationError("Agent Card URL must use HTTPS")
|
|
218
|
+
for runtime_arg in args.runtime_arg:
|
|
219
|
+
lowered = runtime_arg.lower().replace("_", "").replace("-", "")
|
|
220
|
+
if any(term in lowered for term in ("password", "apikey", "accesstoken", "clientsecret")):
|
|
221
|
+
raise AdaptationError("runtime arguments must not contain secret-like values or names")
|
|
222
|
+
|
|
223
|
+
created_at = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
224
|
+
manifest = {
|
|
225
|
+
"contractVersion": CONTRACT_VERSION,
|
|
226
|
+
"provider": {
|
|
227
|
+
"id": args.provider_id,
|
|
228
|
+
"revision": 1,
|
|
229
|
+
"contentHash": "sha256:" + ("0" * 64),
|
|
230
|
+
"createdAt": created_at,
|
|
231
|
+
"createdBy": "urn:fabric:adapter:author",
|
|
232
|
+
"name": args.provider_name,
|
|
233
|
+
"identity": {
|
|
234
|
+
"subject": args.provider_id,
|
|
235
|
+
"method": "local-install" if args.profile == "local-runner" or (args.profile == "mcp" and args.mcp_mode == "stdio") else "tls",
|
|
236
|
+
},
|
|
237
|
+
"supportedContractVersions": [CONTRACT_VERSION],
|
|
238
|
+
},
|
|
239
|
+
"capabilities": [
|
|
240
|
+
{
|
|
241
|
+
"id": args.capability_id,
|
|
242
|
+
"name": args.capability_name,
|
|
243
|
+
"inputSchema": _schema_uri(args.schema_base, "schemas/capability-input.schema.json"),
|
|
244
|
+
"outputSchema": _schema_uri(args.schema_base, "schemas/capability-output.schema.json"),
|
|
245
|
+
"effect": "none",
|
|
246
|
+
"idempotency": "none",
|
|
247
|
+
"dataClasses": ["public"],
|
|
248
|
+
"profile": _profile(args, args.schema_base),
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
}
|
|
252
|
+
lock = {
|
|
253
|
+
"contract": "fabric-agent-contract",
|
|
254
|
+
"version": CONTRACT_VERSION,
|
|
255
|
+
"repository": CONTRACT_REPOSITORY,
|
|
256
|
+
"commit": CONTRACT_COMMIT,
|
|
257
|
+
"profile": args.profile,
|
|
258
|
+
}
|
|
259
|
+
input_schema = {
|
|
260
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
261
|
+
"$id": _schema_uri(args.schema_base, "schemas/capability-input.schema.json"),
|
|
262
|
+
"title": "Capability input",
|
|
263
|
+
"type": "object",
|
|
264
|
+
"additionalProperties": False,
|
|
265
|
+
"properties": {"request": {"type": "string", "minLength": 1}},
|
|
266
|
+
"required": ["request"],
|
|
267
|
+
}
|
|
268
|
+
output_schema = {
|
|
269
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
270
|
+
"$id": _schema_uri(args.schema_base, "schemas/capability-output.schema.json"),
|
|
271
|
+
"title": "Capability output",
|
|
272
|
+
"type": "object",
|
|
273
|
+
"additionalProperties": False,
|
|
274
|
+
"properties": {
|
|
275
|
+
"summary": {"type": "string"},
|
|
276
|
+
"evidence": {"type": "array", "items": {"type": "string", "format": "uri"}},
|
|
277
|
+
},
|
|
278
|
+
"required": ["summary", "evidence"],
|
|
279
|
+
}
|
|
280
|
+
fixture = {"request": "Return one bounded, non-publishing test result."}
|
|
281
|
+
assertions = """# Admission probe assertions
|
|
282
|
+
|
|
283
|
+
- The result validates against `capability-output.schema.json`.
|
|
284
|
+
- Every claimed fact has a resolvable evidence URI.
|
|
285
|
+
- The probe does not publish, message, charge, mutate production, or expose secrets.
|
|
286
|
+
- Timeout and cancellation return a typed partial or stopped result.
|
|
287
|
+
"""
|
|
288
|
+
conformance = """# Fabric conformance
|
|
289
|
+
|
|
290
|
+
Contract: `fabric-agent-contract` 0.1.0 at `%s`
|
|
291
|
+
|
|
292
|
+
| Gate | Status | Receipt / next action |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| Declaration shape | NOT VERIFIED | Run `adapt_project.py check . --contract /path/to/fabric-agent-contract`. |
|
|
295
|
+
| Protocol revision and connection | NOT VERIFIED | Replace profile placeholders, then negotiate the exact pinned revision. |
|
|
296
|
+
| Semantic probes | NOT VERIFIED | Implement and run the bounded fixture and assertions against the provider. |
|
|
297
|
+
| Project binding readiness | NOT VERIFIED | Requires an admitted capability and Fabric host runtime. |
|
|
298
|
+
|
|
299
|
+
The zero content hash, `replace-me` values, and `.invalid` endpoints are deliberate
|
|
300
|
+
placeholders. Scaffolding is not admission and must not be presented as compatibility.
|
|
301
|
+
""" % CONTRACT_COMMIT
|
|
302
|
+
return {
|
|
303
|
+
"fabric-agent.json": _json_text(manifest),
|
|
304
|
+
"fabric-contract.lock.json": _json_text(lock),
|
|
305
|
+
"fabric/schemas/capability-input.schema.json": _json_text(input_schema),
|
|
306
|
+
"fabric/schemas/capability-output.schema.json": _json_text(output_schema),
|
|
307
|
+
"fabric/fixtures/admission-input.json": _json_text(fixture),
|
|
308
|
+
"fabric/probes/assertions.md": assertions,
|
|
309
|
+
"fabric/FABRIC-CONFORMANCE.md": conformance,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
|
|
314
|
+
if not root.is_dir():
|
|
315
|
+
raise AdaptationError("project path is not a directory: %s" % root)
|
|
316
|
+
generated = _generated_files(args)
|
|
317
|
+
collisions = [rel for rel in generated if (root / rel).exists()]
|
|
318
|
+
if collisions and not args.force:
|
|
319
|
+
raise AdaptationError("refusing to overwrite existing paths: %s" % ", ".join(collisions))
|
|
320
|
+
for rel, content in generated.items():
|
|
321
|
+
target = root / rel
|
|
322
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
323
|
+
target.write_text(content, encoding="utf-8")
|
|
324
|
+
return {
|
|
325
|
+
"project": str(root.resolve()),
|
|
326
|
+
"profile": args.profile,
|
|
327
|
+
"created": sorted(generated),
|
|
328
|
+
"replaced": sorted(collisions),
|
|
329
|
+
"next": "Replace all placeholders, implement the adapter, then run check with the pinned contract checkout.",
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _load_json(path: Path, errors: List[str]) -> Optional[Any]:
|
|
334
|
+
try:
|
|
335
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
336
|
+
except FileNotFoundError:
|
|
337
|
+
errors.append("missing:%s" % path.name)
|
|
338
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
339
|
+
errors.append("invalid-json:%s:%s" % (path.name, exc))
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _walk_keys(value: Any, prefix: str = "") -> Iterable[Tuple[str, Any]]:
|
|
344
|
+
if isinstance(value, dict):
|
|
345
|
+
for key, item in value.items():
|
|
346
|
+
here = "%s.%s" % (prefix, key) if prefix else key
|
|
347
|
+
yield here, item
|
|
348
|
+
yield from _walk_keys(item, here)
|
|
349
|
+
elif isinstance(value, list):
|
|
350
|
+
for index, item in enumerate(value):
|
|
351
|
+
yield from _walk_keys(item, "%s[%d]" % (prefix, index))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _contract_shape(manifest: Path, contract: Path) -> Tuple[str, List[str]]:
|
|
355
|
+
if not contract.is_dir():
|
|
356
|
+
return "NOT_RUN", ["contract checkout is not a directory: %s" % contract]
|
|
357
|
+
try:
|
|
358
|
+
head = subprocess.run(
|
|
359
|
+
["git", "rev-parse", "HEAD"], cwd=str(contract), check=True,
|
|
360
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
361
|
+
).stdout.strip()
|
|
362
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
363
|
+
return "NOT_RUN", ["cannot read contract commit: %s" % exc]
|
|
364
|
+
if head != CONTRACT_COMMIT:
|
|
365
|
+
return "NOT_RUN", ["contract checkout is %s; expected %s" % (head, CONTRACT_COMMIT)]
|
|
366
|
+
|
|
367
|
+
code = """import {readFile} from 'node:fs/promises';
|
|
368
|
+
import {createValidator,validateDocument} from './src/validator.ts';
|
|
369
|
+
import {SCHEMA_PREFIX} from './src/contract.ts';
|
|
370
|
+
(async () => {
|
|
371
|
+
const value=JSON.parse(await readFile(process.argv[1],'utf8'));
|
|
372
|
+
const result=validateDocument(await createValidator(),SCHEMA_PREFIX+'schemas/manifest.schema.json',value);
|
|
373
|
+
console.log(JSON.stringify(result));
|
|
374
|
+
process.exit(result.valid?0:3);
|
|
375
|
+
})().catch((error) => { console.error(error); process.exit(4); });"""
|
|
376
|
+
try:
|
|
377
|
+
run = subprocess.run(
|
|
378
|
+
["pnpm", "exec", "tsx", "-e", code, str(manifest.resolve())],
|
|
379
|
+
cwd=str(contract), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
|
380
|
+
timeout=120,
|
|
381
|
+
)
|
|
382
|
+
except FileNotFoundError:
|
|
383
|
+
return "NOT_RUN", ["pnpm is unavailable; install the pinned contract dependencies"]
|
|
384
|
+
except subprocess.TimeoutExpired:
|
|
385
|
+
return "NOT_RUN", ["contract shape validation timed out"]
|
|
386
|
+
if run.returncode != 0:
|
|
387
|
+
detail = (run.stdout + "\n" + run.stderr).strip()
|
|
388
|
+
return "FAIL", [detail[-4000:] or "contract validator failed"]
|
|
389
|
+
return "PASS", ["manifest validated by pinned contract schema"]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def check_project(root: Path, contract: Optional[Path]) -> Dict[str, Any]:
|
|
393
|
+
errors: List[str] = []
|
|
394
|
+
warnings: List[str] = []
|
|
395
|
+
for rel in GENERATED_PATHS:
|
|
396
|
+
if not (root / rel).is_file():
|
|
397
|
+
errors.append("missing:%s" % rel)
|
|
398
|
+
|
|
399
|
+
manifest_path = root / "fabric-agent.json"
|
|
400
|
+
manifest = _load_json(manifest_path, errors)
|
|
401
|
+
lock = _load_json(root / "fabric-contract.lock.json", errors)
|
|
402
|
+
profile_kind = None
|
|
403
|
+
if isinstance(lock, dict):
|
|
404
|
+
if lock.get("version") != CONTRACT_VERSION:
|
|
405
|
+
errors.append("contract lock version must be %s" % CONTRACT_VERSION)
|
|
406
|
+
if lock.get("commit") != CONTRACT_COMMIT:
|
|
407
|
+
errors.append("contract lock commit must be %s" % CONTRACT_COMMIT)
|
|
408
|
+
|
|
409
|
+
if isinstance(manifest, dict):
|
|
410
|
+
if manifest.get("contractVersion") != CONTRACT_VERSION:
|
|
411
|
+
errors.append("manifest contractVersion must be %s" % CONTRACT_VERSION)
|
|
412
|
+
capabilities = manifest.get("capabilities")
|
|
413
|
+
if not isinstance(capabilities, list) or not capabilities:
|
|
414
|
+
errors.append("manifest must declare at least one capability")
|
|
415
|
+
else:
|
|
416
|
+
for index, capability in enumerate(capabilities):
|
|
417
|
+
profile = capability.get("profile") if isinstance(capability, dict) else None
|
|
418
|
+
if not isinstance(profile, dict):
|
|
419
|
+
errors.append("capability[%d] has no profile" % index)
|
|
420
|
+
continue
|
|
421
|
+
kind = profile.get("kind")
|
|
422
|
+
profile_kind = kind
|
|
423
|
+
expected = {
|
|
424
|
+
"mcp": ("protocolRevision", MCP_REVISION),
|
|
425
|
+
"a2a": ("protocolVersion", A2A_VERSION),
|
|
426
|
+
"local-runner": ("profileVersion", LOCAL_VERSION),
|
|
427
|
+
}.get(kind)
|
|
428
|
+
if expected is None:
|
|
429
|
+
errors.append("capability[%d] has unsupported profile %r" % (index, kind))
|
|
430
|
+
elif profile.get(expected[0]) != expected[1]:
|
|
431
|
+
errors.append("capability[%d] must pin %s=%s" % (index, expected[0], expected[1]))
|
|
432
|
+
if not profile.get("probes"):
|
|
433
|
+
errors.append("capability[%d] must declare probes" % index)
|
|
434
|
+
for field in ("inputSchema", "outputSchema"):
|
|
435
|
+
value = capability.get(field)
|
|
436
|
+
try:
|
|
437
|
+
_require_absolute_uri(value, "capability[%d].%s" % (index, field))
|
|
438
|
+
except (AdaptationError, TypeError):
|
|
439
|
+
errors.append("capability[%d].%s must be an absolute URI" % (index, field))
|
|
440
|
+
|
|
441
|
+
for key, value in _walk_keys(manifest):
|
|
442
|
+
normalized = key.lower().replace("_", "").replace("-", "")
|
|
443
|
+
if any(secret in normalized for secret in ("password", "apikey", "accesstoken", "clientsecret", "privatekey")):
|
|
444
|
+
errors.append("secret-like field is forbidden in manifest: %s" % key)
|
|
445
|
+
if isinstance(value, str) and ("replace-me" in value or ".invalid" in value):
|
|
446
|
+
warnings.append("placeholder:%s" % key)
|
|
447
|
+
provider = manifest.get("provider")
|
|
448
|
+
if isinstance(provider, dict) and provider.get("contentHash") == "sha256:" + ("0" * 64):
|
|
449
|
+
warnings.append("placeholder:provider.contentHash")
|
|
450
|
+
|
|
451
|
+
shape_status = "NOT_RUN"
|
|
452
|
+
shape_receipts = ["pass --contract PATH pointing at the pinned checkout"]
|
|
453
|
+
if contract is not None and manifest_path.is_file():
|
|
454
|
+
shape_status, shape_receipts = _contract_shape(manifest_path, contract)
|
|
455
|
+
|
|
456
|
+
local_status = "PASS" if not errors else "FAIL"
|
|
457
|
+
ready = local_status == "PASS" and shape_status == "PASS" and not warnings
|
|
458
|
+
return {
|
|
459
|
+
"project": str(root.resolve()),
|
|
460
|
+
"profile": profile_kind,
|
|
461
|
+
"gates": {
|
|
462
|
+
"localStructure": {"status": local_status, "receipts": sorted(set(errors))},
|
|
463
|
+
"declarationShape": {"status": shape_status, "receipts": shape_receipts},
|
|
464
|
+
"protocolNegotiation": {"status": "NOT_VERIFIED", "receipts": ["requires a live provider and Fabric host"]},
|
|
465
|
+
"semanticProbes": {"status": "NOT_VERIFIED", "receipts": ["requires bounded live probe execution"]},
|
|
466
|
+
"bindingReadiness": {"status": "NOT_VERIFIED", "receipts": ["requires admission and Fabric host runtime"]},
|
|
467
|
+
},
|
|
468
|
+
"warnings": sorted(set(warnings)),
|
|
469
|
+
"readyForAdmission": ready,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _print(value: Dict[str, Any], as_json: bool) -> None:
|
|
474
|
+
if as_json:
|
|
475
|
+
print(_json_text(value), end="")
|
|
476
|
+
return
|
|
477
|
+
for key, item in value.items():
|
|
478
|
+
if isinstance(item, (dict, list)):
|
|
479
|
+
print("%s: %s" % (key, json.dumps(item, ensure_ascii=False, sort_keys=True)))
|
|
480
|
+
else:
|
|
481
|
+
print("%s: %s" % (key, item))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
485
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
486
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
487
|
+
|
|
488
|
+
inspect = sub.add_parser("inspect", help="inspect a project without executing it")
|
|
489
|
+
inspect.add_argument("project", type=Path)
|
|
490
|
+
inspect.add_argument("--json", action="store_true")
|
|
491
|
+
|
|
492
|
+
scaffold = sub.add_parser("scaffold", help="create a contract-pinned provider bundle")
|
|
493
|
+
scaffold.add_argument("project", type=Path)
|
|
494
|
+
scaffold.add_argument("--profile", choices=("mcp", "a2a", "local-runner"), required=True)
|
|
495
|
+
scaffold.add_argument("--provider-id", required=True)
|
|
496
|
+
scaffold.add_argument("--provider-name", default="Fabric capability provider")
|
|
497
|
+
scaffold.add_argument("--capability-id", required=True)
|
|
498
|
+
scaffold.add_argument("--capability-name", required=True)
|
|
499
|
+
scaffold.add_argument("--schema-base", required=True)
|
|
500
|
+
scaffold.add_argument("--mcp-mode", choices=("streamable-http", "stdio"), default="streamable-http")
|
|
501
|
+
scaffold.add_argument("--mcp-url", default="https://replace.invalid/mcp")
|
|
502
|
+
scaffold.add_argument("--agent-card-url", default="https://replace.invalid/.well-known/agent-card.json")
|
|
503
|
+
scaffold.add_argument("--runner-kind", default="replace-me")
|
|
504
|
+
scaffold.add_argument("--executable-ref", default="urn:executable:replace-me")
|
|
505
|
+
scaffold.add_argument("--runtime-arg", action="append", default=[])
|
|
506
|
+
scaffold.add_argument("--force", action="store_true")
|
|
507
|
+
scaffold.add_argument("--json", action="store_true")
|
|
508
|
+
|
|
509
|
+
check = sub.add_parser("check", help="check generated structure and optional pinned schema")
|
|
510
|
+
check.add_argument("project", type=Path)
|
|
511
|
+
check.add_argument("--contract", type=Path)
|
|
512
|
+
check.add_argument("--json", action="store_true")
|
|
513
|
+
return parser
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
517
|
+
args = build_parser().parse_args(argv)
|
|
518
|
+
try:
|
|
519
|
+
if args.command == "inspect":
|
|
520
|
+
result = inspect_project(args.project)
|
|
521
|
+
elif args.command == "scaffold":
|
|
522
|
+
result = scaffold_project(args.project, args)
|
|
523
|
+
else:
|
|
524
|
+
result = check_project(args.project, args.contract)
|
|
525
|
+
_print(result, args.json)
|
|
526
|
+
if args.command == "check":
|
|
527
|
+
gates = result["gates"]
|
|
528
|
+
if gates["localStructure"]["status"] == "FAIL" or gates["declarationShape"]["status"] == "FAIL":
|
|
529
|
+
return 1
|
|
530
|
+
return 0
|
|
531
|
+
except AdaptationError as exc:
|
|
532
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
533
|
+
return 2
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
raise SystemExit(main())
|