@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.
- package/CHANGELOG.md +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- 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
|
|
11
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py
CHANGED
|
@@ -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 = "
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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))
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -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 =
|
|
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:
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
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
|
|
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
|
|
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("
|
|
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")
|