@passioncode-ai/fabric-agent-adapter 0.44.0 → 0.44.2

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 (27) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +1 -1
  3. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  4. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  5. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +1 -1
  6. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +1 -1
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +9 -6
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -1
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -7
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +264 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +31 -254
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +100 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -89
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +13 -11
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -1
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +5 -11
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +54 -369
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +390 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +20 -187
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +207 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +8 -8
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -1
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +263 -67
  25. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  26. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  27. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
@@ -0,0 +1,207 @@
1
+ """fabric-package/1, the schema reading: the package schema bundled from the contract at the pin, read by a JSON Schema
2
+ 2020-12 subset with JavaScript's patterns, lengths in code points and the formats the contract uses, as
3
+ fabric-package.mjs reads it. fabric_package checks, packs, signs and verifies on it and re-exports these names. Part of
4
+ the service kit (FAA-92, split from fabric_package; layer L2). Standard library only, Python 3.9+."""
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List
11
+
12
+ import sys as _sys # noqa: E402
13
+ from pathlib import Path as _Path # noqa: E402
14
+ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
15
+ _sys.path.insert(0, str(_Path(__file__).resolve().parent))
16
+
17
+ # What pack refuses to list is matched with JavaScript's `.`; fabric_package reads it from here.
18
+ _JS_DOT = "[^\n\r\u2028\u2029]"
19
+
20
+ #: JavaScript's whitespace, what String.prototype.trim and the regular expression class \s take (ECMA-262 WhiteSpace
21
+ #: and LineTerminator), so a key file and a URI are read as fabric-package.mjs reads them.
22
+ JS_WHITESPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
23
+
24
+ SCHEMA_PATH = Path(__file__).resolve().parent / "fabric-package.schema.json"
25
+
26
+ # --- the schema (bundled from the contract at the pin) ------------------------------------------------
27
+
28
+ _SCHEMA_CACHE: Dict[str, Any] = {}
29
+
30
+
31
+ def _schema() -> Dict[str, Any]:
32
+ if "s" not in _SCHEMA_CACHE:
33
+ _SCHEMA_CACHE["s"] = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
34
+ return _SCHEMA_CACHE["s"]
35
+
36
+
37
+ from fabric_jsvalue import json_equal as _json_equal # noqa: E402 - the core's (FAA-65)
38
+
39
+
40
+ def _type_of(value: Any) -> str:
41
+ if value is None:
42
+ return "null"
43
+ if isinstance(value, bool):
44
+ return "boolean"
45
+ if isinstance(value, int) or (isinstance(value, float) and value.is_integer()):
46
+ return "integer"
47
+ if isinstance(value, float):
48
+ return "number"
49
+ if isinstance(value, str):
50
+ return "string"
51
+ if isinstance(value, list):
52
+ return "array"
53
+ return "object"
54
+
55
+
56
+ # ASCII digits only and the whole string (fullmatch): Python's \d takes any Unicode digit and its $ a trailing newline,
57
+ # which JavaScript's do not (FAA-78, finding 43).
58
+ _DATE_TIME = re.compile(r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]+)?([Zz]|[+-]([0-9]{2}):([0-9]{2}))")
59
+ _URI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:[^%s]*" % JS_WHITESPACE)
60
+ _MONTH_DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
61
+
62
+
63
+ def _leap(year: int) -> bool:
64
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
65
+
66
+
67
+ def _format_ok(fmt: str, value: Any) -> bool:
68
+ if not isinstance(value, str):
69
+ return True
70
+ if fmt == "date-time":
71
+ m = _DATE_TIME.fullmatch(value)
72
+ if not m:
73
+ return False
74
+ year, month, day, hour, minute, second = (int(g) for g in m.groups()[:6])
75
+ # The proleptic calendar the contract's ajv-formats checks: year 0000 exists and is a leap year (0000-02-29
76
+ # holds). datetime starts at year 1, so the days are counted here, as fabric-package.mjs counts them.
77
+ if not 1 <= month <= 12 or not 1 <= day <= (29 if month == 2 and _leap(year) else _MONTH_DAYS[month]):
78
+ return False
79
+ if hour > 23 or minute > 59 or second > 59:
80
+ return False
81
+ return m.group(8) in ("Z", "z") or (int(m.group(9)) < 24 and int(m.group(10)) < 60)
82
+ if fmt == "uri":
83
+ return bool(_URI.fullmatch(value))
84
+ return True
85
+
86
+
87
+ _PATTERNS: Dict[str, Any] = {}
88
+
89
+
90
+ def _js_pattern(source: str) -> Any:
91
+ """A schema's ECMAScript pattern (``u`` flag) as Python reads it the same way: ``$`` is the end of the string, not
92
+ the end before a final newline; ``.`` stops at JavaScript's line terminators (\\n, \\r, U+2028, U+2029); ``\\d`` and
93
+ ``\\w`` are ASCII. Escapes and character classes are copied as written."""
94
+ if source not in _PATTERNS:
95
+ out: List[str] = []
96
+ i, in_class = 0, False
97
+ while i < len(source):
98
+ ch = source[i]
99
+ if ch == "\\":
100
+ out.append(source[i:i + 2])
101
+ i += 2
102
+ continue
103
+ if in_class:
104
+ in_class = ch != "]"
105
+ elif ch == "[":
106
+ in_class = True
107
+ elif ch == "$":
108
+ ch = r"\Z"
109
+ elif ch == ".":
110
+ ch = _JS_DOT
111
+ out.append(ch)
112
+ i += 1
113
+ _PATTERNS[source] = re.compile("".join(out), re.ASCII)
114
+ return _PATTERNS[source]
115
+
116
+
117
+ def _pointer(token: Any) -> str:
118
+ return str(token).replace("~", "~0").replace("/", "~1")
119
+
120
+
121
+ def _validate(schema: Any, value: Any, at: str, out: List[Dict[str, str]]) -> None:
122
+ """The JSON Schema 2020-12 keywords the package schema uses, nothing more."""
123
+ if schema is True:
124
+ return
125
+ if schema is False:
126
+ out.append({"instancePath": at, "message": "is not allowed here"})
127
+ return
128
+ if "$ref" in schema:
129
+ target = _schema()
130
+ for token in schema["$ref"].lstrip("#/").split("/"):
131
+ target = target[token.replace("~1", "/").replace("~0", "~")]
132
+ _validate(target, value, at, out)
133
+ kind = _type_of(value)
134
+ if "type" in schema:
135
+ allowed = schema["type"] if isinstance(schema["type"], list) else [schema["type"]]
136
+ if not (kind in allowed or (kind == "integer" and "number" in allowed)):
137
+ out.append({"instancePath": at, "message": "must be %s" % " or ".join(allowed)})
138
+ return
139
+ if "const" in schema and not _json_equal(schema["const"], value):
140
+ out.append({"instancePath": at, "message": "must be %s" % json.dumps(schema["const"])})
141
+ if "enum" in schema and not any(_json_equal(item, value) for item in schema["enum"]):
142
+ out.append({"instancePath": at, "message": "must be one of %s" % ", ".join(json.dumps(i) for i in schema["enum"])})
143
+ if kind == "string":
144
+ length = len(value) # code points, as JSON Schema counts
145
+ if length < schema.get("minLength", 0):
146
+ out.append({"instancePath": at, "message": "is shorter than %d" % schema["minLength"]})
147
+ if "maxLength" in schema and length > schema["maxLength"]:
148
+ out.append({"instancePath": at, "message": "is longer than %d" % schema["maxLength"]})
149
+ if "pattern" in schema and not _js_pattern(schema["pattern"]).search(value):
150
+ out.append({"instancePath": at, "message": "does not match %s" % schema["pattern"]})
151
+ if "format" in schema and not _format_ok(schema["format"], value):
152
+ out.append({"instancePath": at, "message": "is not a %s" % schema["format"]})
153
+ if kind in ("integer", "number") and "minimum" in schema and value < schema["minimum"]:
154
+ out.append({"instancePath": at, "message": "is below %s" % schema["minimum"]})
155
+ if kind == "array":
156
+ if len(value) < schema.get("minItems", 0):
157
+ out.append({"instancePath": at, "message": "has fewer than %d items" % schema["minItems"]})
158
+ if "maxItems" in schema and len(value) > schema["maxItems"]:
159
+ out.append({"instancePath": at, "message": "has more than %d items" % schema["maxItems"]})
160
+ if schema.get("uniqueItems") and any(_json_equal(value[i], value[j]) for i in range(len(value)) for j in range(i)):
161
+ out.append({"instancePath": at, "message": "has duplicate items"})
162
+ if "items" in schema:
163
+ for index, item in enumerate(value):
164
+ _validate(schema["items"], item, "%s/%d" % (at, index), out)
165
+ if "contains" in schema:
166
+ matches = sum(1 for item in value if not _problems(schema["contains"], item))
167
+ if matches < schema.get("minContains", 1):
168
+ out.append({"instancePath": at, "message": "has fewer than %d matching items" % schema.get("minContains", 1)})
169
+ if "maxContains" in schema and matches > schema["maxContains"]:
170
+ out.append({"instancePath": at, "message": "has more than %d matching items" % schema["maxContains"]})
171
+ if kind == "object":
172
+ for name in schema.get("required", []):
173
+ if name not in value:
174
+ out.append({"instancePath": at, "message": "must have %s" % name})
175
+ properties = schema.get("properties", {})
176
+ for name, member in value.items():
177
+ if name in properties:
178
+ _validate(properties[name], member, "%s/%s" % (at, _pointer(name)), out)
179
+ elif "additionalProperties" in schema:
180
+ if schema["additionalProperties"] is False:
181
+ out.append({"instancePath": "%s/%s" % (at, _pointer(name)), "message": "is not a member of this object"})
182
+ else:
183
+ _validate(schema["additionalProperties"], member, "%s/%s" % (at, _pointer(name)), out)
184
+ if "propertyNames" in schema:
185
+ _validate(schema["propertyNames"], name, "%s/%s" % (at, _pointer(name)), out)
186
+ for sub in schema.get("allOf", []):
187
+ _validate(sub, value, at, out)
188
+ if "not" in schema and not _problems(schema["not"], value):
189
+ out.append({"instancePath": at, "message": "must not match"})
190
+ if "if" in schema:
191
+ branch = schema.get("then") if not _problems(schema["if"], value) else schema.get("else")
192
+ if branch is not None:
193
+ _validate(branch, value, at, out)
194
+
195
+
196
+ def _problems(schema: Any, value: Any) -> List[Dict[str, str]]:
197
+ out: List[Dict[str, str]] = []
198
+ _validate(schema, value, "", out)
199
+ return out
200
+
201
+
202
+ def schema_problems(pkg: Any) -> List[Dict[str, str]]:
203
+ """Where the package breaks ``fabric-package.schema.json`` (the contract's, bundled at the pin)."""
204
+ return _problems(_schema(), pkg)
205
+
206
+
207
+ # --- the contract's rules (FAC-SEM-064…066, FAC-SEM-102) ------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  """Where a service's things live (fabric-service/0.1): the services directory, a service's data, config, logs and run
2
- directories per OS, private directories and atomic writes. Part of the service kit (FAA-65, layer L3)."""
2
+ directories per OS, private directories and atomic writes. Part of the service kit (FAA-65, layer L1)."""
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
@@ -83,11 +83,11 @@ def atomic_write(path: Path, data: bytes, mode: int = 0o600) -> None:
83
83
  path.parent.mkdir(parents=True, exist_ok=True)
