@passioncode-ai/fabric-agent-adapter 0.80.2 → 0.82.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 (92) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +18 -5
  3. package/SECURITY.md +25 -8
  4. package/bin/fabric-agent-adapter.js +24 -1
  5. package/package.json +1 -1
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +16 -3
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +116 -4
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +33 -3
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +257 -10
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -77
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +899 -0
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +23 -5
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +16 -3
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +5 -2
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +28 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -3
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +47 -10
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +31 -8
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +33 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +19 -28
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +9 -6
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +146 -3
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +3 -2
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +79 -2
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +269 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +64 -5
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -7
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +73 -3
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +89 -22
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +39 -6
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +76 -11
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +8 -153
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +86 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +324 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +34 -3
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +35 -17
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +77 -4
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +156 -133
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +257 -10
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +64 -3
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +246 -24
  64. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -6
  65. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  66. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  67. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +16 -3
  68. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  69. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  70. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  71. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  72. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  73. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  74. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  75. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  76. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +33 -3
  77. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  78. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  79. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  86. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  87. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
  88. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +16 -8
  89. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +46 -31
  90. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
  91. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +18 -16
  92. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -13
@@ -44,6 +44,12 @@ shorter than three characters ignored, matched without regard to case. A list in
44
44
  is refused: it would travel with the source. The summary says whether Git chose the files or every file was read.
45
45
  Under a locale whose file-system encoding is not UTF-8 the check restarts itself in Python's UTF-8 mode, so a name,
46
46
  an argument and a file are read in one encoding. Standard library only.
