@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.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 (120) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +13 -2
  3. package/SECURITY.md +23 -8
  4. package/bin/fabric-agent-adapter.js +50 -5
  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/agents-database.md +4 -4
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
  17. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  18. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
  19. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  86. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  87. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
  88. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  89. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
  90. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  91. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  92. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  93. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  94. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  95. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  96. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
  97. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  98. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  99. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  100. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  101. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  102. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  103. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  104. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  105. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  106. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  107. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
  108. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  109. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  110. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
@@ -7,13 +7,15 @@ first commit, so that its owner can hand the source over later without rewriting
7
7
  by itself: handing over, a new remote, a push or a change of visibility stays the owner's act, each time (a personal
8
8
  agent stays private until its owner says so about that act).
9
9
 
10
- The format of a source manifest, export and import is the contract's to decide (CT-194). What follows does not depend
11
- on it.
10
+ The format of a source manifest, export and import is the contract's: DEC-0149 (`fabric-source/1`, its `source.md`) at
11
+ the pin. The kit lays down `fabric.source.json` ([the source manifest](#the-source-manifest)), and the check reads the
12
+ source under it.
12
13
 
13
14
  ## Contents
14
15
 
15
16
  - [The layout](#the-layout)
16
17
  - [The data home and its kind](#the-data-home-and-its-kind)
18
+ - [The source manifest](#the-source-manifest)
17
19
  - [The check](#the-check)
18
20
 
19
21
  ## The layout
@@ -42,11 +44,11 @@ on it.
42
44
  - **Fixtures on neutral ids.** A test that needs a home folder, an owner or an account uses `example`, `example-agent`,
43
45
  `owner@example.com` or a one-letter home, and a credential-shaped test value says it is synthetic
44
46
  (`sk-test-not-a-key`) or is assembled at run time.
45
- - **Two names stay in the source until the contract decides (CT-194).** A service's supervisor label is the agent's
46
- own name, reverse-DNS on the publisher's domain (`com.example.example-agent`), and `AGENTS.md` names it (LC-09):
47
- never the owner's account or name. A provider's `createdBy` names the publisher; until one is chosen it
48
- keeps the scaffold's sentinel, and a person's name or account is never written there. The adapter moves both when the
49
- contract decides how an instance renames them (FAA-188 part 2).
47
+ - **Two names are slots, never values (DEC-0149, FAA-188 part 2).** A service's supervisor label is the agent's own
48
+ name, reverse-DNS on the publisher's domain (`com.example.example-agent`), and `AGENTS.md` names it (LC-09): never the
49
+ owner's account or name; the label an installation runs under is its `SUPERVISOR_LABEL` slot, with its port in
50
+ `SERVICE_PORT`. A provider's `createdBy` names the publisher; the source keeps the scaffold's sentinel, and the
51
+ publisher's URI is the installation's `PUBLISHER_URI` slot, so a person's name or account is never written there.
50
52
 
51
53
  ## The data home and its kind
52
54
 
@@ -69,6 +71,41 @@ on it.
69
71
  configuration and dependency folders `--every-file` reads too), each exit 0, with the owner's list or its absence recorded, until the contract's export (CT-194)
70
72
  replaces it.
71
73
 
74
+ ## The source manifest
75
+
76
+ `fabric.source.json` at the source's root says what is source and which slots an installation fills, never a value
77
+ (DEC-0149, `fabric-source/1`):
78
+
79
+ | Member | What |
80
+ |---|---|
81
+ | `include` | the source, default deny: a file outside every entry is not source; an entry ending in `/` is a folder, no globs |
82
+ | `exclude` | holes in `include`, read in any letter case and Unicode form (a file an entry names only in another spelling is left out, and the check says `excluded-by-fold`) |
83
+ | `binaries` | the binary files that are source (an icon, a font); another binary blocks |
84
+ | `unchecked` | source paths that travel unread, each with its reason (synthetic test vectors, a licence naming a holder on the owner's list) |
85
+ | `publicAddresses` | the e-mail addresses the source may carry (a support address); another one warns |
86
+ | `slots` | what an installation fills, by name: `secret` (a key), `config` (a setting: a repository URL, a folder, the supervisor label, the publisher's identity), `store` (a data store, with its path in the data folder), `endpoint` (the operator's infrastructure by role) |
87
+
88
+ - **Laying it down.** A new agent gets it from `new_agent.py` (creating-fabric-agents' `references/new-agent.md`); an
89
+ existing project from `adapt_project.py source <project-root>` (adapting-projects-to-fabric's scripts), which proposes `include` from the
90
+ project's top-level entries (what Git would commit, instance data by its name left out) and names the kit's slots
91
+ (`PUBLISHER_URI` where a provider manifest is there, `PLACEMENT_RECORD`, and with `--service` `SUPERVISOR_LABEL` and
92
+ `SERVICE_PORT`); `--secret NAME:PURPOSE` and `--store NAME:PATH:PURPOSE` add the agent's own. `--dry-run` prints the
93
+ proposal and writes nothing; an existing manifest is the owner's and is replaced only with `--force`. The author then
94
+ narrows `include` to what is source.
95
+ - **The check under it.** When the folder it checks holds `fabric.source.json`, `fabric_source_check.py check` reads
96
+ the source the manifest names and reports the contract's kinds (`fabric_source_tree.py`, held to the contract's
97
+ vectors): blocking `home-path`, `credential`, `binary`, `nested-repository`, `instance-string`, `missing`,
98
+ `no-include` and the manifest's own findings (FAC-SEM-156, FAC-SEM-157); warnings `email`, `link-outside`,
99
+ `no-license`, `unused-exclude`, `excluded-by-fold`, each by path, line and column (UTF-16 units), never by the text
100
+ found. Exit 1 on a blocking finding, 0 when only warnings remain and every source file was read, 2 when one was not.
101
+ `.fabric-source-ignore` and `--exclude` are not read there: `exclude` and `unchecked` say it. A declared home
102
+ (`home: <name>`) is still found in every source file.
103
+ - **Packing it.** `pack` (building-fabric-services' `references/package.md`) writes the package's `secrets[]` from the secret slots and `state[]` from the store slots' paths, and refuses a
104
+ package whose members differ (FAC-SEM-160); an empty `state[]` is a copy packed without state, and stays so. A
105
+ hand-written `secrets[]` naming a secret the manifest has no slot for is refused by name, never dropped.
106
+ - **Handing it over** stays the owner's act, each time, on their word for that act (the contract's `source.md`,
107
+ Handing over).
108
+
72
109
  ## The check
73
110
 
74
111
  `fabric_source_check.py check` reads the files the source would carry (what Git would commit: tracked files and
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
25
+ CONTRACT_COMMIT = "93e7db164c7829e2783b4b7705c4113bb5201131"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "eac1a594aa131851f079da8bb7b2149e1951f35f", "3830a3353923e20f826d466610e1c479f120bec0", "00e050ba7429c0c60261cec871779e0fd6aeeb28", "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "18f8d48762edc5bfb1433657cbfe1c16196df1a6", "eac1a594aa131851f079da8bb7b2149e1951f35f", "3830a3353923e20f826d466610e1c479f120bec0", "00e050ba7429c0c60261cec871779e0fd6aeeb28", "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -488,6 +488,98 @@ def scaffold_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
488
488
  }
489
489
 
490
490
 
491
+ # #region source-manifest: fabric.source.json (contract DEC-0149, fabric-source/1; FAA-261)
492
+ SOURCE_FILE = "fabric.source.json"
493
+ _SLOT_NAME = re.compile(r"[A-Z][A-Z0-9_]{0,63}\Z")
494
+ #: Instance data never named in include: the repository, credentials by their name, dependency and build folders.
495
+ _NOT_SOURCE = re.compile(r"\.git|\.env(\..*)?|.*\.(pem|key|p12|pfx|jks|keystore|token|credential)|node_modules|\.venv|venv|"
496
+ r"__pycache__|dist|build|\.DS_Store|" + re.escape(SOURCE_FILE), re.IGNORECASE)
497
+ #: The slots the kit names for every agent: the provider's createdBy and, for a service, its supervisor label and port
498
+ #: are instance data (FAA-188 part 2); the placement record is the operator's infrastructure, read at start.
499
+ PUBLISHER_SLOT = {"name": "PUBLISHER_URI", "kind": "config",
500
+ "purpose": "provider.createdBy: the publisher's URI; the source keeps the scaffold's sentinel"}
501
+ PLACEMENT_SLOT = {"name": "PLACEMENT_RECORD", "kind": "endpoint", "optional": True,
502
+ "purpose": "Project Observatory's placement record (observatory-placement/1), read at start"}
503
+ SERVICE_SLOTS = (
504
+ {"name": "SUPERVISOR_LABEL", "kind": "config",
505
+ "purpose": "the launchd label, systemd unit or Scheduled Task this installation runs under (LC-09)"},
506
+ {"name": "SERVICE_PORT", "kind": "config", "purpose": "the loopback port recorded for this OS account (DEC-0154 HA-1)"},
507
+ )
508
+
509
+
510
+ def _parse_slot(text: str, kind: str) -> Dict[str, Any]:
511
+ parts = text.split(":", 2 if kind == "store" else 1)
512
+ if len(parts) != (3 if kind == "store" else 2) or not _SLOT_NAME.match(parts[0]) or not parts[-1].strip():
513
+ raise AdaptationError("--%s takes %s" % (kind, "NAME:PATH:PURPOSE" if kind == "store" else "NAME:PURPOSE"))
514
+ slot = {"name": parts[0], "kind": kind, "purpose": parts[-1].strip()}
515
+ if kind == "store":
516
+ slot["path"] = parts[1]
517
+ if _secrets.secret_value(slot["purpose"]):
518
+ raise AdaptationError("a slot is named, never filled: --%s %s holds a value shaped like a credential" % (kind, parts[0]))
519
+ return slot
520
+
521
+
522
+ def source_manifest(include: Sequence[str], description: Optional[str] = None, secrets: Sequence[str] = (),
523
+ stores: Sequence[str] = (), service: bool = False, provider: bool = True) -> Dict[str, Any]:
524
+ """A fabric-source/1 manifest: include as given (sorted, default deny), the kit's slots, the slots named."""
525
+ manifest: Dict[str, Any] = {"format": "fabric-source/1"}
526
+ if description:
527
+ manifest["description"] = description
528
+ manifest["include"] = sorted(dict.fromkeys(include))
529
+ slots = [_parse_slot(t, "secret") for t in secrets] + [_parse_slot(t, "store") for t in stores]
530
+ slots += ([dict(PUBLISHER_SLOT)] if provider else []) + [dict(PLACEMENT_SLOT)]
531
+ slots += [dict(s) for s in SERVICE_SLOTS] if service else []
532
+ names = [s["name"] for s in slots]
533
+ if len(set(names)) != len(names):
534
+ raise AdaptationError("a slot is named twice")
535
+ manifest["slots"] = slots
536
+ return manifest
537
+
538
+
539
+ def _top_level(root: Path) -> List[str]:
540
+ """The project's top-level entries as include paths: what Git would commit, else what a walk finds; a folder with
541
+ a trailing /. Instance data by its name is left out."""
542
+ names: List[str] = []
543
+ try:
544
+ listed = subprocess.run(["git", "-C", str(root), "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
545
+ capture_output=True, timeout=60)
546
+ if listed.returncode == 0:
547
+ names = [os.fsdecode(n) for n in listed.stdout.split(b"\0") if n]
548
+ except (OSError, subprocess.SubprocessError):
549
+ names = []
550
+ if not names:
551
+ names = [_relative(path, root) for path in _files(root)]
552
+ tops = []
553
+ for name in names:
554
+ head, _, rest = name.partition("/")
555
+ if not _NOT_SOURCE.fullmatch(head):
556
+ tops.append(head + "/" if rest else head)
557
+ return sorted(dict.fromkeys(tops))
558
+
559
+
560
+ def write_source_manifest(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
561
+ """`source`: lay down fabric.source.json from the project's top-level entries. An existing one is the owner's and
562
+ is kept unless --force; --dry-run writes nothing."""
563
+ if not root.is_dir():
564
+ raise AdaptationError("project path is not a directory: %s" % root)
565
+ provider = (root / "fabric-agent.json").is_file()
566
+ manifest = source_manifest(_top_level(root), args.description, args.secret, args.store, args.service, provider)
567
+ target = root / SOURCE_FILE
568
+ exists = os.path.lexists(str(target))
569
+ result = {"project": str(root.resolve()), "manifest": manifest, "exists": exists}
570
+ if args.dry_run:
571
+ result["dryRun"] = True
572
+ return result
573
+ if exists and not args.force:
574
+ raise AdaptationError("%s exists: it is the owner's; --dry-run shows the proposal, --force replaces it" % SOURCE_FILE)
575
+ target.write_text(_json_text(manifest), encoding="utf-8")
576
+ result["written"] = SOURCE_FILE
577
+ result["next"] = ("Review include (default deny), name every key and store as a slot, then run "
578
+ "fabric_source_check.py check on the project (exit 0).")
579
+ return result
580
+ # #endregion
581
+
582
+
491
583
  def _load_json(path: Path, errors: List[str]) -> Optional[Any]:
492
584
  try:
493
585
  return json.loads(path.read_text(encoding="utf-8"))
@@ -834,6 +926,17 @@ def build_parser() -> argparse.ArgumentParser:
834
926
  scaffold.add_argument("--dry-run", action="store_true", help="print the planned writes and a unified diff of each existing path; write nothing")
835
927
  scaffold.add_argument("--json", action="store_true")
836
928
 
929
+ source = sub.add_parser("source", help="lay down fabric.source.json, the agent's source manifest (DEC-0149)")
930
+ source.add_argument("project", type=Path)
931
+ source.add_argument("--description", help="what the agent is, in plain words")
932
+ source.add_argument("--secret", action="append", default=[], metavar="NAME:PURPOSE", help="a key the agent reads by name; repeat")
933
+ source.add_argument("--store", action="append", default=[], metavar="NAME:PATH:PURPOSE",
934
+ help="a data store relative to the data folder (it becomes the package's state[]); repeat")
935
+ source.add_argument("--service", action="store_true", help="a kit service: its supervisor label and port are slots")
936
+ source.add_argument("--force", action="store_true", help="replace an existing fabric.source.json")
937
+ source.add_argument("--dry-run", action="store_true", help="print the proposal; write nothing")
938
+ source.add_argument("--json", action="store_true")
939
+
837
940
  check = sub.add_parser("check", help="check generated structure and optional pinned schema")
838
941
  check.add_argument("project", type=Path)
839
942
  check.add_argument("--contract", type=Path)
@@ -848,6 +951,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
848
951
  result = inspect_project(args.project)
849
952
  elif args.command == "scaffold":
850
953
  result = scaffold_project(args.project, args)
954
+ elif args.command == "source":
955
+ result = write_source_manifest(args.project, args)
851
956
  else:
852
957
  result = check_project(args.project, args.contract)
853
958
  if result.get("dryRun") and not args.json:
@@ -28,8 +28,14 @@ CREDENTIAL_PAIRS = {
28
28
  'session': {'token', 'id'},
29
29
  'code': {'login', 'auth', 'access', 'session'}}
30
30
  JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
31
- SECRET_VALUE = re.compile(r'(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}'
32
- r'|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})', re.I | re.S)
31
+ # The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key with its
32
+ # prefix in its canonical case after a boundary (the start, a character that is no ASCII letter or digit, or an escape);
33
+ # an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more.
34
+ SECRET_VALUE = re.compile(r'(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"\']|\\u[0-9A-Fa-f]{4}'
35
+ r'|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m'
36
+ r'|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)'
37
+ r'(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})'
38
+ r'|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,}))', re.S)
33
39
  LONG_RUN = re.compile(r'[A-Za-z0-9+/=_-]{40,}')
34
40
 
35
41
 
@@ -115,6 +121,11 @@ def check(data):
115
121
  require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
116
122
  fallback = data.get('fallback', 'if_absent')
117
123
  require(fallback in ('if_absent', 'never'), 'invalid_fallback')
124
+ # DEC-0154 PL-3 and HA-2 (FAA-251): who listens on http_url's port, as the host read it from the socket table
125
+ # (fabric_peer_owner's kinds, or check_service.py's foreign-user). A browser opens a raw loopback address only
126
+ # after that listener is this OS account's; absent, it is unknown.
127
+ owner = data.get('listener_owner', 'unknown')
128
+ require(owner in ('this-account', 'another-account', 'system', 'unknown', 'none', 'foreign-user'), 'invalid_listener_owner')
118
129
  kind = action.get('kind')
119
130
  if kind == 'remote_open':
120
131
  require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
@@ -128,17 +139,26 @@ def check(data):
128
139
  require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
129
140
  require(viewer == target, 'wrong_viewer_device')
130
141
  require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
142
+ require(owner == 'this-account', 'listener_not_this_account')
131
143
  else:
132
144
  raise ValueError('unsupported_dashboard_action')
133
145
  return {'ok': True, 'kind': kind}
134
146
 
135
147
 
148
+ def read_input(limit=65536):
149
+ # The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
150
+ # stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
151
+ stream = getattr(sys.stdin, 'buffer', None)
152
+ raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
153
+ require(len(raw) <= limit, 'input_too_large')
154
+ return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
155
+
156
+
136
157
  def main():
137
158
  try:
138
- raw = sys.stdin.read(65537)
139
- require(len(raw) <= 65536, 'input_too_large')
140
- result = check(json.loads(raw))
141
- except (ValueError, TypeError, KeyError, AttributeError):
159
+ result = check(json.loads(read_input()))
160
+ # RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
161
+ except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
142
162
  # Fixed reasons only: never echo a submitted URL, token or JSON blob.
143
163
  result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
144
164
  print(json.dumps(result))
@@ -17,6 +17,7 @@ itself takes an option of the same name. --env is for a stdio server only and is
17
17
 
18
18
  Normative source: fabric-agent-contract docs/specification/provider.md (DEC-0016, rulings DEC-0017).
19
19
  """
20
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
20
21
 
21
22
  # #region provider-writer — docs: plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md#the-writer
22
23
 
@@ -292,6 +293,13 @@ def _now() -> str:
292
293
  return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
293
294
 
294
295
 
296
+
297
+ def entry_stem(path: str) -> str:
298
+ """CT-193 (FAC-SEM-014): the stem registry-rules.ts reads, `file.replace(/^.*[\\/]/, "").replace(/\\.json$/, "")`,
299
+ with ECMAScript's `.` (no line break: \\n, \\r, U+2028, U+2029) and `$` (the string's end) (FAA-189)."""
300
+ return re.sub(r"\.json\Z", "", re.sub(r"\A[^\n\r\u2028\u2029]*[\\/]", "", path, count=1))
301
+
302
+
295
303
  def main(argv: Optional[List[str]] = None) -> int:
296
304
  parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
297
305
  sub = parser.add_subparsers(dest="command", required=True)
@@ -327,7 +335,7 @@ def main(argv: Optional[List[str]] = None) -> int:
327
335
  entry = json.loads(args.path.read_text(encoding="utf-8"))
328
336
  problems = validate_provider_entry(entry)
329
337
  # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
330
- stem = re.sub(r"\.json\Z", "", re.sub(r"^.*[\\/]", "", str(args.path), flags=re.S))
338
+ stem = entry_stem(str(args.path))
331
339
  if isinstance(entry, dict) and entry.get("id") != stem:
332
340
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
333
341
  if not problems:
@@ -6,6 +6,7 @@ only. The dashboard kit's fabric-button.js carries its own copy of the name and
6
6
 
7
7
  It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
8
8
  U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
9
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
9
10
 
10
11
  from __future__ import annotations
11
12
 
@@ -29,8 +30,20 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
29
30
  JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
30
31
  _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
31
32
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
32
- SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
33
- r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
33
+ #: Where a provider key's prefix may start (DEC-0151): the start of a value, a character that is no ASCII letter or
34
+ #: digit, or an escape (a percent-escape, single or double, a backslash escape, an octal, `\\x` with two hex digits,
35
+ #: `\\u` with four, an ESC colour sequence, raw or escaped). A consuming group, as the contract writes it.
36
+ _KEY_BOUNDARY = (r"(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt\"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}"
37
+ r"|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)")
38
+ #: The provider keys' shapes, prefixes in their canonical case only (DEC-0151 item 7: `SK-` or `GHP_` is no key). An
39
+ #: `sk-` key holds, within 256 characters, a digit and an upper-case letter with 16 or more characters, or a digit with
40
+ #: 32 or more; the look-aheads are bounded to 255 characters and lazy, so a long run of prefixes reads in linear time.
41
+ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})"
42
+ r"|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,})")
43
+ #: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
44
+ #: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
45
+ #: read the matched text or its index.
46
+ SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + _NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
34
47
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
35
48
  _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
36
49
  re.IGNORECASE | re.ASCII)
@@ -142,7 +155,7 @@ def _percent_encode(text: str, also: str) -> str:
142
155
  return "".join(out)
143
156
 
144
157
 
145
- def _url_parts(value: str) -> Tuple[str, str, str]:
158
+ def url_parts(value: str) -> Tuple[str, str, str]:
146
159
  """The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
147
160
  refuses a `[` or `]` outside an IPv6 host (`<https>://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
148
161
  r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
@@ -166,6 +179,9 @@ def _url_parts(value: str) -> Tuple[str, str, str]:
166
179
  _percent_encode(fragment, "`"))
167
180
 
168
181
 
182
+ _url_parts = url_parts # the name before FAA-250, kept for code that reached it
183
+
184
+
169
185
  def secret_url(value: str) -> bool:
170
186
  """Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
171
187
  password in its user information, or a query or fragment parameter whose name can only carry a credential and has
@@ -177,7 +193,7 @@ def secret_url(value: str) -> bool:
177
193
  value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
178
194
  if not _SCHEME.match(value):
179
195
  return False # not an absolute URL (WHATWG URL parsing refuses it)
180
- password, query, fragment = _url_parts(value)
196
+ password, query, fragment = url_parts(value)
181
197
  if password:
182
198
  return True
183
199
  for part in (query, fragment):
@@ -190,7 +206,10 @@ def secret_url(value: str) -> bool:
190
206
  _REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
191
207
  _REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
192
208
  _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
193
- _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % SECRET_VALUE.pattern, re.IGNORECASE | re.ASCII)
209
+ #: A repository name's own key shapes, boundary-free and case-sensitive (DEC-0151): SECRET_VALUE takes a consuming
210
+ #: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
211
+ _REPOSITORY_KEYS = r"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,}"
212
+ _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
194
213
  _JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
195
214
 
196
215
 
@@ -44,7 +44,14 @@ 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
  """
54
+ # Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
48
55
 
49
56
  # #region source-check — docs: plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md#the-check
50
57
 
@@ -64,6 +71,28 @@ from urllib.parse import unquote
64
71
  from typing import Dict, Iterable, List, NoReturn, Optional, Set, Tuple
65
72
 
66
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
+
67
96
  _WROTE_BYTECODE, sys.dont_write_bytecode = sys.dont_write_bytecode, True # a copy inside the checked tree adds no .pyc
68
97
  _STEP = "fabric_secrets" # the sibling being loaded, so a refusal names it whatever the exception says
69
98
  try:
@@ -71,26 +100,13 @@ try:
71
100
  _STEP = "fabric_source_ignore"
72
101
  from fabric_source_ignore import ( # noqa: E402,F401 - how the ignore lines are read, beside this file
73
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
74
105
  _STEP = "fabric_source_homes"
75
106
  from fabric_source_homes import ( # noqa: E402,F401 - how a declared home is read, beside this file
76
107
  HOME_ENTRY, _NAME_JOIN, _NAME_START, _SEP, _dashed_home, _name_parts, _owner_homes, _spellings)
77
108
  except Exception as _missing: # a copy without its siblings, or mixed from two versions: a refusal, never exit 1
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)
109
+ _refuse_sibling(_STEP, _missing)
94
110
  finally:
95
111
  sys.dont_write_bytecode = _WROTE_BYTECODE
96
112
 
@@ -126,13 +142,16 @@ PRIVATE_KEY = re.compile(r"-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----
126
142
  JWK_PRIVATE = re.compile(r"\\?[\"']d\\?[\"'][ \t]*:[ \t]*\\?[\"']([A-Za-z0-9_-]{43,})") # JSON, escaped, Python
127
143
  #: Words a documented or synthetic credential carries (AWS's `...EXAMPLE`, `sk-test-not-a-key`); a random key does not.
128
144
  SYNTHETIC = ("example", "not-a-key", "notakey", "fake", "dummy", "placeholder", "redacted")
129
- # The detector's provider-key shapes without its first alternative, `^bearer\s.+`, which reads a whole header value: in
130
- # 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
131
- # for as `_BEARER_INSIDE` does: the word, then a run of 12 or more with a digit.
132
- _BEARER_FIRST = "(^bearer["
133
- if not _secrets.SECRET_VALUE.pattern.startswith(_BEARER_FIRST): # not an assert: `python -O` would drop it
134
- raise ImportError("fabric_secrets.SECRET_VALUE changed its first branch; update fabric_source_check.py with it")
135
- 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)
136
155
  #: Files that are credentials by their name: pack's own rule (fabric_package.SECRET_FILE and SECRET_FILE_OK, held
137
156
  #: equal by test_source_check), and the kit's token files (`service.token`, fabric_auth; `board.credential`, fabric_hub).
138
157
  _JS_DOT = "[^\n\r\u2028\u2029]"
@@ -200,7 +219,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
200
219
  url = url.rstrip(".,;)]}")
201
220
  if not _secrets._SCHEME.match(url):
202
221
  return False
203
- password, query, fragment = _secrets._url_parts(url)
222
+ password, query, fragment = _secrets.url_parts(url)
204
223
  # what says it is synthetic is the value, never the host or the path (`examples.blob.core.windows.net`)
205
224
  if len(password) >= 8 and re.search(r"[0-9]", password) and re.search(r"[A-Za-z]", password) and _real(unquote(password)):
206
225
  return True
@@ -215,7 +234,7 @@ def _url_secret(url: str, depth: int = 0) -> bool:
215
234
  if not _real(value):
216
235
  continue
217
236
  if (_secrets.credential_name(name) and _secrets._long_secret(value)) or \
218
- _secrets.JWT_SHAPE.search(value) or _secrets.SECRET_VALUE.search(value):
237
+ _secrets.JWT_SHAPE.search(value) or URL_SECRET_VALUE.search(value):
219
238
  return True
220
239
  return False
221
240
 
@@ -398,13 +417,13 @@ def _instance_finding(text: str, entries: List[str]) -> Optional[str]:
398
417
 
399
418
 
400
419
  def _alternatives(pattern: "re.Pattern[str]") -> List[Tuple["re.Pattern[str]", ...]]:
401
- """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
402
421
  length, open-ended, the whole alternative), so a key is read where its own class ends, not where another's does."""
403
422
  found = []
404
423
  for alt in pattern.pattern[1:-1].split("|"):
405
424
  part = re.fullmatch(r"(.+?)(\[[^\]]+\])\{(\d+)(,?)\}", alt)
406
425
  if part is None: # not an assert: `python -O` would drop it
407
- 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])
408
427
  prefix, cls, least, open_ended = part.groups()
409
428
  found.append((re.compile("(?=%s)" % prefix, pattern.flags), re.compile(prefix, pattern.flags),
410
429
  re.compile(cls + "+", pattern.flags), int(least), bool(open_ended), re.compile(alt, pattern.flags)))
@@ -713,6 +732,15 @@ def check(root: Path, entries: List[str], globs: List[str], every_file: bool = F
713
732
  files, mode = _walk_files(root, unreadable, PACK_REFUSES), "walk"
714
733
  elif not files: # not a checkout, Git refused it, or a folder Git ignores or holds nothing of: read what is there
715
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)
716
744
  findings: List[Dict[str, object]] = []
717
745
  read = 0
718
746
  skipped = {"binary": 0, "unreadable": len(unreadable), "excluded": 0, "nested": 0, "dependencies": len(dependencies)}
@@ -891,6 +919,8 @@ def run(argv: Optional[List[str]] = None) -> int:
891
919
  except CheckError as exc:
892
920
  print("fabric_source_check.py: %s" % exc, file=sys.stderr)
893
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)
894
924
  findings = result["findings"]
895
925
  skipped = result["skipped"]
896
926
  listed = sum(1 for e in entries if e and not e.startswith(HOME_ENTRY))
@@ -925,55 +955,6 @@ def run(argv: Optional[List[str]] = None) -> int:
925
955
  return 0 if complete else 2
926
956
 
927
957
 
928
- def _utf8_restart(encoding: str, utf8_mode: int, executable: str, argv: List[str]) -> Optional[List[bytes]]:
929
- """The command that restarts the check in Python's UTF-8 mode, or None when it needs none. Under a locale whose
930
- 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
931
- 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
932
- that byte. Windows names are UTF-16 and need none. The arguments go back as the bytes they came in. An empty list:
933
- a restart is needed and no interpreter is known (the caller exits 2 rather than read in two encodings)."""
934
- if os.name == "nt" or utf8_mode or encoding.lower().replace("_", "-") in ("utf-8", "utf8"):
935
- return None
936
- if not executable:
937
- return []
938
- return [os.fsencode(executable), b"-X", b"utf8"] + [os.fsencode(a) for a in argv]
939
-
940
-
941
- def _interpreter() -> str:
942
- """The running interpreter: Linux's `/proc/self/exe`, which no PATH or working folder can stand in for (Python 3.9
943
- may take a `./python3` of the working folder as sys.executable), else sys.executable, which may be empty."""
944
- try:
945
- return os.readlink("/proc/self/exe")
946
- except (OSError, AttributeError):
947
- return sys.executable or ""
948
-
949
-
950
- def _interpreter_flags(flags: object, xoptions: Dict[str, object]) -> List[str]:
951
- """The interpreter's own options, rebuilt where sys.orig_argv is missing (Python 3.9), so a restart keeps `-I`."""
952
- out: List[str] = []
953
- if getattr(flags, "isolated", 0):
954
- out.append("-I")
955
- else:
956
- out += ["-E"] * bool(getattr(flags, "ignore_environment", 0)) + ["-s"] * bool(getattr(flags, "no_user_site", 0))
957
- out += ["-S"] * bool(getattr(flags, "no_site", 0)) + ["-B"] * bool(getattr(flags, "dont_write_bytecode", 0))
958
- if getattr(flags, "optimize", 0):
959
- out.append("-" + "O" * int(getattr(flags, "optimize")))
960
- for key, value in xoptions.items():
961
- if key not in ("utf8", "fabric_console"):
962
- out += ["-X", key if value is True else "%s=%s" % (key, value)]
963
- return out
964
-
965
-
966
- def _keep_console(xoptions: Dict[str, object]) -> None:
967
- """After a restart, write the console's own encoding again (the parent named it in `-X fabric_console=`)."""
968
- console = xoptions.get("fabric_console")
969
- if isinstance(console, str):
970
- for stream in (sys.stdout, sys.stderr):
971
- try:
972
- stream.reconfigure(encoding=console, errors="backslashreplace") # type: ignore[union-attr]
973
- except (AttributeError, LookupError, ValueError):
974
- pass
975
-
976
-
977
958
  if __name__ == "__main__":
978
959
  _keep_console(getattr(sys, "_xoptions", {}))
979
960
  _argv = (list(sys.orig_argv[1:]) if hasattr(sys, "orig_argv")