@passioncode-ai/fabric-agent-adapter 0.59.0 → 0.61.0
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 +195 -0
- package/README.md +2 -1
- 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 +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +93 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +23 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +117 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +33 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +80 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +14 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +24 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +146 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +31 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +80 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +33 -25
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +19 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +23 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +86 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +14 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +210 -22
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +2 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +27 -27
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +37 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py
CHANGED
|
@@ -145,11 +145,29 @@ def get_string(value: Any, key: str) -> str:
|
|
|
145
145
|
|
|
146
146
|
|
|
147
147
|
def is_number(value: Any) -> bool:
|
|
148
|
-
|
|
148
|
+
"""An int or a float by its real type, a subclass's included; a bool, or a value that only claims a number through
|
|
149
|
+
`__class__` (a mock, a proxy), is not one (0.61.0 seam reading 19)."""
|
|
150
|
+
kind = type(value)
|
|
151
|
+
return kind is not bool and issubclass(kind, (int, float))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def real_number(value: Any) -> Any:
|
|
155
|
+
"""The plain int or float a number holds (is_number), read by what it stores, never by its own float(), int() or
|
|
156
|
+
comparisons; None for anything else (0.61.0 seam reading 18, unit and product readings 19)."""
|
|
157
|
+
if not is_number(value):
|
|
158
|
+
return None
|
|
159
|
+
return float.__float__(value) if issubclass(type(value), float) else int.__int__(value)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def held(value: Any) -> Any:
|
|
163
|
+
"""A number by what it stores (real_number); any other value as given, so it reads as it did before."""
|
|
164
|
+
number = real_number(value)
|
|
165
|
+
return value if number is None else number
|
|
149
166
|
|
|
150
167
|
|
|
151
168
|
def double(value: Any) -> float:
|
|
152
169
|
"""A JSON number as JavaScript holds it: the nearest double, and an integer beyond the double range ±Infinity."""
|
|
170
|
+
value = held(value)
|
|
153
171
|
try:
|
|
154
172
|
return float(value)
|
|
155
173
|
except OverflowError:
|
|
@@ -184,7 +202,8 @@ def set_has(items: List[Any], value: Any) -> bool:
|
|
|
184
202
|
_JS_SPACE = "\t\n\v\f\r
"
|
|
185
203
|
|
|
186
204
|
|
|
187
|
-
|
|
205
|
+
# One way to split the digits, so a long string that fails to match costs its length (0.61.0 seam reading 22).
|
|
206
|
+
_JS_DECIMAL = re.compile(r"[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|Infinity)", re.ASCII)
|
|
188
207
|
|
|
189
208
|
|
|
190
209
|
_JS_RADIX = re.compile(r"0(?:[xX](?P<x>[0-9a-fA-F]+)|[oO](?P<o>[0-7]+)|[bB](?P<b>[01]+))", re.ASCII)
|
|
@@ -196,9 +215,10 @@ def js_to_number(value: Any) -> float:
|
|
|
196
215
|
return math.nan
|
|
197
216
|
if value is None:
|
|
198
217
|
return 0.0
|
|
199
|
-
if isinstance(
|
|
218
|
+
if value is True or value is False: # by identity: isinstance asks a number subclass's own __class__ (unit reading 30)
|
|
200
219
|
return 1.0 if value else 0.0
|
|
201
220
|
if is_number(value):
|
|
221
|
+
value = held(value) # by the number it holds: a subclass's own float() or comparison is never asked
|
|
202
222
|
try:
|
|
203
223
|
return float(value)
|
|
204
224
|
except OverflowError:
|
|
@@ -97,9 +97,23 @@ class InstanceLock:
|
|
|
97
97
|
except BaseException:
|
|
98
98
|
os.close(fd)
|
|
99
99
|
raise
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
try:
|
|
101
|
+
os.ftruncate(fd, 0)
|
|
102
|
+
os.write(fd, str(os.getpid()).encode())
|
|
103
|
+
os.fsync(fd)
|
|
104
|
+
except BaseException: # a full disk or an I/O error: let the lock go, or release() frees nothing
|
|
105
|
+
try:
|
|
106
|
+
os.close(fd)
|
|
107
|
+
except OSError: # a deferred write error (NFS) at the close: the name still goes, the first error stands
|
|
108
|
+
pass
|
|
109
|
+
finally:
|
|
110
|
+
name, self._name = self._name, None
|
|
111
|
+
if name is not None:
|
|
112
|
+
try:
|
|
113
|
+
_drop_lock_name(name)
|
|
114
|
+
except OSError: # as with the close: nothing to add to the first error
|
|
115
|
+
pass
|
|
116
|
+
raise
|
|
103
117
|
self._fd = fd
|
|
104
118
|
return self
|
|
105
119
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
the task, narrowed at each hand-over. Part of the service kit (layer L2); the twin of fabric-mandate.mjs.
|
|
3
3
|
|
|
4
4
|
- ``mandate_covers(inputs)``: whether a callee takes one call as the operator's decision under a mandate (MD-3 to
|
|
5
|
-
MD-5),
|
|
6
|
-
(
|
|
5
|
+
MD-5), as the contract's ``mandateCovers`` answers, but for a spend that is no JSON data a ``json.loads`` gives
|
|
6
|
+
(shared or cyclic), whose lists nest in lists more than MAX_SPEND_DEPTH deep, or that holds a dict with an own
|
|
7
|
+
``toString`` at its top or through lists alone: the core refuses all three, where the contract reads the first,
|
|
8
|
+
reads or throws on the second and throws on the third (FAA-196 (22)). ``inputs`` holds ``mandate``, ``tier``, ``now``,
|
|
9
|
+
``self`` (the callee's own agent id), ``callerAgent`` (only when the callee authenticated one) and ``call`` ({capability,
|
|
7
10
|
action?, effect, spendUsd?, project?}).
|
|
8
11
|
- ``mandate_problem(mandate)``: ``mandate-malformed``, ``mandate-widened`` or None (FAC-SEM-144);
|
|
9
12
|
``mandate_shape(mandate)``: ``mandate.schema.json`` alone; ``mandate_rules(kind, value)``: the findings.
|
|
@@ -18,7 +21,9 @@ the task, narrowed at each hand-over. Part of the service kit (layer L2); the tw
|
|
|
18
21
|
``instruction`` it stays in the receiver's own record: never an event, a usage report, a log line or a page.
|
|
19
22
|
|
|
20
23
|
The kit has no JSON Schema validator: the shape is written here from the schema, lengths in code points and patterns
|
|
21
|
-
over the whole string, as the contract's validator reads them. Never raises on
|
|
24
|
+
over the whole string, as the contract's validator reads them. Never raises on JSON as json.loads gives it (a direct
|
|
25
|
+
caller's key or value that is not exactly the JSON type, with its own methods (__eq__, __hash__, __len__, __class__, a
|
|
26
|
+
dict subclass's get), in the input, the call, the mandate or its scope, is used as given: FAA-196 (21), (22)). Standard library only.
|
|
22
27
|
"""
|
|
23
28
|
from __future__ import annotations
|
|
24
29
|
|
|
@@ -33,7 +38,7 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
33
38
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
34
39
|
|
|
35
40
|
from fabric_base import ServiceError # noqa: E402
|
|
36
|
-
from fabric_jsvalue import MISSING, get, is_number, js_to_number, same, set_has # noqa: E402
|
|
41
|
+
from fabric_jsvalue import MISSING, get, is_number, js_string, js_to_number, real_number, same, set_has # noqa: E402
|
|
37
42
|
from fabric_time import format_rfc3339, parse_rfc3339_ms # noqa: E402
|
|
38
43
|
|
|
39
44
|
#: The longest a mandate stands after the operator gave it (FAC-SEM-144).
|
|
@@ -238,6 +243,66 @@ def _member_of(items: Any, value: Any) -> bool:
|
|
|
238
243
|
return set_has(items, value)
|
|
239
244
|
|
|
240
245
|
|
|
246
|
+
#: A spend whose lists nest in one another more deeply than this, with no dict between them, is no estimate: that
|
|
247
|
+
#: nesting is what JavaScript's String() of a list recurses through (Node and the contract throw RangeError a few
|
|
248
|
+
#: thousand deep, 0.60.0 raised RecursionError a few hundred deep), and the spend a refusal carries as estimateUsd is
|
|
249
|
+
#: then one a serialiser writes. Under a dict there is no bound: Number() never descends a dict, which is NaN at any
|
|
250
|
+
#: depth in all three, but for one with an own toString at the top or through lists alone, on which Node and the
|
|
251
|
+
#: contract throw (0.60.0 read it as NaN; the core refuses it, see _json_data). Stricter than all three between 100
|
|
252
|
+
#: and their limit: FAA-196 (22); no JSON reader of the kit reaches it (0.61.0 readings 25 to 30).
|
|
253
|
+
MAX_SPEND_DEPTH = 100
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _json_data(value: Any) -> bool:
|
|
257
|
+
"""Whether a value is JSON data as json.loads gives it, by exact type all the way down (never `==` or isinstance,
|
|
258
|
+
which a metaclass or `__class__` can answer), walked without recursion in one pass, its lists nested in lists at
|
|
259
|
+
most MAX_SPEND_DEPTH deep (a dict ends the count: its members are walked at any depth), and no dict at its top or
|
|
260
|
+
reached through lists alone with an own toString, on which Number() throws, as strict_string reads it (0.61.0 seam
|
|
261
|
+
reading 28). json.loads never shares a list or dict, so a structure that meets one twice (shared or cyclic) is not
|
|
262
|
+
JSON data (0.61.0 unit reading 21, seam reading 22, readings 25 to 27)."""
|
|
263
|
+
stack, seen = [(value, 0)], set()
|
|
264
|
+
while stack:
|
|
265
|
+
item, depth = stack.pop()
|
|
266
|
+
kind = type(item)
|
|
267
|
+
if item is None or kind is bool or kind is str or kind is int or kind is float:
|
|
268
|
+
continue
|
|
269
|
+
if kind is not list and kind is not dict:
|
|
270
|
+
return False
|
|
271
|
+
if id(item) in seen:
|
|
272
|
+
return False
|
|
273
|
+
seen.add(id(item))
|
|
274
|
+
if kind is list:
|
|
275
|
+
if depth is not None:
|
|
276
|
+
depth += 1
|
|
277
|
+
if depth > MAX_SPEND_DEPTH:
|
|
278
|
+
return False
|
|
279
|
+
stack.extend((sub, depth) for sub in item)
|
|
280
|
+
continue
|
|
281
|
+
for key, sub in item.items():
|
|
282
|
+
if type(key) is not str: # before any comparison: a key's own __eq__ never runs (unit reading 29)
|
|
283
|
+
return False
|
|
284
|
+
if depth is not None and key == "toString": # Number() reaches it: a member of data is never callable
|
|
285
|
+
return False
|
|
286
|
+
stack.append((sub, None))
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _spend_number(spend: Any) -> float:
|
|
291
|
+
"""Number(spend) for JSON data, without the join JavaScript builds for a list: [] is 0, a list of two or more items
|
|
292
|
+
is NaN (the comma never reads as a number), one item is read for itself, all in one descent (0.61.0 seam
|
|
293
|
+
reading 24: a list repeating one long string cost its length times its references)."""
|
|
294
|
+
value = spend
|
|
295
|
+
while type(value) is list:
|
|
296
|
+
if not value:
|
|
297
|
+
return 0.0
|
|
298
|
+
if len(value) > 1:
|
|
299
|
+
return math.nan
|
|
300
|
+
value = value[0]
|
|
301
|
+
if value is spend:
|
|
302
|
+
return js_to_number(value)
|
|
303
|
+
return 0.0 if value is None else js_to_number(js_string(value))
|
|
304
|
+
|
|
305
|
+
|
|
241
306
|
def mandate_covers(inputs: Any) -> Dict[str, Any]:
|
|
242
307
|
"""Whether a callee takes one call as the operator's decision under a mandate (DEC-0125 MD-3 to MD-5), after it has
|
|
243
308
|
placed the caller's tier (DL-25), as the contract's mandateCovers answers.
|
|
@@ -290,10 +355,24 @@ def mandate_covers(inputs: Any) -> Dict[str, Any]:
|
|
|
290
355
|
if not set_has(list(_READS_AND_DRAFTS), effect) and capabilities is MISSING and (action is MISSING or actions is MISSING):
|
|
291
356
|
return _outside("outside-mandate: capability")
|
|
292
357
|
ceiling, spend = get(scope, "perCallMaxUsd"), get(call, "spendUsd")
|
|
293
|
-
|
|
358
|
+
# A spend is read as the contract reads it only when it is JSON data as json.loads gives it, by exact type all the
|
|
359
|
+
# way down, or an int or float (a subclass read by the number it stores); anything else (a mock or proxy, a
|
|
360
|
+
# Decimal, a numpy.int64, a tuple, bytes, a str, list or dict subclass, a list holding one) is no estimate, which a
|
|
361
|
+
# ceiling refuses (0.61.0 seam readings 19 and 20), as is JSON data the core does not read as Number() does: lists
|
|
362
|
+
# nested more than MAX_SPEND_DEPTH deep (stricter than Number() between 100 and its limit), or a dict with an own
|
|
363
|
+
# toString Number() reaches, on which it throws (readings 25 to 29).
|
|
364
|
+
readable = over = False
|
|
365
|
+
if ceiling is not MISSING and spend is not MISSING: # read only when a ceiling asks (no walk without one)
|
|
366
|
+
readable = is_number(spend) or _json_data(spend)
|
|
367
|
+
if readable:
|
|
368
|
+
try:
|
|
369
|
+
over = _spend_number(spend) > ceiling
|
|
370
|
+
except RecursionError: # the number's read at the stack's edge: no estimate, never a raise (seam reading 21)
|
|
371
|
+
readable = False
|
|
372
|
+
if ceiling is not MISSING and (spend is MISSING or not readable or over):
|
|
294
373
|
data: Dict[str, Any] = {"detail": "outside-mandate: spend"}
|
|
295
|
-
if spend is not MISSING:
|
|
296
|
-
data["estimateUsd"] = spend
|
|
374
|
+
if spend is not MISSING and readable:
|
|
375
|
+
data["estimateUsd"] = real_number(spend) if is_number(spend) else spend
|
|
297
376
|
data["perCallMaxUsd"] = ceiling
|
|
298
377
|
return {"ok": False, "code": OUTSIDE_MANDATE, "data": data}
|
|
299
378
|
project, projects = get(call, "project"), get(scope, "projects")
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py
CHANGED
|
@@ -133,7 +133,7 @@ def read_signing_key(path: Any, kid: Optional[str] = None) -> Tuple[bytes, str]:
|
|
|
133
133
|
text = raw.decode("utf-8", "replace").strip(JS_WHITESPACE) # as JavaScript's trim: a leading BOM goes too
|
|
134
134
|
if text.startswith("{"):
|
|
135
135
|
try:
|
|
136
|
-
jwk = json.loads(text)
|
|
136
|
+
jwk = json.loads(text, parse_constant=fd._no_constant) # NaN and Infinity are no JSON, as JSON.parse says
|
|
137
137
|
except ValueError:
|
|
138
138
|
raise PackageError("The key file is neither a JWK nor a PEM private key.") from None
|
|
139
139
|
if not isinstance(jwk, dict) or jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not isinstance(jwk.get("d"), str):
|
|
@@ -394,8 +394,8 @@ def _parsed(data: Optional[bytes]) -> Optional[Dict[str, Any]]:
|
|
|
394
394
|
if data is None:
|
|
395
395
|
return None
|
|
396
396
|
try:
|
|
397
|
-
value = json.loads(data.decode("utf-8"))
|
|
398
|
-
except (ValueError, RecursionError): # invalid UTF-8 is no manifest, in both kits
|
|
397
|
+
value = json.loads(data.decode("utf-8"), parse_constant=fd._no_constant)
|
|
398
|
+
except (ValueError, RecursionError): # invalid UTF-8, NaN or Infinity is no manifest, in both kits
|
|
399
399
|
return None
|
|
400
400
|
return value if isinstance(value, dict) else None
|
|
401
401
|
|
|
@@ -424,7 +424,7 @@ def check_package_folder(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[D
|
|
|
424
424
|
provider = pkg.get("provider")
|
|
425
425
|
if isinstance(provider, str) and provider in files:
|
|
426
426
|
manifest = _parsed(files.get(provider))
|
|
427
|
-
if
|
|
427
|
+
if manifest is None or ("costPresets" in manifest and not ("costPresets" in pkg and _json_equal(manifest["costPresets"], pkg["costPresets"]))):
|
|
428
428
|
problems.append({"problem": "provider-manifest", "path": provider})
|
|
429
429
|
return problems
|
|
430
430
|
|
|
@@ -648,7 +648,7 @@ def _load_json(data: bytes, what: str) -> Dict[str, Any]:
|
|
|
648
648
|
if _nesting(text) > MAX_NESTING:
|
|
649
649
|
raise PackageError("%s nests deeper than %d levels." % (what, MAX_NESTING))
|
|
650
650
|
try:
|
|
651
|
-
value = json.loads(text, object_pairs_hook=fd._no_duplicates)
|
|
651
|
+
value = json.loads(text, object_pairs_hook=fd._no_duplicates, parse_constant=fd._no_constant)
|
|
652
652
|
except fd._Duplicate as exc:
|
|
653
653
|
raise PackageError("%s names %s twice in one object." % (what, json.dumps(exc.args[0], ensure_ascii=False))) from None
|
|
654
654
|
except (ValueError, RecursionError) as exc:
|
|
@@ -704,10 +704,18 @@ def _all_problems(pkg: Dict[str, Any], files: Dict[str, bytes]) -> List[Dict[str
|
|
|
704
704
|
problems = [dict(p, rule="schema") for p in schema_problems(pkg)]
|
|
705
705
|
problems += [dict(p, rule="path") for p in path_problems(pkg)]
|
|
706
706
|
problems += [dict(p, rule=p["code"]) for p in package_findings(pkg)]
|
|
707
|
-
problems += [{"rule": "folder", "problem": p["problem"], "instancePath": p["path"], "message": p["problem"]
|
|
707
|
+
problems += [{"rule": "folder", "problem": p["problem"], "instancePath": p["path"], "message": FOLDER_MESSAGES.get(p["problem"], p["problem"])}
|
|
708
|
+
for p in check_package_folder(pkg, files)]
|
|
708
709
|
return problems
|
|
709
710
|
|
|
710
711
|
|
|
712
|
+
#: What a folder problem means, where its code alone does not say what to change (FAA-193).
|
|
713
|
+
FOLDER_MESSAGES = {
|
|
714
|
+
"provider-manifest": "provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets "
|
|
715
|
+
"fabric.package.json must carry the same costPresets (copy them; pack does not)",
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
711
719
|
def pack(folder: Any, apply: bool = False) -> Dict[str, Any]:
|
|
712
720
|
"""Pack ``folder`` (see the module's doc). The answer names the digest, ``agentRef`` and every problem; with
|
|
713
721
|
``apply`` and no problem, ``fabric.package.json`` is rewritten."""
|