@passioncode-ai/fabric-agent-adapter 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +4 -3
  3. package/SECURITY.md +17 -0
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +6 -2
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -16
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +2 -2
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -1
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -1
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +6 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -1
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +4 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -1
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +10 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +36 -6
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +6 -4
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +19 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +54 -3
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +841 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent_schema.py +496 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +38 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +53 -3
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -5
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +545 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +43 -7
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +29 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +13 -4
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +845 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +35 -5
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +212 -9
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/service-channel-agent-extension.schema.json +700 -0
  35. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  36. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +6 -2
  37. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +21 -8
  38. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/practices.md +15 -6
  39. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  40. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1: wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `eac1a594aa131851f079da8bb7b2149e1951f35f`
3
+ Pinned to `fabric-agent-contract` commit `18f8d48762edc5bfb1433657cbfe1c16196df1a6`
4
4
  (`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
5
5
  DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
6
6
  delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
@@ -36,6 +36,7 @@ this page is the working summary. Extension key:
36
36
  - Well-known document
37
37
  - Events page
38
38
  - Login code and the operator session
39
+ - The listener's owner (DEC-0154 HA-2)
39
40
  - Semantic rules a host applies
40
41
 
41
42
  ## Descriptor: written by the installer
@@ -48,9 +49,9 @@ File `<id>.<instance>.json`, mode 0600 (on Windows a protected ACL), atomic.
48
49
  | Field | Rule |
49
50
  |---|---|
50
51
  | `protocol` | `"fabric-service/0.1"` |
51
- | `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique per machine |
52
+ | `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique within one OS account's services directory (DEC-0154) |
52
53
  | `name`, `summary` | ≤ 80 and ≤ 200 characters |
53
- | `origin` | `http://127.0.0.1:<port>`; the port is a machine-wide claim. Optional for an app's descriptor only (`manager: "person"`, DEC-0069): its live origin and process id are in the app's own liveness file, and a descriptor without an origin claims no port (`FAC-SEM-010`) |
54
+ | `origin` | `http://127.0.0.1:<port>`; the port is a declaration no other descriptor in this account's directory makes (`FAC-SEM-010`), kept free on the computer by HA-1, whose claim the kit does not write yet (FAA-225). Optional for an app's descriptor only (`manager: "person"`, DEC-0069): its live origin and process id are in the app's own liveness file, and a descriptor without an origin claims no port (`FAC-SEM-010`) |
54
55
  | `auth` | `tokenFile` (0600; on Windows a protected ACL: owner and every granting entry the current user, SYSTEM or Administrators: service.md *Windows token files*, DEC-0032, DEC-0033); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
55
56
  | `lifecycle` | `manager` `launchd` (then `label` and `plist`), `systemd` (then `unit`, a `systemctl --user` unit), `task-scheduler` (then `task`, a per-user Scheduled Task), `node` (Project Observatory's node supervises it as its child, with no supervisor file, DEC-0061), `person` (an app a person installs and starts, then `app`, the installed app, DEC-0069) or `none`; `unit` and `task` are refused beside any other manager (DEC-0032), `label` and `plist` beside any but `launchd` (DEC-0057), `app` beside any but `person`. Both kits and the probe accept all six; both kits write the unit and the task (`systemd_unit`, `task_xml` / `systemdUnit`, `taskXml`). An app's descriptor says it is installed, never that it runs: the probe checks `app` is still there (`descriptor.app-installed`) and reads no well-known document; a reader that finds the app gone treats the descriptor as stale (`app_descriptor_stale` / `appDescriptorStale`), and an app that writes its own descriptor rewrites it only when `app_descriptor_action` / `appDescriptorAction` says `write` |
56
57
  | `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
@@ -61,7 +62,7 @@ File `<id>.<instance>.json`, mode 0600 (on Windows a protected ACL), atomic.
61
62
  | `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none`: a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
62
63
  | `peer` | optional `{endpoint, custody, residency, maxDataClass}` (DEC-0061): the service is offered to others. `endpoint` `https://<dns-name>[:<port>][/<path>]`, never a reserved name (`FAC-SEM-085`); `custody` `organization-managed`, `personal` or `processor-hosted` (an organization's cloud a processor runs under a processing agreement, DEC-0099); `residency` 1–32 ISO 3166-1 alpha-2 codes, `EU` admitted, user-assigned codes refused; `maxDataClass` `public` < `internal` < `confidential` < `restricted`. An offer needs `runner` and never runs on a subscription. A host sends a request from anyone but the operator only when `peer_refusal` / `peerRefusal` answers null (`FAC-SEM-088`); usage rows may take `route: "platform-metered"` |
63
64
 
64
- A local descriptor MAY say where a collector on the same machine reads its daily operations digests, under
65
+ A local descriptor MAY say where a collector in the same OS account reads its daily operations digests, under
65
66
  `https://fabric.passioncode.ai/agent-contract/extensions/ops-digest/0.1` (`fabric-ops-digest/0.1`, DEC-0153, OD-9,
66
67
  FAC-SEM-168): 1 to 8 `{dir, source}`, `dir` a folder inside `paths.data`. A remote descriptor carries none, and the key
67
68
  is read nowhere else. The kits know the revision and neither write nor check the declaration yet (FAA-206).
@@ -148,8 +149,9 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
148
149
  `https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1` (`channel-agent-standard/1`, DEC-0150,
149
150
  FAC-SEM-162 to 165) only for a service with no manifest; one with a manifest declares it under its manifest's
150
151
  `provider.extensions` alone (CA-1). A well-known answer lists `channel-agent-standard/1` in its `protocols` only once
151
- its readers have re-pinned to this revision of the contract (channel-agent.md). The kits know the revision (`PROTOCOL_REVISIONS`) and do not build or check the
152
- declaration yet (FAA-197).
152
+ its readers have re-pinned to this revision of the contract (channel-agent.md). The kits know the revision (`PROTOCOL_REVISIONS`) and do not build the declaration;
153
+ `check_service.py` checks it (`channel-agent.declaration`, [interop.md](interop.md#what-the-probe-checks)). Fabric
154
+ Dashboards' sample service keeps its own copy of the protocol table, which no test here holds equal to the kits'.
153
155
  - `surfaces.mcp.auth`: `descriptor` (default), `own` (DEC-0024) or `delegation` (DEC-0039, DL-16):
154
156
  the MCP surface takes only the estate's delegation tokens, and the top-level
155
157
  `auth.delegation {issuer, jwks}` is then required. With `own` or `delegation` the descriptor's
@@ -204,6 +206,34 @@ The session it starts ends 12 h after its last use, 7 days after sign-in, on log
204
206
  descriptor token rotates (DEC-0039); a service may shorten either bound, never lengthen one. An
205
207
  ended session answers `401` on the dashboard page.
206
208
 
209
+ ## The listener's owner (DEC-0154 HA-2)
210
+
211
+ Loopback is one per computer, not one per OS account. Before a client sends any credential to
212
+ `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
+ token), and before a host calls a local service healthy, it tells the owner of the server end of its own connection
214
+ 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. The order of
216
+ a local service's states is `down`, `foreign-user`, `foreign` (`FAC-SEM-009`), then the well-known `status`; the
217
+ well-known `process.pid` is never proof (`docs/specification/service.md#ha-2` at the pin).
218
+
219
+ The kit's module is `fabric_peer_owner` / `fabric-peer-owner.mjs` (layer L0, [kit modules](kit-modules.md)): `listener(port)`,
220
+ `server_end(sock)` / `serverEnd(socket)`, `peer`, and a connection checked before a byte is written
221
+ (`CheckedHTTPConnection`; Node `checkedSocket`, `CheckedAgent`). Python reads macOS through `sysctl
222
+ net.inet.tcp.pcblist_n` (calibrated on a connected pair of its own, else `netstat -anv`), Linux through `/proc/net/tcp`
223
+ and `/proc/net/tcp6`, Windows through `GetExtendedTcpTable` and the owner's token SID; Node reads `netstat -anv` and `ps`
224
+ on macOS, the same `/proc` files on Linux, and `netstat -ano` with the owner SID from PowerShell `GetOwnerSid` against `whoami /user` on Windows (SIDs, never names). Fail closed: everything but
225
+ `this-account` refuses, `unknown` included.
226
+
227
+ Every kit client that sends a credential to loopback checks first: `check_service.py` (rule `listener.owner`, the first that reads the port for
228
+ a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`),
229
+ `fabric_http` / `fabric-http.mjs` for a plain `http://` loopback URL (`HttpError` kind `foreign-user`, never retried),
230
+ `check_dashboard_layout.mjs` before a local login code, and the launchd installers' poll (a listener of another account
231
+ is never a successful install; the systemd and Task Scheduler installers do not poll yet, FAA-253). A running board
232
+ session reports a door another account holds as `board.down` with reason `unreachable` until FAA-252. On macOS the Node
233
+ twin reads the pid from `netstat` and the uid from `ps` a few milliseconds apart, so a pid reused in between would be
234
+ 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
235
+ claim file (HA-1) waits on the contract's CT-228.
236
+
207
237
  ## Semantic rules a host applies
208
238
 
209
239
  | Code | Rule |
@@ -27,8 +27,10 @@
27
27
  (`auth.delegation-declared`). The kits verify the tokens: put `delegation_gate` /
28
28
  `delegationGate` in front of the tools (see [the delegation reference](delegation.md)).
29
29
  - Register it into a client config with the service's own command
30
- (`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
31
- atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
30
+ (`<tool> mcp-register`), which first establishes that the port's listener is this OS account's (the contract's
31
+ DEC-0154 HA-2: `fabric_peer_owner.server_end` on a connection of its own, as `check_service.py`'s `listener.owner`
32
+ does; otherwise `foreign-user` and nothing is written), then writes
33
+ `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}` atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
32
34
  goes in `headers`, never in the URL: every log that records a URL prints its query
33
35
  string. An HTTP registration starts nothing per session: every client session talks to the
34
36
  one resident service (LC-19). A stdio server, by contrast, is a process per session; keep
@@ -39,7 +41,7 @@
39
41
  ## CLI for people and scripts
40
42
 
41
43
  `<tool> service status|start|stop|restart` (launchctl on the label, then the
42
- well-known document), `<tool> dashboard` (resolve the exact registered service with the `fabric-dashboards`
44
+ 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`
43
45
  MCP, run `check_dashboard_link.py` on the action, then open its `open_link` with the host's `open` and
44
46
  `fallback=never`; never `open <url>`, `open -a Safari`, `xdg-open` or `webbrowser.open`; an installed host that
45
47
  failed is reported, never replaced by a browser; only a confirmed `not_installed` host permits an explicit browser
@@ -85,7 +87,7 @@ forbids, so it reads the card only and never calls an interface: an API-key sche
85
87
  - Created once with `ensure_token` at first start, after the instance lock.
86
88
  - Stored at the descriptor's `auth.tokenFile`, 0600, owner-checked, never a symlink.
87
89
  - Compared in constant time (`token_matches`).
88
- - Rotation is an operator act: write a new file, restart, re-run `mcp-register`.
90
+ - Rotation is an operator act: write a new file, restart, re-run `mcp-register` (which checks the listener's owner again).
89
91
  - Same-user processes can read it. The protocol defends against web pages and
90
92
  mistakes, not against hostile code running as the operator; say so in SECURITY.md.
91
93
 
@@ -94,7 +94,24 @@ async function serviceTarget(spec, dir) {
94
94
  const header = auth.header || 'Authorization';
95
95
  const value = (auth.scheme || 'Bearer') === 'Bearer' ? `Bearer ${token}` : token;
96
96
  const remote = d.placement === 'remote';
97
- const wkRes = await fetch(`${origin}/.well-known/fabric-service`, { headers: remote && token ? { [header]: value } : {} });
97
+ // Only an https origin goes through fetch. Every other one, a local origin above all, is asked only on a connection
98
+ // whose server end is this OS account's process (DEC-0154 HA-2): fabric-http (beside this file in the kit) checks it
99
+ // before the token is sent, refuses with kind foreign-user, and refuses plain http to another host. The path follows
100
+ // the URL, never the descriptor's placement (0.74.0 unit reading 7: a "remote" descriptor naming 127.0.0.1 sent its
101
+ // token unchecked). A copy of this file alone cannot tell the owner, so it sends nothing to such an origin.
102
+ const viaKit = !/^https:\/\//i.test(origin);
103
+ let kitRequest = null;
104
+ if (viaKit) {
105
+ kitRequest = (await import('./fabric-http.mjs').catch(() => null))?.request;
106
+ if (!kitRequest) throw new Error('fabric-http.mjs and fabric-peer-owner.mjs are not beside this file: the local listener\'s owner cannot be told, so no token is sent (DEC-0154 HA-2)');
107
+ }
108
+ const call = !viaKit
109
+ ? async (method, url, headers) => { const r = await fetch(url, { method, headers }); return { status: r.status, ok: r.ok, json: () => r.json() }; }
110
+ : async (method, url, headers) => {
111
+ const r = await kitRequest(method, url, { headers, retries: 0, deadlineS: 10 });
112
+ return { status: r.status, ok: r.status >= 200 && r.status < 300, json: async () => r.json() };
113
+ };
114
+ const wkRes = await call('GET', `${origin}/.well-known/fabric-service`, remote && token ? { [header]: value } : {});
98
115
  if (!wkRes.ok) throw new Error(`well-known document answered HTTP ${wkRes.status}`);
99
116
  const wk = await wkRes.json();
100
117
  const dashboard = wk.surfaces?.dashboard;
@@ -103,7 +120,7 @@ async function serviceTarget(spec, dir) {
103
120
  let url = origin + dashboard.path;
104
121
  if (dashboard.login) {
105
122
  if (!token) throw new Error(`no readable token file for ${id}.${instance}`);
106
- const res = await fetch(`${origin}/fabric/v1/login-code`, { method: 'POST', headers: { [header]: value } });
123
+ const res = await call('POST', `${origin}/fabric/v1/login-code`, { [header]: value });
107
124
  if (!res.ok) throw new Error(`login-code answered HTTP ${res.status}`);
108
125
  url = origin + (await res.json()).url;
109
126
  }
@@ -8,6 +8,13 @@
8
8
  --board-kit PATH (board.responder: the service's own fabric_board.py or fabric-board.mjs)
9
9
  --repo PATH (dashboard.opens-in-host: the service's checkout)
10
10
 
11
+ A local placement is read only when this OS account's process holds its port (DEC-0154 HA-2): `listener.owner`
12
+ comes before every rule that reads the port, from the computer's socket table (fabric_peer_owner). Nothing listening is NOT_RUN (`down`, and
13
+ `well-known.answers` fails as before); another OS account's, root's or SYSTEM's listener, or one whose owner cannot be
14
+ told, FAILs as `foreign-user`, and then no rule reads the port and no credential is sent; one `port.rules` NOT_RUN
15
+ line names the rules not read (as it does when the well-known document gives nothing to read). Every local
16
+ connection, a reconnect included, is checked again before a byte goes out on it.
17
+
11
18
  A remote placement (DEC-0019 — an online agent or dashboard at an https origin) is probed over
12
19
  TLS with the certificate verified: the well-known document must refuse a request without the
13
20
  token (401 with none of the service's data: an empty body, or an identity proxy's page, DEC-0109), the guards must refuse a foreign Host/Origin and cross-site requests,
@@ -33,6 +40,12 @@ Actions (fabric-actions/0.1, DEC-0121): `well-known.actions` holds a declared ex
33
40
  actions with no MCP surface; a remote one there is NOT_RUN, its twin may be on A2A), and `dashboard.actions-declared`
34
41
  is NOT_RUN, never FAIL, for a dashboard that declares none (a read-only dashboard cannot declare an empty list).
35
42
 
43
+ channel-agent.declaration (DEC-0150, FAC-SEM-162 to 165): extensions/channel-agent/0.1 under provider.extensions of the
44
+ manifest the descriptor names, or, with no manifest, under extensions of the well-known answer (CA-1). FAC-SEM-164's
45
+ obligations on a declaration still read FAIL, and so does a string shaped like a credential in it or in any copy beside
46
+ it; nothing declared, or a declaration read as not declared (a key elsewhere, another version, a form the schema or
47
+ FAC-SEM-163 refuses), is NOT_RUN.
48
+
36
49
  dashboard.opens-in-host (FAA-118, the operator rule of 2026-10-24): the service's checkout (--repo, else the one of
37
50
  the descriptor's source.repository that holds its commands, manifest or launchd job) opens the service's pages only
38
51
  through Fabric Dashboards. Tracked text files only, within a time budget; a browser opener aimed at its pages outside
@@ -66,6 +79,8 @@ import fabric_service as fs # noqa: E402
66
79
  import fabric_connections as fcn # noqa: E402
67
80
  from check_service_rules import shown_repository # noqa: E402 - a repository in evidence carries no userinfo or query
68
81
  import fabric_interop as fi # noqa: E402
82
+ import fabric_peer_owner as po # noqa: E402 - DEC-0154 HA-2: the listener's owner before any credential
83
+ from check_service_rules import ForeignUser, PORT_RULES_UNREAD, listener_owner_verdict # noqa: E402 - DEC-0154 HA-2
69
84
 
70
85
  Result = Dict[str, str]
71
86
 
@@ -85,6 +100,7 @@ from check_service_rules import well_known_actions_verdict, action_tools_verdict
85
100
  from check_service_rules import opens_in_host_verdict, service_checkout # noqa: E402 - FAA-118
86
101
  from check_service_rules import well_known_a2a_verdict, a2a_card_verdict # noqa: E402 - DEC-0122
87
102
  import fabric_a2a as fa2 # noqa: E402 - DEC-0122
103
+ import check_service_channel_agent as cac # noqa: E402 - DEC-0150, FAA-197
88
104
 
89
105
 
90
106
  class _PinnedHTTPS(http.client.HTTPSConnection):
@@ -146,13 +162,16 @@ class Probe:
146
162
  conn = http.client.HTTPSConnection(self.host_name, self.port, context=context, timeout=8)
147
163
  base = {"Host": self.netloc}
148
164
  else:
149
- conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5)
165
+ # DEC-0154 HA-2: every local connection, a reconnect included, is checked before a byte is sent on it.
166
+ conn = po.CheckedHTTPConnection("127.0.0.1", self.port, timeout=5)
150
167
  base = {"Host": "127.0.0.1:%d" % self.port}
151
168
  base.update(headers or {})
152
169
  try:
153
170
  conn.request(method, path, body=body, headers=base)
154
171
  resp = conn.getresponse()
155
172
  return resp.status, merge_headers(resp.getheaders()), resp.read()
173
+ except po.ListenerRefused as exc:
174
+ raise ForeignUser("foreign-user: %s" % exc) from None
156
175
  finally:
157
176
  conn.close()
158
177
 
@@ -161,6 +180,14 @@ class Probe:
161
180
  name, value = fs.token_header(self.d.get("auth"), self.token or "")
162
181
  return {name: value}
163
182
 
183
+ # the listener's owner (DEC-0154 HA-2) -------------------------------
184
+ def listener_rule(self) -> bool:
185
+ """DEC-0154 HA-2, `down` then `foreign-user` then the answer (check_service_rules.listener_owner_verdict). False
186
+ when the port must not be read (`foreign-user`)."""
187
+ verdict, evidence = listener_owner_verdict(self.port)
188
+ self.add("listener.owner", verdict, evidence)
189
+ return verdict != "FAIL"
190
+
164
191
  # descriptor ------------------------------------------------------------
165
192
  def descriptor_rules(self) -> None:
166
193
  problems = fs.validate_descriptor(self.d)
@@ -193,7 +220,7 @@ class Probe:
193
220
  elif "origin" not in self.d:
194
221
  unique = "an app's descriptor names no origin, so it claims no port (FAC-SEM-010)"
195
222
  else:
196
- unique = "port %d is unique" % self.port
223
+ unique = "no other descriptor here declares port %d (HA-1's claim on the computer is FAA-225)" % self.port
197
224
  self.add("descriptor.port-claim", "FAIL" if clashes else "PASS", "; ".join(clashes) or unique)
198
225
  # FAA-198: the repository that links the agent to its project; a new install is refused without it, an installed one
199
226
  # is named here (NOT_RUN is no pass) until its owner adds it.
@@ -248,11 +275,18 @@ class Probe:
248
275
  return
249
276
  try:
250
277
  self.wk = json.loads(body)
278
+ self.wk_bytes = len(body) # D4 of the 0.73.0 brief: a well-known answer shares a 2 MiB cap
251
279
  except ValueError:
252
280
  if self.remote:
253
281
  self.add_no_token(no_token, None)
254
282
  self.add("well-known.answers", "FAIL", "body is not JSON")
255
283
  return
284
+ if not isinstance(self.wk, dict):
285
+ self.wk = None
286
+ if self.remote:
287
+ self.add_no_token(no_token, None)
288
+ self.add("well-known.answers", "FAIL", "body is not a JSON object")
289
+ return
256
290
  if self.remote:
257
291
  self.add_no_token(no_token, self.wk)
258
292
  self.add("well-known.answers", "PASS", "HTTP 200")
@@ -638,6 +672,14 @@ class Probe:
638
672
  self.trace_rule(listing, sent)
639
673
  self.events_trace_rule(self.events)
640
674
 
675
+ def channel_agent_rule(self) -> None:
676
+ """DEC-0150 (FAA-197): the channel agent's declaration, read from the manifest the descriptor names, else from the
677
+ well-known answer (CA-1); one read as not declared is NOT_RUN with its evidence, never a FAIL (D3), except a string
678
+ shaped like a credential in it or in a copy the probe reads beside it (CA-5)."""
679
+ manifest, _, where = self.load_manifest()
680
+ self.add("channel-agent.declaration", *cac.declaration_verdict(
681
+ manifest, bool(self.d.get("fabricManifest")), where, self.wk, getattr(self, "wk_bytes", None)))
682
+
641
683
  def configuration_rule(self) -> None:
642
684
  """DEC-0046: a well-known `configuration` is {slots, resolved} as the schema types it; absent is NOT_RUN."""
643
685
  if not isinstance(self.wk, dict) or "configuration" not in self.wk:
@@ -883,8 +925,16 @@ class Probe:
883
925
  return self.results
884
926
  if not self.port:
885
927
  return self.results
928
+ if not self.remote and not self.listener_rule():
929
+ self.add("port.rules", "NOT_RUN", "not read: " + PORT_RULES_UNREAD + ", because the listener is not this OS "
930
+ "account's (listener.owner FAIL, DEC-0154 HA-2)")
931
+ self.lifecycle_rules()
932
+ return self.results
886
933
  self.well_known_rules()
887
- if self.wk is not None:
934
+ if self.wk is None:
935
+ self.add("port.rules", "NOT_RUN", "not read: " + PORT_RULES_UNREAD + ", because well-known.answers above "
936
+ "got no document to read")
937
+ else:
888
938
  self.network_rules()
889
939
  self.auth_rules()
890
940
  self.usage_rules()
@@ -893,6 +943,7 @@ class Probe:
893
943
  self.dashboard_page_rules()
894
944
  self.login_rules()
895
945
  self.interop_rules()
946
+ self.channel_agent_rule()
896
947
  verdict, evidence = board_responder_verdict(self.wk, self.board_kit)
897
948
  self.add("board.responder", verdict, evidence)
898
949
  self.lifecycle_rules()