47
+
48
+ Under a source manifest (DEC-0149, FAA-261): when ROOT holds `fabric.source.json`, the manifest decides what is source
49
+ and the contract's tree check reads it (fabric_source_tree.py, held to the contract's vectors): blocking findings exit
50
+ 1, warnings alone (`email`, `link-outside`, `no-license`, `unused-exclude`, `excluded-by-fold`) exit 0, a source file
51
+ not read exits 2; each finding names its path, line and column, never the text found. `.fabric-source-ignore` and
52
+ `--exclude` are not read there.
47
53
  """
48
54
  # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
49
55
 
@@ -65,6 +71,28 @@ from urllib.parse import unquote
65
71
  from typing import Dict, Iterable, List, NoReturn, Optional, Set, Tuple
66
72
 
67
73
  sys.path.insert(0, str(Path(__file__).resolve().parent))
74
+
75
+
76
+ def _refuse_sibling(step: str, missing: BaseException) -> NoReturn:
77
+ """A sibling module that did not load: name it and exit 2, never a traceback and never exit 1 (a finding)."""
78
+ # the exception's own text is never printed: an OSError's carries a path, which may be under the owner's home
79
+ why = "a module beside this file refused to load"
80
+ try:
81
+ here, sibling = Path(__file__).resolve().parent, step + ".py"
82
+ file = getattr(missing, "filename", None) if isinstance(missing, OSError) else None
83
+ why = ("%s must sit beside this file" % sibling if not (here / sibling).is_file()
84
+ else "%s beside this file cannot be read" % sibling
85
+ if isinstance(file, (str, bytes)) and Path(os.fsdecode(file)).resolve() == here / sibling
86
+ else "%s beside this file is from another version: copy it with this one" % sibling
87
+ if isinstance(missing, (ImportError, SyntaxError))
88
+ else "%s beside this file refused to load (%s): copy the scripts folder whole, from one version"
89
+ % (sibling, type(missing).__name__))
90
+ except Exception: # naming the cause must never be what fails
91
+ pass
92
+ sys.stderr.write("fabric_source_check.py: %s\n" % why)
93
+ sys.exit(2)
94
+
95
+
68
96
  _WROTE_BYTECODE, sys.dont_write_bytecode = sys.dont_write_bytecode, True # a copy inside the checked tree adds no .pyc
69
97
  _STEP = "fabric_secrets" # the sibling being loaded, so a refusal names it whatever the exception says
70
98
  try:
@@ -72,26 +100,13 @@ try:
72
100
  _STEP = "fabric_source_ignore"
73
101
  from fabric_source_ignore import ( # noqa: E402,F401 - how the ignore lines are read, beside this file
74
102
  IGNORE_FILE, CheckError, _as_bytes, _glob, _ignored, _line, _readable_glob, ignore_globs)
103
+ _STEP = "fabric_source_restart"
104
+ from fabric_source_restart import _interpreter, _interpreter_flags, _keep_console, _utf8_restart # noqa: E402,F401
75
105
  _STEP = "fabric_source_homes"
76
106
  from fabric_source_homes import ( # noqa: E402,F401 - how a declared home is read, beside this file
77
107
  HOME_ENTRY, _NAME_JOIN, _NAME_START, _SEP, _dashed_home, _name_parts, _owner_homes, _spellings)
78
108
  except Exception as _missing: # a copy without its siblings, or mixed from two versions: a refusal, never exit 1
79
- # the exception's own text is never printed: an OSError's carries a path, which may be under the owner's home
80
- _why = "a module beside this file refused to load"
81
- try:
82
- _HERE, _sibling = Path(__file__).resolve().parent, _STEP + ".py"
83
- _file = getattr(_missing, "filename", None) if isinstance(_missing, OSError) else None
84
- _why = ("%s must sit beside this file" % _sibling if not (_HERE / _sibling).is_file()
85
- else "%s beside this file cannot be read" % _sibling
86
- if isinstance(_file, (str, bytes)) and Path(os.fsdecode(_file)).resolve() == _HERE / _sibling
87
- else "%s beside this file is from another version: copy it with this one" % _sibling
88
- if isinstance(_missing, (ImportError, SyntaxError))
89
- else "%s beside this file refused to load (%s): copy the scripts folder whole, from one version"
90
- % (_sibling, type(_missing).__name__))
91
- except Exception: # naming the cause must never be what fails
92
- pass
93
- sys.stderr.write("fabric_source_check.py: %s\n" % _why)
94
- sys.exit(2)
109
+ _refuse_sibling(_STEP, _missing)
95
110
  finally:
96
111
  sys.dont_write_bytecode = _WROTE_BYTECODE
97
112
 
@@ -127,13 +142,16 @@ PRIVATE_KEY = re.compile(r"-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----
127
142
  JWK_PRIVATE = re.compile(r"\\?[\"']d\\?[\"'][ \t]*:[ \t]*\\?[\"']([A-Za-z0-9_-]{43,})") # JSON, escaped, Python
128
143
  #: Words a documented or synthetic credential carries (AWS's `...EXAMPLE`, `sk-test-not-a-key`); a random key does not.
129
144
  SYNTHETIC = ("example", "not-a-key", "notakey", "fake", "dummy", "placeholder", "redacted")
130
- # The detector's provider-key shapes without its first alternative, `^bearer\s.+`, which reads a whole header value: in
131
- # a file it would take prose that starts with the word and hide a key later on the same line. A bearer token is looked
132
- # for as `_BEARER_INSIDE` does: the word, then a run of 12 or more with a digit.
133
- _BEARER_FIRST = "(^bearer["
134
- if not _secrets.SECRET_VALUE.pattern.startswith(_BEARER_FIRST): # not an assert: `python -O` would drop it
135
- raise ImportError("fabric_secrets.SECRET_VALUE changed its first branch; update fabric_source_check.py with it")
136
- PROVIDER_KEY = re.compile("(" + _secrets.SECRET_VALUE.pattern.split("|", 1)[1], _secrets.SECRET_VALUE.flags)
145
+ # This check's own key shapes, the detector's before DEC-0151 (unbounded, ASCII case-insensitive), apart from the shared
146
+ # SECRET_VALUE for parity with the contract's tree check (KEY_SHAPE_SOURCES, `src/source-rules.ts` at the pin; CT-207,
147
+ # FAA-258). Not the detector's `^bearer\s.+`, which reads a whole header value and would take prose that starts with the
148
+ # word: a bearer token is looked for as `_BEARER_INSIDE` does, the word, then a run of 12 or more with a digit.
149
+ KEY_SHAPE_SOURCES = ("sk-[A-Za-z0-9_-]{16,}", "gh[pousr]_[A-Za-z0-9]{20,}", "xox[abposr]-[A-Za-z0-9-]{10,}", "AKIA[0-9A-Z]{16}", "glpat-[A-Za-z0-9_-]{20,}")
150
+ PROVIDER_KEY = re.compile("(" + "|".join(KEY_SHAPE_SOURCES) + ")", re.IGNORECASE | re.ASCII)
151
+ #: A URL parameter's value read as the contract's tree check reads it (`URL_SECRET_VALUE` in `src/source-rules.ts`): the
152
+ #: detector's whole-value `^bearer` branch and this check's own unbounded shapes, never the shared DEC-0151 detector.
153
+ URL_SECRET_VALUE = re.compile("(^bearer[" + _secrets.JS_SPACE + "]" + _secrets._NOT_LINE_END + "+|" + "|".join(KEY_SHAPE_SOURCES) + ")",
154
+ re.IGNORECASE | re.ASCII)
137
155
  #: Files that are credentials by their name: pack's own rule (fabric_package.SECRET_FILE and SECRET_FILE_OK, held
138
156
  #: equal by test_source_check), and the kit's token files (`service.token`, fabric_auth; `board.credential`, fabric_hub).
139
157
  _JS_DOT = "[^\n\r\u2028\u2029]"
@@ -216,7 +234,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
216
234
  if not _real(value):
217
235
  continue
218
236
  if (_secrets.credential_name(name) and _secrets._long_secret(value)) or \
219
- _secrets.JWT_SHAPE.search(value) or _secrets.SECRET_VALUE.search(value):
237
+ _secrets.has_jwt(value) or URL_SECRET_VALUE.search(value):
220
238
  return True
221
239
  return False
222
240
 
@@ -269,7 +287,7 @@ def _secret_in(line: str) -> Optional[str]:
269
287
  return "a private key block"
270
288
  if any(_real(m.group(1)) for m in JWK_PRIVATE.finditer(line)):
271
289
  return "a private key (a JWK's `d`)"
272
- if any(_real(m.group(0)) for m in _secrets.JWT_SHAPE.finditer(line)):
290
+ if any(_real(line[a:b]) for a, b in _secrets.jwt_spans(line)): # one pass (FAA-313)
273
291
  return "a token shaped like a JWT"
274
292
  # The detector reads one configuration value; across a line, `sk-` needs a word start of its own, or every name
275
293
  # with a segment ending in `sk` (`task-management-service`) would read as a key.
@@ -399,13 +417,13 @@ def _instance_finding(text: str, entries: List[str]) -> Optional[str]:
399
417
 
400
418
 
401
419
  def _alternatives(pattern: "re.Pattern[str]") -> List[Tuple["re.Pattern[str]", ...]]:
402
- """Each provider-key alternative of the contract's pattern as (its starts, its prefix, a run of its class, least
420
+ """Each provider-key alternative of this check's pattern as (its starts, its prefix, a run of its class, least
403
421
  length, open-ended, the whole alternative), so a key is read where its own class ends, not where another's does."""
