@passioncode-ai/fabric-agent-adapter 0.79.1 → 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 +134 -0
- 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/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- 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 +5 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- 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 +9 -1
- 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/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 +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -1
- 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 +8 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +3 -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-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- 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-descriptor.mjs +41 -3
- 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-peer-owner.mjs +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +38 -9
- 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_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- 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_descriptor.py +35 -2
- 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_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 +20 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +46 -6
- 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 +340 -24
- 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 +3 -1
- 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 +5 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -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 +21 -21
- 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 +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- 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 +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- 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/references/lifecycle.md
CHANGED
|
@@ -45,7 +45,8 @@ Starting, stopping and restarting go through the supervisor and nothing else:
|
|
|
45
45
|
atomic, mode 0600) and the uninstaller removes it; a running service never writes its own, except an app
|
|
46
46
|
([below](#an-apps-own-descriptor-and-the-node)). It names the supervisor and its file (`label` and `plist`, `unit`,
|
|
47
47
|
or `task`; none for `node` and `person`), the `origin` `http://127.0.0.1:<port>` (a port is a claim: an installer
|
|
48
|
-
refuses a port another instance declares
|
|
48
|
+
refuses a port another instance declares, and one another OS account listens on, read with the kit's
|
|
49
|
+
`fabric_peer_owner`, FAA-225; the recorded port per account and its claim file wait on contract DEC-0160), the service's directories, and commands as argument arrays that never
|
|
49
50
|
carry a credential. Only `doctor` and `update` are commands a host may run.
|
|
50
51
|
|
|
51
52
|
**Started is not ready.** Readiness is the well-known document, `GET /.well-known/fabric-service`: answered without
|
|
@@ -205,6 +206,10 @@ if the holder leaves, and exits 75 only when the wait runs out: a few starts an
|
|
|
205
206
|
360. It touches nothing while it waits. Two installs sharing one data directory are still the
|
|
206
207
|
defect to remove; the back-off only stops the loop from costing the machine.
|
|
207
208
|
|
|
209
|
+
A context refusal (`fabric_context`, exit 78: no record, a revoked grant, a folder of another context) is the same
|
|
210
|
+
loop under `KeepAlive`, so a supervised copy waits the same back-off, holding nothing and with the lock released,
|
|
211
|
+
before it exits 78 (FAA-279); started by hand it exits 78 at once.
|
|
212
|
+
|
|
208
213
|
The marker (contract DEC-0057, CT-01) is the variable `FABRIC_SERVICE_SUPERVISOR=<manager>` or
|
|
209
214
|
the argument `--fabric-supervisor <manager>` (also `--fabric-supervisor=<manager>`), after the
|
|
210
215
|
executable and before any `--`; `<manager>` is `launchd`, `systemd`, `task-scheduler` or `node`
|
|
@@ -323,7 +328,7 @@ descriptor says `"lifecycle": {"manager": "systemd", "unit": "<id>.<instance>.se
|
|
|
323
328
|
|
|
324
329
|
| Verb | Command |
|
|
325
330
|
|---|---|
|
|
326
|
-
| install, upgrade | `systemd_install` / `systemdInstall`: write `~/.config/systemd/user/<unit>`, `daemon-reload`, `enable`, `restart` (so the new unit is what runs). A unit already installed that `is-enabled` reports `disabled` stays off: the file is rewritten and nothing starts (LC-14), unless `force_enable` |
|
|
331
|
+
| install, upgrade | `systemd_install` / `systemdInstall`: write `~/.config/systemd/user/<unit>`, `daemon-reload`, `enable`, `restart` (so the new unit is what runs). A unit already installed that `is-enabled` reports `disabled` stays off: the file is rewritten and nothing starts (LC-14), unless `force_enable`. With `origin` and `service_id` it then polls the well-known answer as `launchd_install` does until this account's listener answers (another account's is refused, DEC-0154 HA-2; FAA-253); Node's `systemdInstall` then returns a promise |
|
|
327
332
|
| stop, keep off | `systemctl --user disable --now <unit>` |
|
|
328
333
|
| uninstall | `systemd_uninstall` / `systemdUninstall`: `disable --now`, remove the unit, `daemon-reload` |
|
|
329
334
|
|
|
@@ -340,7 +345,7 @@ CI job has the real `schtasks` create, read back and delete such a task. The des
|
|
|
340
345
|
|
|
341
346
|
| Verb | Command |
|
|
342
347
|
|---|---|
|
|
343
|
-
| install, upgrade | `task_install` / `taskInstall`: `schtasks /Create /TN <task> /XML <file> /F` from a UTF-16 file it then removes, then `/End` (when it existed) and `/Run`. A task already installed and disabled stays off: it is recreated and disabled again (LC-14), unless `force_enable` |
|
|
348
|
+
| install, upgrade | `task_install` / `taskInstall`: `schtasks /Create /TN <task> /XML <file> /F` from a UTF-16 file it then removes, then `/End` (when it existed) and `/Run`. A task already installed and disabled stays off: it is recreated and disabled again (LC-14), unless `force_enable`. With `origin` and `service_id` it then polls as `launchd_install` does and returns the answer (FAA-253); Node's `taskInstall` then returns a promise |
|
|
344
349
|
| stop, keep off | `schtasks /End /TN <task>`, then `schtasks /Change /TN <task> /DISABLE` |
|
|
345
350
|
| uninstall | `task_uninstall` / `taskUninstall`: `/End`, then `/Delete /F` |
|
|
346
351
|
|
|
@@ -241,8 +241,9 @@ IPv4 wildcard for a LaunchAgent's installer, exactly `127.0.0.1` for a LaunchDae
|
|
|
241
241
|
the contract's ruling on vouching is CT-261.
|
|
242
242
|
|
|
243
243
|
Every kit client that sends a credential to loopback checks first: `check_service.py` (rule `listener.owner`, the first that reads the port for
|
|
244
|
-
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user
|
|
245
|
-
`
|
|
244
|
+
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`, its
|
|
245
|
+
`data.reason` and sentence the owner's reason: `table-restricted`, then not retryable, or the owner's kind or `no-row`, FAA-272),
|
|
246
|
+
`fabric_http` / `fabric-http.mjs` for a plain `http://` loopback URL (`HttpError` kind `foreign-user` with the same `reason`, never retried),
|
|
246
247
|
`check_dashboard_layout.mjs` before a local login code, and the launchd installers' poll (a listener of another account
|
|
247
248
|
is never a successful install, except a LaunchDaemon's job as its root installer proves it; the systemd and Task Scheduler installers do not poll yet, FAA-253). A running board
|
|
248
249
|
session reports a door another account holds as `board.down` with reason `unreachable` until FAA-252. On macOS the Node
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -10,6 +10,7 @@ DEC-0019, at the commit this skill pins. This page is the how-to.
|
|
|
10
10
|
- [On the platform](#on-the-platform)
|
|
11
11
|
- [Checking it](#checking-it)
|
|
12
12
|
- [What a host does and will not do](#what-a-host-does-and-will-not-do)
|
|
13
|
+
- [Registered from several hosts](#registered-from-several-hosts)
|
|
13
14
|
- [Rotation](#rotation)
|
|
14
15
|
|
|
15
16
|
## Is it a remote service?
|
|
@@ -68,8 +69,9 @@ watch the gate fail.
|
|
|
68
69
|
## On the platform
|
|
69
70
|
|
|
70
71
|
- **Token**: a random value of at least 16 characters (32 bytes recommended), set as a secret;
|
|
71
|
-
the same value is written on the
|
|
72
|
-
time (`tokenMatches`).
|
|
72
|
+
the same value is written on the registering host by `registerRemote`. Compare in constant
|
|
73
|
+
time (`tokenMatches`). A service registered from more than one host holds one token per host
|
|
74
|
+
([Registered from several hosts](#registered-from-several-hosts)).
|
|
73
75
|
- **Session key**: 32 random bytes, a separate secret; rotating it signs every operator out.
|
|
74
76
|
- **Codes and sessions**: `MemoryCodeStore`: a restart forgets every outstanding code, so none
|
|
75
77
|
can be replayed, and every session record, so a deploy signs the operator out (DEC-0039: the
|
|
@@ -122,10 +124,49 @@ The login rules keep every `Set-Cookie` of the answer and judge the session cook
|
|
|
122
124
|
| start, stop, restart, update | not offered |
|
|
123
125
|
| a host older than the remote placement | shows the service as invalid and never contacts it |
|
|
124
126
|
|
|
127
|
+
## Registered from several hosts
|
|
128
|
+
|
|
129
|
+
One cloud service is often registered from more than one host: the operator's two computers, or an organization's
|
|
130
|
+
space beside a personal one (FAA-280). Each registering host gets **its own token**; a token is never copied from one
|
|
131
|
+
host to another, so one host can be revoked or rotated without touching the others.
|
|
132
|
+
|
|
133
|
+
- **On the platform**, keep one secret per host (`FABRIC_HOST_TOKEN_<HOST>`, each at least 16 characters) and accept
|
|
134
|
+
a request whose bearer matches any of them. Compare it with every token, each with `tokenMatches` /
|
|
135
|
+
`token_matches`, with no early return, so the answer's timing does not say which host's token matched:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
def host_of(authorization, tokens): # tokens: {host name: token}; the kit's comparison, once per token
|
|
139
|
+
matched = [host for host, token in sorted(tokens.items()) if fs.token_matches(authorization, token)]
|
|
140
|
+
return matched[0] if matched else None # None: a 401 with an empty body, as with one token
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`well_known_allowed` / `wellKnownAllowed` take one token: call it once per token the same way, or gate the route on
|
|
144
|
+
`host_of(...) is not None`.
|
|
145
|
+
- **On each host**, run `registerRemote` / `register_remote` with that host's token only. The descriptor is the same
|
|
146
|
+
on every host but its `tokenFile`, which holds that host's value and nothing else (0600, never printed).
|
|
147
|
+
- **Sign-in**: `LoginCodes` binds a session to one token's revision. Give each host its own `LoginCodes` with
|
|
148
|
+
`token=` that host's token and its own store (`MemoryCodeStore`, or a durable row per host); the session key may be
|
|
149
|
+
shared. Issue the login code from the instance whose token the bearer matched; redeem a code and check a session by
|
|
150
|
+
trying each instance in turn (a code another instance issued is not in its store, and a session cookie carries
|
|
151
|
+
another token's revision, so neither changes anything there). Removing or rotating one host's token then ends that
|
|
152
|
+
host's sessions only. One `LoginCodes` whose `token=` is a callable returning every token joined also works, and
|
|
153
|
+
signs every host out when any one rotates.
|
|
154
|
+
- **The probe**: `check_service.py <id>` run on a host sends that host's token, so `well-known.requires-token` passes
|
|
155
|
+
on each; the request without a token, or with a value no host holds, still gets the `401` with an empty body.
|
|
156
|
+
- **Revoking a host**: delete its secret on the platform and restart; that host then sees `401` and shows the service
|
|
157
|
+
`down`, never `foreign`. Remove its descriptor there with `remove_descriptor` / `removeDescriptor`.
|
|
158
|
+
- **Adding a host**: create its secret, restart, then `registerRemote` on the new host. The other hosts' tokens and
|
|
159
|
+
sessions do not change.
|
|
160
|
+
|
|
161
|
+
A token per host still gives each host the descriptor token's whole authority over the service. A remote surface no
|
|
162
|
+
host token reaches (callers with their own delegation, the page opened only with Fabric Dashboards' one-time code)
|
|
163
|
+
is the "local and cloud" form, FAA-204, which waits on the contract recording that form.
|
|
164
|
+
|
|
125
165
|
## Rotation
|
|
126
166
|
|
|
127
167
|
Rotate the token by setting the new value on the platform and re-running `registerRemote` on the
|
|
128
|
-
|
|
168
|
+
registering host with it (on a service registered from several hosts, only that host's token,
|
|
169
|
+
[above](#registered-from-several-hosts)); the old token stops working the moment the platform restarts with the
|
|
129
170
|
new value, and so does every session begun under it when `LoginCodes` has `token=`. The re-run keeps the
|
|
130
171
|
installed `source` when it passes none (one the kit refuses, edited by hand, is dropped with the warning); pass
|
|
131
172
|
`runner`, `peer`, `summary`, `doctor` and `installedBy` / `installed_by` again, since a re-run writes only what it is
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py
CHANGED
|
@@ -133,12 +133,20 @@ def check(data):
|
|
|
133
133
|
return {'ok': True, 'kind': kind}
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def read_input(limit=65536):
|
|
137
|
+
# The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
|
|
138
|
+
# stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
|
|
139
|
+
stream = getattr(sys.stdin, 'buffer', None)
|
|
140
|
+
raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
|
|
141
|
+
require(len(raw) <= limit, 'input_too_large')
|
|
142
|
+
return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
|
|
143
|
+
|
|
144
|
+
|
|
136
145
|
def main():
|
|
137
146
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
except (ValueError, TypeError, KeyError, AttributeError):
|
|
147
|
+
result = check(json.loads(read_input()))
|
|
148
|
+
# RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
|
|
149
|
+
except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
|
|
142
150
|
# Fixed reasons only: never echo a submitted URL, token or JSON blob.
|
|
143
151
|
result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
|
|
144
152
|
print(json.dumps(result))
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -673,12 +673,14 @@ class Probe:
|
|
|
673
673
|
self.events_trace_rule(self.events)
|
|
674
674
|
|
|
675
675
|
def channel_agent_rule(self) -> None:
|
|
676
|
-
"""DEC-0150 (FAA-197): the
|
|
677
|
-
|
|
678
|
-
shaped like a credential in it or in a copy the probe reads beside it (CA-5)."""
|
|
676
|
+
"""DEC-0150 (FAA-197): the declaration, from the manifest the descriptor names, else the well-known answer (CA-1); one
|
|
677
|
+
read as not declared is NOT_RUN, never a FAIL (D3), except a credential's shape in it or a copy beside it (CA-5)."""
|
|
679
678
|
manifest, _, where = self.load_manifest()
|
|
680
|
-
|
|
681
|
-
manifest, bool(self.d.get("fabricManifest")), where, self.wk, getattr(self, "wk_bytes", None))
|
|
679
|
+
try: # a copy of the probe without its schema file is NOT_RUN, and the later rules run (FAA-250)
|
|
680
|
+
verdict = cac.declaration_verdict(manifest, bool(self.d.get("fabricManifest")), where, self.wk, getattr(self, "wk_bytes", None))
|
|
681
|
+
except (OSError, ValueError) as exc:
|
|
682
|
+
verdict = ("NOT_RUN", "the probe cannot read %s beside it (%s)" % (cac.cas.SCHEMA_FILE.name, type(exc).__name__))
|
|
683
|
+
self.add("channel-agent.declaration", *verdict)
|
|
682
684
|
|
|
683
685
|
def configuration_rule(self) -> None:
|
|
684
686
|
"""DEC-0046: a well-known `configuration` is {slots, resolved} as the schema types it; absent is NOT_RUN."""
|
|
@@ -18,7 +18,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
18
18
|
|
|
19
19
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
20
20
|
from fabric_jsvalue import indexed_objects, js_keys # noqa: E402
|
|
21
|
-
from fabric_secrets import JS_SPACE, JWT_SHAPE, SECRET_VALUE,
|
|
21
|
+
from fabric_secrets import JS_SPACE, JWT_SHAPE, SECRET_VALUE, raw_values, secret_argument, secret_url, secret_value # noqa: E402
|
|
22
|
+
from fabric_secrets import url_parts as _whatwg_url_parts # noqa: E402 - public since FAA-250
|
|
22
23
|
import check_service_channel_agent_schema as cas # noqa: E402
|
|
23
24
|
|
|
24
25
|
KEY = "https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1"
|
|
@@ -62,6 +63,9 @@ _IGNORABLE = ((0x00AD, 0x00AD), (0x034F, 0x034F), (0x061C, 0x061C), (0x115F, 0x1
|
|
|
62
63
|
(0x200B, 0x200F), (0x202A, 0x202E), (0x2060, 0x206F), (0x3164, 0x3164), (0xFE00, 0xFE0F), (0xFEFF, 0xFEFF),
|
|
63
64
|
(0xFFA0, 0xFFA0), (0xFFF0, 0xFFF8), (0x1BCA0, 0x1BCA3), (0x1D173, 0x1D17A), (0xE0000, 0xE0FFF))
|
|
64
65
|
_FILLERS = frozenset("\u115f\u1160\u3164\uffa0\u2800\ufffc")
|
|
66
|
+
#: Format characters (\p{Cf}) assigned after Unicode 13, which Python 3.9's unicodedata reads as unassigned (FAA-250):
|
|
67
|
+
#: U+0890..0891 (Unicode 14) and U+13439..1343F (Unicode 15); the white-space and control categories gained none since.
|
|
68
|
+
_LATER_FORMATS = ((0x0890, 0x0891), (0x13439, 0x1343F))
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
def utf16(text: str) -> str:
|
|
@@ -75,7 +79,7 @@ def utf16(text: str) -> str:
|
|
|
75
79
|
def _blank_char(char: str) -> bool:
|
|
76
80
|
code = ord(char)
|
|
77
81
|
return (char in JS_SPACE or char in _FILLERS or unicodedata.category(char) in ("Cf", "Cc", "Zs", "Zl", "Zp")
|
|
78
|
-
or any(low <= code <= high for low, high in _IGNORABLE))
|
|
82
|
+
or any(low <= code <= high for low, high in _IGNORABLE + _LATER_FORMATS))
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
def blank(text: str) -> bool:
|
|
@@ -397,7 +401,7 @@ def url_parts(text: str) -> Optional[Tuple[str, str]]:
|
|
|
397
401
|
return None
|
|
398
402
|
if host and not _host_ok(host, special):
|
|
399
403
|
return None
|
|
400
|
-
_, query, fragment =
|
|
404
|
+
_, query, fragment = _whatwg_url_parts(text)
|
|
401
405
|
has_query = "?" in rest.partition("#")[0]
|
|
402
406
|
has_fragment = "#" in rest
|
|
403
407
|
return ("?" + query if has_query and query else "", "#" + fragment if has_fragment and fragment else "")
|
|
@@ -823,8 +827,9 @@ def declaration_verdict(manifest: Optional[Dict[str, Any]], manifest_named: bool
|
|
|
823
827
|
where = "the manifest's provider.extensions" if site == "manifest" else "the well-known answer's extensions"
|
|
824
828
|
shapes = _credential_shapes(others + copy)
|
|
825
829
|
if shapes: # a credential's shape in a copy no reader reads still sits in a document the service publishes (CA-5)
|
|
826
|
-
|
|
827
|
-
|
|
830
|
+
also = ("; on the read declaration itself, %d finding(s): %s" % (len(own), _shown(own))) if own else ""
|
|
831
|
+
return "FAIL", "%d string(s) shaped like a credential outside the read declaration: %s (DEC-0150 CA-5)%s%s" % (
|
|
832
|
+
len(shapes), _shown(shapes), also, tail)
|
|
828
833
|
if declared and own:
|
|
829
834
|
if not channel_agent_view(doc, site)["declared"] and not _credential_shapes(own):
|
|
830
835
|
# D3: what the contract's reader reads as not declared (an older form, a repeat, a dangling reference) is
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py
CHANGED
|
@@ -3,6 +3,7 @@ network guard's verdict, the board responder's fixture conversation, a session c
|
|
|
3
3
|
whether two repository URLs name one repository, a service's actions declaration (DEC-0121) and whether its checkout
|
|
4
4
|
opens its pages in a browser (FAA-118), and its A2A member and public Agent Card (DEC-0122). ``check_service.py`` runs them against a live service.
|
|
5
5
|
Standard library only, Python 3.9+."""
|
|
6
|
+
# Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import email.utils
|
|
@@ -104,6 +104,8 @@ export class Responder extends SlotKeeper {
|
|
|
104
104
|
this.state = 'idle';
|
|
105
105
|
this.waitUntil = 0;
|
|
106
106
|
this.stopping = false;
|
|
107
|
+
/** Whether shutdown found work to drain, published before it waits; null before shutdown starts (FAA-186). */
|
|
108
|
+
this.draining = null;
|
|
107
109
|
this.claimKeys = {};
|
|
108
110
|
this.probing = new Set(); // moved slots whose move-back claim lost its answer: asked again under the same key
|
|
109
111
|
this.said = {};
|
|
@@ -939,6 +941,7 @@ export class Responder extends SlotKeeper {
|
|
|
939
941
|
async shutdown(drain = DRAIN_SECONDS) {
|
|
940
942
|
this.stopping = true;
|
|
941
943
|
const work = this.current;
|
|
944
|
+
this.draining = Boolean(work); // what this shutdown waits for (FAA-186)
|
|
942
945
|
if (work && !work.done) {
|
|
943
946
|
await Promise.race([work.promise, new Promise((resolve) => setTimeout(resolve, drain * 1000).unref?.())]);
|
|
944
947
|
if (!work.done) work.ctx.stopped = true;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -254,6 +254,7 @@ export class SlotKeeper {
|
|
|
254
254
|
if (!(e instanceof BoardError)) throw e;
|
|
255
255
|
this.retryLater(capability, entries, e);
|
|
256
256
|
if (SESSION_CODES.has(e.code)) throw e;
|
|
257
|
+
if (e.code === 'idempotency_window_expired') await this.status(); // the epoch moved: read it before the retry (FAA-186)
|
|
257
258
|
}
|
|
258
259
|
}
|
|
259
260
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// An organization's instance whose context the record does not pin (`organizations`, DEC-0127) still starts: the helper
|
|
7
7
|
// checks the placement and its share grant, not the pin (decision D5 of 0.52.0). Its tokens are refused instead:
|
|
8
8
|
// hubKeyCertified reads a context's hub keys only while the record pins that organization (DL-14a, DEC-0128), so the
|
|
9
|
-
// service passes
|
|
9
|
+
// service passes contextGateOptions(info, record) (its context and its pin's issuer, FAA-150) to its delegation gate. A refusal at start would stop every organization
|
|
10
10
|
// instance on today's records, which pin none until every reader is pinned at or after DEC-0127.
|
|
11
11
|
import crypto from 'node:crypto';
|
|
12
12
|
import fs from 'node:fs';
|
|
@@ -14,7 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import { INSTANCE, ServiceError } from './fabric-base.mjs';
|
|
15
15
|
import { descriptorPath } from './fabric-descriptor.mjs';
|
|
16
16
|
import { parseContext } from './fabric-estate.mjs';
|
|
17
|
-
import { InstanceLock, holdSingleInstance } from './fabric-lock.mjs';
|
|
17
|
+
import { InstanceLock, LOCK_BACKOFF_FIRST_SECONDS, LOCK_BACKOFF_MAX_SECONDS, holdSingleInstance, lockWaitBudgetSeconds, supervisorMarker } from './fabric-lock.mjs';
|
|
18
18
|
import { atomicWrite, instanceDirs, servicesDir } from './fabric-paths.mjs';
|
|
19
19
|
import { instanceShareGrant } from './fabric-share.mjs';
|
|
20
20
|
import { clockMs } from './fabric-time.mjs';
|
|
@@ -112,9 +112,29 @@ function markerMatches(file, expected) {
|
|
|
112
112
|
return found !== null && typeof found === 'object' && Object.entries(expected).every(([key, value]) => found[key] === value);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* What delegationGate takes from this instance's context (FAA-150): `estateContext`, and for an organization whose
|
|
117
|
+
* context `record` pins, `issuer`: the pin's context issuer (DL-14a, DEC-0128), so an organization's instance refuses a
|
|
118
|
+
* token the personal hub key signed. `info` is contextInstance's (or holdContextInstance's) answer; spread it after the
|
|
119
|
+
* service's own estate issuer: `delegationGate({ ..., issuer: estateIssuer, ...contextGateOptions(info, record) })`.
|
|
120
|
+
* Throws ContextRefused (`context-invalid`) for an answer with no context.
|
|
121
|
+
*/
|
|
122
|
+
export function contextGateOptions(info, record) {
|
|
123
|
+
const context = info !== null && typeof info === 'object' ? info.context : undefined;
|
|
124
|
+
const parsed = parseContext(context);
|
|
125
|
+
if (!parsed) throw new ContextRefused('context-invalid');
|
|
126
|
+
const options = { estateContext: context };
|
|
127
|
+
const pins = record !== null && typeof record === 'object' && !Array.isArray(record) ? record.organizations : undefined;
|
|
128
|
+
if (parsed.kind === 'org' && Array.isArray(pins)) {
|
|
129
|
+
const pin = pins.find((p) => p !== null && typeof p === 'object' && p.org === context && typeof p.iss === 'string');
|
|
130
|
+
if (pin) options.issuer = pin.iss;
|
|
131
|
+
}
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
|
|
115
135
|
/**
|
|
116
136
|
* Run as `context`'s one instance, before any side effect (brief D17): contextInstance (a refusal writes one sentence
|
|
117
|
-
* naming the code and exits 78, nothing created); then holdSingleInstance on the instance's data folder (its markers,
|
|
137
|
+
* naming the code and exits 78, nothing created; a supervised copy backs off first, FAA-279); then holdSingleInstance on the instance's data folder (its markers,
|
|
118
138
|
* its supervised wait, its exit 75); then `<data>/fabric-context.json`: absent, written (0600, atomic); equal, kept;
|
|
119
139
|
* another context or instance, or unreadable, releases the lock and exits 78 (`context-mismatch`). Resolves to the
|
|
120
140
|
* instance with its held `lock`; `exit` is injectable as holdSingleInstance has it, and its answer is returned.
|
|
@@ -123,8 +143,20 @@ export async function holdContextInstance(id, context, {
|
|
|
123
143
|
record, machineId, agentRef, at, personalInstance = 'default', servicesDirectory, platform = process.platform,
|
|
124
144
|
waitSeconds, sleep, clock, exit = (code) => process.exit(code), env, argv,
|
|
125
145
|
} = {}) {
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
// FAA-279: under a supervisor that restarts on exit an exit 78 at once is a respawn every few seconds, forever; a
|
|
147
|
+
// supervised copy first waits the lock's back-off (0.5 s doubling to 30 s, 300 s in all), holding nothing.
|
|
148
|
+
const refuse = async (code) => {
|
|
149
|
+
const marker = supervisorMarker({ ...(env === undefined ? {} : { env }), ...(argv === undefined ? {} : { argv }) });
|
|
150
|
+
const wait = Math.max(0, waitSeconds ?? lockWaitBudgetSeconds(marker));
|
|
151
|
+
const tail = wait > 0 ? ` Waiting ${Math.round(wait)} s before exiting, so its supervisor does not restart it every few seconds.` : '';
|
|
152
|
+
process.stderr.write(`${id} will not run for ${shown(context)} (${code}): ${REASONS[code]}.${tail}\n`);
|
|
153
|
+
const now = clock ?? (() => performance.now() / 1000);
|
|
154
|
+
const pause = sleep ?? ((seconds) => new Promise((resolve) => { setTimeout(resolve, seconds * 1000); }));
|
|
155
|
+
const deadline = now() + wait;
|
|
156
|
+
for (let delay = LOCK_BACKOFF_FIRST_SECONDS, remaining = deadline - now(); remaining > 0; remaining = deadline - now()) {
|
|
157
|
+
await pause(Math.min(delay, remaining));
|
|
158
|
+
delay = Math.min(delay * 2, LOCK_BACKOFF_MAX_SECONDS);
|
|
159
|
+
}
|
|
128
160
|
return exit(EXIT_CONTEXT_REFUSED);
|
|
129
161
|
};
|
|
130
162
|
let info;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// How a service is found (fabric-service/0.1): its descriptor, peers and app descriptors (DEC-0061, DEC-0069), build identity and the well-known document.
|
|
2
2
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_descriptor.py. Node 20+, no dependency.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { credentialName, repositoryCredential, secretArgument, secretUrl, secretValue } from './fabric-secrets.mjs';
|
|
6
7
|
import { ID, INSTANCE, LOCAL_PATH, PRIMARY_CAPABILITY, PROTOCOL, ServiceError, TASK, UNIT, expand } from './fabric-base.mjs';
|
|
7
8
|
import { atomicWrite, ensurePrivateDir, servicesDir } from './fabric-paths.mjs';
|
|
9
|
+
import { ANOTHER_ACCOUNT } from './fabric-peer-owner.mjs'; // L0: who listens on a port (DEC-0154 HA-2, FAA-225)
|
|
8
10
|
import { protocolsFindings } from './fabric-protocols.mjs';
|
|
9
11
|
import { CONNECTIONS_EXTENSION_KEY, connectionsView } from './fabric-connections.mjs';
|
|
10
12
|
import { ACTIONS_EXTENSION_KEY, actionsExtension, actionsExtensionRules } from './fabric-actions.mjs';
|
|
@@ -313,9 +315,43 @@ export function sourceRefusal(d, dir = servicesDir()) {
|
|
|
313
315
|
return `Descriptor ${d?.id}.${instance} ${NO_SOURCE_REPOSITORY}. ${PASS_SOURCE}`;
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
318
|
+
// The child that answers portOwner: a plain listen on 127.0.0.1:<port> that succeeds means nothing holds it (the table
|
|
319
|
+
// is not read); one that fails asks fabric-peer-owner's listener. A child, because writeDescriptor is synchronous.
|
|
320
|
+
const PORT_OWNER_CHILD = `
|
|
321
|
+
import net from 'node:net';
|
|
322
|
+
const [port, url] = process.argv.slice(1);
|
|
323
|
+
const say = (value) => process.stdout.write(JSON.stringify(value));
|
|
324
|
+
const server = net.createServer();
|
|
325
|
+
server.once('error', async () => {
|
|
326
|
+
try { const { listener } = await import(url); const who = await listener(Number(port)); say([who.kind, Number.isInteger(who.uid) ? who.uid : null]); }
|
|
327
|
+
catch { say(['unknown', null]); }
|
|
328
|
+
});
|
|
329
|
+
server.listen({ port: Number(port), host: '127.0.0.1', exclusive: true }, () => server.close(() => say(null)));
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
/** Who listens on 127.0.0.1:<port> now, as [kind, uid] from the kit's fabric-peer-owner, or null when nothing holds it
|
|
333
|
+
* (FAA-225, DEC-0154 HA-2). */
|
|
334
|
+
export function portOwner(port) {
|
|
335
|
+
const done = spawnSync(process.execPath, ['--input-type=module', '-e', PORT_OWNER_CHILD, String(port),
|
|
336
|
+
new URL('./fabric-peer-owner.mjs', import.meta.url).href], { encoding: 'utf8', timeout: 10000 });
|
|
337
|
+
try { return JSON.parse(done.stdout); } catch { return ['unknown', null]; }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Why an install may not take `port`: another OS account listens on it (FAA-225). This account, the system (launchd
|
|
341
|
+
* holds a socket-activated job's socket), nobody and an unreadable table pass. The recorded port per account and its
|
|
342
|
+
* claim file wait on contract DEC-0160. */
|
|
343
|
+
export function portListenerRefusal(port, reader = portOwner) {
|
|
344
|
+
const found = reader(port);
|
|
345
|
+
if (!Array.isArray(found) || found[0] !== ANOTHER_ACCOUNT) return null;
|
|
346
|
+
const uid = Number.isInteger(found[1]) ? ` (uid ${found[1]})` : '';
|
|
347
|
+
return `Port ${port} is listened on by another OS account${uid}: choose a port no other account holds (DEC-0154, FAA-225).`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Installer-only. Refuses a port or id.instance another descriptor claims (a port is a claim), and a port another OS
|
|
351
|
+
* account listens on (FAA-225; `portOwner(port) -> [kind, uid] | null` reads it, the kit's own by default).
|
|
352
|
+
* FAA-198: a new descriptor names its source.repository; rewriting one already installed without it only warns
|
|
353
|
+
* (process.emitWarning, code FABRIC_NO_SOURCE_REPOSITORY), so an installed agent's update keeps working. */
|
|
354
|
+
export function writeDescriptor(d, dir = servicesDir(), { portOwner: reader = portOwner } = {}) {
|
|
319
355
|
const problems = validateDescriptor(d);
|
|
320
356
|
if (problems.length) throw new ServiceError(`Descriptor is invalid: ${problems.join('; ')}.`);
|
|
321
357
|
const me = `${d.id}.${d.instance}`;
|
|
@@ -334,6 +370,8 @@ export function writeDescriptor(d, dir = servicesDir()) {
|
|
|
334
370
|
try { otherPort = portOf(String(other.origin ?? '')); } catch { continue; }
|
|
335
371
|
if (otherPort === port) throw new ServiceError(`Port ${port} is already claimed by ${key} (${file}).`);
|
|
336
372
|
}
|
|
373
|
+
const held = port === null ? null : portListenerRefusal(port, reader);
|
|
374
|
+
if (held) throw new ServiceError(held);
|
|
337
375
|
ensurePrivateDir(dir);
|
|
338
376
|
const target = descriptorPath(d.id, d.instance, dir);
|
|
339
377
|
atomicWrite(target, `${JSON.stringify(d, null, 2)}\n`, 0o600);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs
CHANGED
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
// The transport is injected in tests: async ({ method, url, headers, body, timeoutS, maxBytes }) =>
|
|
17
17
|
// { status, headers, body: Uint8Array }, throwing on a connection error (a `TimeoutError` name is a timeout) or an
|
|
18
18
|
// HttpError. Node 20 or newer, no dependencies. Part of the service kit (FAA-65, layer L4).
|
|
19
|
+
//
|
|
20
|
+
// Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
19
21
|
|
|
20
22
|
import http from 'node:http';
|
|
21
|
-
import { CheckedAgent, ListenerRefused } from './fabric-peer-owner.mjs';
|
|
23
|
+
import { CheckedAgent, ListenerRefused, TABLE_RESTRICTED } from './fabric-peer-owner.mjs';
|
|
22
24
|
|
|
23
25
|
export const RETRY_STATUSES = Object.freeze([408, 425, 429, 500, 502, 503, 504]);
|
|
24
26
|
export const IDEMPOTENT_METHODS = Object.freeze(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
@@ -27,7 +29,7 @@ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
|
27
29
|
|
|
28
30
|
export class HttpError extends Error {
|
|
29
31
|
constructor(message, { status = null, url = '', attempts = 0, retryAfterS = null, body = new Uint8Array(0),
|
|
30
|
-
kind = 'http' } = {}) {
|
|
32
|
+
kind = 'http', reason = null } = {}) {
|
|
31
33
|
super(message);
|
|
32
34
|
this.name = 'HttpError';
|
|
33
35
|
this.status = status;
|
|
@@ -36,6 +38,8 @@ export class HttpError extends Error {
|
|
|
36
38
|
this.retryAfterS = retryAfterS;
|
|
37
39
|
this.body = body.slice(0, 2048);
|
|
38
40
|
this.kind = kind; // http, connection, timeout, too-large, refused, foreign-user
|
|
41
|
+
// For foreign-user, why (FAA-272): table-restricted (never clears), the owner's kind, or the check's reason.
|
|
42
|
+
this.reason = reason;
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -184,7 +188,9 @@ export function checkedLoopbackTransport({ method, url, headers, body, timeoutS,
|
|
|
184
188
|
req.on('error', (err) => {
|
|
185
189
|
clearTimeout(timer);
|
|
186
190
|
if (err instanceof ListenerRefused) {
|
|
187
|
-
|
|
191
|
+
const why = String(err.owner?.reason ?? err.owner?.kind ?? err.code ?? 'unknown');
|
|
192
|
+
const reason = why.startsWith(TABLE_RESTRICTED) ? TABLE_RESTRICTED : why.split(':', 1)[0];
|
|
193
|
+
reject(new HttpError(`${method} ${redactUrl(url)}: foreign-user: ${err.message}`, { url: redactUrl(url), kind: 'foreign-user', reason }));
|
|
188
194
|
} else reject(err);
|
|
189
195
|
});
|
|
190
196
|
if (body) req.write(body);
|
|
@@ -27,7 +27,7 @@ import os from 'node:os';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { atomicWrite, ensurePrivateDir, readToken } from './fabric-service.mjs';
|
|
29
29
|
import { hubKeyCertified } from './fabric-estate.mjs';
|
|
30
|
-
import { CheckedAgent, ListenerRefused } from './fabric-peer-owner.mjs';
|
|
30
|
+
import { CheckedAgent, ListenerRefused, TABLE_RESTRICTED } from './fabric-peer-owner.mjs';
|
|
31
31
|
|
|
32
32
|
// DEC-0154 HA-2: every connection to the door is checked before a byte goes out; kept alive, so a busy session reads the
|
|
33
33
|
// socket table once per connection, not once per call. A free socket is closed after 2 s idle, before a server's own
|
|
@@ -53,6 +53,20 @@ export class HubError extends Error {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned contract's
|
|
58
|
+
* state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted` when this
|
|
59
|
+
* runtime's socket table hides other processes' sockets, which never clears, so it is not retryable; otherwise the
|
|
60
|
+
* owner's kind or the check's reason (`another-account`, `no-row`), retryable as before.
|
|
61
|
+
*/
|
|
62
|
+
export function listenerRefusal(e) {
|
|
63
|
+
const kind = e?.owner?.kind ?? null;
|
|
64
|
+
const why = String(e?.owner?.reason ?? kind ?? e?.code ?? 'unknown');
|
|
65
|
+
const reason = why.startsWith(TABLE_RESTRICTED) ? TABLE_RESTRICTED : why.split(':', 1)[0];
|
|
66
|
+
const shown = kind && e?.owner?.reason ? `${kind}: ${e.owner.reason}` : String(kind ?? why);
|
|
67
|
+
return new HubError('foreign-user', `The hub's port is not held by this OS account's process (${shown}); nothing was sent.`, reason !== TABLE_RESTRICTED, { reason });
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
/** Fabric's registry root, where hub.json lives (Fabric agentRegistry.ts#registryDirs). */
|
|
57
71
|
export function fabricRoot(platform = process.platform, env = process.env) {
|
|
58
72
|
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
@@ -202,7 +216,7 @@ export function postMcp(origin, body, bearer, { session, timeout = 15000, method
|
|
|
202
216
|
});
|
|
203
217
|
req.on('timeout', () => req.destroy(new Error('timeout')));
|
|
204
218
|
req.on('error', (e) => reject(e instanceof ListenerRefused
|
|
205
|
-
?
|
|
219
|
+
? listenerRefusal(e)
|
|
206
220
|
: new HubError('hub-unavailable', `Fabric's hub did not answer (${e.code ?? e.message}).`, true)));
|
|
207
221
|
if (payload) req.write(payload);
|
|
208
222
|
req.end();
|