84
84
  fd, tmp = tempfile.mkstemp(prefix="." + path.name + ".", dir=str(path.parent))
85
85
  try:
86
- if WINDOWS:
87
- # The file's ACL is its privacy (DEC-0032): set explicitly and protected — before the bytes go in — so
88
- # it does not depend on what the folder hands down. A rename keeps it.
89
- _win_protect(Path(tmp), folder=False)
90
- with os.fdopen(fd, "wb") as handle:
86
+ with os.fdopen(fd, "wb") as handle: # closed on every path: Windows cannot delete a file still open
87
+ if WINDOWS:
88
+ # The file's ACL is its privacy (DEC-0032): set explicitly and protected — before the bytes go in —
89
+ # so it does not depend on what the folder hands down. A rename keeps it.
90
+ _win_protect(Path(tmp), folder=False)
91
91
  handle.write(data)
92
92
  handle.flush()
93
93
  os.fsync(handle.fileno())
@@ -97,6 +97,6 @@ def atomic_write(path: Path, data: bytes, mode: int = 0o600) -> None:
97
97
  except BaseException:
98
98
  try:
99
99
  os.unlink(tmp)
100
- except FileNotFoundError:
101
- pass
100
+ except OSError:
101
+ pass # the error that got us here is the one to report, not a failed cleanup (FAA-85)
102
102
  raise
@@ -1,6 +1,6 @@
1
1
  """Windows, where the kit needs it (DEC-0032): named pipes that hold one copy, protecting a file to its owner, and
2
2
  reading a file's ACL to refuse a token file others can read. On other systems the same names exist and do nothing
3
- or answer "absent". Part of the service kit (FAA-65, layer L3)."""
3
+ or answer "absent". Part of the service kit (FAA-65, layer L1)."""
4
4
  from __future__ import annotations
5
5
 
6
6
  import os