@passioncode-ai/fabric-agent-adapter 0.78.0 → 0.80.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 +256 -0
- package/README.md +14 -7
- package/bin/fabric-agent-adapter.js +26 -4
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +23 -8
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +6 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +34 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +11 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +26 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +15 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +30 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +48 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +9 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +54 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +8 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +55 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +49 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +41 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +24 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +543 -37
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +12 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +42 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +6 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +13 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +32 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +12 -4
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +27 -26
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +59 -38
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +52 -26
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +169 -46
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +146 -19
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py
CHANGED
|
@@ -13,7 +13,8 @@ not re-export this module, so a service that does not run per context keeps the
|
|
|
13
13
|
An organization's instance whose context the record does not pin (``organizations``, DEC-0127) still starts: the helper
|
|
14
14
|
checks the placement and its share grant, not the pin (decision D5 of 0.52.0). Its tokens are refused instead:
|
|
15
15
|
``fabric_estate.hub_key_certified`` reads a context's hub keys only while the record pins that organization (DL-14a,
|
|
16
|
-
DEC-0128), so the service passes ``
|
|
16
|
+
DEC-0128), so the service passes ``context_gate_options(info, record)`` (its context and its pin's issuer, FAA-150) to its
|
|
17
|
+
delegation gate. The contract lets Fabric
|
|
17
18
|
write ``organizations`` only once every reader is pinned at or after DEC-0127, so a refusal at start would stop every
|
|
18
19
|
organization instance on today's records."""
|
|
19
20
|
from __future__ import annotations
|
|
@@ -33,7 +34,8 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
33
34
|
from fabric_base import ServiceError, _INSTANCE # noqa: E402
|
|
34
35
|
from fabric_descriptor import descriptor_path # noqa: E402
|
|
35
36
|
from fabric_estate import parse_context # noqa: E402
|
|
36
|
-
from fabric_lock import InstanceLock, hold_single_instance # noqa: E402
|
|
37
|
+
from fabric_lock import (InstanceLock, LOCK_BACKOFF_FIRST_SECONDS, LOCK_BACKOFF_MAX_SECONDS, hold_single_instance, # noqa: E402
|
|
38
|
+
lock_wait_budget_seconds, supervisor_marker)
|
|
37
39
|
from fabric_paths import atomic_write, instance_dirs # noqa: E402
|
|
38
40
|
from fabric_share import instance_share_grant # noqa: E402
|
|
39
41
|
from fabric_time import clock_ms # noqa: E402
|
|
@@ -125,9 +127,49 @@ def context_instance(service_id: str, context: Any, *, record: Optional[Dict[str
|
|
|
125
127
|
return answer
|
|
126
128
|
|
|
127
129
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
def context_gate_options(info: Any, record: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
131
|
+
"""What ``delegation_gate`` takes from this instance's context (FAA-150): ``estate_context``, and for an organization
|
|
132
|
+
whose context ``record`` pins, ``issuer``: the pin's context issuer (DL-14a, DEC-0128), so an organization's instance
|
|
133
|
+
refuses a token the personal hub key signed without wiring either by hand. ``info`` is ``context_instance``'s answer
|
|
134
|
+
(or ``hold_context_instance``'s); ``record`` the verified estate revision applied. Spread it after the service's own
|
|
135
|
+
estate issuer: ``delegation_gate(..., **{"issuer": estate_issuer, **context_gate_options(info, record)})``. Raises
|
|
136
|
+
ContextRefused (``context-invalid``) for an answer with no context."""
|
|
137
|
+
context = info.get("context") if isinstance(info, dict) else None
|
|
138
|
+
parsed = parse_context(context)
|
|
139
|
+
if parsed is None:
|
|
140
|
+
raise ContextRefused("context-invalid")
|
|
141
|
+
options: Dict[str, Any] = {"estate_context": context}
|
|
142
|
+
pins = record.get("organizations") if isinstance(record, dict) else None
|
|
143
|
+
if parsed["kind"] == "org" and isinstance(pins, list):
|
|
144
|
+
for pin in pins:
|
|
145
|
+
if isinstance(pin, dict) and pin.get("org") == context and isinstance(pin.get("iss"), str):
|
|
146
|
+
options["issuer"] = pin["iss"]
|
|
147
|
+
break
|
|
148
|
+
return options
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _refuse(service_id: Any, context: Any, code: str, wait_seconds: Optional[float] = 0.0, *,
|
|
152
|
+
sleep: Callable[[float], None] = time.sleep, clock: Callable[[], float] = time.monotonic,
|
|
153
|
+
env: Optional[Dict[str, str]] = None, argv: Optional[Sequence[str]] = None) -> None:
|
|
154
|
+
"""One sentence naming the code, then exit 78. Under a supervisor that restarts on exit (launchd's KeepAlive with
|
|
155
|
+
ThrottleInterval 10, systemd's Restart=always) an exit at once is a respawn every few seconds, forever (FAA-279);
|
|
156
|
+
so a supervised copy first waits the lock's back-off (0.5 s doubling to 30 s, 300 s in all), holding nothing, and
|
|
157
|
+
exits 78 when it runs out: a few restarts an hour. ``wait_seconds`` overrides the choice as the lock's does."""
|
|
158
|
+
if wait_seconds is None:
|
|
159
|
+
wait_seconds = lock_wait_budget_seconds(supervisor_marker(env, argv))
|
|
160
|
+
wait_seconds = max(0.0, wait_seconds)
|
|
161
|
+
tail = " Waiting %.0f s before exiting, so its supervisor does not restart it every few seconds." % wait_seconds \
|
|
162
|
+
if wait_seconds > 0 else ""
|
|
163
|
+
print("%s will not run for %s (%s): %s.%s" % (service_id, _shown(context), code, _REASONS[code], tail),
|
|
164
|
+
file=sys.stderr, flush=True)
|
|
165
|
+
deadline = clock() + wait_seconds
|
|
166
|
+
delay = LOCK_BACKOFF_FIRST_SECONDS
|
|
167
|
+
while True:
|
|
168
|
+
remaining = deadline - clock()
|
|
169
|
+
if remaining <= 0:
|
|
170
|
+
raise SystemExit(EXIT_CONTEXT_REFUSED)
|
|
171
|
+
sleep(min(delay, remaining))
|
|
172
|
+
delay = min(delay * 2, LOCK_BACKOFF_MAX_SECONDS)
|
|
131
173
|
|
|
132
174
|
|
|
133
175
|
def _marker_matches(path: Path, expected: Dict[str, str]) -> Optional[bool]:
|
|
@@ -152,7 +194,7 @@ def hold_context_instance(service_id: str, context: Any, *, record: Optional[Dic
|
|
|
152
194
|
env: Optional[Dict[str, str]] = None, argv: Optional[Sequence[str]] = None,
|
|
153
195
|
services_directory: Optional[Path] = None) -> Tuple[Dict[str, Any], InstanceLock]:
|
|
154
196
|
"""Run as ``context``'s one instance, before any side effect (brief D17): ``context_instance`` (a refusal prints one
|
|
155
|
-
sentence naming the code and exits 78, nothing created); then ``hold_single_instance`` on the instance's data
|
|
197
|
+
sentence naming the code and exits 78, nothing created; a supervised copy backs off first, FAA-279); then ``hold_single_instance`` on the instance's data
|
|
156
198
|
folder (its markers, its supervised wait, its exit 75); then the marker ``<data>/fabric-context.json``: absent, it
|
|
157
199
|
is written (0600, atomic); equal, it is kept; another context or instance, or unreadable, releases the lock and
|
|
158
200
|
exits 78 (``context-mismatch``). Returns the instance and the held lock."""
|
|
@@ -160,7 +202,7 @@ def hold_context_instance(service_id: str, context: Any, *, record: Optional[Dic
|
|
|
160
202
|
info = context_instance(service_id, context, record=record, machine_id=machine_id, agent_ref=agent_ref, at=at,
|
|
161
203
|
personal_instance=personal_instance, services_directory=services_directory)
|
|
162
204
|
except ContextRefused as refused:
|
|
163
|
-
_refuse(service_id, context, refused.code)
|
|
205
|
+
_refuse(service_id, context, refused.code, wait_seconds, sleep=sleep, clock=clock, env=env, argv=argv)
|
|
164
206
|
lock = hold_single_instance(info["dirs"]["data"], wait_seconds, sleep=sleep, clock=clock, env=env, argv=argv)
|
|
165
207
|
expected = {"service": service_id, "instance": info["instance"], "context": context}
|
|
166
208
|
marker = Path(info["dirs"]["data"]) / CONTEXT_MARKER
|
|
@@ -173,5 +215,5 @@ def hold_context_instance(service_id: str, context: Any, *, record: Optional[Dic
|
|
|
173
215
|
raise
|
|
174
216
|
if matches is False:
|
|
175
217
|
lock.release()
|
|
176
|
-
_refuse(service_id, context, "context-mismatch")
|
|
218
|
+
_refuse(service_id, context, "context-mismatch", wait_seconds, sleep=sleep, clock=clock, env=env, argv=argv)
|
|
177
219
|
return info, lock
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py
CHANGED
|
@@ -45,6 +45,7 @@ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
|
|
|
45
45
|
jcs,
|
|
46
46
|
)
|
|
47
47
|
from fabric_paths import atomic_write as _atomic_write # noqa: E402 - the kit's private writer (FAA-85)
|
|
48
|
+
import fabric_peer_owner as _po # noqa: E402 - a loopback key set is read from this account's listener only (FAA-255)
|
|
48
49
|
from fabric_mandate import effect_closure # noqa: E402,F401 - defined once beside the mandate that reads it too (DEC-0125)
|
|
49
50
|
|
|
50
51
|
REFUSAL_CODES = (
|
|
@@ -672,9 +673,21 @@ class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
672
673
|
return None
|
|
673
674
|
|
|
674
675
|
|
|
675
|
-
|
|
676
|
+
class _CheckedHTTPConnection(_po.CheckedHTTPConnection):
|
|
677
|
+
"""Plain http, which a key set source allows on loopback only (DEC-0154 HA-2, FAA-255): every connect, a reconnect
|
|
678
|
+
included, tells the server end's owner and refuses (closed, nothing sent) unless it is this OS account's process,
|
|
679
|
+
so keys another account's listener serves are never read; built the way urllib builds a connection."""
|
|
680
|
+
|
|
681
|
+
def __init__(self, host: str, port: Optional[int] = None, timeout: Any = 10.0, **kwargs: Any) -> None:
|
|
682
|
+
http.client.HTTPConnection.__init__(self, host, port, timeout=timeout, **kwargs)
|
|
683
|
+
self.expect_pid = None
|
|
684
|
+
self.vouch_pid = None # an issuer's URL is never a process this caller started (FAA-267)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _opener(deadline: Dict[str, Any], direct: bool = False) -> urllib.request.OpenerDirector:
|
|
676
688
|
"""No redirect, and a timer that shuts the socket down at the deadline: a socket timeout bounds one read, so an
|
|
677
|
-
issuer trickling a byte a second would otherwise hold the caller for as long as it likes (finding 13).
|
|
689
|
+
issuer trickling a byte a second would otherwise hold the caller for as long as it likes (finding 13). `direct`:
|
|
690
|
+
a loopback source never goes through a proxy from the environment, so the owner check reads the URL's own port."""
|
|
678
691
|
def watched(base: type) -> type:
|
|
679
692
|
class Watched(base): # type: ignore[misc, valid-type]
|
|
680
693
|
def connect(self) -> None:
|
|
@@ -695,11 +708,13 @@ def _opener(deadline: Dict[str, Any]) -> urllib.request.OpenerDirector:
|
|
|
695
708
|
|
|
696
709
|
class Http(urllib.request.HTTPHandler):
|
|
697
710
|
def http_open(self, req: Any) -> Any:
|
|
698
|
-
return self.do_open(watched(
|
|
711
|
+
return self.do_open(watched(_CheckedHTTPConnection), req) # http is loopback only (the source check)
|
|
699
712
|
|
|
700
713
|
class Https(urllib.request.HTTPSHandler):
|
|
701
714
|
def https_open(self, req: Any) -> Any:
|
|
702
715
|
return self.do_open(watched(http.client.HTTPSConnection), req, context=self._context)
|
|
716
|
+
if direct:
|
|
717
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect, Http, Https)
|
|
703
718
|
return urllib.request.build_opener(_NoRedirect, Http, Https)
|
|
704
719
|
|
|
705
720
|
|
|
@@ -709,7 +724,8 @@ def _fetch(source: str) -> Any:
|
|
|
709
724
|
request = urllib.request.Request(source.partition("#")[0], headers={"Accept": "application/json"})
|
|
710
725
|
deadline: Dict[str, Any] = {"end": time.monotonic() + JWKS_FETCH_SECONDS, "fired": False, "timer": None}
|
|
711
726
|
try:
|
|
712
|
-
|
|
727
|
+
direct = source.lower().startswith("http://")
|
|
728
|
+
with _opener(deadline, direct).open(request, timeout=JWKS_FETCH_SECONDS) as response: # https or loopback http only
|
|
713
729
|
body = b""
|
|
714
730
|
while len(body) <= 65536 and not deadline["fired"]:
|
|
715
731
|
chunk = response.read1(65537 - len(body))
|
|
@@ -724,6 +740,8 @@ def _fetch(source: str) -> Any:
|
|
|
724
740
|
raise
|
|
725
741
|
except http.client.HTTPException as exc: # a garbage status line or headers: the issuer cannot be read
|
|
726
742
|
raise ConnectionError(type(exc).__name__) from None
|
|
743
|
+
except _po.ListenerRefused as exc: # FAA-255: another account's listener, or one that cannot be told
|
|
744
|
+
raise ConnectionError("foreign-user: %s" % exc) from None
|
|
727
745
|
finally:
|
|
728
746
|
if deadline["timer"] is not None:
|
|
729
747
|
deadline["timer"].cancel()
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""How a service is found (fabric-service/0.1): its descriptor, peers and app descriptors (DEC-0061, DEC-0069),
|
|
2
2
|
build identity and the well-known document, the loopback HTTP server. Part of the service kit (FAA-65, layer L3)."""
|
|
3
|
+
# Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
3
4
|
from __future__ import annotations
|
|
4
5
|
|
|
5
6
|
import hashlib
|
|
@@ -7,6 +8,7 @@ import http.server
|
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
11
|
+
import socket
|
|
10
12
|
import socketserver
|
|
11
13
|
import subprocess
|
|
12
14
|
import warnings
|
|
@@ -18,6 +20,7 @@ from pathlib import Path as _Path # noqa: E402
|
|
|
18
20
|
if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from any working directory (FAA-65)
|
|
19
21
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
20
22
|
from fabric_base import (
|
|
23
|
+
PRIMARY_CAPABILITY,
|
|
21
24
|
PROTOCOL,
|
|
22
25
|
ServiceError,
|
|
23
26
|
_ID,
|
|
@@ -28,6 +31,7 @@ from fabric_base import (
|
|
|
28
31
|
expand,
|
|
29
32
|
now_iso,
|
|
30
33
|
)
|
|
34
|
+
import fabric_peer_owner # noqa: E402 - L0: who listens on a port (DEC-0154 HA-2, FAA-225)
|
|
31
35
|
from fabric_paths import (
|
|
32
36
|
atomic_write,
|
|
33
37
|
ensure_private_dir,
|
|
@@ -49,7 +53,7 @@ from fabric_usage import (
|
|
|
49
53
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
50
54
|
|
|
51
55
|
|
|
52
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.
|
|
56
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.6"
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
|
@@ -476,8 +480,35 @@ def source_refusal(descriptor: Dict[str, Any], directory: Optional[Path] = None)
|
|
|
476
480
|
return "Descriptor %s %s. %s" % (me, NO_SOURCE_REPOSITORY, PASS_SOURCE)
|
|
477
481
|
|
|
478
482
|
|
|
479
|
-
def
|
|
480
|
-
"""
|
|
483
|
+
def port_owner(port: int) -> Optional[Tuple[str, Optional[int]]]:
|
|
484
|
+
"""Who listens on 127.0.0.1:<port> now, as (kind, uid) from the kit's fabric_peer_owner, or None when a plain bind
|
|
485
|
+
of that address succeeds (nothing holds it, and the socket table is not read). FAA-225, DEC-0154 HA-2."""
|
|
486
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
487
|
+
try:
|
|
488
|
+
probe.bind(("127.0.0.1", port))
|
|
489
|
+
return None
|
|
490
|
+
except OSError:
|
|
491
|
+
pass
|
|
492
|
+
owner = fabric_peer_owner.listener(port)
|
|
493
|
+
return owner.kind, owner.uid
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def port_listener_refusal(port: int, reader: Optional[Any] = None) -> Optional[str]:
|
|
497
|
+
"""Why an install may not take ``port``: another OS account listens on it (FAA-225). This account, the system
|
|
498
|
+
(launchd holds a socket-activated job's socket), nobody and an unreadable table pass: the refusal is for a port
|
|
499
|
+
that is surely another account's. The recorded port per account and its claim file wait on contract DEC-0160."""
|
|
500
|
+
found = (reader or port_owner)(port)
|
|
501
|
+
if not found or found[0] != fabric_peer_owner.ANOTHER_ACCOUNT:
|
|
502
|
+
return None
|
|
503
|
+
uid = " (uid %d)" % found[1] if isinstance(found[1], int) else ""
|
|
504
|
+
return ("Port %d is listened on by another OS account%s: choose a port no other account holds (DEC-0154, "
|
|
505
|
+
"FAA-225)." % (port, uid))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = None, *, _stacklevel: int = 2,
|
|
509
|
+
port_owner: Optional[Any] = None) -> Path:
|
|
510
|
+
"""Installer-only. Refuses a port or id.instance another descriptor claims (a port is a claim), and a port another
|
|
511
|
+
OS account listens on (FAA-225; ``port_owner(port) -> (kind, uid) | None`` reads it, the kit's own by default).
|
|
481
512
|
|
|
482
513
|
FAA-198: a new descriptor names its ``source.repository``; rewriting one already installed without it only
|
|
483
514
|
warns (``NoSourceRepositoryWarning``), so an installed agent's update keeps working."""
|
|
@@ -504,6 +535,9 @@ def write_descriptor(descriptor: Dict[str, Any], directory: Optional[Path] = Non
|
|
|
504
535
|
continue
|
|
505
536
|
if other_port == port:
|
|
506
537
|
raise ServiceError("Port %d is already claimed by %s (%s)." % (port, key, path))
|
|
538
|
+
held = port_listener_refusal(port, port_owner) if port is not None else None
|
|
539
|
+
if held:
|
|
540
|
+
raise ServiceError(held)
|
|
507
541
|
ensure_private_dir(root)
|
|
508
542
|
target = descriptor_path(descriptor["id"], descriptor["instance"], root)
|
|
509
543
|
atomic_write(target, (json.dumps(descriptor, indent=2, ensure_ascii=False) + "\n").encode(), 0o600)
|
|
@@ -628,6 +662,11 @@ def configuration_problems(doc: Any) -> List[str]:
|
|
|
628
662
|
return problems
|
|
629
663
|
|
|
630
664
|
|
|
665
|
+
#: FAA-244: DEC-0140 PR-1 reserves the primary responder's capability; a service never publishes it.
|
|
666
|
+
_RESERVED = ("%%s: %s is reserved for a Project's primary responder, a session (DEC-0140 PR-1); a service never "
|
|
667
|
+
"serves it as a tool or declares it." % PRIMARY_CAPABILITY)
|
|
668
|
+
|
|
669
|
+
|
|
631
670
|
def build_well_known(*, service_id: str, instance: str, name: str, version: str, build: Dict[str, Any],
|
|
632
671
|
started_at: str, status: str, degraded: Sequence[Dict[str, str]],
|
|
633
672
|
surfaces: Dict[str, Any], summary: Optional[Sequence[Dict[str, Any]]] = None,
|
|
@@ -664,6 +703,10 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
664
703
|
problems = a2a_surface_problems(surfaces["a2a"])
|
|
665
704
|
if problems:
|
|
666
705
|
raise ServiceError("surfaces.a2a: %s." % "; ".join(problems))
|
|
706
|
+
mcp_surface = surfaces.get("mcp")
|
|
707
|
+
listed = mcp_surface.get("capabilities") if isinstance(mcp_surface, dict) else None
|
|
708
|
+
if isinstance(listed, list) and PRIMARY_CAPABILITY in listed:
|
|
709
|
+
raise ServiceError(_RESERVED % "surfaces.mcp.capabilities")
|
|
667
710
|
doc: Dict[str, Any] = {
|
|
668
711
|
"protocol": PROTOCOL,
|
|
669
712
|
"service": {"id": service_id, "instance": instance, "name": name, "version": version, "build": build},
|
|
@@ -698,6 +741,9 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
698
741
|
"surfaces": surfaces})
|
|
699
742
|
if findings:
|
|
700
743
|
raise ServiceError("Actions declaration: %s." % "; ".join(f["message"] for f in findings))
|
|
744
|
+
if any(isinstance(a, dict) and a.get("tool") == PRIMARY_CAPABILITY
|
|
745
|
+
for a in extensions[ACTIONS_EXTENSION_KEY].get("actions") or []):
|
|
746
|
+
raise ServiceError(_RESERVED % "an action's tool")
|
|
701
747
|
doc["extensions"] = dict(extensions)
|
|
702
748
|
if configuration is not None:
|
|
703
749
|
problems = configuration_problems(configuration)
|
|
@@ -28,6 +28,7 @@ The transport is injected in tests: `transport(method, url, headers, body, timeo
|
|
|
28
28
|
`(status, headers, body_bytes)` or raises `OSError` (a connection error, `TimeoutError` included) or `HttpError`.
|
|
29
29
|
Standard library only. Part of the service kit (FAA-65, layer L4).
|
|
30
30
|
"""
|
|
31
|
+
# Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
31
32
|
from __future__ import annotations
|
|
32
33
|
|
|
33
34
|
import datetime as _dt
|
|
@@ -64,7 +65,7 @@ class HttpError(Exception):
|
|
|
64
65
|
refused URL or a body over the cap; `url` is redacted; `body` holds at most 2048 bytes of an answer's body."""
|
|
65
66
|
|
|
66
67
|
def __init__(self, message: str, *, status: Optional[int] = None, url: str = "", attempts: int = 0,
|
|
67
|
-
retry_after_s: Optional[float] = None, body: bytes = b"", kind: str = "http"):
|
|
68
|
+
retry_after_s: Optional[float] = None, body: bytes = b"", kind: str = "http", reason: Optional[str] = None):
|
|
68
69
|
super().__init__(message)
|
|
69
70
|
self.status = status
|
|
70
71
|
self.url = url
|
|
@@ -72,6 +73,9 @@ class HttpError(Exception):
|
|
|
72
73
|
self.retry_after_s = retry_after_s
|
|
73
74
|
self.body = bytes(body[:2048])
|
|
74
75
|
self.kind = kind # http (an answer), connection, timeout, too-large, refused, foreign-user
|
|
76
|
+
#: For foreign-user, why (FAA-272): `table-restricted` (this process sees no other process's sockets, which
|
|
77
|
+
#: never clears), the owner's kind (`another-account`, `system`) or the check's reason (`no-row`).
|
|
78
|
+
self.reason = reason
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
class Response:
|
|
@@ -385,7 +389,10 @@ def urllib_transport(method: str, url: str, headers: Dict[str, str], body: Optio
|
|
|
385
389
|
return _send(req, timeout_s, max_bytes, end, watchdog)
|
|
386
390
|
except _po.ListenerRefused as exc:
|
|
387
391
|
where = "%s %s" % (method, redact_url(url))
|
|
388
|
-
|
|
392
|
+
owner = getattr(exc, "owner", None)
|
|
393
|
+
why = str(getattr(owner, "reason", None) or getattr(owner, "kind", None) or getattr(exc, "code", "") or "unknown")
|
|
394
|
+
reason = _po.TABLE_RESTRICTED if why.startswith(_po.TABLE_RESTRICTED) else why.split(":", 1)[0]
|
|
395
|
+
raise HttpError("%s: foreign-user: %s" % (where, exc), url=redact_url(url), kind="foreign-user", reason=reason) from None
|
|
389
396
|
except (OSError, http.client.HTTPException) as exc:
|
|
390
397
|
if watchdog.fired:
|
|
391
398
|
raise TimeoutError("the answer did not arrive within the deadline")
|
|
@@ -67,6 +67,20 @@ class HubError(Exception):
|
|
|
67
67
|
return "HubError(%r, %r)" % (self.code, self.message)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def listener_refusal(exc: Any) -> HubError:
|
|
71
|
+
"""The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned
|
|
72
|
+
contract's state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted`
|
|
73
|
+
when this process's socket table hides other processes' sockets, which never clears, so it is not retryable;
|
|
74
|
+
otherwise the owner's kind or the check's reason (`another-account`, `no-row`), retryable as before."""
|
|
75
|
+
owner = getattr(exc, "owner", None)
|
|
76
|
+
why = (getattr(owner, "reason", None) or getattr(owner, "kind", None) or getattr(exc, "code", None) or "unknown")
|
|
77
|
+
reason = _po.TABLE_RESTRICTED if str(why).startswith(_po.TABLE_RESTRICTED) else str(why).split(":", 1)[0]
|
|
78
|
+
kind = getattr(owner, "kind", None)
|
|
79
|
+
shown = "%s: %s" % (kind, owner.reason) if kind and getattr(owner, "reason", None) else str(kind or why)
|
|
80
|
+
return HubError("foreign-user", "The hub's port is not held by this OS account's process (%s); nothing was sent." % shown,
|
|
81
|
+
reason != _po.TABLE_RESTRICTED, {"reason": reason})
|
|
82
|
+
|
|
83
|
+
|
|
70
84
|
# --- where Fabric is --------------------------------------------------------------------------------------------
|
|
71
85
|
|
|
72
86
|
def fabric_root(platform: str = sys.platform, env: Optional[Dict[str, str]] = None) -> Path:
|
|
@@ -233,8 +247,7 @@ def post_mcp(origin: str, body: Dict[str, Any], bearer: str, session: Optional[s
|
|
|
233
247
|
raw = resp.read(4 * 1024 * 1024)
|
|
234
248
|
got = {k.lower(): v for k, v in resp.getheaders()}
|
|
235
249
|
except _po.ListenerRefused as exc:
|
|
236
|
-
raise
|
|
237
|
-
% (exc.owner.kind if exc.owner else exc.code), True) from None
|
|
250
|
+
raise listener_refusal(exc) from None
|
|
238
251
|
except (OSError, http.client.HTTPException) as exc:
|
|
239
252
|
raise HubError("hub-unavailable", "Fabric's hub did not answer (%s)." % exc.__class__.__name__, True) from None
|
|
240
253
|
finally:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py
CHANGED
|
@@ -825,7 +825,7 @@ class McpToolServer:
|
|
|
825
825
|
gated = self._gate_action(action, "fabric.job.get", {"id": job_id, "inputResponses": {key: value}}, None, None,
|
|
826
826
|
headers, tier, meta, caller, mandate)
|
|
827
827
|
if not gated["ok"]:
|
|
828
|
-
return gated
|
|
828
|
+
return dict(gated, capability=private.get("capability")) # the one read names the fault (FAA-186)
|
|
829
829
|
records[key] = gated["record"]
|
|
830
830
|
return {"ok": True, "records": records}
|
|
831
831
|
|
|
@@ -933,7 +933,7 @@ class McpToolServer:
|
|
|
933
933
|
if not inputs["ok"] and "invalid" in inputs:
|
|
934
934
|
return self._error(rid, -32602, inputs["invalid"])
|
|
935
935
|
if not inputs["ok"]:
|
|
936
|
-
stored =
|
|
936
|
+
stored = inputs.get("capability") # read once, by _gate_inputs (FAA-186)
|
|
937
937
|
self._fault(stored if isinstance(stored, str) else name, inputs) # as the start heard it (FAA-179)
|
|
938
938
|
import fabric_delegation as _fd # noqa: E402 - DL-19
|
|
939
939
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(inputs["refusal"], traceparent=span)}
|