@passioncode-ai/fabric-agent-adapter 0.24.0 → 0.26.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 +71 -1
- package/README.md +1 -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 +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +17 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +182 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +32 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +156 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +9 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +128 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +11 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +182 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +11 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +176 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- 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.py +1 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py
CHANGED
|
@@ -19,6 +19,7 @@ import shutil
|
|
|
19
19
|
import subprocess
|
|
20
20
|
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
21
21
|
|
|
22
|
+
import fabric_identifiers as _ids
|
|
22
23
|
import fabric_secrets as _secrets
|
|
23
24
|
import fabric_service as _fs
|
|
24
25
|
|
|
@@ -314,9 +315,12 @@ def _object_at(value: Any, key: str) -> Dict[str, Any]:
|
|
|
314
315
|
|
|
315
316
|
|
|
316
317
|
def _ref_key(ref: Any) -> str:
|
|
318
|
+
"""A revision reference compared whole: id, revision and content hash. The id is an identifier, so it enters the key
|
|
319
|
+
in its normal form (DEC-0076)."""
|
|
317
320
|
if not isinstance(ref, dict):
|
|
318
321
|
return ""
|
|
319
|
-
|
|
322
|
+
normal = _ids.normal_identifier(ref.get("id"))
|
|
323
|
+
return "%s@%s#%s" % (normal if normal is not None else _field(ref, "id"), _field(ref, "revision"), _field(ref, "contentHash"))
|
|
320
324
|
|
|
321
325
|
|
|
322
326
|
def _same_ref(a: Any, b: Any) -> bool:
|
|
@@ -343,7 +347,7 @@ def _runner_route(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
343
347
|
def _route_bundle(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
344
348
|
route = _object_at(value, "route")
|
|
345
349
|
admitted = {_ref_key(a.get("provider")) for a in _list(value.get("admissions"))
|
|
346
|
-
if a.get("state") == "admitted" and a.get("capability", _MISSING)
|
|
350
|
+
if a.get("state") == "admitted" and _ids.same_identifier(a.get("capability", _MISSING), route.get("capability", _MISSING))}
|
|
347
351
|
findings = []
|
|
348
352
|
for index, candidate in enumerate(_candidates(route)):
|
|
349
353
|
key = _ref_key(candidate.get("provider"))
|
|
@@ -363,9 +367,10 @@ def _binding_agreement(binding: Dict[str, Any], route: Dict[str, Any]) -> List[D
|
|
|
363
367
|
def disagree(path: str, message: str) -> None:
|
|
364
368
|
findings.append(_finding("FAC-SEM-032", "/binding/" + path, message))
|
|
365
369
|
|
|
366
|
-
|
|
370
|
+
# DEC-0076: project and capability are identifiers, compared by normal form.
|
|
371
|
+
if not _ids.same_identifier(binding.get("project", _MISSING), route.get("project", _MISSING)):
|
|
367
372
|
disagree("project", "the binding is for %s, the route for %s" % (_field(binding, "project"), _field(route, "project")))
|
|
368
|
-
if binding.get("capability", _MISSING)
|
|
373
|
+
if not _ids.same_identifier(binding.get("capability", _MISSING), route.get("capability", _MISSING)):
|
|
369
374
|
disagree("capability", "the binding binds %s, the route orders %s" % (_field(binding, "capability"), _field(route, "capability")))
|
|
370
375
|
if binding.get("profileKind") != "local-runner":
|
|
371
376
|
disagree("profileKind", "a runner route serves the local-runner profile, not %s" % _field(binding, "profileKind"))
|
|
@@ -408,9 +413,9 @@ def _route_event(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
408
413
|
|
|
409
414
|
if not _same_ref(event.get("route"), route.get("meta")):
|
|
410
415
|
wrong("/route", "the event names route %s, not %s" % (_ref_key(event.get("route")), _ref_key(route.get("meta"))))
|
|
411
|
-
if event.get("project", _MISSING)
|
|
416
|
+
if not _ids.same_identifier(event.get("project", _MISSING), route.get("project", _MISSING)):
|
|
412
417
|
wrong("/project", "the event is for %s, the route for %s" % (_field(event, "project"), _field(route, "project")))
|
|
413
|
-
if event.get("capability", _MISSING)
|
|
418
|
+
if not _ids.same_identifier(event.get("capability", _MISSING), route.get("capability", _MISSING)):
|
|
414
419
|
wrong("/capability", "the event names %s, the route orders %s" % (_field(event, "capability"), _field(route, "capability")))
|
|
415
420
|
|
|
416
421
|
def candidate_at(ref: Dict[str, Any], path: str) -> Optional[Dict[str, Any]]:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py
CHANGED
|
@@ -1305,7 +1305,8 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
1305
1305
|
surfaces: Dict[str, Any], summary: Optional[Sequence[Dict[str, Any]]] = None,
|
|
1306
1306
|
update_available: Optional[str] = None, pid: Optional[int] = None,
|
|
1307
1307
|
extensions: Optional[Dict[str, Any]] = None,
|
|
1308
|
-
configuration: Optional[Dict[str, Any]] = None
|
|
1308
|
+
configuration: Optional[Dict[str, Any]] = None,
|
|
1309
|
+
protocols: Optional[Sequence[str]] = None) -> Dict[str, Any]:
|
|
1309
1310
|
"""The unauthenticated identity answer. `degraded` is always present; ready means empty.
|
|
1310
1311
|
`configuration` (DEC-0046) says what the operator can configure and what applies now; fabric_profiles builds it.
|
|
1311
1312
|
`extensions` is kept as given (a reader ignores keys it does not know); a dashboard declaration
|
|
@@ -1339,15 +1340,182 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
1339
1340
|
if problems:
|
|
1340
1341
|
raise ServiceError("configuration: %s." % "; ".join(problems))
|
|
1341
1342
|
doc["configuration"] = configuration
|
|
1343
|
+
if protocols is not None:
|
|
1344
|
+
# DEC-0067: the revisions this service accepts. The document is closed for a reader pinned before DEC-0067, so
|
|
1345
|
+
# a service lists them only once its hosts are pinned at or after it.
|
|
1346
|
+
problems = protocols_findings(list(protocols))
|
|
1347
|
+
if problems:
|
|
1348
|
+
raise ServiceError("protocols: %s." % "; ".join(f["message"] for f in problems))
|
|
1349
|
+
doc["protocols"] = list(protocols)
|
|
1342
1350
|
return doc
|
|
1343
1351
|
|
|
1344
1352
|
|
|
1353
|
+
# --- DEC-0067: protocol revisions; DEC-0070: an event's machine-readable data --------------------------------------
|
|
1354
|
+
|
|
1355
|
+
#: Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`, DEC-0067); a test
|
|
1356
|
+
#: holds it equal to the contract's register at the pin.
|
|
1357
|
+
PROTOCOL_REVISIONS: Dict[str, Tuple[str, ...]] = {
|
|
1358
|
+
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "estate-record": ("1",), "execution-packet": ("1",),
|
|
1359
|
+
"fabric-activity": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
1360
|
+
"fabric-interop": ("0.1",), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",),
|
|
1361
|
+
"fabric-operator-channel": ("0.1",), "fabric-package": ("1",), "fabric-project-comms": ("0.1",), "fabric-proposal": ("1",),
|
|
1362
|
+
"fabric-provider": ("0.1",), "fabric-relay": ("0.1",), "fabric-secret-envelope": ("1",), "fabric-service": ("0.1",),
|
|
1363
|
+
"fabric-settings-backup": ("1",), "heartbeat": ("1",), "learning-note": ("1",), "memory": ("0.1",), "pipeline": ("0.1",),
|
|
1364
|
+
"revocation-delta": ("1",), "session-handoff": ("1",), "window-lease": ("1",),
|
|
1365
|
+
}
|
|
1366
|
+
MAX_PROTOCOLS = 64
|
|
1367
|
+
_REVISION = re.compile(r"([a-z][a-z0-9.-]{0,63})/([0-9]{1,4}(?:\.[0-9]{1,4}){0,2})", re.ASCII)
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def parse_revision(value: Any) -> Optional[Tuple[str, str]]:
|
|
1371
|
+
"""`<name>/<revision>` split, or None."""
|
|
1372
|
+
match = _REVISION.fullmatch(value) if isinstance(value, str) else None
|
|
1373
|
+
return (match.group(1), match.group(2)) if match else None
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def accepted_revisions(name: str, newest: Optional[str] = None) -> List[str]:
|
|
1377
|
+
"""The revisions a receiver implementing ``name`` up to ``newest`` accepts: N and, when the register has one, N−1."""
|
|
1378
|
+
known = PROTOCOL_REVISIONS.get(name, ())
|
|
1379
|
+
if not known:
|
|
1380
|
+
return []
|
|
1381
|
+
top = len(known) - 1 if newest is None else (known.index(newest) if newest in known else -1)
|
|
1382
|
+
if top < 0:
|
|
1383
|
+
return []
|
|
1384
|
+
return ["%s/%s" % (name, r) for r in reversed(known[max(0, top - 1):top + 1])]
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def negotiate_revision(received: Any, accepted: Sequence[Any]) -> Dict[str, Any]:
|
|
1388
|
+
"""A receiver's answer to a document naming ``received`` (DEC-0067, the contract's `negotiateRevision`):
|
|
1389
|
+
``{"ok": True}``; the `schema-unsupported` refusal naming what was received (at most 80 characters) and the accepted
|
|
1390
|
+
revisions of that protocol (all of them when it names none the receiver reads); or ``{"fault":
|
|
1391
|
+
"no-accepted-revisions"}``, the receiver's own, when it accepts no well-formed revision. Revisions compare exactly."""
|
|
1392
|
+
usable: List[str] = []
|
|
1393
|
+
for entry in accepted if isinstance(accepted, (list, tuple)) else []:
|
|
1394
|
+
if parse_revision(entry) and entry not in usable:
|
|
1395
|
+
usable.append(entry)
|
|
1396
|
+
usable = usable[:MAX_PROTOCOLS]
|
|
1397
|
+
if not usable:
|
|
1398
|
+
return {"fault": "no-accepted-revisions"}
|
|
1399
|
+
if isinstance(received, str) and received in usable:
|
|
1400
|
+
return {"ok": True}
|
|
1401
|
+
parsed = parse_revision(received)
|
|
1402
|
+
same = [e for e in usable if parsed and parse_revision(e)[0] == parsed[0]]
|
|
1403
|
+
return {"error": {"code": "schema-unsupported", "message": "this receiver does not read that protocol revision",
|
|
1404
|
+
"data": {"protocol": received[:80] if isinstance(received, str) else "", "accepted": same or usable}}}
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def protocols_findings(protocols: Any, prefix: str = "/protocols") -> List[Dict[str, str]]:
|
|
1408
|
+
"""FAC-SEM-099 (DEC-0067): 1 to 64 entries, each `<name>/<revision>` once; at most two revisions of one protocol (an
|
|
1409
|
+
unpublished one refused and not counted), and two of a contract protocol are N and N−1, consecutive in the register."""
|
|
1410
|
+
flag = lambda at, message: {"code": "FAC-SEM-099", "instancePath": at, "message": message} # noqa: E731
|
|
1411
|
+
if not isinstance(protocols, list):
|
|
1412
|
+
return [flag(prefix, "protocols is a list of protocol revisions")]
|
|
1413
|
+
if not 1 <= len(protocols) <= MAX_PROTOCOLS:
|
|
1414
|
+
return [flag(prefix, "protocols lists 1 to %d revisions" % MAX_PROTOCOLS)]
|
|
1415
|
+
findings: List[Dict[str, str]] = []
|
|
1416
|
+
by_name: Dict[str, List[int]] = {}
|
|
1417
|
+
seen: List[str] = []
|
|
1418
|
+
for i, entry in enumerate(protocols):
|
|
1419
|
+
at = "%s/%d" % (prefix, i)
|
|
1420
|
+
parsed = parse_revision(entry)
|
|
1421
|
+
if not parsed:
|
|
1422
|
+
findings.append(flag(at, "a protocol revision is <name>/<revision>, such as fabric-service/0.1"))
|
|
1423
|
+
continue
|
|
1424
|
+
if entry in seen:
|
|
1425
|
+
findings.append(flag(at, "each protocol revision once"))
|
|
1426
|
+
continue
|
|
1427
|
+
seen.append(entry)
|
|
1428
|
+
known = PROTOCOL_REVISIONS.get(parsed[0])
|
|
1429
|
+
if known and parsed[1] not in known:
|
|
1430
|
+
findings.append(flag(at, "%s has no published revision %s" % parsed))
|
|
1431
|
+
continue
|
|
1432
|
+
indices = by_name.setdefault(parsed[0], [])
|
|
1433
|
+
indices.append(known.index(parsed[1]) if known else -1)
|
|
1434
|
+
if len(indices) > 2:
|
|
1435
|
+
findings.append(flag(at, "at most 2 revisions of one protocol: N and N−1"))
|
|
1436
|
+
elif known and len(indices) == 2 and abs(indices[0] - indices[1]) != 1:
|
|
1437
|
+
findings.append(flag(at, "two revisions of one protocol are N and N−1, consecutive in the register"))
|
|
1438
|
+
return findings
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
_SCHEMA_URI = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:[^\s]+", re.ASCII)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _scalar(value: Any) -> bool:
|
|
1445
|
+
return value is None or isinstance(value, (bool, int, float)) or (isinstance(value, str) and len(value) <= 500)
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def feed_data_problems(data: Any) -> List[str]:
|
|
1449
|
+
"""DEC-0070's `data {schema, value}` as the schema bounds it, then FAC-SEM-101: no string in `value`, at either
|
|
1450
|
+
level, under a credential's name or of a credential's shape (named by its place, never its value)."""
|
|
1451
|
+
import fabric_secrets # noqa: E402 - beside this file; the contract's detector
|
|
1452
|
+
if not isinstance(data, dict) or set(data) != {"schema", "value"}:
|
|
1453
|
+
return ["data is {schema, value}"]
|
|
1454
|
+
problems: List[str] = []
|
|
1455
|
+
if not (isinstance(data["schema"], str) and _SCHEMA_URI.fullmatch(data["schema"])):
|
|
1456
|
+
problems.append("data.schema is the absolute URI of a payload schema the service publishes")
|
|
1457
|
+
value = data["value"]
|
|
1458
|
+
if not isinstance(value, dict) or len(value) > 32:
|
|
1459
|
+
return problems + ["data.value is an object of at most 32 members"]
|
|
1460
|
+
for key, member in value.items():
|
|
1461
|
+
if _scalar(member):
|
|
1462
|
+
continue
|
|
1463
|
+
if isinstance(member, list) and len(member) <= 32 and all(_scalar(m) for m in member):
|
|
1464
|
+
continue
|
|
1465
|
+
if isinstance(member, dict) and len(member) <= 32 and all(_scalar(m) for m in member.values()):
|
|
1466
|
+
continue
|
|
1467
|
+
problems.append("data.value.%s is a scalar (a string of at most 500 characters), a list of at most 32 scalars or an object of at most 32 scalar members" % key)
|
|
1468
|
+
if problems:
|
|
1469
|
+
return problems
|
|
1470
|
+
for finding in feed_data_findings({"events": [{"data": data}]}):
|
|
1471
|
+
problems.append("FAC-SEM-101: %s %s" % (finding["instancePath"].replace("/events/0", ""), finding["message"]))
|
|
1472
|
+
return problems
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _pointer_token(name: str) -> str:
|
|
1476
|
+
return str(name).replace("~", "~0").replace("/", "~1")
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def feed_data_findings(page: Any) -> List[Dict[str, str]]:
|
|
1480
|
+
"""FAC-SEM-101 over an events page (the contract's `feedDataFindings`): a list's items and an object's members sit
|
|
1481
|
+
under the outer name too; a number is a count, never a credential."""
|
|
1482
|
+
import fabric_secrets # noqa: E402
|
|
1483
|
+
findings: List[Dict[str, str]] = []
|
|
1484
|
+
|
|
1485
|
+
def check(named: bool, member: Any, at: str) -> None:
|
|
1486
|
+
if not isinstance(member, str):
|
|
1487
|
+
return
|
|
1488
|
+
if named:
|
|
1489
|
+
findings.append({"code": "FAC-SEM-101", "instancePath": at, "message": "an event's data puts no string under a credential's name"})
|
|
1490
|
+
elif fabric_secrets.secret_value(member) or fabric_secrets.secret_url(member):
|
|
1491
|
+
findings.append({"code": "FAC-SEM-101", "instancePath": at, "message": "an event's data carries no value shaped like a credential"})
|
|
1492
|
+
|
|
1493
|
+
events = page.get("events") if isinstance(page, dict) else None
|
|
1494
|
+
for i, event in enumerate(events if isinstance(events, list) else []):
|
|
1495
|
+
data = event.get("data") if isinstance(event, dict) else None
|
|
1496
|
+
value = data.get("value") if isinstance(data, dict) else None
|
|
1497
|
+
if not isinstance(value, dict):
|
|
1498
|
+
continue
|
|
1499
|
+
for key, member in value.items():
|
|
1500
|
+
at = "/events/%d/data/value/%s" % (i, _pointer_token(key))
|
|
1501
|
+
named = fabric_secrets.credential_name(key)
|
|
1502
|
+
check(named, member, at)
|
|
1503
|
+
if isinstance(member, list):
|
|
1504
|
+
for j, item in enumerate(member):
|
|
1505
|
+
check(named, item, "%s/%d" % (at, j))
|
|
1506
|
+
elif isinstance(member, dict):
|
|
1507
|
+
for inner, leaf in member.items():
|
|
1508
|
+
check(named or fabric_secrets.credential_name(inner), leaf, "%s/%s" % (at, _pointer_token(inner)))
|
|
1509
|
+
return findings
|
|
1510
|
+
|
|
1511
|
+
|
|
1345
1512
|
# --- events ------------------------------------------------------------------
|
|
1346
1513
|
|
|
1347
1514
|
def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subject: Optional[Dict[str, str]] = None,
|
|
1348
1515
|
link: Optional[str] = None, notify: bool = False, trace_id: Optional[str] = None,
|
|
1349
|
-
span_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1350
|
-
"""One activity event: one sentence a person reads, never a machine id.
|
|
1516
|
+
span_id: Optional[str] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
1517
|
+
"""One activity event: one sentence a person reads, never a machine id. `data` (DEC-0070) adds machine-readable
|
|
1518
|
+
fields beside the sentence, `{schema, value}`, never a secret: it is refused here, so no feed carries one.
|
|
1351
1519
|
|
|
1352
1520
|
An event about traced work carries its trace as a pair, `trace_id` and `span_id`
|
|
1353
1521
|
(fabric-interop/0.1 C3.4 c); fabric_interop.trace_ids(traceparent) gives both."""
|
|
@@ -1373,6 +1541,11 @@ def make_event(event_id: Any, at: str, kind: str, level: str, text: str, *, subj
|
|
|
1373
1541
|
event["notify"] = True
|
|
1374
1542
|
if trace_id is not None:
|
|
1375
1543
|
event["traceId"], event["spanId"] = trace_id, span_id
|
|
1544
|
+
if data is not None:
|
|
1545
|
+
problems = feed_data_problems(data)
|
|
1546
|
+
if problems:
|
|
1547
|
+
raise ServiceError("Event data: %s." % "; ".join(problems))
|
|
1548
|
+
event["data"] = {"schema": data["schema"], "value": data["value"]}
|
|
1376
1549
|
return event
|
|
1377
1550
|
|
|
1378
1551
|
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). 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.
|
|
8
|
+
version: "0.26.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -123,7 +123,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
123
123
|
The effect class is declared, never defaulted: ask the author what the agent does outside itself when the request
|
|
124
124
|
does not say. Cost presets are only for a run whose cost varies; their amounts come from the author or measured runs.
|
|
125
125
|
|
|
126
|
-
Pin exactly contract `0.1.0` at commit `
|
|
126
|
+
Pin exactly contract `0.1.0` at commit `a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b` and
|
|
127
127
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
128
128
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
129
129
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|
|
@@ -60,7 +60,7 @@ WATCH_MAX_BACKOFF_S = 900
|
|
|
60
60
|
SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
|
|
61
61
|
LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
|
|
62
62
|
LOCAL_ORIGIN = re.compile(r'"origin": "http://(?:127\.0\.0\.1|localhost)')
|
|
63
|
-
VERSION = "0.
|
|
63
|
+
VERSION = "0.26.0"
|
|
64
64
|
HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code"}
|
|
65
65
|
# A session Fabric launched. FABRIC_LAUNCHED_SESSION=1 is the marker Fabric is asked to set (need D6); the others are
|
|
66
66
|
# what Fabric already gives a launched session (fabric sessionBundle.ts): only their NAMES are looked at, never a value
|