404
422
  found = []
405
423
  for alt in pattern.pattern[1:-1].split("|"):
406
424
  part = re.fullmatch(r"(.+?)(\[[^\]]+\])\{(\d+)(,?)\}", alt)
407
425
  if part is None: # not an assert: `python -O` would drop it
408
- raise ImportError("fabric_secrets.SECRET_VALUE has a key shape this check cannot read: %s" % alt[:12])
426
+ raise ImportError("KEY_SHAPE_SOURCES has a key shape this check cannot read: %s" % alt[:12])
409
427
  prefix, cls, least, open_ended = part.groups()
410
428
  found.append((re.compile("(?=%s)" % prefix, pattern.flags), re.compile(prefix, pattern.flags),
411
429
  re.compile(cls + "+", pattern.flags), int(least), bool(open_ended), re.compile(alt, pattern.flags)))
@@ -468,7 +486,7 @@ def _secret_spans(text: str) -> List[Tuple[int, int]]:
468
486
 
469
487
  def _raw_secret_spans(text: str) -> List[Tuple[int, int]]:
470
488
  spans = [m.span() for m in PRIVATE_KEY.finditer(text)]
471
- spans += [m.span() for m in _secrets.JWT_SHAPE.finditer(text) if _real(m.group(0))]
489
+ spans += [(a, b) for a, b in _secrets.jwt_spans(text) if _real(text[a:b])] # one pass (FAA-313)
472
490
  spans += [m.span(1) for m in JWK_PRIVATE.finditer(text) if _real(m.group(1))]
473
491
  spans += list(_provider_keys(text))
474
492
  spans += [m.span() for m in _overlapping(_secrets._BEARER_INSIDE, text) if _real(m.group(0))]
