@passioncode-ai/fabric-agent-adapter 0.72.0 → 0.73.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 (19) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +1 -1
  3. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  4. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  5. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  6. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +1 -0
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +3 -1
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +3 -2
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +17 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +841 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent_schema.py +496 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +91 -1
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/service-channel-agent-extension.schema.json +700 -0
  15. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  16. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +21 -8
  17. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/practices.md +15 -6
  18. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  19. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
@@ -0,0 +1,841 @@
1
+ """The probe's channel-agent rule (FAA-197): an agent that answers people on a channel declares
2
+ extensions/channel-agent/0.1 (Fabric Agent Contract DEC-0150) under provider.extensions of its manifest, or, for a
3
+ service with no manifest, under extensions of its well-known answer (CA-1). A port of the contract's
4
+ `src/channel-agent.ts` at the pin: channelAgentRules (FAC-SEM-162 to 165) and channelAgentView, held to the contract's
5
+ own vectors (test/fixtures/contract/channel-agent-vectors.json); the schema half is check_service_channel_agent_schema.py.
6
+
7
+ Strings are read as ECMAScript holds them where the contract counts or indexes UTF-16 units (an astral character is
8
+ two), and as code points where its regular expressions carry the `u` flag. No finding's message carries a declared
9
+ value: the instancePath places it (CA-5). Not a kit module (the probe has no Node twin). Standard library only."""
10
+ from __future__ import annotations
11
+
12
+ import ipaddress
13
+ import re
14
+ import sys
15
+ import unicodedata
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, List, Optional, Tuple
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ from fabric_jsvalue import indexed_objects, js_keys # noqa: E402
21
+ from fabric_secrets import JS_SPACE, JWT_SHAPE, SECRET_VALUE, _url_parts, raw_values, secret_argument, secret_url, secret_value # noqa: E402
22
+ import check_service_channel_agent_schema as cas # noqa: E402
23
+
24
+ KEY = "https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/0.1"
25
+ #: Any version of the key: one other than 0.1 is not read until a decision moves it (CA-7).
26
+ KEY_PREFIX = "https://fabric.passioncode.ai/agent-contract/extensions/channel-agent/"
27
+ KEY_TOKEN = KEY.replace("~", "~0").replace("/", "~1")
28
+ CHANNEL_AGENT_PATHS = {"manifest": "/provider/extensions/" + KEY_TOKEN, "well-known": "/extensions/" + KEY_TOKEN}
29
+ TOKEN_SLOT = re.compile(r"[a-z0-9][a-z0-9._-]{0,127}/[a-z][a-z0-9-]{0,31}/[A-Z][A-Z0-9_]{0,63}\Z")
30
+ NAME_CHARACTERS = re.compile(r"[A-Za-z0-9_-]{1,64}\Z")
31
+ SECRET_FINDINGS = 64
32
+ PATH_DEPTH = 64
33
+ DETECTOR_CODE_POINTS = 4097
34
+ LONGEST_STRING = cas.LONGEST_STRING
35
+ MESSAGE_ERRORS = 10
36
+ SECRET_BUDGET = 1_000_000
37
+ VISIT_COST = 16
38
+ PIECE_COST = 8 * VISIT_COST
39
+ ARG_PIECE_COST = 4
40
+ KEY_REREAD = 255
41
+ LONG_RUN_FLOOR = 40
42
+ SK_FLOOR = 16
43
+ PREFIX_FLOORS = ((re.compile(r"gh[pousr]_", re.I | re.A), 20), (re.compile(r"xox[abposr]-", re.I | re.A), 10),
44
+ (re.compile(r"akia", re.I | re.A), 16), (re.compile(r"glpat-", re.I | re.A), 20))
45
+ EM_DASH = "\u2014"
46
+ #: The 2 MiB cap Fabric Dashboards' probe puts on a well-known answer (until the contract's CT-223), and "near" it.
47
+ WELL_KNOWN_CAP = 2 * 1024 * 1024
48
+ WELL_KNOWN_NEAR = WELL_KNOWN_CAP * 3 // 4
49
+
50
+ _SK = re.compile(r"sk-", re.I | re.A)
51
+ _ESCAPE_BEFORE = re.compile(r"(?:%25[0-9A-Fa-f]{2}|%[0-9A-Fa-f]{2}|\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\[0-7]{2,3}|\\[^\n\r\u2028\u2029])\Z")
52
+ _ESC_BEFORE = re.compile(r"(?:\x1b|\\x1[bB]|\\0?33|\\u001[bB]|\\e)\Z")
53
+ _SEPARATOR = re.compile("[%s=/\\\\]" % JS_SPACE)
54
+ _SCHEME = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*:")
55
+ _HIGH_ESCAPE = re.compile(r"%[89A-Fa-f][0-9A-Fa-f]")
56
+ _RELATIVE_REF = re.compile(r"(?![/~])(?![A-Za-z][A-Za-z0-9+.-]*:)(?!(?:[^/]*/)*\.\.(?:/|\Z))[A-Za-z0-9._:/-]+\Z")
57
+ _OPAQUE_ID = re.compile(r"[A-Za-z0-9_-]{8,128}\Z")
58
+ _ASTRAL = re.compile("[\U00010000-\U0010ffff]")
59
+ _ID_MEMBERS = frozenset(("id", "code", "lead_agent", "owner", "decision", "answered_signal", "target", "spend_record", "project", "side"))
60
+ # Unicode's Default_Ignorable_Code_Point (DerivedCoreProperties), which Python's unicodedata does not carry.
61
+ _IGNORABLE = ((0x00AD, 0x00AD), (0x034F, 0x034F), (0x061C, 0x061C), (0x115F, 0x1160), (0x17B4, 0x17B5), (0x180B, 0x180F),
62
+ (0x200B, 0x200F), (0x202A, 0x202E), (0x2060, 0x206F), (0x3164, 0x3164), (0xFE00, 0xFE0F), (0xFEFF, 0xFEFF),
63
+ (0xFFA0, 0xFFA0), (0xFFF0, 0xFFF8), (0x1BCA0, 0x1BCA3), (0x1D173, 0x1D17A), (0xE0000, 0xE0FFF))
64
+ _FILLERS = frozenset("\u115f\u1160\u3164\uffa0\u2800\ufffc")
65
+
66
+
67
+ def utf16(text: str) -> str:
68
+ """The text as ECMAScript's UTF-16 units, one Python character each (an astral character is a surrogate pair)."""
69
+ if not _ASTRAL.search(text):
70
+ return text
71
+ return "".join(c if ord(c) < 0x10000 else chr(0xD800 + ((ord(c) - 0x10000) >> 10)) + chr(0xDC00 + ((ord(c) - 0x10000) & 0x3FF))
72
+ for c in text)
73
+
74
+
75
+ def _blank_char(char: str) -> bool:
76
+ code = ord(char)
77
+ 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))
79
+
80
+
81
+ def blank(text: str) -> bool:
82
+ """Nothing left after white space, \\p{Cf}, \\p{Cc}, \\p{Z}, \\p{Default_Ignorable_Code_Point} and the fillers."""
83
+ return all(_blank_char(char) for char in text)
84
+
85
+
86
+ class _Frame:
87
+ __slots__ = ("container", "keys", "next", "at", "depth", "name", "parent", "collapsed", "role")
88
+
89
+ def __init__(self, container: Any, at: str, depth: int, name: Optional[str], parent: Optional[str], collapsed: bool, role: Optional[str]):
90
+ self.container = container
91
+ self.keys = None if isinstance(container, list) else js_keys(container)
92
+ self.next = 0
93
+ self.at, self.depth, self.name, self.parent, self.collapsed, self.role = at, depth, name, parent, collapsed, role
94
+
95
+
96
+ class _Spot:
97
+ __slots__ = ("frame", "key", "is_name")
98
+
99
+ def __init__(self, frame: Optional[_Frame], key: str, is_name: bool):
100
+ self.frame, self.key, self.is_name = frame, key, is_name
101
+
102
+
103
+ _ROLES = {"root": {"projects": "projects"}, "project": {"evals": "evals", "tickets": "tickets", "handoff_queues": "queues", "channels": "channels"},
104
+ "channel": {"texts": "texts"}}
105
+
106
+
107
+ def _role_of(holder: Optional[str], key: str, index: bool) -> Optional[str]:
108
+ if holder == "texts":
109
+ return "texts"
110
+ if index:
111
+ return {"projects": "project", "queues": "queue", "channels": "channel"}.get(holder or "")
112
+ return _ROLES.get(holder or "", {}).get(key)
113
+
114
+
115
+ def walk(value: Any, root: str, token_of: Callable[[_Frame, str, bool], Optional[str]], visit: Callable[[str, _Spot, Callable[[_Spot], str]], None],
116
+ stop: Callable[[], bool] = lambda: False, on_enter: Callable[[], None] = lambda: None) -> None:
117
+ """Every member name and string value under `value`, with an explicit stack of frames (the contract's `walk`)."""
118
+ def token(frame: _Frame, key: str) -> Optional[str]:
119
+ return None if frame.collapsed or frame.depth >= PATH_DEPTH else token_of(frame, key, frame.keys is None)
120
+
121
+ def place(spot: _Spot) -> str:
122
+ frame = spot.frame
123
+ if frame is None:
124
+ return root
125
+ written = None if spot.is_name else token(frame, spot.key)
126
+ return frame.at if written is None else "%s/%s" % (frame.at, written)
127
+
128
+ if isinstance(value, str) and not stop():
129
+ visit(value, _Spot(None, "", False), place)
130
+ if not isinstance(value, (list, dict)):
131
+ if not isinstance(value, str):
132
+ on_enter()
133
+ return
134
+ on_enter()
135
+ stack = [_Frame(value, root, 0, None, None, False, "root")]
136
+ while stack and not stop():
137
+ frame = stack[-1]
138
+ length = len(frame.container) if frame.keys is None else len(frame.keys)
139
+ if frame.next >= length:
140
+ stack.pop()
141
+ continue
142
+ i = frame.next
143
+ frame.next += 1
144
+ index = frame.keys is None
145
+ key = str(i) if index else frame.keys[i]
146
+ child = frame.container[i] if index else frame.container[key]
147
+ if not index:
148
+ visit(key, _Spot(frame, key, True), place)
149
+ if isinstance(child, str):
150
+ visit(child, _Spot(frame, key, False), place)
151
+ elif isinstance(child, (list, dict)):
152
+ on_enter()
153
+ if len(child) == 0:
154
+ continue
155
+ written = token(frame, key)
156
+ stack.append(_Frame(child, frame.at if written is None else "%s/%s" % (frame.at, written), frame.depth + 1, key, frame.name,
157
+ written is None, _role_of(frame.role, key, index)))
158
+ else:
159
+ on_enter()
160
+
161
+
162
+ def oversize(value: Any) -> List[str]:
163
+ """What is refused before the schema runs (CA-2): widths, and a string or name over LONGEST_STRING code points."""
164
+ names = cas.schema_names()
165
+ long: Dict[str, None] = {}
166
+
167
+ def visit(text: str, spot: _Spot, place: Callable[[_Spot], str]) -> None:
168
+ if len(text) > LONGEST_STRING:
169
+ long["maxLength at %s (at most %d code points)" % (place(spot) or "/", LONGEST_STRING)] = None
170
+ walk(value, "", lambda frame, key, index: cas.placed_token(frame.name, frame.parent, key, index, names, cas.is_none(frame.container)), visit)
171
+ return cas.widths(value) + list(long)
172
+
173
+
174
+ def _extensions_at(doc: Dict[str, Any], site: str) -> Optional[Dict[str, Any]]:
175
+ holder = doc.get("provider") if site == "manifest" else doc
176
+ return holder["extensions"] if isinstance(holder, dict) and isinstance(holder.get("extensions"), dict) else None
177
+
178
+
179
+ def schema_view(doc: Any, site: str) -> Dict[str, Any]:
180
+ """The schema's verdict on the declaration at one site (CA-1, CA-2), before the reader's rules."""
181
+ extensions = _extensions_at(doc, site) if isinstance(doc, dict) else None
182
+ if extensions is None or KEY not in extensions:
183
+ return {"declared": False}
184
+ value = extensions[KEY]
185
+ refused = oversize(value)
186
+ errors = refused if refused else cas.schema_errors(value)
187
+ if errors:
188
+ return {"declared": True, "valid": False, "errors": errors}
189
+ return {"declared": True, "valid": True, "standard": value["standard"], "projects": value["projects"]}
190
+
191
+
192
+ # The detector (FAC-SEM-101's, fabric_secrets.py) and what reading with it costs.
193
+
194
+ def head(text: str) -> str:
195
+ """The first DETECTOR_CODE_POINTS code points: all the detector reads."""
196
+ return text[:DETECTOR_CODE_POINTS]
197
+
198
+
199
+ def read_as_url(read: str) -> bool:
200
+ """A string's head is read as a URL when, leading C0 controls and spaces stripped and tab, line feed and carriage
201
+ return removed, it is ASCII only, holds no percent-escape of a byte at or above 0x80 and opens with a scheme (CA-5)."""
202
+ text = re.sub(r"[\t\n\r]", "", re.sub(r"\A[\x00-\x20]+", "", read))
203
+ return text.isascii() and not _HIGH_ESCAPE.search(text) and bool(_SCHEME.match(text))
204
+
205
+
206
+ def _url_reading(read: str) -> bool:
207
+ """secretUrl where read_as_url admits the head, secretArgument otherwise. The kit's secret_url also reads a URL the
208
+ WHATWG parser refuses (the safe direction for its own callers); the contract's `new URL` throws there and finds only
209
+ what secretArgument finds, so a refused URL is read by secret_argument alone here."""
210
+ if not read_as_url(read):
211
+ return secret_argument(read)
212
+ return secret_argument(read) or (url_parts(read) is not None and secret_url(read))
213
+
214
+
215
+ def shaped_read(read: str) -> bool:
216
+ return secret_value(read) or _url_reading(read)
217
+
218
+
219
+ def _key_shaped(text: str) -> bool:
220
+ read = head(text)
221
+ return bool(JWT_SHAPE.search(read) or SECRET_VALUE.search(read))
222
+
223
+
224
+ def plain_name(name: str) -> bool:
225
+ return bool(NAME_CHARACTERS.match(name)) and not re.search(r"[0-9]{7}", re.sub(r"[-_]", "", name)) and not shaped_read(head(name))
226
+
227
+
228
+ def _relative_ref(text: str) -> bool:
229
+ return 1 <= len(text) <= 256 and bool(_RELATIVE_REF.match(text))
230
+
231
+
232
+ def _run_from(text: str, start: int) -> int:
233
+ end = start
234
+ while end < len(text) and (text[end].isascii() and (text[end].isalnum() or text[end] in "_-")):
235
+ end += 1
236
+ return end
237
+
238
+
239
+ def jwt_rereads(text: str) -> int:
240
+ """From each `eyJ`, its base64url run, `eyJ` included, and the dot and the run after it when the run ends at one."""
241
+ cost, reach, run_end = 0, 0, 0
242
+ at = text.find("eyJ")
243
+ while at >= 0:
244
+ if run_end <= at:
245
+ run_end = _run_from(text, at)
246
+ reach = _run_from(text, run_end + 1) if run_end < len(text) and text[run_end] == "." else run_end
247
+ cost += reach - at
248
+ at = text.find("eyJ", at + 1)
249
+ return cost
250
+
251
+
252
+ def _units(text: str, limit: int, start: int) -> int:
253
+ return max(0, min(limit, len(text) - start))
254
+
255
+
256
+ def _colour_before(text: str, at: int) -> bool:
257
+ if at < 1 or text[at - 1] != "m":
258
+ return False
259
+ start = at - 2
260
+ while start >= 0 and text[start] in "0123456789;:":
261
+ start -= 1
262
+ return start >= 0 and text[start] == "[" and bool(_ESC_BEFORE.search(text[max(0, start - 6):start]))
263
+
264
+
265
+ def key_rereads(text: str) -> int:
266
+ """From each `sk-` at a boundary up to KEY_REREAD units, SK_FLOOR after one elsewhere, a floor after each other prefix."""
267
+ cost = 0
268
+ match = _SK.search(text)
269
+ while match:
270
+ at = match.start()
271
+ before = text[at - 1] if at > 0 else ""
272
+ boundary = (at == 0 or not (before.isascii() and before.isalnum()) or bool(_ESCAPE_BEFORE.search(text[max(0, at - 6):at]))
273
+ or _colour_before(text, at))
274
+ cost += _units(text, KEY_REREAD, at) if boundary else _units(text, SK_FLOOR, at + 3)
275
+ match = _SK.search(text, at + 1)
276
+ for prefix, floor in PREFIX_FLOORS:
277
+ match = prefix.search(text)
278
+ while match:
279
+ cost += _units(text, floor, match.end())
280
+ match = prefix.search(text, match.start() + 1)
281
+ return cost
282
+
283
+
284
+ def run_rereads(text: str) -> int:
285
+ """For each maximal base64 run shorter than LONG_RUN_FLOOR in each of three partitions, min(length, 39) per unit."""
286
+ def charge(run: int) -> int:
287
+ return run * min(run, LONG_RUN_FLOOR - 1) if run < LONG_RUN_FLOOR else 0
288
+ cost = whole = no_equals = no_slash = 0
289
+ i, length = 0, len(text)
290
+ while i <= length:
291
+ c = ord(text[i]) if i < length else -1
292
+ if 65 <= c <= 90 or 97 <= c <= 122 or 48 <= c <= 57 or c in (43, 95, 45):
293
+ whole += 1
294
+ no_equals += 1
295
+ no_slash += 1
296
+ elif c == 47:
297
+ whole += 1
298
+ no_equals += 1
299
+ cost += charge(no_slash)
300
+ no_slash = 0
301
+ elif c == 61:
302
+ whole += 1
303
+ cost += charge(no_equals) + charge(no_slash)
304
+ no_equals = no_slash = 0
305
+ else:
306
+ cost += charge(whole) + charge(no_equals) + charge(no_slash)
307
+ whole = no_equals = no_slash = 0
308
+ if 0xD800 <= c <= 0xDBFF and i + 1 < length and 0xDC00 <= ord(text[i + 1]) <= 0xDFFF:
309
+ i += 1
310
+ i += 1
311
+ return cost
312
+
313
+
314
+ def argument_pieces(text: str) -> int:
315
+ return ARG_PIECE_COST * len(_SEPARATOR.findall(text))
316
+
317
+
318
+ def _ipv4_number(part: str) -> Optional[int]:
319
+ if part == "":
320
+ return None
321
+ base, digits = 10, part
322
+ if len(part) >= 2 and part[:2] in ("0x", "0X"):
323
+ base, digits = 16, part[2:]
324
+ elif len(part) >= 2 and part[0] == "0":
325
+ base, digits = 8, part[1:]
326
+ if digits == "":
327
+ return 0
328
+ try:
329
+ return int(digits, base) if re.fullmatch({10: "[0-9]+", 16: "[0-9A-Fa-f]+", 8: "[0-7]+"}[base], digits) else None
330
+ except ValueError:
331
+ return None
332
+
333
+
334
+ def _host_ok(host: str, special: bool) -> bool:
335
+ """Whether the WHATWG URL parser takes an ASCII host (its failures only: forbidden code points, IPv6, IPv4)."""
336
+ if host.startswith("["):
337
+ if not host.endswith("]") or "%" in host:
338
+ return False
339
+ try:
340
+ ipaddress.IPv6Address(host[1:-1])
341
+ return True
342
+ except ValueError:
343
+ return False
344
+ forbidden = "\x00\t\n\r #/:<>?@[\\]^|"
345
+ if not special:
346
+ return not any(c in forbidden for c in host)
347
+ try:
348
+ decoded = re.sub(r"%([0-9A-Fa-f]{2})", lambda m: chr(int(m.group(1), 16)), host).lower()
349
+ except ValueError:
350
+ return False
351
+ if decoded == "" or any(c in forbidden or c == "%" or ord(c) < 0x20 or ord(c) == 0x7F for c in decoded):
352
+ return False
353
+ # An `xn--` label is not decoded: Node's parser (the reference runs on it) takes every ASCII one.
354
+ parts = decoded.split(".")
355
+ if parts[-1] == "" and len(parts) > 1:
356
+ parts = parts[:-1]
357
+ last = parts[-1]
358
+ if not (last and (last.isdigit() or (_ipv4_number(last) is not None and last[:2] in ("0x", "0X")))):
359
+ return True
360
+ numbers = [_ipv4_number(part) for part in parts]
361
+ if len(parts) > 4 or any(n is None for n in numbers) or any(n > 255 for n in numbers[:-1]):
362
+ return False
363
+ return numbers[-1] < 256 ** (5 - len(numbers))
364
+
365
+
366
+ _SPECIAL = ("ftp", "http", "https", "ws", "wss", "file")
367
+
368
+
369
+ def url_parts(text: str) -> Optional[Tuple[str, str]]:
370
+ """`new URL(text)`'s search and hash for an ASCII string read as a URL, or None where the parser throws."""
371
+ text = re.sub(r"[\t\n\r]", "", text.strip("".join(map(chr, range(0x21)))))
372
+ scheme, _, rest = text.partition(":")
373
+ lowered = scheme.lower()
374
+ special = lowered in _SPECIAL
375
+ body = rest.partition("#")[0].partition("?")[0]
376
+ authority: Optional[str] = None
377
+ if special and lowered != "file":
378
+ authority = re.split(r"[/\\]", body.lstrip("/\\"), maxsplit=1)[0]
379
+ elif lowered == "file" and re.match(r"[/\\]{2}", body):
380
+ authority = re.split(r"[/\\]", body[2:], maxsplit=1)[0]
381
+ elif not special and body.startswith("//"):
382
+ authority = body[2:].partition("/")[0]
383
+ if authority is not None and lowered == "file":
384
+ # A file host takes no user information and no port: the whole authority is the host.
385
+ if authority and authority.lower() != "localhost" and not _host_ok(authority, True):
386
+ return None
387
+ elif authority is not None:
388
+ userinfo, at, hostport = authority.rpartition("@")
389
+ bracket = hostport.rfind("]")
390
+ colon = hostport.rfind(":")
391
+ host, port = (hostport[:colon], hostport[colon + 1:]) if colon > bracket else (hostport, "")
392
+ if port and (not port.isdigit() or int(port) > 65535):
393
+ return None
394
+ if at and hostport == "":
395
+ return None
396
+ if host == "" and special and lowered != "file":
397
+ return None
398
+ if host and not _host_ok(host, special):
399
+ return None
400
+ _, query, fragment = _url_parts(text)
401
+ has_query = "?" in rest.partition("#")[0]
402
+ has_fragment = "#" in rest
403
+ return ("?" + query if has_query and query else "", "#" + fragment if has_fragment and fragment else "")
404
+
405
+
406
+ def detector_cost(whole: str, is_head: bool = False) -> int:
407
+ """The UTF-16 units FAC-SEM-165's detector may read in a string's head (the contract's detectorCost)."""
408
+ text = whole if is_head else head(whole)
409
+ units = utf16(text)
410
+ cost = len(units) + jwt_rereads(units) + key_rereads(units) + run_rereads(units) + argument_pieces(units)
411
+ if not read_as_url(text):
412
+ return cost
413
+ cost += PIECE_COST
414
+ parts = url_parts(text)
415
+ if parts is None:
416
+ return cost
417
+ for part in parts:
418
+ cost += ARG_PIECE_COST * part.count("&")
419
+ for name, values in raw_values(part[1:]).items():
420
+ name_units = len(utf16(name))
421
+ upper = bool(re.search(r"[A-Z]", name))
422
+ for value in values:
423
+ value_units = utf16(value)
424
+ cost += PIECE_COST + name_units + len(value_units) + jwt_rereads(value_units) + key_rereads(value_units) + run_rereads(value_units)
425
+ if value != "" and upper:
426
+ cost += name_units * (name_units + 1) // 2
427
+ return cost
428
+
429
+
430
+ class Budget:
431
+ """What is left of FAC-SEM-165's reading for one document, shared by every copy of its declaration."""
432
+
433
+ def __init__(self) -> None:
434
+ self.left = SECRET_BUDGET
435
+ self.unread = False
436
+ self.named = 0
437
+ self.more = 0
438
+ self.unread_at: Optional[str] = None
439
+ self.copies: set = set()
440
+
441
+
442
+ def _finding(code: str, at: str, message: str) -> Dict[str, str]:
443
+ return {"code": code, "instancePath": at, "message": message}
444
+
445
+
446
+ def secrets(value: Any, root: str, budget: Budget) -> List[Dict[str, str]]:
447
+ """FAC-SEM-165: no member name and no string of the declaration is shaped like a credential (CA-5)."""
448
+ findings: List[Dict[str, str]] = []
449
+ names = cas.schema_names()
450
+ more = [0]
451
+ last: Dict[str, Any] = {"frame": None, "key": "", "plain": False}
452
+
453
+ def flag(at: str, what: str, text: str, ident: bool) -> None:
454
+ if budget.named < SECRET_FINDINGS:
455
+ budget.named += 1
456
+ rename = "; an id that starts with sk- is read as a key; rename it" if ident and re.match(r"sk-", text, re.I | re.A) else ""
457
+ findings.append(_finding("FAC-SEM-165", at, "%s is shaped like a credential: a token is named by its vault slot, never written%s (DEC-0150 CA-5)" % (what, rename)))
458
+ else:
459
+ budget.more += 1
460
+ more[0] += 1
461
+
462
+ def afford(read: str) -> bool:
463
+ if budget.unread:
464
+ return False
465
+ cost = (0 if read == "" else detector_cost(read, True)) + VISIT_COST
466
+ if cost > budget.left:
467
+ budget.unread, budget.unread_at = True, root
468
+ return False
469
+ budget.left -= cost
470
+ return True
471
+
472
+ def token_of(frame: _Frame, key: str, index: bool) -> Optional[str]:
473
+ if index:
474
+ return cas.placed_token(frame.name, frame.parent, key, index, names)
475
+ plain = last["frame"] is frame and last["key"] == key and last["plain"]
476
+ return cas.placed_token(frame.name, frame.parent, key, index, names, cas.is_none(frame.container)) if plain else None
477
+
478
+ def in_texts(frame: Optional[_Frame]) -> bool:
479
+ return frame is not None and frame.role == "texts"
480
+
481
+ def visit(text: str, spot: _Spot, place: Callable[[_Spot], str]) -> None:
482
+ read = head(text)
483
+ if spot.is_name:
484
+ last.update(frame=spot.frame, key=text, plain=False)
485
+ if not afford(read):
486
+ return
487
+ is_shaped = shaped_read(read)
488
+ last["plain"] = not is_shaped and bool(NAME_CHARACTERS.match(text)) and not re.search(r"[0-9]{7}", re.sub(r"[-_]", "", text))
489
+ if is_shaped:
490
+ flag(place(spot), "a member's name", text, not in_texts(spot.frame))
491
+ return
492
+ if not afford(read):
493
+ return
494
+ role = spot.frame.role if spot.frame is not None else None
495
+ key = spot.key
496
+ if role == "tickets" and key == "token_slot" and TOKEN_SLOT.match(text):
497
+ found = any(secret_value(segment) for segment in text.split("/"))
498
+ elif ((role == "evals" and key == "suite") or (role == "tickets" and key == "outbox")) and _relative_ref(text):
499
+ found = _url_reading(read)
500
+ elif role == "queue" and key == "recommendation" and _OPAQUE_ID.match(text):
501
+ found = _key_shaped(text)
502
+ else:
503
+ found = shaped_read(read)
504
+ if found:
505
+ flag(place(spot), "a value", text, key in _ID_MEMBERS and not in_texts(spot.frame))
506
+
507
+ def on_enter() -> None:
508
+ if budget.unread:
509
+ return
510
+ if VISIT_COST > budget.left:
511
+ budget.unread, budget.unread_at = True, root
512
+ return
513
+ budget.left -= VISIT_COST
514
+
515
+ walk(value, root, token_of, visit, lambda: budget.unread, on_enter)
516
+ if more[0] > 0:
517
+ findings.append(_finding("FAC-SEM-165", root, "and %d more names or values shaped like a credential (DEC-0150 CA-5)" % more[0]))
518
+ return findings
519
+
520
+
521
+ def summary(budget: Budget) -> List[Dict[str, str]]:
522
+ """The document's closing FAC-SEM-165 finding, once: that the rest was not read."""
523
+ if not budget.unread:
524
+ return []
525
+ at = budget.unread_at or ""
526
+ fix = ("this copy is not read by a reader and should be removed" if at in budget.copies
527
+ else "a reader treats the declaration as not declared; declare fewer or shorter texts or names")
528
+ return [_finding("FAC-SEM-165", at, "reading the declarations for credentials takes more than %d UTF-16 units of reading; %s (DEC-0150 CA-5)" % (SECRET_BUDGET, fix))]
529
+
530
+
531
+ def misplaced(doc: Dict[str, Any], budget: Budget) -> List[Dict[str, str]]:
532
+ """FAC-SEM-162 on a manifest: the key where a reader does not read it (the manifest's own extensions, a capability's)."""
533
+ findings: List[Dict[str, str]] = []
534
+
535
+ def flag(extensions: Dict[str, Any], at: str) -> None:
536
+ root = "%s/%s" % (at, KEY_TOKEN)
537
+ findings.append(_finding("FAC-SEM-162", root, "a reader reads a manifest's channel-agent declaration under provider.extensions only, so it is not read here (DEC-0150 CA-1)"))
538
+ budget.copies.add(root)
539
+ findings.extend(secrets(extensions[KEY], root, budget))
540
+ if isinstance(doc.get("extensions"), dict) and KEY in doc["extensions"]:
541
+ flag(doc["extensions"], "/extensions")
542
+ for capability, i in indexed_objects(doc.get("capabilities")):
543
+ if isinstance(capability.get("extensions"), dict) and KEY in capability["extensions"]:
544
+ flag(capability["extensions"], "/capabilities/%d/extensions" % i)
545
+ return findings
546
+
547
+
548
+ class _Cap:
549
+ def __init__(self) -> None:
550
+ self.named = 0
551
+ self.more = 0
552
+
553
+
554
+ def _capped(findings: List[Dict[str, str]], cap: _Cap, code: str, at: Callable[[], str], message: str) -> None:
555
+ if cap.named < SECRET_FINDINGS:
556
+ cap.named += 1
557
+ findings.append(_finding(code, at(), message))
558
+ else:
559
+ cap.more += 1
560
+
561
+
562
+ def closing(code: str, cap: _Cap, root: str) -> List[Dict[str, str]]:
563
+ if cap.more == 0:
564
+ return []
565
+ what = ("repeated ids or decisions not named (DEC-0150 CA-3)" if code == "FAC-SEM-163" else
566
+ "places that owe a text, a recommendation or a spend record, or carry a blank or refused text (DEC-0150 CA-4)")
567
+ return [_finding(code, root, "and %d more %s" % (cap.more, what))]
568
+
569
+
570
+ def _repeats(items: List[Tuple[Dict[str, Any], int]], member: str) -> List[int]:
571
+ seen: List[Any] = []
572
+ found = []
573
+ for item, i in items:
574
+ value = item.get(member)
575
+ if value in seen:
576
+ found.append(i)
577
+ seen.append(value)
578
+ return found
579
+
580
+
581
+ def references(project: Dict[str, Any], at: str, cap: _Cap) -> List[Dict[str, str]]:
582
+ """FAC-SEM-163: ids once, and every channel held by a decision names a recorded one (CA-3)."""
583
+ findings: List[Dict[str, str]] = []
584
+
585
+ def flag(path: str, message: str) -> None:
586
+ _capped(findings, cap, "FAC-SEM-163", lambda: at + path, "%s (DEC-0150 CA-3)" % message)
587
+ for i in _repeats(indexed_objects(project["taxonomy"].get("codes")), "code"):
588
+ flag("/taxonomy/codes/%d/code" % i, "this category code repeats an earlier one")
589
+ for i in _repeats(indexed_objects(project.get("tiers")), "code"):
590
+ flag("/tiers/%d/code" % i, "this tier code repeats an earlier one")
591
+ channels = indexed_objects(project.get("channels"))
592
+ for i in _repeats(channels, "id"):
593
+ flag("/channels/%d/id" % i, "this channel id repeats an earlier channel's")
594
+ source_ids: List[Any] = []
595
+ for member, message in (("sources", "this source id repeats an earlier source's"),
596
+ ("declined_sources", "this declined source's id repeats a source read or declined before it")):
597
+ for source, i in indexed_objects(project.get(member)):
598
+ if source.get("id") in source_ids:
599
+ flag("/%s/%d/id" % (member, i), message)
600
+ source_ids.append(source.get("id"))
601
+ for i in _repeats(indexed_objects(project.get("handoff_queues")), "id"):
602
+ flag("/handoff_queues/%d/id" % i, "this hand-off queue id repeats an earlier queue's")
603
+ decisions = indexed_objects(project.get("recorded_decisions"))
604
+ for i in _repeats(decisions, "id"):
605
+ flag("/recorded_decisions/%d/id" % i, "this decision id repeats an earlier recorded decision's")
606
+ recorded = [decision.get("id") for decision, _ in decisions]
607
+ for channel, i in channels:
608
+ if channel.get("decision") is None:
609
+ if channel.get("replies_enabled") is False:
610
+ flag("/channels/%d/decision" % i, "a channel with replies off names the recorded decision that holds it")
611
+ elif channel["decision"] not in recorded:
612
+ flag("/channels/%d/decision" % i, "this channel's decision is not one recorded_decisions lists")
613
+ return findings
614
+
615
+
616
+ def obligations(project: Dict[str, Any], at: str, cap: _Cap) -> List[Dict[str, str]]:
617
+ """FAC-SEM-164: what a channel, a queue and the budgets owe (CA-4)."""
618
+ findings: List[Dict[str, str]] = []
619
+
620
+ def flag(path: Callable[[], str], message: str) -> None:
621
+ _capped(findings, cap, "FAC-SEM-164", lambda: at + path(), "%s (DEC-0150 CA-4)" % message)
622
+ for channel, i in indexed_objects(project.get("channels")):
623
+ texts = channel["texts"]
624
+ replies, holding = channel.get("replies_enabled") is True, channel.get("holding_reply") is True
625
+ final = texts.get("final")
626
+ if replies and holding and "holding" not in texts:
627
+ flag(lambda i=i: "/channels/%d/texts/holding" % i, "a channel that sends a holding reply declares the holding text")
628
+ if replies and "final" not in texts:
629
+ flag(lambda i=i: "/channels/%d/texts/final" % i, "a channel that replies declares the final text")
630
+ if replies and holding and isinstance(final, dict) and final.get("none") == "not_used":
631
+ flag(lambda i=i: "/channels/%d/texts/final" % i, "a channel that sends a holding reply owes a final reply, so its final text is not one it does not use")
632
+ if channel.get("kind") == "store_review" and channel.get("clarifying_question") is True:
633
+ flag(lambda i=i: "/channels/%d/clarifying_question" % i, "a store review gets one public reply and no clarifying question")
634
+ for which in ("holding", "final"):
635
+ if which == "final" and cas.is_none(final):
636
+ continue
637
+ entries = texts.get(which) or {}
638
+ for language in js_keys(entries):
639
+ text = entries[language]
640
+
641
+ def place(i: int = i, which: str = which, language: str = language) -> str:
642
+ return "/channels/%d/texts/%s%s" % (i, which, "/*" if plain_name(language) else "")
643
+ if blank(text):
644
+ flag(place, "a declared text is blank, and nothing empty is sent")
645
+ elif EM_DASH in text:
646
+ flag(place, "a declared text carries U+2014, which a send's checks refuse")
647
+ for tier, i in indexed_objects(project.get("tiers")):
648
+ label = tier["label"]
649
+ if blank(label):
650
+ flag(lambda i=i: "/tiers/%d/label" % i, "a tier's label is blank, and nothing empty is shown")
651
+ elif EM_DASH in label:
652
+ flag(lambda i=i: "/tiers/%d/label" % i, "a tier's label carries U+2014, which a send's checks refuse")
653
+ for queue, i in indexed_objects(project.get("handoff_queues")):
654
+ if queue.get("answered_signal") == "none" and "recommendation" not in queue:
655
+ flag(lambda i=i: "/handoff_queues/%d/recommendation" % i, "a hand-off queue with no answered signal names the recommendation to its owner")
656
+ if queue.get("answered_signal") != "none" and "recommendation" in queue:
657
+ flag(lambda i=i: "/handoff_queues/%d/recommendation" % i, "a hand-off queue with an answered signal needs no recommendation, and a reader does not read this one")
658
+ evals = project["evals"]
659
+ if (evals["judge_budget_usd"] > 0 or evals["replay_budget_usd"] > 0) and evals.get("spend_record") is None:
660
+ flag(lambda: "/evals/spend_record", "a budget above 0 names the usage report's limit it is spent under; without one it reads 0")
661
+ return findings
662
+
663
+
664
+ def _repeat(at: str, cap: _Cap) -> List[Dict[str, str]]:
665
+ findings: List[Dict[str, str]] = []
666
+ _capped(findings, cap, "FAC-SEM-163", lambda: at + "/project", "this project repeats an earlier entry's (DEC-0150 CA-3)")
667
+ return findings
668
+
669
+
670
+ def channel_agent_rules(doc: Any, site: str = "manifest") -> List[Dict[str, str]]:
671
+ """DEC-0150's checks on a manifest or a well-known answer: FAC-SEM-162 a misplaced (manifest only) or invalid
672
+ declaration, FAC-SEM-165 credential shapes, FAC-SEM-163 and FAC-SEM-164 per project of a valid one."""
673
+ if not isinstance(doc, dict):
674
+ return []
675
+ budget = Budget()
676
+ view = schema_view(doc, site)
677
+ root = CHANNEL_AGENT_PATHS[site]
678
+ read = secrets(_extensions_at(doc, site)[KEY], root, budget) if view["declared"] else []
679
+ findings = misplaced(doc, budget) if site == "manifest" else []
680
+ if not view["declared"]:
681
+ return findings + summary(budget)
682
+ findings += read + summary(budget)
683
+ if not view["valid"]:
684
+ named = ", ".join(cas.message_places(view["errors"][:MESSAGE_ERRORS]))
685
+ more = " and %d more" % (len(view["errors"]) - MESSAGE_ERRORS) if len(view["errors"]) > MESSAGE_ERRORS else ""
686
+ findings.append(_finding("FAC-SEM-162", root, "the channel-agent declaration fails its schema: %s%s; a reader treats it as not declared and "
687
+ "reports it (DEC-0150 CA-2). The grammars are in docs/specification/channel-agent.md#the-declaration" % (named, more)))
688
+ return findings
689
+ seen: List[Any] = []
690
+ repeated, owing, owed = _Cap(), _Cap(), []
691
+ for i, project in enumerate(view["projects"]):
692
+ at = "%s/projects/%d" % (root, i)
693
+ if project.get("project") in seen:
694
+ findings += _repeat(at, repeated)
695
+ seen.append(project.get("project"))
696
+ findings += references(project, at, repeated)
697
+ owed += obligations(project, at, owing)
698
+ return findings + closing("FAC-SEM-163", repeated, root) + owed + closing("FAC-SEM-164", owing, root)
699
+
700
+
701
+ def view_line(finding: Dict[str, str]) -> str:
702
+ """A view's line: the rule, the place and the finding's reason, its message without the decision it cites."""
703
+ return "%s at %s: %s" % (finding["code"], finding["instancePath"] or "/", re.sub(r" \(DEC-0150 CA-[0-9]\)\Z", "", finding["message"]))
704
+
705
+
706
+ def schema_line(error: str) -> str:
707
+ parts = re.match(r"(\S+) at (/\S*)(.*)\Z", error)
708
+ return "FAC-SEM-162 at /: %s" % error if parts is None else "FAC-SEM-162 at %s: %s%s" % (parts.group(2), parts.group(1), parts.group(3))
709
+
710
+
711
+ def _once(lines: List[str]) -> List[str]:
712
+ return list(dict.fromkeys(lines))
713
+
714
+
715
+ def channel_agent_view(doc: Any, site: str = "manifest") -> Dict[str, Any]:
716
+ """The reader of extensions/channel-agent/0.1 at one site (CA-1, CA-2, CA-5): declared, reported and errors, and for
717
+ a read and valid declaration its standard, projects and FAC-SEM-164's findings as obligations."""
718
+ view = schema_view(doc, site)
719
+ budget = Budget()
720
+ if not view["declared"]:
721
+ copies = misplaced(doc, budget) + summary(budget) if site == "manifest" and isinstance(doc, dict) else []
722
+ return {"declared": False, "reported": True, "errors": _once([view_line(f) for f in copies])} if copies else {"declared": False, "reported": False, "errors": []}
723
+ value = _extensions_at(doc, site)[KEY]
724
+ credentials = [view_line(f) for f in secrets(value, "", budget) + summary(budget)]
725
+ if not view["valid"]:
726
+ lines = [schema_line(error) for error in view["errors"][:SECRET_FINDINGS]]
727
+ if len(view["errors"]) > SECRET_FINDINGS:
728
+ lines.append("FAC-SEM-162 at /: and %d more" % (len(view["errors"]) - SECRET_FINDINGS))
729
+ return {"declared": False, "reported": True, "errors": _once(lines + credentials)}
730
+ repeated, owing = _Cap(), _Cap()
731
+ blocking: List[Dict[str, str]] = []
732
+ owed: List[Dict[str, str]] = []
733
+ seen: List[Any] = []
734
+ for i, project in enumerate(view["projects"]):
735
+ at = "/projects/%d" % i
736
+ if project.get("project") in seen:
737
+ blocking += _repeat(at, repeated)
738
+ seen.append(project.get("project"))
739
+ blocking += references(project, at, repeated)
740
+ owed += obligations(project, at, owing)
741
+ blocking += closing("FAC-SEM-163", repeated, "")
742
+ errors = credentials + [view_line(f) for f in blocking]
743
+ if errors:
744
+ return {"declared": False, "reported": True, "errors": _once(errors)}
745
+ return {"declared": True, "reported": False, "errors": [], "standard": view["standard"], "projects": view["projects"],
746
+ "obligations": _once([view_line(f) for f in owed + closing("FAC-SEM-164", owing, "")])}
747
+
748
+
749
+ # The probe's rule (D1 to D4 of the 0.73.0 brief).
750
+
751
+ def _other_keys(doc: Any) -> List[str]:
752
+ """Places of a channel-agent key of another version, which a reader does not read (CA-7)."""
753
+ found: List[str] = []
754
+ if not isinstance(doc, dict):
755
+ return found
756
+ holders = [("/provider/extensions", (doc.get("provider") or {}).get("extensions") if isinstance(doc.get("provider"), dict) else None),
757
+ ("/extensions", doc.get("extensions"))]
758
+ holders += [("/capabilities/%d/extensions" % i, c.get("extensions")) for c, i in indexed_objects(doc.get("capabilities"))]
759
+ for at, extensions in holders:
760
+ if isinstance(extensions, dict):
761
+ for key in extensions:
762
+ if key.startswith(KEY_PREFIX) and key != KEY:
763
+ version = key[len(KEY_PREFIX):]
764
+ found.append("%s (version %s)" % (at, version if re.fullmatch(r"[0-9]{1,4}(?:\.[0-9]{1,4}){0,3}", version) else "not written"))
765
+ return found
766
+
767
+
768
+ def _shown(findings: List[Dict[str, str]], limit: int = 5) -> str:
769
+ text = "; ".join("%s at %s" % (f["code"], f["instancePath"] or "/") for f in findings[:limit])
770
+ return text + ("; and %d more" % (len(findings) - limit) if len(findings) > limit else "")
771
+
772
+
773
+ def _credential_shapes(findings: List[Dict[str, str]]) -> List[Dict[str, str]]:
774
+ """FAC-SEM-165's findings of a credential's shape, not its budget run-out: they fail the rule wherever they sit."""
775
+ return [f for f in findings if f["code"] == "FAC-SEM-165" and "shaped like a credential" in f["message"]]
776
+
777
+
778
+ def _size_note(well_known_bytes: int) -> str:
779
+ return ("the well-known answer is %d bytes, %s the 2 MiB cap of Fabric Dashboards' probe: keep a large declaration in "
780
+ "the manifest (CT-223)" % (well_known_bytes, "over" if well_known_bytes > WELL_KNOWN_CAP else "near"))
781
+
782
+
783
+ def declaration_verdict(manifest: Optional[Dict[str, Any]], manifest_named: bool, manifest_problem: str,
784
+ well_known: Optional[Dict[str, Any]], well_known_bytes: Optional[int] = None) -> Tuple[str, str]:
785
+ """`channel-agent.declaration`: PASS for a declaration read and free of findings; FAIL with its first findings when
786
+ the contract's reader still reads it as declared (FAC-SEM-164's obligations) or a string is shaped like a credential
787
+ (FAC-SEM-165), in the read declaration or in any copy beside it; NOT_RUN, never FAIL, when nothing is declared or the reader reads it as not declared (D3): a key
788
+ where a reader does not read it, another version's key, a copy in the well-known answer of a service with a
789
+ manifest (D1), or a declaration the schema or FAC-SEM-163 refuses (an older form)."""
790
+ notes: List[str] = []
791
+ wk_has = isinstance(well_known, dict) and KEY in (_extensions_at(well_known, "well-known") or {})
792
+ if manifest_named and manifest is None:
793
+ copy_shapes = _credential_shapes(channel_agent_rules(well_known, "well-known")) if wk_has else []
794
+ if wk_has:
795
+ notes.append("the well-known answer's copy is not read: a service with a manifest declares there only (CA-1)")
796
+ if well_known_bytes is not None and well_known_bytes >= WELL_KNOWN_NEAR:
797
+ notes.append(_size_note(well_known_bytes))
798
+ if copy_shapes:
799
+ return "FAIL", "the well-known answer's copy holds %d string(s) shaped like a credential: %s (DEC-0150 CA-5)%s" % (
800
+ len(copy_shapes), _shown(copy_shapes), (" (%s)" % "; ".join(notes)) if notes else "")
801
+ return "NOT_RUN", "the manifest the descriptor names is not read (%s); a service with a manifest declares there only (CA-1)%s" % (
802
+ manifest_problem, (" (%s)" % "; ".join(notes)) if notes else "")
803
+ site, doc = ("manifest", manifest) if manifest is not None else ("well-known", well_known)
804
+ root = CHANNEL_AGENT_PATHS[site]
805
+ findings = channel_agent_rules(doc, site)
806
+ mine = [f["instancePath"] == root or f["instancePath"].startswith(root + "/") for f in findings]
807
+ own = [f for f, ours in zip(findings, mine) if ours]
808
+ others = [f for f, ours in zip(findings, mine) if not ours]
809
+ if others:
810
+ notes.append("not read, reported: %s" % _shown(others))
811
+ other_keys = _other_keys(doc) + (_other_keys(well_known) if site == "manifest" else [])
812
+ if other_keys:
813
+ notes.append("a channel-agent key of another version is not read (CA-7): %s" % ", ".join(other_keys[:5]))
814
+ copy: List[Dict[str, str]] = []
815
+ if site == "manifest" and wk_has:
816
+ copy = channel_agent_rules(well_known, "well-known")
817
+ notes.append("the well-known answer's copy is ignored: a service with a manifest is read from it only (CA-1)%s"
818
+ % ("; the copy has %d finding(s): %s" % (len(copy), _shown(copy, 3)) if copy else ""))
819
+ if wk_has and well_known_bytes is not None and well_known_bytes >= WELL_KNOWN_NEAR:
820
+ notes.append(_size_note(well_known_bytes))
821
+ tail = (" (%s)" % "; ".join(notes)) if notes else ""
822
+ declared = isinstance(doc, dict) and KEY in (_extensions_at(doc, site) or {})
823
+ where = "the manifest's provider.extensions" if site == "manifest" else "the well-known answer's extensions"
824
+ shapes = _credential_shapes(others + copy)
825
+ if shapes: # a credential's shape in a copy no reader reads still sits in a document the service publishes (CA-5)
826
+ return "FAIL", "%d string(s) shaped like a credential outside the read declaration: %s (DEC-0150 CA-5)%s" % (
827
+ len(shapes), _shown(shapes), tail)
828
+ if declared and own:
829
+ if not channel_agent_view(doc, site)["declared"] and not _credential_shapes(own):
830
+ # D3: what the contract's reader reads as not declared (an older form, a repeat, a dangling reference) is
831
+ # reported while producers move to this revision; a credential's shape (FAC-SEM-165) always fails, while a
832
+ # declaration too large to read for credentials is read as not declared.
833
+ return "NOT_RUN", "read as not declared, never a failure while producers move to this revision: %d finding(s) on the declaration in %s: %s%s" % (
834
+ len(own), where, _shown(own), tail)
835
+ return "FAIL", "%d finding(s) on the declaration in %s: %s%s" % (len(own), where, _shown(own), tail)
836
+ if declared:
837
+ projects = schema_view(doc, site).get("projects") or []
838
+ return "PASS", "declared in %s: channel-agent-standard/1, %d project(s), no finding on it%s" % (where, len(projects), tail)
839
+ if notes:
840
+ return "NOT_RUN", "read as not declared, never a failure while producers move to this revision%s" % tail
841
+ return "NOT_RUN", "declares no %s in %s: not a channel agent" % (KEY, where)