@passioncode-ai/fabric-agent-adapter 0.77.0 → 0.79.1
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 +207 -0
- package/README.md +31 -11
- package/SECURITY.md +17 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -2
- package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +37 -5
- 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 +2 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +14 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +27 -39
- 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/references/board.md +8 -5
- 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 +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +74 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +8 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +3 -2
- 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 +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +2 -2
- 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 +7 -2
- 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 +3 -1
- 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 +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +339 -26
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +198 -0
- 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 +13 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -2
- 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 +14 -1
- 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_service.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +367 -27
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +208 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +323 -25
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +19 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +37 -5
- 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 +6 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +179 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/channels.md +129 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/cycle.md +129 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/scripts/observer_inventory.py +259 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +30 -39
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +57 -37
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +50 -25
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +26 -10
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -5
|
@@ -157,6 +157,9 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
157
157
|
`auth.delegation {issuer, jwks}` is then required. With `own` or `delegation` the descriptor's
|
|
158
158
|
token is not an MCP credential: a host and the probe never call `/mcp` with it as a caller.
|
|
159
159
|
The kits verify them with `fabric_delegation` / `fabric-delegation.mjs` ([delegation](delegation.md)).
|
|
160
|
+
- `surfaces.mcp.capabilities` and an action's `tool` never name `project.work` (`PRIMARY_CAPABILITY`): DEC-0140 PR-1
|
|
161
|
+
reserves it for a Project's primary responder, a session, and no service serves or declares it. `build_well_known`
|
|
162
|
+
/ `buildWellKnown` refuse it with `ServiceError` (FAA-244); a longer name such as `project.workspace` is not reserved.
|
|
160
163
|
|
|
161
164
|
## Events page: `GET /fabric/v1/events?after=<cursor>&limit=<n>`
|
|
162
165
|
|
|
@@ -212,7 +215,9 @@ Loopback is one per computer, not one per OS account. Before a client sends any
|
|
|
212
215
|
`http://127.0.0.1:<port>` (a descriptor's token, a delegation token, a login code's request, a session cookie, a door's
|
|
213
216
|
token), and before a host calls a local service healthy, it tells the owner of the server end of its own connection
|
|
214
217
|
from the computer's socket table. A listener of another OS account, of root, SYSTEM or a service account, or one whose
|
|
215
|
-
owner cannot be told, is `foreign-user`: nothing is sent and nothing it answers is read as the service's.
|
|
218
|
+
owner cannot be told, is `foreign-user`: nothing is sent and nothing it answers is read as the service's. One exception:
|
|
219
|
+
a LaunchDaemon's root installer reads its own job's token-free well-known answer, proved by the job's pid, uid and exact
|
|
220
|
+
`127.0.0.1` bind (lifecycle.md, *The system domain*; DEC-0161 accepted in principle, proposed; FAA-288). The order of
|
|
216
221
|
a local service's states is `down`, `foreign-user`, `foreign` (`FAC-SEM-009`), then the well-known `status`; the
|
|
217
222
|
well-known `process.pid` is never proof (`docs/specification/service.md#ha-2` at the pin).
|
|
218
223
|
|
|
@@ -232,14 +237,14 @@ listener of this account's pid on the port counts as this account's (`CheckedHTT
|
|
|
232
237
|
`checkedSocket({vouchPid})`). The launchd installers vouch with their own job's pid; elsewhere a restricted table
|
|
233
238
|
refuses (the probe, the hub client and `fabric_http` included, FAA-272), and Apple's `/usr/bin/python3` sees the
|
|
234
239
|
whole table (FAA-267). A vouch is a weaker proof than the table's: it shows the job's pid listens on the port (`127.0.0.1` or the
|
|
235
|
-
IPv4 wildcard), not that it accepted this connection, so a more specific bind by root over a wildcard job would pass;
|
|
240
|
+
IPv4 wildcard for a LaunchAgent's installer, exactly `127.0.0.1` for a LaunchDaemon's), not that it accepted this connection, so a more specific bind by root over a wildcard job would pass;
|
|
236
241
|
the contract's ruling on vouching is CT-261.
|
|
237
242
|
|
|
238
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
|
|
239
244
|
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`),
|
|
240
245
|
`fabric_http` / `fabric-http.mjs` for a plain `http://` loopback URL (`HttpError` kind `foreign-user`, never retried),
|
|
241
246
|
`check_dashboard_layout.mjs` before a local login code, and the launchd installers' poll (a listener of another account
|
|
242
|
-
is never a successful install; the systemd and Task Scheduler installers do not poll yet, FAA-253). A running board
|
|
247
|
+
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
|
|
243
248
|
session reports a door another account holds as `board.down` with reason `unreachable` until FAA-252. On macOS the Node
|
|
244
249
|
twin reads the pid from `netstat` and the uid from `ps` a few milliseconds apart, so a pid reused in between would be
|
|
245
250
|
misread; the Python twin's `sysctl` reader takes both from one record. A service's own loopback server refusing another account's peer (HA-3) is FAA-227, and the port
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -79,6 +79,11 @@ watch the gate fail.
|
|
|
79
79
|
"used before honoured" write); memory works for one process.
|
|
80
80
|
- **Behind a router**: check the platform-set forwarded scheme; `Host` is preserved by common
|
|
81
81
|
platforms and must equal the origin's host.
|
|
82
|
+
- **A provider key** the remote service needs is issued and kept in Project Observatory, pushed to the platform, and
|
|
83
|
+
the movement recorded with
|
|
84
|
+
`python3 "$(project-observatory full-path)/tools/vault.py" moved <project> <env> <NAME> --how "<what moved, where, evidence>" --at <provider>`,
|
|
85
|
+
so the platform's copy is known rather than invisible. Without Project Observatory, record where the key lives in
|
|
86
|
+
the person's own secret manager.
|
|
82
87
|
|
|
83
88
|
## Checking it
|
|
84
89
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- [MCP for agents](#mcp-for-agents)
|
|
6
6
|
- [CLI for people and scripts](#cli-for-people-and-scripts)
|
|
7
7
|
- [A2A for remote agents](#a2a-for-remote-agents)
|
|
8
|
+
- [Behind tailscale serve](#behind-tailscale-serve)
|
|
8
9
|
- [Token](#token)
|
|
9
10
|
- [Dashboard session](#dashboard-session)
|
|
10
11
|
- [Headers every response carries](#headers-every-response-carries)
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
|
|
41
42
|
## CLI for people and scripts
|
|
42
43
|
|
|
43
|
-
`<tool> service status|start|stop|restart` (launchctl on the label
|
|
44
|
+
`<tool> service status|start|stop|restart` (launchctl on the label in the domain `launchd_domain(plist)` names; for a
|
|
45
|
+
LaunchDaemon start, stop and restart are root's, and a CLI without root reports the job read-only, lifecycle.md, The system
|
|
46
|
+
domain; then the
|
|
44
47
|
well-known document from this OS account's listener, else `foreign-user`: protocol.md, The listener's owner), `<tool> dashboard` (resolve the exact registered service with the `fabric-dashboards`
|
|
45
48
|
MCP, run `check_dashboard_link.py` on the action, then open its `open_link` with the host's `open` and
|
|
46
49
|
`fallback=never`; never `open <url>`, `open -a Safari`, `xdg-open` or `webbrowser.open`; an installed host that
|
|
@@ -82,6 +85,45 @@ A probe cannot see whether an interface would take the descriptor's token withou
|
|
|
82
85
|
forbids, so it reads the card only and never calls an interface: an API-key scheme in the descriptor's own
|
|
83
86
|
`auth.header` is a finding, and with `Authorization` (the default) the card cannot show it.
|
|
84
87
|
|
|
88
|
+
## Behind tailscale serve
|
|
89
|
+
|
|
90
|
+
An agent on a server (a VM, a teammate's machine) can reach people on the tailnet through `tailscale serve`, which
|
|
91
|
+
proxies HTTPS on the node's tailnet name to a loopback listener and adds the caller's `Tailscale-User-Login`. The
|
|
92
|
+
kit's `fabric_tailnet.py` / `fabric-tailnet.mjs` (FAA-297) turn that into a decision; the agent wires it into its own
|
|
93
|
+
handler.
|
|
94
|
+
|
|
95
|
+
- **A listener of serve's own.** Any process on the machine can send `Tailscale-User-Login` to a loopback port, so the
|
|
96
|
+
header is believed only on a listener that serve alone uses: a second loopback port, or, where the installed
|
|
97
|
+
`tailscale serve` can proxy to one, a Unix socket whose mode keeps other accounts out. Pass `listener="serve"` and the
|
|
98
|
+
connection's peer (`"unix"` for a socket). On any other listener a request carrying serve's headers is refused
|
|
99
|
+
(`serve-header-off-serve`); the Fabric listener keeps its session key and never reads them.
|
|
100
|
+
- **A perimeter generated, not kept by hand.** `load_perimeter` / `loadPerimeter` take a `tailnet-perimeter/1`
|
|
101
|
+
document: `generatedFrom {registry, revision, at}` (required: a file without its source is refused,
|
|
102
|
+
`perimeter-not-generated`) and `groups {name: {members, dashboards, capabilities}}`. The organization's generator
|
|
103
|
+
writes it from its access registry; the agent reloads it when the file changes.
|
|
104
|
+
- **A personal key for MCP.** `load_keys` / `loadKeys` take `[{id, login, sha256, capabilities}]`, the key's
|
|
105
|
+
`hash_key` / `hashKey` only. A call on the `mcp` surface needs `Authorization: Bearer <key>`; the key must belong to
|
|
106
|
+
the caller's login and its capabilities may not be wider than that login's groups.
|
|
107
|
+
- **The answer.** `authorize(listener=, peer=, headers=, perimeter=, surface="dashboard"|"mcp", target=, keys=)`
|
|
108
|
+
answers `{allowed, status, code, login, groups}` (and `key`, the key's id, on an allowed MCP call). Send the
|
|
109
|
+
status and the code as they are; no answer carries a key or its hash.
|
|
110
|
+
|
|
111
|
+
| Code | Status | When |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `serve-header-off-serve` | 403 | serve's headers on a listener that is not serve's |
|
|
114
|
+
| `peer-not-loopback` | 403 | the serve listener was reached from another address |
|
|
115
|
+
| `identity-missing`, `identity-ambiguous`, `identity-malformed` | 401 | no login, two, or not a login |
|
|
116
|
+
| `not-in-perimeter` | 403 | the login is in no group |
|
|
117
|
+
| `dashboard-not-granted` | 403 | no group of the login lists the dashboard |
|
|
118
|
+
| `key-required`, `key-unknown` | 401 | an MCP call without a bearer key, or with one not on the list |
|
|
119
|
+
| `key-not-yours` | 403 | the key belongs to another login |
|
|
120
|
+
| `key-wider-than-group` | 403 | the key's capabilities exceed the login's groups |
|
|
121
|
+
| `capability-not-granted` | 403 | the key does not carry the capability called |
|
|
122
|
+
|
|
123
|
+
A tagged node (a server, not a person) gets no identity header from serve and is refused `identity-missing`; a
|
|
124
|
+
machine caller uses the estate's delegation tokens instead. The serve listener is an outward surface: watch it like
|
|
125
|
+
the A2A tunnel above.
|
|
126
|
+
|
|
85
127
|
## Token
|
|
86
128
|
|
|
87
129
|
- Created once with `ensure_token` at first start, after the instance lock.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/when-to-use.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# When to use building-fabric-services
|
|
2
|
+
|
|
3
|
+
The skill's full trigger description, kept here so the front matter's `description` stays within the client's skill listing budget (about 350 characters a skill). Read it when choosing between this skill and a neighbour.
|
|
4
|
+
|
|
5
|
+
Use when running an agent as a Fabric service (fabric-service/0.1, its kits and probe): a local service (launchd, systemd, Windows), an ONLINE agent or dashboard on https, its dashboard link and kit (narrow panel, themes), data and cache paths, how callers reach and authenticate it, long jobs (fabric.job.*), spend report and limits, runner routes and model profiles, the estate record and delegation tokens, answering a Project's board requests, or packing and signing it. «сделай агенту дашборд», «локальный сервис агента», «подключи агента к Fabric Dashboards», «отвечать на запросы с доски проекта», «расходы агента на модели», "make this agent a local service", "make an online dashboard a Fabric service", "add the well-known endpoint". NOT for a one-off script or cron job, a hosted product with no agent, the provider manifest (adapting-projects-to-fabric), a new agent's design (creating-fabric-agents), a session using Fabric or its board (working-with-fabric), or building the Dashboards app (window, tray).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -833,11 +833,12 @@ class Probe:
|
|
|
833
833
|
if sys.platform != "darwin" or not shutil.which("launchctl"):
|
|
834
834
|
self.add("lifecycle.one-copy", "NOT_RUN", "launchctl is not available")
|
|
835
835
|
return
|
|
836
|
-
|
|
836
|
+
domain = fs.launchd_domain(plist_path) # a plist in /Library/LaunchDaemons is a LaunchDaemon (FAA-278)
|
|
837
|
+
out = subprocess.run(["launchctl", "print", "%s/%s" % (domain, life.get("label"))], capture_output=True, text=True)
|
|
837
838
|
match = re.search(r"^\s*pid = (\d+)", out.stdout, re.MULTILINE)
|
|
838
839
|
served = ((self.wk or {}).get("process") or {}).get("pid")
|
|
839
840
|
if out.returncode != 0:
|
|
840
|
-
self.add("lifecycle.one-copy", "FAIL", "launchd job %s is not loaded" % life.get("label"))
|
|
841
|
+
self.add("lifecycle.one-copy", "FAIL", "launchd job %s is not loaded in %s" % (life.get("label"), domain))
|
|
841
842
|
elif not match:
|
|
842
843
|
self.add("lifecycle.one-copy", "FAIL", "launchd job loaded but not running")
|
|
843
844
|
else:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs
CHANGED
|
@@ -11,6 +11,11 @@ export const PROTOCOL = 'fabric-service/0.1';
|
|
|
11
11
|
|
|
12
12
|
export const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/service/0.1';
|
|
13
13
|
|
|
14
|
+
// DEC-0140 PR-1 (contract project-comms.md, interop.md's C3.1 amendment): the capability of a Project's primary
|
|
15
|
+
// responder, a session's. No service serves it as a tool or declares it (surfaces.mcp.capabilities, an action's tool),
|
|
16
|
+
// and the board kit never enrolls a service's responder for it (FAA-244).
|
|
17
|
+
export const PRIMARY_CAPABILITY = 'project.work';
|
|
18
|
+
|
|
14
19
|
// Lifecycle rules: references/lifecycle.md (the rule for this kit's users; it implements the public
|
|
15
20
|
// contract's service specification, Lifecycle), the same values as fabric_service.py.
|
|
16
21
|
// A launchd-supervised copy that finds the lock held backs off in-process before its exit 75 (LC-03).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs
CHANGED
|
@@ -30,6 +30,8 @@ export class BoardSession {
|
|
|
30
30
|
try {
|
|
31
31
|
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: timeout > 0 ? timeout : this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
32
32
|
} catch (e) {
|
|
33
|
+
// FAA-252: another OS account's process holds the port; nothing was sent
|
|
34
|
+
if (e instanceof HubError && e.code === 'foreign-user') throw new BoardError('foreign-user', 'The hub\'s port is held by another OS account\'s process; nothing was sent.', true);
|
|
33
35
|
if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored, so send it again with the same idempotency key.', true);
|
|
34
36
|
throw e;
|
|
35
37
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Part of the Node board connector (FAA-65, layer L4); the twin of fabric_board_session.py. Node 20+, no dependency.
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { Backoff, HubError, normalProject } from './fabric-hub.mjs';
|
|
6
|
-
import { acceptedRevisions, feedDataProblems, negotiateRevision } from './fabric-service.mjs';
|
|
6
|
+
import { PRIMARY_CAPABILITY, acceptedRevisions, feedDataProblems, negotiateRevision } from './fabric-service.mjs';
|
|
7
7
|
import { BoardError, CAPABILITY, LEASE_SECONDS, REQUEST_STATES, SUMMARY_MAX, Stopped, isInt, isObj, outcomeProblems, receiptProblems } from './fabric-board-shapes.mjs';
|
|
8
8
|
import { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD, covers, mandateOptions, readMandate } from './fabric-board-link.mjs';
|
|
9
9
|
import { HOLD_RETRY_MAX_SECONDS, LEASE_MARGIN_SECONDS, SESSION_CODES, SlotKeeper } from './fabric-board-slots.mjs';
|
|
@@ -87,6 +87,8 @@ export class Responder extends SlotKeeper {
|
|
|
87
87
|
if (typeof admit !== 'function' || typeof handler !== 'function') throw new BoardError('invalid_arguments', 'A responder needs the service\'s own input gate (admit) and a handler.');
|
|
88
88
|
const caps = [...(capabilities ?? [])];
|
|
89
89
|
if (!caps.length || caps.some((c) => typeof c !== 'string' || !CAPABILITY.test(c)) || new Set(caps).size !== caps.length) throw new BoardError('invalid_arguments', 'Name each capability once, [a-z][a-z0-9._-]{1,127}.');
|
|
90
|
+
// FAA-244: DEC-0140 PR-1, a session's slot, never a service's
|
|
91
|
+
if (caps.includes(PRIMARY_CAPABILITY)) throw new BoardError('invalid_arguments', `${PRIMARY_CAPABILITY} is a Project's primary responder's, a session's: a service answers the capabilities it declares (DEC-0140 PR-1).`);
|
|
90
92
|
Object.assign(this, { hub, capabilities: caps, credentialFile, handler, admit, observe, emitTo: emit, now, rand });
|
|
91
93
|
this.project = normalProject(projectId);
|
|
92
94
|
// FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; null reads none)
|
|
@@ -196,7 +198,7 @@ export class Responder extends SlotKeeper {
|
|
|
196
198
|
this.session = session;
|
|
197
199
|
const status = await this.status();
|
|
198
200
|
if (missing.length) {
|
|
199
|
-
this.emit('board.access.needed', 'warning', `The operator has not let this service answer ${missing.join(', ')} on this Project; ask in Fabric (
|
|
201
|
+
this.emit('board.access.needed', 'warning', `The operator has not let this service answer ${missing.join(', ')} on this Project; ask in Fabric (Settings → Agent access).`, { notify: true, episode: missing.join(','), capabilities: missing });
|
|
200
202
|
}
|
|
201
203
|
this.slots = {};
|
|
202
204
|
await this.recover(status, this.capabilities.filter((c) => grant.capabilities.includes(c)));
|
|
@@ -375,7 +377,8 @@ export class Responder extends SlotKeeper {
|
|
|
375
377
|
try {
|
|
376
378
|
this.receipt('com.reply', await this.call('com.reply', { ...this.replyExtra(entry), fence: this.fence({ ...entry, generation, attempt, digest, revision }, key) }));
|
|
377
379
|
} catch (e) {
|
|
378
|
-
|
|
380
|
+
// foreign-user: nothing was sent now, so whether the old reply was stored is unknown (0.79.0 seam reading)
|
|
381
|
+
if (['unreachable', 'foreign-user', 'session-ended', 'authority_changed', 'not_authorized', 'not_available', 'capacity_exceeded'].includes(e.code) || !(e instanceof BoardError)) throw e;
|
|
379
382
|
replied = false;
|
|
380
383
|
}
|
|
381
384
|
if (replied) entry.replied = true;
|
|
@@ -496,7 +499,7 @@ export class Responder extends SlotKeeper {
|
|
|
496
499
|
if (['not_authorized', 'authority_changed', 'credential'].includes(code)) {
|
|
497
500
|
this.state = 'not-authorized';
|
|
498
501
|
await this.dropSession();
|
|
499
|
-
this.emit('board.access.needed', 'warning', 'This service may not answer requests for this Project: the operator has to allow it in Fabric (
|
|
502
|
+
this.emit('board.access.needed', 'warning', 'This service may not answer requests for this Project: the operator has to allow it in Fabric (Settings → Agent access). Nothing is claimed until then.', { notify: true, episode: 'access', reason: code });
|
|
500
503
|
} else if (code === 'held') {
|
|
501
504
|
this.state = 'paused';
|
|
502
505
|
this.emit('board.held', 'info', 'The operator holds new work on this Project; the responder waits and asks again with a growing pause.', { episode: 'held', reason: code });
|
|
@@ -508,8 +511,11 @@ export class Responder extends SlotKeeper {
|
|
|
508
511
|
} else {
|
|
509
512
|
this.state = 'down';
|
|
510
513
|
// A refusal in recovery that left no slot held reconnects (seam 6); one that left some held serves them (seam 7).
|
|
511
|
-
if ((connecting || ['session-ended', 'unreachable', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol'].includes(code)) && !keep) await this.dropSession();
|
|
512
|
-
|
|
514
|
+
if ((connecting || ['session-ended', 'unreachable', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol', 'foreign-user'].includes(code)) && !keep) await this.dropSession();
|
|
515
|
+
// FAA-252: a port another OS account holds is said as such; nothing was sent there
|
|
516
|
+
this.emit('board.down', 'warning', code === 'foreign-user'
|
|
517
|
+
? 'Fabric\'s board port is held by another OS account\'s process; nothing was sent, and the responder tries again with a growing pause.'
|
|
518
|
+
: 'Fabric\'s board cannot be reached; the responder tries again with a growing pause.', { episode: 'down', reason: code });
|
|
513
519
|
}
|
|
514
520
|
return wait;
|
|
515
521
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export const HOLD_RETRY_MAX_SECONDS = 900;
|
|
|
14
14
|
|
|
15
15
|
/** Refusals that are about the board session, not one capability: they end recovery and go to the responder's fault. */
|
|
16
16
|
export const SESSION_CODES = Object.freeze(new Set(['unreachable', 'session-ended', 'not_authorized', 'authority_changed', 'credential', 'no-epoch',
|
|
17
|
-
'schema-unsupported', 'checkpoint', 'receipt-shape', 'idempotency_conflict', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol', 'not_available']));
|
|
17
|
+
'schema-unsupported', 'checkpoint', 'receipt-shape', 'idempotency_conflict', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol', 'not_available', 'foreign-user']));
|
|
18
18
|
|
|
19
19
|
const sameRecord = (a, b) => JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
|
|
20
20
|
|
|
@@ -83,7 +83,7 @@ export class SlotKeeper {
|
|
|
83
83
|
const moved = isInt(before, 1) && isInt(rest.generation, 1) && before > rest.generation;
|
|
84
84
|
if (isInt(generation, 1) && (answer.repeated === true || !moved)) record = { project: this.project, generation };
|
|
85
85
|
} catch (e) {
|
|
86
|
-
if (LOST.includes(e.code)) throw e;
|
|
86
|
+
if (LOST.includes(e.code) || e.code === 'foreign-user') throw e; // foreign-user: the earlier ask is unknown
|
|
87
87
|
if (e.code === 'idempotency_window_expired') record = {}; // a retired epoch: unknown, so a first start, no move
|
|
88
88
|
}
|
|
89
89
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
14
|
import fs from 'node:fs';
|
|
15
|
+
import http from 'node:http';
|
|
15
16
|
import net from 'node:net';
|
|
16
17
|
import { hubKeyCertified } from './fabric-estate.mjs';
|
|
17
18
|
import { atomicWrite } from './fabric-paths.mjs'; // the kit's private writer (FAA-85)
|
|
19
|
+
import { CheckedAgent, ListenerRefused } from './fabric-peer-owner.mjs'; // a loopback key set from this account's listener only (FAA-255)
|
|
18
20
|
import { effectClosure } from './fabric-mandate.mjs'; // defined once beside the mandate that reads it too (DEC-0125)
|
|
19
21
|
|
|
20
22
|
export const REFUSAL_CODES = Object.freeze([
|
|
@@ -411,8 +413,34 @@ export const actionDigest = (action) => `sha256:${crypto.createHash('sha256').up
|
|
|
411
413
|
|
|
412
414
|
const loopback = (host) => host === 'localhost' || (net.isIP(host.replace(/^\[|\]$/g, '')) !== 0 && (/^127\./.test(host) || host.replace(/^\[|\]$/g, '') === '::1'));
|
|
413
415
|
|
|
416
|
+
const KEY_SET_MAX = 65536;
|
|
417
|
+
const KEY_SET_AGENT = new CheckedAgent({ keepAlive: false });
|
|
418
|
+
|
|
419
|
+
// Plain http, which a key set source allows on loopback only: through a CheckedAgent, so the listener's owner is told
|
|
420
|
+
// before a byte is written and keys another OS account's process serves are never read (DEC-0154 HA-2, FAA-255).
|
|
421
|
+
function loopbackKeySet(source, agent) {
|
|
422
|
+
return new Promise((resolve, reject) => {
|
|
423
|
+
const request = http.get(source.split('#')[0], { agent, headers: { Accept: 'application/json' }, timeout: 5000 }, (response) => {
|
|
424
|
+
if (response.statusCode < 200 || response.statusCode > 299) { response.resume(); reject(new Error(`HTTP ${response.statusCode}`)); return; }
|
|
425
|
+
let text = '';
|
|
426
|
+
response.setEncoding('utf8');
|
|
427
|
+
response.on('data', (chunk) => { text += chunk; if (text.length > KEY_SET_MAX) { request.destroy(); reject(new Error('the key set is too large')); } });
|
|
428
|
+
response.on('end', () => { try { resolve(JSON.parse(text)); } catch (e) { reject(e); } });
|
|
429
|
+
response.on('error', reject);
|
|
430
|
+
});
|
|
431
|
+
request.on('timeout', () => request.destroy(new Error('the key set did not arrive within 5 s')));
|
|
432
|
+
request.on('error', (e) => reject(e instanceof ListenerRefused ? new Error(`foreign-user: ${e.message}`) : e));
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Read a key set source: an https URL (fetch, no redirect), a loopback http URL (checked listener) or a file. */
|
|
437
|
+
export async function fetchKeySet(source, { agent = KEY_SET_AGENT } = {}) {
|
|
438
|
+
if (/^http:\/\//.test(source)) return loopbackKeySet(source, agent);
|
|
439
|
+
return defaultFetch(source);
|
|
440
|
+
}
|
|
441
|
+
|
|
414
442
|
async function defaultFetch(source) {
|
|
415
|
-
if (/^https
|
|
443
|
+
if (/^https:\/\//.test(source)) {
|
|
416
444
|
const response = await fetch(source, { headers: { Accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(5000) });
|
|
417
445
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
418
446
|
if (Number(response.headers.get('content-length') ?? 0) > 65536) throw new Error('the key set is too large');
|
|
@@ -428,7 +456,7 @@ async function defaultFetch(source) {
|
|
|
428
456
|
* `await get(kid)` answers null when no set younger than 10 minutes can be had (issuer-unavailable).
|
|
429
457
|
*/
|
|
430
458
|
export class JwksCache {
|
|
431
|
-
constructor(source, { fetch: fetcher =
|
|
459
|
+
constructor(source, { fetch: fetcher = fetchKeySet, clock = () => Date.now() / 1000 } = {}) {
|
|
432
460
|
if (typeof source !== 'string' || !source) throw new Error('the key set needs a source');
|
|
433
461
|
if (/^https?:\/\//.test(source)) {
|
|
434
462
|
const url = new URL(source);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { credentialName, repositoryCredential, secretArgument, secretUrl, secretValue } from './fabric-secrets.mjs';
|
|
6
|
-
import { ID, INSTANCE, LOCAL_PATH, PROTOCOL, ServiceError, TASK, UNIT, expand } from './fabric-base.mjs';
|
|
6
|
+
import { ID, INSTANCE, LOCAL_PATH, PRIMARY_CAPABILITY, PROTOCOL, ServiceError, TASK, UNIT, expand } from './fabric-base.mjs';
|
|
7
7
|
import { atomicWrite, ensurePrivateDir, servicesDir } from './fabric-paths.mjs';
|
|
8
8
|
import { protocolsFindings } from './fabric-protocols.mjs';
|
|
9
9
|
import { CONNECTIONS_EXTENSION_KEY, connectionsView } from './fabric-connections.mjs';
|
|
@@ -15,7 +15,7 @@ import { ROUTES } from './fabric-usage.mjs';
|
|
|
15
15
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
16
16
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
17
17
|
|
|
18
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
18
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.6';
|
|
19
19
|
|
|
20
20
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
21
21
|
|
|
@@ -418,6 +418,9 @@ export function configurationProblems(doc) {
|
|
|
418
418
|
// `a2a` (DEC-0122) is surfaces.a2a, where the service serves its public Agent Card (a2aSurface builds one); passed here
|
|
419
419
|
// or in `surfaces`, never both, and refused when it is not the schema's. A reader pinned before DEC-0122 refuses the
|
|
420
420
|
// member, so a service carries it only once its readers are pinned at or after it.
|
|
421
|
+
// FAA-244: DEC-0140 PR-1 reserves the primary responder's capability; a service never publishes it.
|
|
422
|
+
const reserved = (where) => `${where}: ${PRIMARY_CAPABILITY} is reserved for a Project's primary responder, a session (DEC-0140 PR-1); a service never serves it as a tool or declares it.`;
|
|
423
|
+
|
|
421
424
|
export function buildWellKnown({ id, instance, name, version, build, startedAt, status, degraded = [], surfaces, summary, updateAvailable = null, pid = process.pid, extensions, configuration, protocols, actions, a2a }) {
|
|
422
425
|
if (!STATUSES.includes(status)) throw new ServiceError(`status must be one of ${STATUSES.join(', ')}.`);
|
|
423
426
|
if (!surfaces?.events) throw new ServiceError('surfaces.events is required.');
|
|
@@ -430,6 +433,7 @@ export function buildWellKnown({ id, instance, name, version, build, startedAt,
|
|
|
430
433
|
const problems = a2aSurfaceProblems(surfaces.a2a);
|
|
431
434
|
if (problems.length) throw new ServiceError(`surfaces.a2a: ${problems.join('; ')}.`);
|
|
432
435
|
}
|
|
436
|
+
if (Array.isArray(surfaces.mcp?.capabilities) && surfaces.mcp.capabilities.includes(PRIMARY_CAPABILITY)) throw new ServiceError(reserved('surfaces.mcp.capabilities'));
|
|
433
437
|
if (!(build?.commit || build?.digest)) throw new ServiceError('build needs a commit or a digest.');
|
|
434
438
|
const doc = {
|
|
435
439
|
protocol: PROTOCOL,
|
|
@@ -461,6 +465,7 @@ export function buildWellKnown({ id, instance, name, version, build, startedAt,
|
|
|
461
465
|
// compared with the capabilities this document's MCP surface lists.
|
|
462
466
|
const findings = actionsExtensionRules({ extensions: { [ACTIONS_EXTENSION_KEY]: extensions[ACTIONS_EXTENSION_KEY] }, surfaces });
|
|
463
467
|
if (findings.length) throw new ServiceError(`Actions declaration: ${findings.map((f) => f.message).join('; ')}.`);
|
|
468
|
+
if ((extensions[ACTIONS_EXTENSION_KEY]?.actions ?? []).some((a) => a?.tool === PRIMARY_CAPABILITY)) throw new ServiceError(reserved("an action's tool"));
|
|
464
469
|
}
|
|
465
470
|
doc.extensions = { ...extensions };
|
|
466
471
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs
CHANGED
|
@@ -27,6 +27,9 @@ export class AlreadyRunning extends ServiceError {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export class InstanceLock {
|
|
30
|
+
/** How many times acquire re-opens service.lock that moved under it before it gives up (FAA-293). */
|
|
31
|
+
static INODE_RETRIES = 3;
|
|
32
|
+
|
|
30
33
|
constructor(dataDir) {
|
|
31
34
|
this.path = path.join(dataDir, 'service.lock');
|
|
32
35
|
this.fd = null;
|
|
@@ -36,27 +39,12 @@ export class InstanceLock {
|
|
|
36
39
|
async acquire() {
|
|
37
40
|
ensurePrivateDir(path.dirname(this.path));
|
|
38
41
|
if (process.platform === 'darwin') {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (error.code === 'EAGAIN' || error.code === 'EWOULDBLOCK') throw new AlreadyRunning(readPid(this.path), this.path);
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
fs.ftruncateSync(this.fd, 0);
|
|
48
|
-
fs.writeSync(this.fd, String(process.pid));
|
|
49
|
-
fs.fsyncSync(this.fd);
|
|
50
|
-
} catch (error) { // a full disk or an I/O error: let the lock go, or nobody can (the caller drops this object)
|
|
51
|
-
const fd = this.fd;
|
|
52
|
-
this.fd = null;
|
|
53
|
-
try {
|
|
54
|
-
fs.closeSync(fd);
|
|
55
|
-
} catch { // a deferred write error (NFS) at the close: the lock still goes, the first error stands
|
|
56
|
-
}
|
|
57
|
-
throw error;
|
|
42
|
+
// FAA-293: a folder swapped under service.lock (a data import) leaves this copy holding a file that is no longer
|
|
43
|
+
// at the path while another copy holds the one that is. Re-open a bounded number of times, then fail clearly.
|
|
44
|
+
for (let attempt = 0; attempt < InstanceLock.INODE_RETRIES; attempt += 1) {
|
|
45
|
+
if (this.acquireDarwinOnce()) return this;
|
|
58
46
|
}
|
|
59
|
-
|
|
47
|
+
throw new ServiceError(`${this.path} kept moving while it was locked (a folder swapped under the data folder?): no copy holds a file that is no longer at its path; start again once the folder is settled.`);
|
|
60
48
|
}
|
|
61
49
|
// Linux: an abstract unix socket; Windows: a named pipe. Each is exclusive, has no file, and dies
|
|
62
50
|
// with the process. The Python kit holds the same name beside its file lock (fabric_service.lock_name).
|
|
@@ -90,6 +78,52 @@ export class InstanceLock {
|
|
|
90
78
|
return this;
|
|
91
79
|
}
|
|
92
80
|
|
|
81
|
+
lockedFileIsAtPath(fd) {
|
|
82
|
+
let here;
|
|
83
|
+
try {
|
|
84
|
+
here = fs.statSync(this.path);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code === 'ENOENT') return false;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const held = fs.fstatSync(fd);
|
|
90
|
+
return here.dev === held.dev && here.ino === held.ino;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
acquireDarwinOnce() {
|
|
94
|
+
const flags = fs.constants.O_RDWR | fs.constants.O_CREAT | O_EXLOCK | fs.constants.O_NONBLOCK;
|
|
95
|
+
try {
|
|
96
|
+
this.fd = fs.openSync(this.path, flags, 0o600);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === 'EAGAIN' || error.code === 'EWOULDBLOCK') throw new AlreadyRunning(readPid(this.path), this.path);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
if (!this.lockedFileIsAtPath(this.fd)) { // another copy may hold the file at the path: re-open that one
|
|
102
|
+
fs.closeSync(this.fd);
|
|
103
|
+
this.fd = null;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
fs.ftruncateSync(this.fd, 0);
|
|
108
|
+
fs.writeSync(this.fd, String(process.pid));
|
|
109
|
+
fs.fsyncSync(this.fd);
|
|
110
|
+
} catch (error) { // a full disk or an I/O error: let the lock go, or nobody can (the caller drops this object)
|
|
111
|
+
const fd = this.fd;
|
|
112
|
+
this.fd = null;
|
|
113
|
+
try {
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
} catch { // a deferred write error (NFS) at the close: the lock still goes, the first error stands
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (!this.lockedFileIsAtPath(this.fd)) { // swapped while the pid was written: this copy holds nothing real
|
|
120
|
+
fs.closeSync(this.fd);
|
|
121
|
+
this.fd = null;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
93
127
|
release() {
|
|
94
128
|
if (this.fd !== null) fs.closeSync(this.fd);
|
|
95
129
|
if (this.server) this.server.close();
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs
CHANGED
|
@@ -277,6 +277,9 @@ export class Client {
|
|
|
277
277
|
}
|
|
278
278
|
attempts.push({ model: current, status: result.status, outcome: result.outcome });
|
|
279
279
|
if (result.answer) return { ...result.answer, attempts };
|
|
280
|
+
if (result.outcome === 'foreign-user') { // FAA-257: the fallback would go to the same refused listener
|
|
281
|
+
throw new OpenRouterError('The base URL\'s port is held by another OS account\'s process, or its owner cannot be told; nothing was sent (DEC-0154 HA-2)', { kind: 'foreign-user', attempts });
|
|
282
|
+
}
|
|
280
283
|
if (Object.values(STOP_AT_ONCE).includes(result.outcome)) {
|
|
281
284
|
throw new OpenRouterError(`OpenRouter refused the key or the request (${result.status})`, { kind: result.outcome, attempts });
|
|
282
285
|
}
|
|
@@ -299,6 +302,7 @@ export class Client {
|
|
|
299
302
|
// may have been billed (a cut connection looks like a refused one from here): counted, never as $0
|
|
300
303
|
this.ledger.record(this.unmeasured(asked, taskRunId, worst));
|
|
301
304
|
}
|
|
305
|
+
if (err.kind === 'foreign-user') return { outcome: 'foreign-user', status: err.status }; // nothing sent, nothing billed (FAA-257)
|
|
302
306
|
return { outcome: ['connection', 'timeout'].includes(err.kind) ? 'retry' : 'refused', status: err.status };
|
|
303
307
|
}
|
|
304
308
|
const { status } = response;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs
CHANGED
|
@@ -534,7 +534,9 @@ export async function pidListens(pid, port, { run = runProgram, platform = proce
|
|
|
534
534
|
// Rejects with ListenerRefused (the socket destroyed, nothing written) or with the connection's own error.
|
|
535
535
|
export function checkedSocket(port, { host = '127.0.0.1', timeoutMs = 10000, expectPid = null, vouchPid = null, ...ask } = {}) {
|
|
536
536
|
return new Promise((resolveSocket, reject) => {
|
|
537
|
-
|
|
537
|
+
// autoSelectFamily: Node 18 connects to the first address only, and localhost resolves to ::1 first on Linux, so a
|
|
538
|
+
// listener on 127.0.0.1 was never reached; Python tries every address, as Node 20 and later do by default.
|
|
539
|
+
const socket = net.connect({ host, port, noDelay: true, autoSelectFamily: true }); // as a plain http.Agent's socket
|
|
538
540
|
const timer = setTimeout(() => socket.destroy(Object.assign(new Error('connect timeout'), { code: 'ETIMEDOUT' })), timeoutMs);
|
|
539
541
|
socket.once('error', (err) => { clearTimeout(timer); reject(err); });
|
|
540
542
|
socket.once('connect', async () => {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs
CHANGED
|
@@ -66,7 +66,10 @@ export function rawValues(query) {
|
|
|
66
66
|
if (piece === '') continue;
|
|
67
67
|
const at = piece.indexOf('=');
|
|
68
68
|
const name = [...new URLSearchParams(at < 0 ? piece : piece.slice(0, at)).keys()][0] ?? '';
|
|
69
|
-
|
|
69
|
+
const value = at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1));
|
|
70
|
+
const list = values.get(name);
|
|
71
|
+
if (list) list.push(value); // appended in place: copying the list per piece made a long query quadratic (FAA-245)
|
|
72
|
+
else values.set(name, [value]);
|
|
70
73
|
}
|
|
71
74
|
return values;
|
|
72
75
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export {
|
|
6
6
|
PROTOCOL,
|
|
7
7
|
EXTENSION_KEY,
|
|
8
|
+
PRIMARY_CAPABILITY,
|
|
8
9
|
SUPERVISOR_ENV,
|
|
9
10
|
DRAIN_SECONDS,
|
|
10
11
|
ServiceError,
|
|
@@ -177,6 +178,11 @@ export {
|
|
|
177
178
|
taskInstall,
|
|
178
179
|
taskUninstall,
|
|
179
180
|
launchdPlist,
|
|
181
|
+
SYSTEM_DAEMONS,
|
|
182
|
+
launchdDomain,
|
|
183
|
+
wellKnownOf,
|
|
184
|
+
jobWellKnownOf,
|
|
185
|
+
ForeignUserAnswer,
|
|
180
186
|
launchdLoaded,
|
|
181
187
|
launchdPid,
|
|
182
188
|
launchdOverride,
|