@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.
- package/CHANGELOG.md +40 -0
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +9 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +264 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +31 -254
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +100 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -89
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +13 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +5 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +54 -369
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +20 -187
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +207 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +8 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +263 -67
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- 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) ------------------------------------------------
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py
CHANGED
|
@@ -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
|
|
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
|