@@ -714,6 +732,15 @@ def check(root: Path, entries: List[str], globs: List[str], every_file: bool = F
714
732
  files, mode = _walk_files(root, unreadable, PACK_REFUSES), "walk"
715
733
  elif not files: # not a checkout, Git refused it, or a folder Git ignores or holds nothing of: read what is there
716
734
  files, mode = _walk_files(root, unreadable, WALK_SKIP, dependencies), "walk"
735
+ if os.path.lexists(str(root / "fabric.source.json")): # DEC-0149: the manifest decides (fabric_source_tree.py)
736
+ wrote, sys.dont_write_bytecode = sys.dont_write_bytecode, True # a copy inside the tree adds no .pyc
737
+ try:
738
+ import fabric_source_tree # noqa: E402 - beside this file; imported here, since it imports this one
739
+ except Exception as missing: # the same named-sibling refusal as the imports at the top (exit 2)
740
+ _refuse_sibling("fabric_source_tree", missing)
741
+ finally:
742
+ sys.dont_write_bytecode = wrote
743
+ return fabric_source_tree.check_folder(root, files, mode, entries, unreadable, dependencies)
717
744
  findings: List[Dict[str, object]] = []
718
745
  read = 0
719
746
  skipped = {"binary": 0, "unreadable": len(unreadable), "excluded": 0, "nested": 0, "dependencies": len(dependencies)}
@@ -892,6 +919,8 @@ def run(argv: Optional[List[str]] = None) -> int:
892
919
  except CheckError as exc:
893
920
  print("fabric_source_check.py: %s" % exc, file=sys.stderr)
894
921
  return 2
922
+ if result.get("manifest"): # under fabric.source.json: its own report (fabric_source_tree.print_run)
923
+ return sys.modules["fabric_source_tree"].print_run(result, entries, list_path is not None, args.json, _print)
895
924
  findings = result["findings"]
896
925
  skipped = result["skipped"]
897
926
  listed = sum(1 for e in entries if e and not e.startswith(HOME_ENTRY))
@@ -926,55 +955,6 @@ def run(argv: Optional[List[str]] = None) -> int:
926
955
  return 0 if complete else 2
927
956
 
928
957
 
929
- def _utf8_restart(encoding: str, utf8_mode: int, executable: str, argv: List[str]) -> Optional[List[bytes]]:
930
- """The command that restarts the check in Python's UTF-8 mode, or None when it needs none. Under a locale whose
931
- file-system encoding is not UTF-8 (ISO-8859-1, KOI8-R, a C locale with UTF-8 mode off), a name, an argument and a
932
- file's text would each be read in its own encoding; in UTF-8 mode all three are, and a byte that is not UTF-8 stays
933
- that byte. Windows names are UTF-16 and need none. The arguments go back as the bytes they came in. An empty list:
934
- a restart is needed and no interpreter is known (the caller exits 2 rather than read in two encodings)."""
935
- if os.name == "nt" or utf8_mode or encoding.lower().replace("_", "-") in ("utf-8", "utf8"):
936
- return None
937
- if not executable:
938
- return []
939
- return [os.fsencode(executable), b"-X", b"utf8"] + [os.fsencode(a) for a in argv]
940
-
941
-
942
- def _interpreter() -> str:
943
- """The running interpreter: Linux's `/proc/self/exe`, which no PATH or working folder can stand in for (Python 3.9
944
- may take a `./python3` of the working folder as sys.executable), else sys.executable, which may be empty."""
945
- try:
946
- return os.readlink("/proc/self/exe")
947
- except (OSError, AttributeError):
948
- return sys.executable or ""
949
-
950
-
951
- def _interpreter_flags(flags: object, xoptions: Dict[str, object]) -> List[str]:
952
- """The interpreter's own options, rebuilt where sys.orig_argv is missing (Python 3.9), so a restart keeps `-I`."""
953
- out: List[str] = []
954
- if getattr(flags, "isolated", 0):
955
- out.append("-I")
956
- else:
957
- out += ["-E"] * bool(getattr(flags, "ignore_environment", 0)) + ["-s"] * bool(getattr(flags, "no_user_site", 0))
958
- out += ["-S"] * bool(getattr(flags, "no_site", 0)) + ["-B"] * bool(getattr(flags, "dont_write_bytecode", 0))
959
- if getattr(flags, "optimize", 0):
960
- out.append("-" + "O" * int(getattr(flags, "optimize")))
961
- for key, value in xoptions.items():
962
- if key not in ("utf8", "fabric_console"):
963
- out += ["-X", key if value is True else "%s=%s" % (key, value)]
964
- return out
965
-
966
-
967
- def _keep_console(xoptions: Dict[str, object]) -> None:
968
- """After a restart, write the console's own encoding again (the parent named it in `-X fabric_console=`)."""
969
- console = xoptions.get("fabric_console")
970
- if isinstance(console, str):
971
- for stream in (sys.stdout, sys.stderr):
972
- try:
973
- stream.reconfigure(encoding=console, errors="backslashreplace") # type: ignore[union-attr]
974
- except (AttributeError, LookupError, ValueError):
975
- pass
976
-
977
-
978
958
  if __name__ == "__main__":
979
959
  _keep_console(getattr(sys, "_xoptions", {}))
980
960
  _argv = (list(sys.orig_argv[1:]) if hasattr(sys, "orig_argv")
@@ -0,0 +1,56 @@
1
+ """How fabric_source_check.py restarts itself in Python's UTF-8 mode under a locale whose file-system encoding is not
2
+ UTF-8, and keeps the console's encoding and the interpreter's own options across the restart. Standard library only."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from typing import Dict, List, Optional
8
+
9
+
10
+ def _utf8_restart(encoding: str, utf8_mode: int, executable: str, argv: List[str]) -> Optional[List[bytes]]:
11
+ """The command that restarts the check in Python's UTF-8 mode, or None when it needs none. Under a locale whose
12
+ file-system encoding is not UTF-8 (ISO-8859-1, KOI8-R, a C locale with UTF-8 mode off), a name, an argument and a
13
+ file's text would each be read in its own encoding; in UTF-8 mode all three are, and a byte that is not UTF-8 stays
14
+ that byte. Windows names are UTF-16 and need none. The arguments go back as the bytes they came in. An empty list:
15
+ a restart is needed and no interpreter is known (the caller exits 2 rather than read in two encodings)."""
16
+ if os.name == "nt" or utf8_mode or encoding.lower().replace("_", "-") in ("utf-8", "utf8"):
17
+ return None
18
+ if not executable:
19
+ return []
20
+ return [os.fsencode(executable), b"-X", b"utf8"] + [os.fsencode(a) for a in argv]
21
+
22
+
23
+ def _interpreter() -> str:
24
+ """The running interpreter: Linux's `/proc/self/exe`, which no PATH or working folder can stand in for (Python 3.9
25
+ may take a `./python3` of the working folder as sys.executable), else sys.executable, which may be empty."""
26
+ try:
27
+ return os.readlink("/proc/self/exe")
28
+ except (OSError, AttributeError):
29
+ return sys.executable or ""
30
+
31
+
32
+ def _interpreter_flags(flags: object, xoptions: Dict[str, object]) -> List[str]:
33
+ """The interpreter's own options, rebuilt where sys.orig_argv is missing (Python 3.9), so a restart keeps `-I`."""
34
+ out: List[str] = []
35
+ if getattr(flags, "isolated", 0):
36
+ out.append("-I")
37
+ else:
38
+ out += ["-E"] * bool(getattr(flags, "ignore_environment", 0)) + ["-s"] * bool(getattr(flags, "no_user_site", 0))
39
+ out += ["-S"] * bool(getattr(flags, "no_site", 0)) + ["-B"] * bool(getattr(flags, "dont_write_bytecode", 0))
40
+ if getattr(flags, "optimize", 0):
41
+ out.append("-" + "O" * int(getattr(flags, "optimize")))
42
+ for key, value in xoptions.items():
43
+ if key not in ("utf8", "fabric_console"):
44
+ out += ["-X", key if value is True else "%s=%s" % (key, value)]
45
+ return out
46
+
47
+
48
+ def _keep_console(xoptions: Dict[str, object]) -> None:
49
+ """After a restart, write the console's own encoding again (the parent named it in `-X fabric_console=`)."""
50
+ console = xoptions.get("fabric_console")
51
+ if isinstance(console, str):
52
+ for stream in (sys.stdout, sys.stderr):
53
+ try:
54
+ stream.reconfigure(encoding=console, errors="backslashreplace") # type: ignore[union-attr]
55
+ except (AttributeError, LookupError, ValueError):
56
+ pass