Compare commits

..

No commits in common. "main" and "codex/setup-caddy-with-variable-configuration" have entirely different histories.

488 changed files with 1106 additions and 27402 deletions

View File

@ -1,44 +0,0 @@
name: Validate Release PR
# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
# 详见 iac_modules/docs/tldr-github-branch-model.md
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
jobs:
validate-release-source:
runs-on: ubuntu-latest
if: startsWith(github.base_ref, 'release/')
steps:
- name: Check PR source branch
run: |
SRC="${{ github.head_ref }}"
TGT="${{ github.base_ref }}"
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
echo "🔍 Validating PR into release branch"
echo " source: $SRC"
echo " target: $TGT"
echo " labels: $LABELS"
if [[ "$SRC" =~ ^hotfix/ ]]; then
echo "✅ Allowed: hotfix/* branch"
exit 0
fi
if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then
echo "✅ Allowed: cherry-pick/backport labeled PR"
exit 0
fi
echo "❌ Rejected."
echo "release/* 仅接受:"
echo " - 来自 hotfix/* 的 PR"
echo " - 带 cherry-pick 或 backport 标签的 PR已验证 feature 的 backport/cherry-pick"
echo "禁止从 main / develop / feature/* 直接合并到 release/*。"
exit 1

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
xfce-secrets.yml
inventory/__pycache__/
.playwright-mcp/
.env
.artifacts/
.artifacts/acp_codex/xworkmate-go-core
.artifacts/acp_opencode/xworkmate-go-core

View File

@ -1,3 +0,0 @@
dcdc9bea7b49f045e1ac0a30f85a5e0c84c1e8db:group_vars/xworkmate_bridge_distributed.yml:generic-api-key:41
ba4daa35977d3c7aaecc1f9dd42a6dc41794d04c:group_vars/xworkmate_bridge_distributed.yml:generic-api-key:35
126a19e2828f52b2a510e107ef66a9ef1d1e88cf:docs/tldr-ssh-security.md:hashicorp-tf-password:78

810
LICENSE
View File

@ -1,202 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Preamble
1. Definitions.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
The precise terms and conditions for copying, distribution and
modification follow.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
TERMS AND CONDITIONS
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
0. Definitions.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
"This License" refers to version 3 of the GNU General Public License.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
A "covered work" means either the unmodified Program or a work based
on the Program.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
1. Source Code.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
END OF TERMS AND CONDITIONS
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
APPENDIX: How to apply the Apache License to your work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
The Corresponding Source for a work in source code form is that
same work.
Copyright (C) 2018-2026 Ruohang Feng, @Vonng (rh@vonng.com)
2. Basic Permissions.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
http://www.apache.org/licenses/LICENSE-2.0
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

115
README.md
View File

@ -1,114 +1 @@
# playbooks
## XWorkmate Bridge Distributed VPN
The bidirectional WireGuard-over-VLESS transport for the two XWorkmate bridge
nodes is deployed by:
```bash
ansible-playbook -i inventory.ini vpn-wireguard-over-vless.yml
```
The implementation uses split bridge groups (`xworkmate_bridge` and
`cn_xworkmate_bridge`) under `xworkmate_bridge_distributed`, stores private keys
and the shared management-side Xray UUID in `https://vault.svc.plus`, and keeps
the host's default `xray.service` untouched. The runbook lives in
[`roles/vhosts/xworkmate_bridge_distributed_vpn/README.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md).
## Cloud Dev Desktop
The cloud dev desktop flow lives here as two playbooks:
1. `bootstrap_cloud_dev_desktop.yml`
2. `destroy_cloud_dev_desktop.yml`
`bootstrap_cloud_dev_desktop.yml` now includes the create/bootstrap/verify sequence in one entry point. The control-plane repo calls these playbooks from `../playbooks`.
## Traffic Billing Stack
The traffic billing stack now has a single aggregate playbook:
`deploy_svc_plus_core_services_stack.yml`
It orchestrates these existing playbooks in dependency order:
1. `deploy_billing_service.yml`
2. `deploy_xworkmate_bridge_vhosts.yml`
3. `deploy_xray_exporter.yml`
4. `deploy_agent_svc_plus.yml`
5. `deploy_accounts_svc_plus.yml`
6. `deploy_stunnel-client.yml`
7. `deploy_apisix.yml`
8. `deploy_console_svc_plus.yml`
### Full stack deploy
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
export STACK_TARGET_HOST=jp_xhttp_contabo_host
export console_service_sync_dns=true
ansible-playbook -i inventory.ini deploy_svc_plus_core_services_stack.yml
```
`STACK_ENV_FILE=./.env` is optional. Use it when you want the aggregate playbook to read a local `.env` file; GitHub Actions or other CI runners can skip it and pass values with `-e` instead.
### Deploy to one target host directly
Use `STACK_TARGET_HOST` to override the stack host groups when you want all services to target the same inventory host. For console-only runs, use Ansible's `-l jp_xhttp_contabo_host` limit instead of a separate host variable, and keep `console_service_sync_dns=true` if you want DNS reconciliation.
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export STACK_TARGET_HOST=jp_xhttp_contabo_host
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
export console_service_sync_dns=true
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
```
### Deploy only selected services
Use `STACK_SERVICES` with a comma-separated list:
- `billing-service`
- `xworkmate-bridge`
- `xray-exporter`
- `agent`
- `accounts`
- `stunnel-client`
- `apisix`
- `console`
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
export STACK_TARGET_HOST=jp-xhttp-contabo.svc.plus
export STACK_SERVICES=xray-exporter,billing-service,agent,xworkmate-bridge
export INTERNAL_SERVICE_TOKEN=...
export DATABASE_URL=postgres://...
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
```
### Notes
- `accounts` and `console` still use their existing role contracts.
- `console` requires `FRONTEND_IMAGE` because the target host only does pull-only compose deployment.
- `console` now writes a Caddy fragment named like `<server-name>-<release_id>-<hostname>-<domain>.caddy` instead of managing the Caddy service container itself.
- `billing-service` requires `DATABASE_URL`.
- `xray-exporter` and `agent` require `INTERNAL_SERVICE_TOKEN`.
- `xworkmate-bridge` accepts `XWORKMATE_BRIDGE_HOSTS`, and also follows `STACK_TARGET_HOST` when you want to deploy the whole stack to one host.
### Deploy console to a specific host and sync DNS
`deploy_console_svc_plus.yml` now accepts `console_service_sync_dns=true` to rebuild and reconcile DNS records after deployment. For host selection, use Ansible's `-l jp_xhttp_contabo_host` limit.
Example:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
ansible-playbook -i inventory.ini deploy_console_svc_plus.yml \
-e console_service_sync_dns=true \
-e FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
```
# playbooks

View File

@ -1,19 +1,15 @@
[defaults]
allow_world_readable_tmpfiles = True
# 常用参数
# 默认清单文件路径,可按需改
inventory = ./inventory.ini
inventory = ./inventory # 默认清单文件路径,可按需改
vault_password_file = ~/.vault_password
timeout = 10
forks = 10
poll_interval = 10
transport = smart
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
# 输出配置:使用 ansible-core 内置 callback避免在轻量 CI 环境里缺少额外插件
stdout_callback = default
# 输出配置:推荐 yaml兼容性最好
stdout_callback = yaml
bin_ansible_callbacks = True
callbacks_enabled = profile_tasks,timer
@ -28,6 +24,3 @@ deprecation_warnings = False
cache = True
cache_plugin = jsonfile
cache_timeout = 3600
[ssh_connection]
pipelining = True

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>plus.svc.xworkspace.api</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>
source "{{ xworkspace_console_config_dir }}/portal.env"
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
exec {{ xworkspace_console_api_exec }}
</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>{{ xworkspace_console_api_working_dir }}</string>
<key>StandardOutPath</key>
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/api.log</string>
<key>StandardErrorPath</key>
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/api.err.log</string>
</dict>
</plist>

View File

@ -1,289 +0,0 @@
- name: Normalize cloud dev desktop request
hosts: localhost
connection: local
gather_facts: true
roles:
- role: cloud_vm_request_validate
- name: Create cloud dev desktop infrastructure
hosts: localhost
connection: local
gather_facts: true
roles:
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
vars:
cloud_lifecycle_action: create
- role: cloud_vm_inventory_emit
- name: Bootstrap remote cloud dev desktop
hosts: cloud_desktop
gather_facts: true
become: "{{ os_family != 'windows' }}"
roles:
- role: dev_desktop_common
when: os_family != "windows"
- role: dev_desktop_windows
when: os_family == "windows"
- role: dev_desktop_fedora_gnome
when: os_family == "fedora-gnome"
- role: dev_desktop_debian_kde
when: os_family == "debian-kde"
- name: Verify remote cloud dev desktop
hosts: cloud_desktop
gather_facts: true
become: "{{ os_family != 'windows' }}"
tasks:
- name: Verify common Linux workspace marker
ansible.builtin.stat:
path: /opt/cloud-dev-desktop/profile.env
register: common_profile_marker
when: os_family != "windows"
- name: Assert Linux profile marker exists
ansible.builtin.assert:
that:
- common_profile_marker.stat.exists
fail_msg: "Missing /opt/cloud-dev-desktop/profile.env marker on Linux host."
when: os_family != "windows"
- name: Verify Fedora GNOME desktop packages
ansible.builtin.command: rpm -q gnome-shell gtk3-devel gtk4-devel glib2-devel clang cmake ninja-build
changed_when: false
when: os_family == "fedora-gnome"
- name: Verify Debian KDE desktop packages
ansible.builtin.shell: |
set -euo pipefail
dpkg-query -W plasma-desktop clang cmake ninja-build >/dev/null
if dpkg-query -W qt6-base-dev >/dev/null 2>&1; then
exit 0
fi
dpkg-query -W qtbase5-dev >/dev/null
args:
executable: /bin/bash
changed_when: false
when: os_family == "debian-kde"
- name: Verify Node.js 22+ on Linux
ansible.builtin.shell: |
set -euo pipefail
test "$(node --version | sed 's/^v//' | cut -d. -f1)" -ge 22
args:
executable: /bin/bash
become_user: "{{ admin_username }}"
changed_when: false
when: os_family != "windows"
- name: Verify Go toolchain on Linux
ansible.builtin.command: /usr/local/go/bin/go version
become_user: "{{ admin_username }}"
changed_when: false
when: os_family != "windows"
- name: Verify Codex CLI on Linux
ansible.builtin.command: codex --version
become_user: "{{ admin_username }}"
changed_when: false
when:
- os_family != "windows"
- toolchains.codex | bool
- name: Verify Flutter is installed on Linux
ansible.builtin.shell: |
set -euo pipefail
test -x {{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter
{{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter --version
args:
executable: /bin/bash
become_user: "{{ admin_username }}"
environment:
HOME: "/home/{{ admin_username }}"
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
changed_when: false
when:
- os_family != "windows"
- toolchains.flutter | bool
- name: Verify Codex CLI on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
$extraPaths = @(
$npmUserBin,
'C:\Program Files\nodejs',
'C:\tools\flutter\bin',
'C:\Program Files\Microsoft VS Code\bin',
'C:\ProgramData\chocolatey\bin'
) | Where-Object { $_ }
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
$codexCmd = Join-Path $env:APPDATA 'npm\codex.cmd'
if (-not (Test-Path $codexCmd)) {
throw "Missing Codex CLI launcher at $codexCmd"
}
$codexCmdLine = '"' + $codexCmd + '" --version'
cmd.exe /d /c $codexCmdLine | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Codex CLI version probe failed with exit code $LASTEXITCODE"
}
changed_when: false
when: os_family == "windows"
- name: Verify Node.js 22+ on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
$extraPaths = @(
$npmUserBin,
'C:\Program Files\nodejs',
'C:\tools\flutter\bin',
'C:\Program Files\Microsoft VS Code\bin',
'C:\ProgramData\chocolatey\bin'
) | Where-Object { $_ }
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
$nodeMajor = [int]((node --version).Trim().TrimStart('v').Split('.')[0])
if ($nodeMajor -lt 22) {
throw "Node.js 22+ is required, found $(node --version)"
}
changed_when: false
when: os_family == "windows"
- name: Verify Flutter on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
$extraPaths = @(
$npmUserBin,
'C:\Program Files\nodejs',
'C:\tools\flutter\bin',
'C:\Program Files\Microsoft VS Code\bin',
'C:\ProgramData\chocolatey\bin'
) | Where-Object { $_ }
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
flutter --version | Out-Null
changed_when: false
when:
- os_family == "windows"
- toolchains.flutter | bool
- name: Verify VS Code on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
$extraPaths = @(
$npmUserBin,
'C:\Program Files\nodejs',
'C:\tools\flutter\bin',
'C:\Program Files\Microsoft VS Code\bin',
'C:\ProgramData\chocolatey\bin'
) | Where-Object { $_ }
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
Get-Command code | Out-Null
changed_when: false
when:
- os_family == "windows"
- toolchains.vscode | bool
- name: Verify Git on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
$extraPaths = @(
$npmUserBin,
'C:\Program Files\Git\cmd',
'C:\Program Files\nodejs',
'C:\tools\flutter\bin',
'C:\Program Files\Microsoft VS Code\bin',
'C:\ProgramData\chocolatey\bin'
) | Where-Object { $_ }
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
git --version | Out-Null
changed_when: false
when: os_family == "windows"
- name: Verify Android Studio on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
if (-not (Test-Path 'C:\Program Files\Android\Android Studio\bin\studio64.exe')) {
throw 'Missing Android Studio executable at C:\Program Files\Android\Android Studio\bin\studio64.exe'
}
changed_when: false
when:
- os_family == "windows"
- toolchains.android_studio | bool
- name: Verify Visual Studio desktop C++ toolchain on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
if (-not (Test-Path $vswhere)) {
throw "Missing vswhere at $vswhere"
}
$installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $installPath) {
throw 'Visual Studio Build Tools with Desktop C++ workload is not installed'
}
changed_when: false
when: os_family == "windows"
- name: Verify Android SDK and emulator on Windows
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$androidSdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk'
$requiredPaths = @(
(Join-Path $androidSdkRoot 'platform-tools\adb.exe'),
(Join-Path $androidSdkRoot 'emulator\emulator.exe'),
(Join-Path $androidSdkRoot 'cmdline-tools\latest\bin\sdkmanager.bat')
)
foreach ($required in $requiredPaths) {
if (-not (Test-Path $required)) {
throw "Missing Android SDK component: $required"
}
}
changed_when: false
when:
- os_family == "windows"
- toolchains.android_studio | bool
- name: Verify Windows Android virtualization features
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$featureNames = @(
'Microsoft-Hyper-V-All',
'HypervisorPlatform',
'VirtualMachinePlatform'
)
foreach ($featureName in $featureNames) {
$feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName
if ($feature.State -ne 'Enabled') {
throw "Windows optional feature $featureName is not enabled"
}
}
& (Join-Path $env:LOCALAPPDATA 'Android\Sdk\emulator\emulator-check.exe') accel | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Android emulator acceleration probe failed with exit code $LASTEXITCODE"
}
changed_when: false
when:
- os_family == "windows"
- toolchains.android_studio | bool
- name: Verify Windows SSHD service
ansible.windows.win_shell: |
$ErrorActionPreference = "Stop"
$sshd = Get-Service sshd
if ($sshd.Status -ne 'Running') {
throw "SSHD service is not running"
}
changed_when: false
when: os_family == "windows"

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>plus.svc.xworkspace.console</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
# 预编译 runtime 只发 dashboard/dist无 package.json且 dashboard 是
# 无客户端路由的单页应用,故用 python3 静态伺服 dist 即可macOS 无 caddy
exec /usr/bin/env python3 -m http.server {{ xworkspace_console_port }} --bind 127.0.0.1 --directory "{{ xworkspace_console_dashboard_dir }}/dist"
</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>{{ xworkspace_console_dashboard_dir }}/dist</string>
<key>StandardOutPath</key>
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/console.log</string>
<key>StandardErrorPath</key>
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/console.err.log</string>
</dict>
</plist>

View File

@ -1,24 +0,0 @@
---
- name: Create a root-managed SSH audit user on selected hosts
hosts: all
become: true
gather_facts: true
vars:
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
readonly_ssh_user_profile: audit
readonly_ssh_user_lock_password: true
readonly_ssh_user_manage_sudoers: true
readonly_ssh_user_authorized_keys: >-
{{
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
else []
}}
roles:
- role: readonly_ssh_user

View File

@ -1,25 +0,0 @@
---
- name: Create a readonly SSH user on selected hosts
hosts: all
become: true
gather_facts: true
vars:
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
readonly_ssh_user_profile: "{{ lookup('env', 'READONLY_SSH_USER_PROFILE') | default('readonly', true) }}"
readonly_ssh_user_password_hash: "{{ lookup('env', 'READONLY_SSH_USER_PASSWORD_HASH') | default('', true) }}"
readonly_ssh_user_lock_password: "{{ lookup('env', 'READONLY_SSH_LOCK_PASSWORD') | default('true', true) | bool }}"
readonly_ssh_user_manage_sudoers: "{{ lookup('env', 'READONLY_SSH_ENABLE_SUDO') | default('false', true) | bool }}"
readonly_ssh_user_authorized_keys: >-
{{
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
else []
}}
roles:
- role: readonly_ssh_user

View File

@ -1,11 +0,0 @@
---
- name: Deploy QMD extended memory
hosts: "{{ qmd_hosts | default('all') }}"
become: true
gather_facts: true
module_defaults:
ansible.builtin.apt:
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
roles:
- role: roles/vhosts/qmd/
tags: [qmd]

View File

@ -1,26 +0,0 @@
- name: Deploy managed accounts.svc.plus service
hosts: "{{ accounts_service_hosts | default('accounts') }}"
gather_facts: false
become: true
vars:
accounts_service_image_ref: >-
{{
(lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REF') | default('', true) | trim)
or
(
(lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO') | default('ghcr.io/x-evor/accounts', true))
~ ':'
~ (lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG') | default('latest', true))
)
}}
accounts_service_image_repo: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
| default('ghcr.io/x-evor/accounts', true) }}
accounts_service_image_tag: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
| default('70c6a3f8', true) }}
accounts_service_pull_image: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_PULL_IMAGE')
| default(false, true) | bool }}
roles:
- roles/vhosts/accounts_service

View File

@ -1,14 +0,0 @@
---
- name: Deploy ACP Codex vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_codex/
tags: [acp_codex]
- role: roles/vhosts/xworkmate_bridge/
vars:
deploy_acp_codex: true
deploy_acp_opencode: false
deploy_acp_gemini: false
tags: [xworkmate_bridge, acp_codex]

View File

@ -1,14 +0,0 @@
---
- name: Deploy ACP Gemini vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_gemini/
tags: [acp_gemini]
- role: roles/vhosts/xworkmate_bridge/
vars:
deploy_acp_codex: false
deploy_acp_opencode: false
deploy_acp_gemini: true
tags: [xworkmate_bridge, acp_gemini]

View File

@ -1,14 +0,0 @@
---
- name: Deploy ACP OpenCode vhosts
hosts: all
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_opencode/
tags: [acp_opencode]
- role: roles/vhosts/xworkmate_bridge/
vars:
deploy_acp_codex: false
deploy_acp_opencode: true
deploy_acp_gemini: false
tags: [xworkmate_bridge, acp_opencode]

View File

@ -1,9 +0,0 @@
---
- name: Deploy Hermes ACP agent adapter
hosts: "{{ acp_hermes_hosts | default('all') }}"
become: true
gather_facts: true
roles:
- role: roles/vhosts/acp_server_hermes/
tags: [acp_hermes, hermes]

View File

@ -1,98 +0,0 @@
- name: Deploy managed agent.svc.plus service
hosts: "{{ agent_service_hosts | default('agent_svc_plus') }}"
gather_facts: true
become: true
vars:
agent_svc_plus_repo_url: >-
{{ lookup('ansible.builtin.env', 'AGENT_REPO_URL')
| default('https://github.com/x-evor/agent.svc.plus.git', true) }}
agent_svc_plus_repo_version: >-
{{ lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
| default('main', true) }}
agent_svc_plus_release_tag: >-
{{ lookup('ansible.builtin.env', 'AGENT_RELEASE_TAG')
| default(
(lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
| default('main', true))
if ((lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
| default('main', true)) is match('^v.+'))
else '',
true
) }}
agent_svc_plus_binary_src: >-
{{ lookup('ansible.builtin.env', 'AGENT_BINARY_SRC')
| default('', true) }}
agent_svc_plus_app_dir: >-
{{ lookup('ansible.builtin.env', 'AGENT_APP_DIR')
| default('/opt/agent.svc.plus', true) }}
agent_svc_plus_go_version: >-
{{ lookup('ansible.builtin.env', 'AGENT_GO_VERSION')
| default('1.25.1', true) }}
agent_id: >-
{{ lookup('ansible.builtin.env', 'AGENT_ID')
| default('node-xhttp.svc.plus', true) }}
agent_tls_cert_name: >-
{{ lookup('ansible.builtin.env', 'AGENT_TLS_CERT_NAME')
| default(agent_id, true) }}
agent_controller_url: >-
{{ lookup('ansible.builtin.env', 'AGENT_CONTROLLER_URL')
| default('https://accounts.svc.plus', true) }}
agent_api_token: >-
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
agent_billing_enabled: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_ENABLED')
| default(true, true) | bool }}
agent_billing_base_url: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
| default('http://127.0.0.1:8081', true) }}
agent_billing_http_timeout: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_HTTP_TIMEOUT')
| default('15s', true) }}
agent_billing_collect_interval: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_COLLECT_INTERVAL')
| default('1m', true) }}
agent_billing_reconcile_interval: >-
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_RECONCILE_INTERVAL')
| default('5m', true) }}
xray_enabled: >-
{{ lookup('ansible.builtin.env', 'AGENT_XRAY_ENABLED')
| default(true, true) | bool }}
xray_uuid: >-
{{ lookup('ansible.builtin.env', 'XRAY_UUID')
| default('00000000-0000-0000-0000-000000000000', true) }}
pre_tasks:
- name: Validate INTERNAL_SERVICE_TOKEN is present
ansible.builtin.assert:
that:
- agent_api_token | length > 0
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
success_msg: "INTERNAL_SERVICE_TOKEN found"
- name: Gather service facts
ansible.builtin.service_facts:
- name: Assert host is bootstrapped with setup-proxy.sh services
ansible.builtin.assert:
that:
- "'xray.service' in ansible_facts.services"
- "'xray-tcp.service' in ansible_facts.services"
- "'caddy.service' in ansible_facts.services"
fail_msg: "Target host must already be bootstrapped by setup-proxy.sh (missing xray.service, xray-tcp.service, or caddy.service)."
success_msg: "Target host already has the setup-proxy.sh service layout."
- name: Assert setup-proxy.sh config paths exist
ansible.builtin.stat:
path: "{{ item }}"
loop:
- /etc/caddy/Caddyfile
- /usr/local/etc/xray/templates
register: agent_bootstrap_paths
- name: Validate setup-proxy.sh config paths are present
ansible.builtin.assert:
that:
- agent_bootstrap_paths.results | map(attribute='stat.exists') | min
fail_msg: "Target host is missing /etc/caddy/Caddyfile or /usr/local/etc/xray/templates. Run setup-proxy.sh first."
success_msg: "setup-proxy.sh config paths exist."
roles:
- roles/vhosts/agent-svc-plus

View File

@ -1,2 +0,0 @@
---
- import_playbook: deploy_apisix_svc.plus.yaml

View File

@ -1,7 +0,0 @@
---
- name: Deploy managed api.svc.plus service
hosts: "{{ apisix_service_hosts | default('apisix') }}"
gather_facts: false
become: true
roles:
- roles/vhosts/apisix_service

View File

@ -1,87 +0,0 @@
- name: Deploy billing-service
hosts: "{{ billing_service_hosts | default('billing_service') }}"
gather_facts: true
become: true
vars:
billing_service_binary_artifact: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BINARY_ARTIFACT')
| default('', true) }}
billing_service_image_ref: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_IMAGE_REF')
| default('', true) }}
billing_service_exporter_base_url: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
| default('http://127.0.0.1:8080', true) }}
billing_service_exporter_sources_json: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_SOURCES_JSON')
| default('', true) }}
billing_service_internal_service_token: >-
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
billing_service_database_url: >-
{{ lookup('ansible.builtin.env', 'DATABASE_URL')
| default('', true) }}
billing_service_listen_addr: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_LISTEN_ADDR')
| default('127.0.0.1:8081', true) }}
billing_service_collect_interval: >-
{{ lookup('ansible.builtin.env', 'COLLECT_INTERVAL')
| default('1m', true) }}
billing_service_default_region: >-
{{ lookup('ansible.builtin.env', 'DEFAULT_REGION')
| default('', true) }}
billing_service_source_revision: >-
{{ lookup('ansible.builtin.env', 'SOURCE_REVISION')
| default('billing-service-v1', true) }}
billing_service_price_per_byte: >-
{{ lookup('ansible.builtin.env', 'PRICE_PER_BYTE')
| default('0', true) }}
billing_service_initial_included_quota_bytes: >-
{{ lookup('ansible.builtin.env', 'INITIAL_INCLUDED_QUOTA_BYTES')
| default('0', true) }}
billing_service_initial_balance: >-
{{ lookup('ansible.builtin.env', 'INITIAL_BALANCE')
| default('0', true) }}
pre_tasks:
- name: Validate BILLING_SERVICE_BINARY_ARTIFACT is present
ansible.builtin.assert:
that:
- billing_service_binary_artifact | length > 0
fail_msg: "BILLING_SERVICE_BINARY_ARTIFACT must be exported before running this playbook."
success_msg: "BILLING_SERVICE_BINARY_ARTIFACT found"
- name: Validate BILLING_SERVICE_BINARY_ARTIFACT exists
ansible.builtin.stat:
path: "{{ billing_service_binary_artifact }}"
register: billing_service_binary_artifact_stat
delegate_to: localhost
become: false
run_once: true
- name: Assert BILLING_SERVICE_BINARY_ARTIFACT exists on controller
ansible.builtin.assert:
that:
- billing_service_binary_artifact_stat.stat.exists
- billing_service_binary_artifact_stat.stat.isreg
fail_msg: "BILLING_SERVICE_BINARY_ARTIFACT must point to an existing binary artifact."
success_msg: "BILLING_SERVICE_BINARY_ARTIFACT exists"
delegate_to: localhost
become: false
run_once: true
- name: Validate BILLING_SERVICE_IMAGE_REF is present
ansible.builtin.assert:
that:
- billing_service_image_ref | length > 0
fail_msg: "BILLING_SERVICE_IMAGE_REF must be exported before running this playbook."
success_msg: "BILLING_SERVICE_IMAGE_REF found"
- name: Validate DATABASE_URL is present
ansible.builtin.assert:
that:
- billing_service_database_url | length > 0
fail_msg: "DATABASE_URL must be exported before running this playbook."
success_msg: "DATABASE_URL found"
- name: Validate INTERNAL_SERVICE_TOKEN is present
ansible.builtin.assert:
that:
- billing_service_internal_service_token | length > 0
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
success_msg: "INTERNAL_SERVICE_TOKEN found"
roles:
- roles/vhosts/billing-service

View File

@ -1,21 +0,0 @@
- name: Deploy managed console.svc.plus service
hosts: "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"
gather_facts: true
become: true
roles:
- roles/vhosts/docker
- roles/vhosts/caddy
- roles/vhosts/console_service
- name: Sync console DNS records when requested
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Reconcile Cloudflare DNS for console target host
when: console_service_sync_dns | default(false)
ansible.builtin.include_role:
name: cloudflare_svc_plus_dns
vars:
cloudflare_dns_source_hosts:
- "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"

View File

@ -1,58 +0,0 @@
- name: Deploy managed docs.svc.plus service
hosts: "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}"
gather_facts: true
become: true
vars:
docs_service_image_ref: >-
{{
(lookup('ansible.builtin.env', 'DOCS_IMAGE_REF') | default('', true) | trim)
or
(
(lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO') | default('ghcr.io/x-evor/docs', true))
~ ':'
~ (lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG') | default('latest', true))
)
}}
docs_service_image_repo: >-
{{ lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO')
| default('ghcr.io/x-evor/docs', true) }}
docs_service_image_tag: >-
{{ lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG')
| default('latest', true) }}
docs_service_pull_image: >-
{{ lookup('ansible.builtin.env', 'DOCS_PULL_IMAGE')
| default(true, true) | bool }}
docs_service_knowledge_repo_path_host: >-
{{ lookup('ansible.builtin.env', 'DOCS_KNOWLEDGE_REPO_PATH_HOST')
| default('', true) }}
docs_service_internal_service_token: >-
{{
lookup('ansible.builtin.env', 'DOCS_INTERNAL_SERVICE_TOKEN')
| default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true)
}}
docs_service_reload_interval: >-
{{ lookup('ansible.builtin.env', 'DOCS_RELOAD_INTERVAL')
| default('5m', true) }}
docs_service_container_port: >-
{{ lookup('ansible.builtin.env', 'DOCS_SERVICE_PORT')
| default('8084', true) }}
docs_service_host_port: >-
{{ lookup('ansible.builtin.env', 'DOCS_HOST_PORT')
| default('18086', true) }}
roles:
- roles/vhosts/docker
- roles/vhosts/caddy
- roles/vhosts/docs_service
- name: Sync docs DNS records when requested
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Reconcile Cloudflare DNS for docs target host
when: docs_service_sync_dns | default(false)
ansible.builtin.include_role:
name: cloudflare_svc_plus_dns
vars:
cloudflare_dns_source_hosts:
- "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}"

View File

@ -1,11 +0,0 @@
---
- name: Deploy OpenClaw gateway vhost
hosts: "{{ gateway_openclaw_hosts | default('all') }}"
become: true
gather_facts: true
module_defaults:
ansible.builtin.apt:
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
roles:
- role: roles/vhosts/gateway_openclaw/
tags: [gateway_openclaw, openclaw]

View File

@ -1,10 +0,0 @@
---
- name: Deploy Modern IT History Docusaurus ebook
hosts: "{{ modern_it_history_target_host | default('jp_xhttp_contabo_host') }}"
gather_facts: true
become: true
vars:
nodejs_version: "24.x"
roles:
- roles/vhosts/nodejs
- roles/vhosts/modern_it_history

View File

@ -5,8 +5,8 @@
become: yes
vars:
# Choose Node.js version
# Examples: "22.x" (LTS), "20.x", "18.x", or specific version like "20.11.0"
nodejs_version: "22.x"
# Examples: "20.x" (LTS), "18.x", "22.x", or specific version like "20.11.0"
nodejs_version: "20.x"
# Install Yarn package manager (default: true)
# install_yarn: false

View File

@ -0,0 +1,8 @@
- name: Deploy PostgreSQL on vhosts
hosts: "{{ postgresql_target | default('postgresql') }}"
become: true
vars:
group: "{{ group | default(postgresql_target | default('postgresql')) }}"
roles:
- roles/vhosts/common/
- roles/vhosts/postgres/

View File

@ -1,25 +0,0 @@
- name: Deploy managed postgresql.svc.plus service
hosts: "{{ postgresql_service_hosts | default('postgresql') }}"
gather_facts: false
become: true
vars:
postgresql_service_postgres_image_repo: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_REPO')
| default('postgres-extensions', true) }}
postgresql_service_postgres_image_tag: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_TAG')
| default('17', true) }}
postgresql_service_postgres_pull_image: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_PULL_IMAGE')
| default(false, true) | bool }}
postgresql_service_stunnel_image_repo: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_REPO')
| default('ghcr.io/x-evor/stunnel-server', true) }}
postgresql_service_stunnel_image_tag: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_TAG')
| default('2330d36', true) }}
postgresql_service_stunnel_pull_image: >-
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_PULL_IMAGE')
| default(false, true) | bool }}
roles:
- roles/vhosts/postgresql_service

View File

@ -1,20 +0,0 @@
---
- name: Validate shared stunnel-client release entrypoint
hosts: "{{ stunnel_client_hosts | default(lookup('ansible.builtin.env', 'STACK_TARGET_HOST') | default('jp_xhttp_contabo_host', true), true) }}"
become: true
gather_facts: false
tasks:
- name: Acknowledge shared stunnel-client dry run
ansible.builtin.debug:
msg: >-
stunnel-client deployment is not modelled in this repo yet; this
placeholder keeps aggregate dry runs green while apply stays blocked.
changed_when: false
when: ansible_check_mode
- name: Block apply until shared stunnel-client role exists
ansible.builtin.fail:
msg: >-
stunnel-client apply is not implemented in the playbooks repo yet.
Add a real stunnel-client role before using this entrypoint in apply mode.
when: not ansible_check_mode

View File

@ -1,218 +0,0 @@
- name: Load stack environment values from local .env file
hosts: all
gather_facts: false
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
tasks:
- name: Parse stack .env file into a dictionary
run_once: true
delegate_to: localhost
ansible.builtin.command:
argv:
- python3
- -c
- |
import json
import os
import re
import shlex
import sys
path = sys.argv[1]
data = {}
if os.path.exists(path):
with open(path, encoding="utf-8", errors="ignore") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
match = re.match(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$", line)
if not match:
continue
key, value = match.groups()
value = value.strip()
if value:
try:
parts = shlex.split(value, comments=False, posix=True)
value = parts[0] if parts else ""
except ValueError:
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1]
data[key] = value
print(json.dumps(data))
- "{{ stack_env_file }}"
register: stack_env_parse_result
changed_when: false
check_mode: false
- name: Store parsed stack environment values
run_once: true
delegate_to: localhost
delegate_facts: true
ansible.builtin.set_fact:
stack_env_map: "{{ stack_env_parse_result.stdout | default('{}', true) | from_json }}"
- import_playbook: deploy_billing_service.yml
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
billing_service_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'BILLING_SERVICE_HOSTS')
| default('billing_service', true), true) }}
billing_service_database_url: >-
{{
lookup('ansible.builtin.env', 'DATABASE_URL')
| default(hostvars['localhost'].stack_env_map.DATABASE_URL
| default('', true), true)
or (
'postgres://%s:%s@%s:%s/%s?sslmode=disable'
| format(
lookup('ansible.builtin.env', 'POSTGRES_USER')
| default(hostvars['localhost'].stack_env_map.POSTGRES_USER
| default('svcplus_vps', true), true),
lookup('ansible.builtin.env', 'POSTGRES_PASSWORD')
| default(hostvars['localhost'].stack_env_map.POSTGRES_PASSWORD
| default('', true), true),
lookup('ansible.builtin.env', 'BILLING_DB_HOST')
| default(hostvars['localhost'].stack_env_map.BILLING_DB_HOST
| default('stunnel-client', true), true),
lookup('ansible.builtin.env', 'BILLING_DB_PORT')
| default(hostvars['localhost'].stack_env_map.BILLING_DB_PORT
| default('15432', true), true),
lookup('ansible.builtin.env', 'BILLING_DB_NAME')
| default(hostvars['localhost'].stack_env_map.BILLING_DB_NAME
| default('account', true), true)
)
)
}}
billing_service_exporter_base_url: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
| default(hostvars['localhost'].stack_env_map.EXPORTER_BASE_URL
| default('http://127.0.0.1:8080', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'billing-service' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_xworkmate_bridge_vhosts.yml
vars:
xworkmate_bridge_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_HOSTS')
| default('all', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'xworkmate-bridge' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_xray_exporter.yml
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
xray_exporter_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'XRAY_EXPORTER_HOSTS')
| default('xray_exporter', true), true) }}
xray_exporter_internal_service_token: >-
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
| default('', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'xray-exporter' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_agent_svc_plus.yml
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
agent_service_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'AGENT_SERVICE_HOSTS')
| default('agent_svc_plus', true), true) }}
agent_api_token: >-
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
| default('', true), true) }}
agent_billing_base_url: >-
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
| default(hostvars['localhost'].stack_env_map.BILLING_SERVICE_BASE_URL
| default('http://127.0.0.1:8081', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'agent' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_accounts_svc_plus.yml
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
accounts_service_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'ACCOUNTS_SERVICE_HOSTS')
| default('accounts', true), true) }}
accounts_service_image_repo: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_REPO
| default('ghcr.io/x-evor/accounts', true), true) }}
accounts_service_image_tag: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_TAG
| default('latest', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'accounts' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_stunnel-client.yml
vars:
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'stunnel-client' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_apisix.yml
vars:
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'apisix' in (stack_services.split(',') | map('trim') | list)"
- import_playbook: deploy_console_svc_plus.yml
vars:
stack_env_file: >-
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
| default(playbook_dir ~ '/.env', true) }}
console_service_hosts: >-
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
| default(lookup('ansible.builtin.env', 'CONSOLE_SERVICE_HOSTS')
| default('console', true), true) }}
console_service_sync_dns: >-
{{ lookup('ansible.builtin.env', 'console_service_sync_dns')
| default(lookup('ansible.builtin.env', 'CONSOLE_SERVICE_SYNC_DNS')
| default(false, true), true) | bool }}
console_service_frontend_image: >-
{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE')
| default(hostvars['localhost'].stack_env_map.FRONTEND_IMAGE
| default('', true), true) }}
console_service_registry_username: >-
{{ lookup('ansible.builtin.env', 'GHCR_USERNAME')
| default(hostvars['localhost'].stack_env_map.GHCR_USERNAME
| default('', true), true) }}
console_service_registry_password: >-
{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD')
| default(hostvars['localhost'].stack_env_map.GHCR_PASSWORD
| default('', true), true) }}
stack_services: >-
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
when: "'console' in (stack_services.split(',') | map('trim') | list)"

View File

@ -1,33 +0,0 @@
- name: Validate extended services target host for svc.plus
hosts: "{{ extended_services_hosts | default('jp_xhttp_contabo_host') }}"
gather_facts: true
become: true
tasks:
- name: Gather service facts
ansible.builtin.service_facts:
- name: Confirm the host already exposes the shared svc.plus runtime
ansible.builtin.assert:
that:
- "'caddy.service' in ansible_facts.services"
fail_msg: "Target host must already provide the shared svc.plus runtime (missing caddy.service)."
success_msg: "Target host already provides the shared svc.plus runtime."
- name: Check caddy configuration path
ansible.builtin.stat:
path: /etc/caddy/Caddyfile
register: extended_services_caddyfile
- name: Validate caddy configuration path exists
ansible.builtin.assert:
that:
- extended_services_caddyfile.stat.exists
fail_msg: "Target host is missing /etc/caddy/Caddyfile."
success_msg: "Target host has /etc/caddy/Caddyfile."
- name: Show extended-services dry-run scope
ansible.builtin.debug:
msg:
- "Extended services dry-run entrypoint is wired correctly."
- "Target host: {{ inventory_hostname }}"
- "This playbook currently validates the shared runtime prerequisites used by svc.plus extended services."

View File

@ -1,46 +0,0 @@
- name: Deploy xray-exporter service
hosts: "{{ xray_exporter_hosts | default('xray_exporter') }}"
gather_facts: true
become: true
vars:
xray_exporter_source_dir: >-
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_SOURCE_DIR')
| default(playbook_dir ~ '/../xray-exporter', true) }}
xray_exporter_node_id: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_NODE_ID')
| default(xray_exporter_node_id_custom | default(inventory_hostname, true), true) }}
xray_exporter_env_name: >-
{{ lookup('ansible.builtin.env', 'EXPORTER_ENV')
| default('prod', true) }}
xray_exporter_stats_url: >-
{{ lookup('ansible.builtin.env', 'XRAY_STATS_URL')
| default('http://127.0.0.1:49227/debug/vars', true) }}
xray_exporter_stats_token: >-
{{ lookup('ansible.builtin.env', 'XRAY_STATS_TOKEN')
| default('', true) }}
xray_exporter_accounts_base_url: >-
{{ lookup('ansible.builtin.env', 'ACCOUNTS_BASE_URL')
| default('https://accounts.svc.plus', true) }}
xray_exporter_internal_service_token: >-
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
xray_exporter_snapshot_store_path: >-
{{ lookup('ansible.builtin.env', 'SNAPSHOT_STORE_PATH')
| default('/var/lib/xray-exporter/snapshots.db', true) }}
xray_exporter_snapshot_retention: >-
{{ lookup('ansible.builtin.env', 'SNAPSHOT_RETENTION')
| default('72h', true) }}
xray_exporter_listen_addr: >-
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_LISTEN_ADDR')
| default('127.0.0.1:8080', true) }}
xray_exporter_scrape_interval: >-
{{ lookup('ansible.builtin.env', 'SCRAPE_INTERVAL')
| default('1m', true) }}
pre_tasks:
- name: Validate INTERNAL_SERVICE_TOKEN is present
ansible.builtin.assert:
that:
- xray_exporter_internal_service_token | length > 0
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
success_msg: "INTERNAL_SERVICE_TOKEN found"
roles:
- roles/vhosts/xray-exporter

View File

@ -1,19 +0,0 @@
---
- name: Deploy ACP vhosts through xworkmate bridge
hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
become: true
gather_facts: true
module_defaults:
ansible.builtin.apt:
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
roles:
- role: roles/vhosts/acp_server_codex/
tags: [acp_codex]
- role: roles/vhosts/acp_server_opencode/
tags: [acp_opencode]
- role: roles/vhosts/acp_server_gemini/
tags: [acp_gemini]
- role: roles/vhosts/acp_server_hermes/
tags: [acp_hermes]
- role: roles/vhosts/xworkmate_bridge/
tags: [xworkmate_bridge]

View File

@ -1,9 +0,0 @@
- name: Destroy cloud dev desktop infrastructure
hosts: localhost
connection: local
gather_facts: true
roles:
- role: cloud_vm_request_validate
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
vars:
cloud_lifecycle_action: destroy

View File

@ -1,170 +0,0 @@
# AI Workspace Runtime 交付计划
## 1. 目标与边界
本计划定义 AI Workspace 核心运行时从源码仓库构建、发布、离线聚合到目标机部署的完整交付链路。
核心原则:
- LiteLLM、xworkspace-console、xworkmate-bridge、QMD 分别在各自源码仓库的 GitHub Actions build job 中构建。
- 每个组件独立发布 `runtime-*` GitHub Release 及其 SHA256 清单。
- offline package 只下载已发布产物,逐文件完成 SHA256 校验后再聚合。
- 目标机只允许校验、解包、安装、配置、启动和健康检查,禁止源码编译、依赖构建及镜像构建。
- 所有未经过 CI 或目标机矩阵实测的能力均保持 `TODO`,不得仅依据设计或局部实现标记完成。
## 2. 目标架构
```text
LiteLLM repository ---------- build job --> runtime-litellm-* ----------\
xworkspace-console repository build job --> runtime-xworkspace-console-* --\
xworkmate-bridge repository -- build job --> runtime-xworkmate-bridge-* -----+--> offline package job
QMD repository -------------- build job --> runtime-qmd-* ------------------/ |
| download
| SHA256 verify
| manifest aggregate
v
offline-package-*
|
v
target host: verify/install only
```
### 2.1 组件 Release
四个组件必须由各自仓库负责构建,聚合仓库不得从源码代建组件。
| 组件 | 构建责任 | Release 命名 | 必需产物 |
| --- | --- | --- | --- |
| LiteLLM | LiteLLM 仓库 GitHub Actions build job | `runtime-litellm-*` | 固定版本 Python runtime/依赖包、启动入口、组件 manifest、SHA256 清单 |
| xworkspace-console | xworkspace-console 仓库 GitHub Actions build job | `runtime-xworkspace-console-*` | dashboard 静态产物、API 二进制、运行配置模板、组件 manifest、SHA256 清单 |
| xworkmate-bridge | xworkmate-bridge 仓库 GitHub Actions build job | `runtime-xworkmate-bridge-*` | bridge 二进制、systemd/运行配置模板、组件 manifest、SHA256 清单 |
| QMD | QMD 仓库 GitHub Actions build job | `runtime-qmd-*` | 已安装依赖和已构建 CLI/runtime、组件 manifest、SHA256 清单 |
资产文件名必须精确匹配,聚合器和目标机均不得尝试别名、模糊匹配或兼容猜测:
- Console`xworkspace-console-runtime-linux-{amd64|arm64}.tar.gz`
- Bridge`xworkmate-bridge-linux-{amd64|arm64}.tar.gz`
- QMD`qmd-runtime-linux-{amd64|arm64}.tar.gz`
- LiteLLM`litellm-runtime-{distro}-{version}-{arch}.tar.gz`
每个组件 manifest 至少记录:组件名、源码 commit、版本、构建时间、目标 OS、目标架构、入口文件、文件列表及每个文件的 SHA256。
### 2.2 offline package 聚合
offline package job 必须:
1. 从四个组件的 `runtime-*` Release 下载与目标平台匹配的产物和 SHA256 清单。
2. 在聚合前执行 SHA256 校验;缺少清单、文件缺失或摘要不一致时立即失败。
3. 生成聚合 manifest固定四个组件的 Release tag、源码 commit、资产 URL、资产大小和 SHA256。
4. 将已校验组件产物、部署 playbook 所需依赖及聚合 manifest 打包为 `offline-package-*`
5. 对最终 offline package 再生成 SHA256并在 CI 中执行一次解包与结构校验。
禁止以 `latest` 作为不可追溯的部署输入;重新聚合必须基于明确 tag 或不可变 commit。
### 2.3 目标机部署
目标机部署必须开启 prebuilt-only 约束。缺少任一预构建产物时直接失败,不得回退到以下行为:
- `git clone` 或源码 checkout
- `npm install`、`npm run build`、`go build`、`go run`
- `pip install` 从公网或源码解析构建依赖;
- `docker build`、`podman build` 或其他本地镜像构建;
- 任何需要编译器、SDK 或前端构建工具链的安装步骤。
部署仅执行offline package SHA256 校验、manifest 校验、解包、文件安装、权限设置、配置渲染、服务启动、健康检查和结果汇总。
## 3. 资源与性能约束
### 3.1 并发控制
- 全局并发硬上限必须满足 `并发数 <= 2 * 在线 CPU 数`,在线 CPU 数以执行时实际可用 CPU 为准。
- 初始并发取任务上限、配置上限和 `2 * 在线 CPU 数` 三者最小值。
- 调度器必须随 load 动态收缩:负载超过阈值时停止发放新任务并逐级降低并发;负载恢复且持续稳定后再缓慢扩容。
- 动态收缩不得中断正在执行的不可重入安装步骤;只限制后续任务进入。
- 日志和最终摘要必须记录 CPU 数、load 采样、每次并发调整的时间、原因及调整前后值。
### 3.2 部署耗时分布
每次部署必须记录总耗时及至少以下阶段耗时:
- offline package 下载;
- SHA256 与 manifest 校验;
- 解包;
- 各组件安装;
- 配置渲染;
- 服务启动;
- 健康检查。
CI/验收报告按 OS、架构、冷启动/缓存命中、首次执行/幂等重跑分组,统计样本数、最小值、最大值、平均值以及 P50、P90、P95、P99。样本不足时保留原始数据并明确标注不以单次耗时代替分布结论。
## 4. 支持矩阵与验收
目标支持以下全部组合:
| 发行版 | 版本 | 架构 |
| --- | --- | --- |
| Debian | 11、12、13 | amd64、arm64 |
| Ubuntu | 22.04、24.04、26.04 | amd64、arm64 |
每个矩阵项必须验证:
1. offline package 下载和 SHA256 校验成功。
2. 目标机在无源码、无构建工具链、组件外网访问受限的条件下部署成功。
3. 四个组件版本与聚合 manifest 完全一致。
4. 服务启动、健康检查和关键 smoke test 成功。
5. 同一主机使用同一输入至少连续执行两次;第二次成功且无非预期变更、无重复资源、无凭据轮换、无构建行为。
6. 首次部署和幂等重跑均产出阶段耗时及完整摘要。
Ubuntu 26.04 在实际可用 runner/镜像和依赖生态完成验证前,只能保持计划支持状态,不得标记已验证。
## 5. 当前事实
以下状态只记录当前仓库或相邻交付文档能够证明的事实,不把目标设计视为完成:
- [x] 聚合入口已拆分为 preflight 与 runtime playbookpreflight 已校验 `docker`、`k3s`、`systemd` 运行模式组合。
- [x] xworkspace-console 与 QMD 的部署代码已出现预构建 archive 输入及 prebuilt-only 缺包失败入口。
- [x] 相邻一键部署文档已记录xworkspace-console 离线包 `publish-release` 链路和 Release 产物上传曾核对完成。
- [x] 相邻一键部署文档已记录:一键安装脚本优先使用离线安装包。
- [ ] xworkspace-console 与 QMD 当前仍存在目标机源码 checkout/依赖安装/构建回退,尚未满足“目标机禁止构建”。
- [ ] LiteLLM 当前可覆盖 package spec但未证明其独立 `runtime-litellm-*` Release 和完全离线、免构建安装链路。
- [ ] xworkmate-bridge 独立 `runtime-xworkmate-bridge-*` Release 和预构建消费链路尚未在本计划范围内验证。
- [ ] 四组件 Release 的一致命名、manifest 和 SHA256 契约尚未完成验证。
- [ ] offline package 的逐文件下载、SHA256 校验、聚合 manifest 和最终包校验尚未完成验证。
- [ ] 并发硬上限、基于 load 的动态收缩及调整日志尚未完成验证。
- [ ] 部署耗时分布统计尚未完成验证。
- [ ] 连续重复执行的幂等性验收尚未完成。
- [ ] Debian 11/12/13、Ubuntu 22.04/24.04/26.04 的 amd64/arm64 全矩阵尚未完成验证。
## 6. TODO
### P0构建与发布闭环
- [ ] TODO在 LiteLLM 仓库建立 build job发布 `runtime-litellm-*` Release、组件 manifest 和 SHA256 清单。
- [ ] TODO在 xworkspace-console 仓库固化 build job确认每次发布 `runtime-xworkspace-console-*` Release、组件 manifest 和 SHA256 清单。
- [ ] TODO在 xworkmate-bridge 仓库建立 build job发布 `runtime-xworkmate-bridge-*` Release、组件 manifest 和 SHA256 清单。
- [ ] TODO在 QMD 仓库建立 build job发布 `runtime-qmd-*` Release、组件 manifest 和 SHA256 清单。
- [ ] TODO为 amd64、arm64 分别产出可安装资产;若资产与发行版相关,则按支持矩阵拆分并在 manifest 中明确兼容范围。
- [ ] TODO增加 Release 契约测试拒绝缺失入口、manifest、SHA256 或架构资产的发布。
### P0离线聚合与目标机免构建
- [ ] TODO实现 offline package job按固定 tag 下载四组件 Release并在聚合前逐文件执行 SHA256 校验。
- [ ] TODO生成可追溯聚合 manifest并为最终 `offline-package-*` 生成和发布 SHA256。
- [ ] TODO在目标机部署入口强制 prebuilt-only删除或禁用四组件所有源码构建回退。
- [ ] TODO增加“目标机禁止构建”守卫检测到编译器调用、包构建命令、源码 checkout 或镜像构建即失败。
- [ ] TODO在断网或仅允许访问 offline package 源的目标机上完成端到端部署验证。
### P1并发、性能与可观测性
- [ ] TODO实现在线 CPU 探测和 `<= 2 * 在线 CPU` 的全局并发硬限制。
- [ ] TODO定义 load 采样窗口、收缩/恢复阈值、迟滞策略和最低并发,完成动态收缩测试。
- [ ] TODO记录阶段级耗时、组件级耗时、并发变化和环境标签产出结构化 JSON 及人类可读摘要。
- [ ] TODO汇总部署耗时分布至少输出 count/min/max/avg/P50/P90/P95/P99并区分首次执行与幂等重跑。
### P1幂等与平台矩阵
- [ ] TODO为每个支持矩阵项连续执行至少两次验证第二次无非预期 changed、服务中断、重复资源或凭据变化。
- [ ] TODO覆盖 Debian 11/12/13 amd64/arm64。
- [ ] TODO覆盖 Ubuntu 22.04/24.04/26.04 amd64/arm64。
- [ ] TODO保存每个矩阵项的 Release tag、offline package SHA256、部署日志、耗时数据和验收结论。
- [ ] TODO全部矩阵通过后再把“计划支持”更新为“已验证支持”部分通过时逐项记录不做整体完成声明。

View File

@ -1,77 +0,0 @@
# cert-manager Architecture
This document records the complete certificate control-plane contract for the `svc.plus` platform.
## Scope
The system is split into four distinct responsibilities:
- `cert-manager` owns certificate issuance, renewal, and the target `Secret` objects.
- `Caddy` remains the ingress surface and serves HTTP-01 challenge traffic.
- `external-dns` only manages DNS records for public hostnames.
- `external-secrets` continues to materialize Vault-sourced application secrets, AK/SK pairs, future provider credentials such as the Cloudflare API token, and image pull secrets.
## Default Contract
- `postgresql-prod.svc.plus` defaults to `cert-manager + ACME HTTP-01`.
- `DNS-01 + Cloudflare` is predeclared for wildcard certificates and future subdomains.
- `selfSigned` remains available as an internal temporary or recovery fallback.
- `cert-manager` owns `postgresql-tls` in every namespace that consumes it, so there is no cross-namespace Secret sync job.
## System Diagram
```mermaid
flowchart LR
Vault[(Vault)]
ESO[external-secrets]
CloudflareToken[(cloudflare-api-token Secret)]
ExternalDNS[external-dns]
DNSZone[(svc.plus DNS zone)]
Caddy[Caddy Ingress]
CertMgr[cert-manager]
Http01["ACME HTTP-01"]
Dns01["ACME DNS-01 + Cloudflare"]
SelfSigned["selfSigned fallback"]
PlatformCert["platform/postgresql-tls Certificate"]
PlatformSecret["platform/postgresql-tls Secret"]
DatabaseCert["database/postgresql-tls Certificate"]
DatabaseSecret["database/postgresql-tls Secret"]
PostgreSQL["postgresql-prod.svc.plus"]
Stunnel["database/stunnel-server"]
Vault --> ESO
ESO --> CloudflareToken
CloudflareToken --> Dns01
ExternalDNS --> DNSZone
DNSZone --> Caddy
Caddy --> Http01
Http01 --> CertMgr
Dns01 --> CertMgr
SelfSigned -. fallback .-> CertMgr
CertMgr --> PlatformCert
CertMgr --> DatabaseCert
PlatformCert --> PlatformSecret
DatabaseCert --> DatabaseSecret
PlatformSecret --> Caddy
DatabaseSecret --> Stunnel
Caddy --> PostgreSQL
```
## Operational Rules
- Keep `cert-manager` as the source of truth for TLS Secret ownership.
- Keep `Caddy` as the traffic and HTTP-01 routing layer only.
- Keep `external-dns` focused on DNS record reconciliation.
- Keep `external-secrets` focused on external secret materialization.
- Treat the Cloudflare API token as an external input secret; it can be bootstrapped manually or delivered by `external-secrets` when that path is wired in.
- Prefer namespace-local `Certificate` objects for each consumer namespace.
- Avoid cross-namespace certificate copying or Secret sync controllers.
## Related Playbook Roles
- `vhosts/k3s_platform_bootstrap`
- installs the platform node and prepares GitOps handoff
- `vhosts/k3s_platform_addon`
- installs shared platform services such as `cert-manager`, `external-secrets`, `caddy`, and `external-dns`
- `GitOps`
- owns the namespace-local `Certificate` manifests and workload wiring

View File

@ -1,258 +0,0 @@
# K3S Role Map
This document defines the recommended reuse path for `playbooks/roles/vhosts/k3s*`.
## Goal
The `k3s*` roles are no longer treated as one flat group.
They are split into:
- baseline roles used by all nodes
- platform bootstrap roles used by GitOps-driven single-node platform deployments
- cluster lifecycle roles used by multi-node cluster deployments
- frozen legacy roles kept only for compatibility
- reset roles kept for teardown and recovery
## Recommended Mainline
Recommended path for a new platform project:
`common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
### Edge And Certificate System
The full certificate control-plane contract is documented in
[cert-manager-arch.md](./cert-manager-arch.md).
Responsibilities:
- `common`
- host baseline
- package baseline
- file limits
- shared OS hardening
- `k3s_platform_bootstrap`
- install single-node k3s
- render `/etc/rancher/k3s/config.yaml`
- disable built-in components such as `traefik`
- install Flux
- connect the node to the GitOps repository
- `k3s_platform_addon`
- install platform-side shared components into Kubernetes
- examples: `cert-manager`, `external-secrets`, `reloader`, `caddy`, `apisix`, `external-dns`
- certificate control plane, DNS automation, and external secret sync all belong in this layer
- see [cert-manager-arch.md](./cert-manager-arch.md) for the full edge and certificate contract
- `GitOps`
- own dynamic platform configuration
- own workload manifests
- own service wiring and dependencies
Entry playbooks:
- `playbooks/k3s_platform_bootstrap_with_gitops.yml`
- `playbooks/k3s_platform_addon.yml`
Use this path when:
- the target is a standard platform node
- Flux/GitOps is the desired source of truth
- platform addons should be declared instead of installed by ad hoc scripts
## Cluster Mainline
Recommended path for a new multi-node cluster project:
`common -> k3s-cluster-server / k3s-cluster-agent`
Responsibilities:
- `common`
- shared host baseline before cluster work starts
- `k3s-cluster-server`
- server lifecycle role
- dispatches by `action`
- current action set includes `bootstrap`, `add-master`, `backup`, `recovery`, `upgrade`, `destroy`
- `k3s-cluster-agent`
- agent lifecycle role
- dispatches by `action`
- current action set includes `bootstrap`, `upgrade`, `destroy`
Entry playbooks:
- `playbooks/init_k3s_cluster_server`
- `playbooks/init_k3s_cluster_agent`
Use this path when:
- the target is a server/agent topology
- cluster lifecycle needs to be managed explicitly
- node roles and actions are different between control plane and workers
## Role Status
| Role | Status | Use |
| --- | --- | --- |
| `vhosts/k3s_platform_bootstrap` | recommended | single-node platform bootstrap |
| `vhosts/k3s_platform_addon` | recommended | platform addon installation before GitOps takeover |
| `vhosts/k3s-cluster-server` | recommended | cluster server lifecycle |
| `vhosts/k3s-cluster-agent` | recommended | cluster agent lifecycle |
| `vhosts/k3s-reset` | keep | reset / teardown |
| `vhosts/k3s` | frozen | legacy single-node installer |
| `vhosts/k3s-cluster` | frozen | legacy script-driven cluster installer |
| `vhosts/k3s-addon` | frozen | legacy ingress and DNS addon wrapper |
## Frozen Role Migration
The frozen roles are not the recommended extension points anymore.
### `vhosts/k3s`
Legacy behavior:
- runs `setup-k3s.sh`
- optionally installs `kubeovn`
Replace with:
- `common`
- `k3s_platform_bootstrap`
Notes:
- `k3s_platform_bootstrap` is the right place to absorb the installation capability from `vhosts/k3s`
- the target direction is capability fusion followed by gradual retirement of the old `vhosts/k3s` role
- the preferred reuse mode is capability extraction and inward migration, not direct role chaining
- shared logic worth absorbing includes installer download, mirror selection, default k3s flags, and helm bootstrap
- `kubeovn` and legacy CNI-specific behavior should not be pulled into the new platform bootstrap path by default
- new single-node installs should be expressed through rendered k3s config and Flux bootstrap
- do not add new platform capabilities to `vhosts/k3s`
### `vhosts/k3s-cluster`
Legacy behavior:
- copies shell scripts to remote hosts
- runs `setup_k3s.sh`
- runs `set-registry.sh`
Replace with:
- `common`
- `k3s-cluster-server`
- `k3s-cluster-agent`
Notes:
- new lifecycle actions should be added to the server or agent action roles
- do not extend the script-wrapper model further
### `vhosts/k3s-addon`
Legacy behavior:
- wraps ingress and DNS shell scripts
- mixes ingress installation details with addon orchestration
Replace with:
- `k3s_platform_addon`
- GitOps-managed service configuration
Notes:
- new ingress, gateway, DNS, and platform addon logic should move into `k3s_platform_addon`
- keep `k3s-addon` only for compatibility with older environments
## New Project Selection
For a new project, choose the path by deployment style.
### Option A: Platform Node With GitOps
Choose:
`common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
Best for:
- `svc.plus` style platform nodes
- single-node or platform-first installations
- shared addons managed centrally
- GitOps as the long-term control plane
### Option B: Multi-Node Cluster Lifecycle
Choose:
`common -> k3s-cluster-server / k3s-cluster-agent`
Best for:
- explicit server/agent topologies
- controlled bootstrap, upgrade, backup, and recovery flows
- clusters that need role-aware lifecycle operations
### Option C: Reset Only
Choose:
`k3s-reset`
Best for:
- teardown
- rebuild preparation
- cleanup after failed bootstrap
## Extension Rules
When adding new capabilities:
- host-level baseline belongs in `common`
- k3s installation and Flux bootstrap belong in `k3s_platform_bootstrap`
- platform shared addons belong in `k3s_platform_addon`
- TLS issuers and namespace-local certificate lifecycle should also live there
- the complete edge and certificate contract lives in [cert-manager-arch.md](./cert-manager-arch.md)
- server and agent lifecycle actions belong in `k3s-cluster-server` or `k3s-cluster-agent`
- dynamic service configuration belongs in GitOps
- reset and cleanup behavior belongs in `k3s-reset`
GitOps certificate rule of thumb:
- use `cert-manager` to own the `Certificate` in the namespace that consumes it
- avoid cross-namespace Secret sync jobs when the same certificate can be declared directly in each namespace
- use `Caddy` only as the ingress / HTTP-01 routing surface for those certificates
- use `external-dns` only for DNS record updates
- keep `external-secrets` for Vault-sourced app credentials, cloud API keys, and image pull secrets
Do not add new functionality to:
- `vhosts/k3s`
- `vhosts/k3s-cluster`
- `vhosts/k3s-addon`
## Reuse Guidance
`vhosts/k3s` is frozen as an external entry role, but its useful install logic should be migrated inward.
Preferred direction:
- keep `vhosts/k3s` frozen as a compatibility wrapper
- move reusable k3s install logic into `k3s_platform_bootstrap` over time
- let `k3s_platform_bootstrap` become the single canonical owner of the platform install path
- shrink `vhosts/k3s` as pieces are absorbed until it can be removed safely
- treat `k3s_platform_bootstrap` as the canonical owner of the new single-node platform bootstrap path
Avoid:
- adding `include_role: vhosts/k3s` inside `k3s_platform_bootstrap`
- extending the old script-wrapper interface as the long-term contract
## Short Decision Rule
If the question is "which role should a new project reuse?", use:
- platform project: `common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
- multi-node cluster project: `common -> k3s-cluster-server / k3s-cluster-agent`
- cleanup project: `k3s-reset`

View File

@ -1,576 +0,0 @@
# LiteLLM Gateway 部署指南
## 目标架构
```
┌─────────────────────────────────────────┐
│ Caddy (HTTPS Entry) │
│ │
│ ┌──────────────────────────────────┐ │
Internet ──────────►│ │ api.svc.plus/v1/openai/* │ │
│ │ api.svc.plus/v1/anthropic/* │ │
│ │ api.svc.plus/ui/* │ │
│ └──────────────────────────────────┘ │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│ LiteLLM Proxy (127.0.0.1:4000) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ /v1/chat/completions (OpenAI) │ │
│ │ /v1/messages (Anthropic) │ │
│ │ /ui (Admin Dashboard) │ │
│ └──────────────────────────────────┘ │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│ Model Providers (External) │
│ │
│ • OpenAI (GPT-4o-mini) │
│ • Anthropic (Claude 3.5 Sonnet) │
│ • DeepSeek (deepseek-chat) │
│ • Local Models (OAI-compatible) │
└─────────────────────────────────────────┘
```
## 推荐目录结构
```
/etc/litellm/
├── config.yaml # LiteLLM 配置文件
└── litellm.env # 环境变量 (包含 API Keys)
/etc/systemd/system/
└── litellm-proxy.service # systemd 服务单元
/etc/caddy/conf.d/
└── litellm.caddy # Caddy 路由配置
```
## 一、Caddyfile 配置示例
```caddy
# /etc/caddy/conf.d/litellm.caddy
# API Gateway + LiteLLM Admin UI (统一入口)
api.svc.plus {
# LiteLLM Admin UI (Basic Auth 保护)
@ui_admin {
path /ui/*
}
@ui_admin_unauthorized {
not header Authorization "Basic *"
}
handle @ui_admin_unauthorized {
respond "Unauthorized" 401 {
www-authenticate Basic realm="LiteLLM Admin UI"
}
}
handle @ui_admin {
reverse_proxy 127.0.0.1:4000
}
# OpenAI-Compatible API
@openai_api {
path /v1/openai/*
}
handle @openai_api {
rewrite * /v1{path}
reverse_proxy 127.0.0.1:4000 {
flush_interval -1
transport http {
dial_timeout 30s
read_timeout 600s
write_timeout 600s
}
}
}
# Anthropic-Compatible API
@anthropic_api {
path /v1/anthropic/*
}
handle @anthropic_api {
rewrite * /v1{path}
reverse_proxy 127.0.0.1:4000 {
flush_interval -1
transport http {
dial_timeout 30s
read_timeout 600s
write_timeout 600s
}
}
}
# 通用代理
handle {
reverse_proxy 127.0.0.1:4000
}
encode gzip zstd
header {
X-Real-IP
X-Forwarded-For
X-Forwarded-Proto
Host
}
log {
output file /var/log/caddy/litellm.access.log
}
}
```
### 关键路径映射
| 外部路径 | 内部路径 | 说明 |
|---------------------------------------|--------------------------|--------------|
| `https://api.svc.plus/v1/openai/chat/completions` | `http://127.0.0.1:4000/v1/chat/completions` | OpenAI 兼容 API |
| `https://api.svc.plus/v1/anthropic/messages` | `http://127.0.0.1:4000/v1/messages` | Anthropic 兼容 API |
| `https://api.svc.plus/ui/*` | `http://127.0.0.1:4000/ui/*` | Admin UI (Basic Auth) |
| `https://api.svc.plus/v1/chat/completions` | `http://127.0.0.1:4000/v1/chat/completions` | 短路径兼容 (可选) |
---
## 二、LiteLLM config.yaml 示例
```yaml
# /etc/litellm/config.yaml
model_list:
# OpenAI 模型
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
# Anthropic 模型
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-3-5-sonnet-latest
api_key: os.environ/ANTHROPIC_API_KEY
# DeepSeek 模型
- model_name: deepseek-chat
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
# 本地 OpenAI-Compatible 模型
- model_name: local-qwen
litellm_params:
model: openai/qwen
api_base: http://127.0.0.1:8000/v1
api_key: os.environ/LOCAL_MODEL_API_KEY
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
drop_rate_limit_requests: true
set_verbose: false
router_settings:
model_group_alias:
gpt-4o-mini: gpt-4o-mini
claude-sonnet: claude-sonnet
deepseek-chat: deepseek-chat
routing_strategy: latency-based-routing
enable_pre_call_checks: false
retry_after: 60
num_retries: 3
litellm_settings:
drop_params: true
set_verbose: true
request_timeout: 600
telemetry: false
max_parallel_requests: 1000
environment_variables:
OPENAI_API_KEY: os.environ/OPENAI_API_KEY
ANTHROPIC_API_KEY: os.environ/ANTHROPIC_API_KEY
DEEPSEEK_API_KEY: os.environ/DEEPSEEK_API_KEY
LOCAL_MODEL_API_KEY: os.environ/LOCAL_MODEL_API_KEY
LITELLM_MASTER_KEY: os.environ/LITELLM_MASTER_KEY
```
---
## 三、litellm.env 示例
```bash
# /etc/litellm/litellm.env
# API Keys (从环境变量读取)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
LOCAL_MODEL_API_KEY=sk-local-placeholder
# LiteLLM Master Key (必须设置,用于 API 认证)
LITELLM_MASTER_KEY=your-secure-random-master-key-here-min-32-chars
# 可选配置
# LITELLM_SALT_KEY=your-salt-key
# DATABASE_URL=postgresql://user:pass@host:5432/litellm
```
**文件权限**: `chmod 600 /etc/litellm/litellm.env`
---
## 四、systemd 服务单元示例
```ini
# /etc/systemd/system/litellm-proxy.service
[Unit]
Description=LiteLLM Proxy Service
After=network.target
[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu
EnvironmentFile=/etc/litellm/litellm.env
ExecStart=/usr/local/bin/litellm \
--host 127.0.0.1 \
--port 4000 \
--config /etc/litellm/config.yaml
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=litellm-proxy
[Install]
WantedBy=multi-user.target
```
---
## 五、部署步骤
### 1. 安装依赖
```bash
# 安装 Python 和 pip
apt update && apt install -y python3 python3-pip python3-venv
# 使用 pipx 安装 LiteLLM (推荐)
pip install pipx
pipx install litellm
# 或直接用 pip 安装
pip install litellm
```
### 2. 创建配置目录
```bash
mkdir -p /etc/litellm
chmod 755 /etc/litellm
```
### 3. 写入配置文件
```bash
# 写入 config.yaml
cat > /etc/litellm/config.yaml << 'EOF'
model_list:
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
# ... 其他模型
EOF
# 写入环境变量文件
cat > /etc/litellm/litellm.env << 'EOF'
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
DEEPSEEK_API_KEY=sk-xxx
LITELLM_MASTER_KEY=your-secure-master-key
EOF
chmod 600 /etc/litellm/litellm.env
chmod 640 /etc/litellm/config.yaml
```
### 4. 部署 systemd 服务
```bash
cat > /etc/systemd/system/litellm-proxy.service << 'EOF'
[Unit]
Description=LiteLLM Proxy Service
After=network.target
[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu
EnvironmentFile=/etc/litellm/litellm.env
ExecStart=/usr/local/bin/litellm --host 127.0.0.1 --port 4000 --config /etc/litellm/config.yaml
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=litellm-proxy
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable litellm-proxy
systemctl start litellm-proxy
systemctl status litellm-proxy
```
### 5. 配置 Caddy
```bash
# 确保 Caddy 导入 conf.d 目录
echo 'import /etc/caddy/conf.d/*.caddy' >> /etc/caddy/Caddyfile
# 创建 litellm Caddy 配置
cat > /etc/caddy/conf.d/litellm.caddy << 'EOF'
# ... 见上面的 Caddyfile 配置
EOF
# 验证并重载
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
```
### 6. 验证部署
```bash
# 检查 LiteLLM 健康状态
curl http://127.0.0.1:4000/health
# 检查 API Gateway
curl -X POST "https://api.svc.plus/v1/openai/chat/completions" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'
# 访问 Admin UI
# https://api.svc.plus/ui/
```
---
## 六、API 验证命令
### 1. 健康检查
```bash
# 本地健康检查
curl http://127.0.0.1:4000/health
# 外部健康检查
curl https://api.svc.plus/health
```
### 2. OpenAI-Compatible API 测试
```bash
curl -X POST "https://api.svc.plus/v1/openai/chat/completions" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-chat",
"messages": [
{
"role": "user",
"content": "Hello from OpenAI-compatible endpoint"
}
]
}'
```
### 3. Anthropic-Compatible API 测试
```bash
curl -X POST "https://api.svc.plus/v1/anthropic/messages" \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet",
"max_tokens": 256,
"messages": [
{
"role": "user",
"content": "Hello from Anthropic-compatible endpoint"
}
]
}'
```
### 4. Admin UI 访问
```bash
# 如果启用了 Basic Auth
# 访问 https://api.svc.plus/ui/
# 使用配置的 admin 用户名和密码登录
```
---
## 七、安全注意事项
### 1. 网络隔离
- **4000 端口只监听 127.0.0.1**,不暴露到公网
- VPS 防火墙**不要开放 4000 端口**
- 对外只开放 **443** (HTTPS)
- **Caddy 是唯一公网入口**
### 2. Admin UI 保护
LiteLLM Admin UI **不应裸奔**,建议启用以下至少一种保护:
| 保护方式 | 说明 |
|--------------|------------------------------|
| Basic Auth | Caddy 内置,配置用户名密码 |
| IP 白名单 | 只允许特定 IP 访问 api.svc.plus/ui |
| Cloudflare Access | Cloudflare Zero Trust 认证 |
| VPN / Tailscale | 通过私有网络访问 |
### 3. API 认证
- 所有 API 调用必须使用 `Authorization: Bearer <LITELLM_MASTER_KEY>`
- `LITELLM_MASTER_KEY` 必须足够长且随机 (建议 32+ 字符)
### 4. 文件权限
```bash
chmod 600 /etc/litellm/litellm.env # 保护 API Keys
chmod 640 /etc/litellm/config.yaml # 配置文件
```
---
## 八、Ansible 部署命令
```bash
# 部署 LiteLLM Gateway
ansible-playbook -i inventory.ini setup-litellm.yaml
# 指定 API Keys 部署
LITELLM_MASTER_KEY=your-secure-key \
OPENAI_API_KEY=sk-xxx \
ANTHROPIC_API_KEY=sk-ant-xxx \
DEEPSEEK_API_KEY=sk-xxx \
ansible-playbook -i inventory.ini setup-litellm.yaml
# 只部署 Caddy 配置 (不重启 LiteLLM)
ansible-playbook -i inventory.ini setup-litellm.yaml --tags litellm --start-at-task="Create LiteLLM Caddy fragment"
```
---
## 九、故障排查
### LiteLLM 服务无法启动
```bash
# 查看日志
journalctl -u litellm-proxy -f
# 验证配置
litellm --config /etc/litellm/config.yaml --test
```
### Caddy 配置无效
```bash
# 验证 Caddy 配置
caddy validate --config /etc/caddy/Caddyfile
# 查看 Caddy 日志
tail -f /var/log/caddy/litellm-*.log
```
### API 调用失败
```bash
# 检查端口绑定
ss -tlnp | grep 4000
# 测试本地连通性
curl http://127.0.0.1:4000/health
# 检查 API Key
source /etc/litellm/litellm.env
echo $LITELLM_MASTER_KEY
```
---
## 十、后续扩展
### 启用 PostgreSQL 数据库 (用于用量统计、团队管理等)
```bash
# 1. 安装 PostgreSQL
apt install -y postgresql postgresql-contrib
# 2. 创建数据库和用户
su - postgres
psql -c "CREATE USER litellm WITH PASSWORD 'your-password';"
psql -c "CREATE DATABASE litellm OWNER litellm;"
exit
# 3. 更新环境变量
echo "DATABASE_URL=postgresql://litellm:your-password@localhost:5432/litellm" >> /etc/litellm/litellm.env
# 4. 重启服务
systemctl restart litellm-proxy
```
### 集成 Vault (可选)
```bash
# 设置 Vault 环境变量
echo "VAULT_URL=https://vault.svc.plus" >> /etc/litellm/litellm.env
echo "VAULT_API_KEY_PATH=secret/litellm/api-keys" >> /etc/litellm/litellm.env
systemctl restart litellm-proxy
```
---
## 十一、Agent 接入配置
各 Agent 接入时只需配置 Base URL
| Agent 类型 | Base URL | 认证 |
|--------------|-----------------------------------|---------------|
| OpenAI SDK | `https://api.svc.plus/v1/openai` | `LITELLM_MASTER_KEY` |
| Anthropic SDK | `https://api.svc.plus/v1/anthropic` | `LITELLM_MASTER_KEY` |
| LiteLLM SDK | `https://api.svc.plus` | `LITELLM_MASTER_KEY` |
示例 (Python):
```python
from openai import OpenAI
client = OpenAI(
api_key="your-litellm-master-key",
base_url="https://api.svc.plus/v1/openai"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Hello"}]
)
```

View File

@ -1,128 +0,0 @@
# AI Workspace 一键部署与全局安全网络配置向导
`setup-ai-workspace-all-in-one.yml` 是用于在目标 VPS 上完整、自动化地拉起 AI 研发环境底层组件与服务的聚合 Playbook。
> [!TIP]
> ## ⏳ TL;DR (太长不看版)
>
> **一键标准部署 (无需配置任何前置环境,自带随机密钥保护)**
> ```bash
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
> ```
>
> **一键极严防御部署 (瘫痪所有外网接口,强制全内网/VPN架构)**
> ```bash
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | AI_WORKSPACE_SECURITY_LEVEL=strict bash -
> ```
>
> **组合技:极严防御 + 单独开白名单口子 (如仅开放 LiteLLM 接口)**
> ```bash
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | AI_WORKSPACE_SECURITY_LEVEL=strict LITELLM_API_CADDY_STRICT_WHITELIST=true bash -
> ```
>
> **高级定制:一键部署全架构并按需开启可选功能 (如 XRDP并自定义认证 Token)**
> ```bash
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | \
> XWORKSPACE_CONSOLE_ENABLE_XRDP=true \
> XWORKSPACE_CONSOLE_PUBLIC_ACCESS=true \
> XWORKMATE_BRIDGE_PUBLIC_ACCESS=true \
> GATEWAY_OPENCLAW_PUBLIC_ACCESS=false \
> VAULT_PUBLIC_ACCESS=false \
> LITELLM_API_CADDY_STRICT_WHITELIST=true \
> DEPLOY_TOKEN="my-secure-custom-token-123" \
> bash -
> ```
本文档将详细介绍它的基础用法,并重点讲解如何通过内置的全局开关与细粒度 `public_access` 控制,打造出“最严安全网络架构”(断开一切外部 Web 端口代理,仅限加密 VPN 内网互联)。
## TODO
- [x] 等待并核对 `xworkspace-console` 的离线包 GitHub Actions 发布链路,确认 `publish-release` 完整结束且 release 产物上传成功。
- [ ] 继续核对 `root@acp-bridge.onwalk.net` 的远程部署进度,确认 `setup-ai-workspace-all-in-one.sh` 最终完成并输出统一摘要。
- [x] `setup-ai-workspace-all-in-one.sh` 在目标主机上优先使用离线安装包加速部署,减少在线拉取与安装耗时。
- [ ] 验证 `setup-ai-workspace-all-in-one.sh` 幂等性:同一主机连续执行两次均成功,复用凭据、离线包缓存与已导入镜像,并安全等待部署/APT 锁。
- [ ] 完成最终验收核对Bridge 对外可达、其余服务默认仅本地监听、`acp-codex` / `opencode` / `gemini` / `hermes` / `qmd` / `litellm` 状态正常。
- [ ] 记录最终提交哈希与远端验证结果,回填到本计划的交付结果部分。
---
## 1. 常规快速部署
如果您希望采用**标准Standard安全模式**部署(即:允许需要对外提供部分 Web/API 接口的应用如 `XWorkmate Bridge` 通过 HTTPS 暴露到公网,但内部组件互相隔离)。
```bash
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
--limit jp-xhttp-contabo.svc.plus \
--vault-password-file ~/.vault_password
```
---
## 2. 极致安全:强制全隔离模式 (VPN Only)
如果您正在处理高敏感度的业务,或目标服务器被作为纯后台的 AI 基础设施节点。您可以选择将其配置为**最严的安全等级 (Strict)**。
在此模式下,任何默认开放外网的应用,都将被**强制剥夺公网入口(其 Caddy 代理配置或 K8s Ingress 将被直接销毁删除)。外部黑客或扫描器即便知道子域名,也无法解析请求到您的端口,此时访问服务器上的任何 AI 服务,全部必须经过内部加密隧道(例如 WireGuard / Tailscale 等 VPN 虚拟局域网)。**
**执行部署命令:**
```bash
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
--limit jp-xhttp-contabo.svc.plus \
--vault-password-file ~/.vault_password \
-e "ai_workspace_security_level=strict"
```
---
## 3. 个性化服务放行与阻断 (-e 开关详解)
系统设计了精细化的权限参数,可以在 `standard` 安全模式的基础下,针对某个独立应用进行公网切断;又或者在 `strict` 极致安全模式的底座上,单独给某个应用“开一个白名单口子”。
### 全局策略控制开关
- `-e "ai_workspace_security_level=strict"`
* **作用:** 一键切断所有默认带有对外出口的组件。覆盖掉下述开关的默认策略,将其全部强转为 `false`
### 细粒度服务暴露开关 (支持针对性覆盖)
1. **XWorkspace Console (底层主工作区门户) 公网访问控制**
- **默认值:** `true` (standard 下) / `false` (strict 下)
- **参数:** `-e "xworkspace_console_public_access=false"`
- **作用:** 设为 true 时,会自动将本地 17000 端口通过 Caddy 反向代理到绑定的 `workspace.svc.plus` 域名提供公网访问。设为 false 时则销毁对应代理文件,只能进服务器内网/XRDP访问。
2. **XWorkmate Bridge 公网访问控制**
- **默认值:** `true` (standard 下) / `false` (strict 下)
- **参数:** `-e "xworkmate_bridge_public_access=false"`
- **作用:** 设为 false 时,会彻底删除该服务在 Caddy `/etc/caddy/conf.d` 中的 `.caddy` 文件,使其失去从外界 HTTPS 进入内部 8787 端口的路径。
3. **OpenClaw Gateway 公网访问控制**
- **默认值:** `false` (无论在何种策略下,底层模型网关默认不允许直接向公网打开界面入口)
- **参数:** `-e "gateway_openclaw_public_access=true"`
- **作用:** 当您在出差时,身边没有 VPN 环境,但迫切需要连接远程 OpenClaw 平台时,可以通过将其设为 true 临时生成 Caddy 文件,恢复它的公网域名入口访问。
4. **Vault KMS 密钥中心公网访问控制**
- **默认值:** `false`
- **参数:** `-e "vault_public_access=true"`
- **作用:** 设为 false 时,该服务在 K8s 中部署的 Helm `ingress.enabled` 配置会被强制渲染为 false不会向集群外网注册路由。设为 true 时方可绑定公网 Ingress Class 域名。
5. **LiteLLM 轻量网关访问行为控制**
- **默认值:** `false`
- **参数:** `-e "litellm_api_caddy_strict_whitelist=true"`
- **作用:** 这个参数用于对 Caddy 代理行为做进一步保护开启后Caddy 会拦截一切没有命中官方兼容模型路径(如 `/v1/chat/completions`)的请求并拦截响应为 `404`,例如阻断前端 Dashboard UI`/ui*`)的外网暴露。
6. **按需开启 XRDP 远程桌面连接**
- **默认值:** `false`
- **参数:** `-e "xworkspace_console_enable_xrdp=true"`
- **作用:** XFCE 桌面环境默认仅提供基于 Web 浏览器的 Console UI如需通过原生 RDP 客户端(如 Windows 远程桌面)连接目标主机,可增加此参数。
## 典型组合使用场景
**场景:开启 Strict 全局断网防护,但唯独开放 LiteLLM 模型 API 入口供第三方业务端点调用,且通过最严格白名单防护。**
```bash
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
--limit jp-xhttp-contabo.svc.plus \
--vault-password-file ~/.vault_password \
-e "ai_workspace_security_level=strict" \
-e "litellm_api_caddy_strict_whitelist=true"
```
这种精细的声明式管理,能确保基础设施按照 Infrastructure as Code (IaC) 的最佳安全实践被可预测地配置。

View File

@ -1,108 +0,0 @@
# TLDR: SSH Security & Hardening Playbook
Quick reference for SSH security hardening, firewall controls, Fail2ban management, and connection checking.
## 1. SSH Hardening (Key-Only Auth)
Password login is completely disabled for all users. Direct root login is restricted to key-only.
### Configuration file
Drop-in config is deployed to:
`/etc/ssh/sshd_config.d/00-disable-password.conf`
```text
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
```
### Apply Changes
If you update SSH configurations, reload sshd:
```bash
# Debian/Ubuntu
sudo systemctl reload ssh
# RedHat/CentOS
sudo systemctl reload sshd
```
---
## 2. Fail2ban Management
Fail2ban monitors SSH authentication failures and bans offensive IPs.
### Default Settings
* **Bantime**: 24 hours (`86400` seconds)
* **Findtime**: 10 minutes (`600` seconds)
* **Maxretry**: 3 attempts
### Useful Commands
```bash
# Check Fail2ban service status
sudo systemctl status fail2ban
# Check sshd jail status (banned IPs)
sudo fail2ban-client status sshd
# Unban a specific IP
sudo fail2ban-client set sshd unbanip <IP>
# Manually ban a specific IP
sudo fail2ban-client set sshd banip <IP>
# View fail2ban logs
sudo tail -f /var/log/fail2ban.log
```
---
## 3. SSH Proxy Connection Helper (`ssh_check.exp`)
A generic `expect` helper script to verify ProxyJump-ed SSH connectivity.
### Usage
To prevent password leaks in shell history (`~/.bash_history` or `~/.zsh_history`), **never** pass the password as a command-line argument. Instead, use one of the secure methods below:
#### Option A: Read securely from input (Recommended)
```bash
# Type your password securely (input will not echo on screen)
read -s SSH_CHECK_PASSWORD
export SSH_CHECK_PASSWORD
# Run the helper script (picks up password from env var)
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129
```
#### Option B: Set via env var with leading space
If your shell is configured to ignore commands starting with a space (e.g. `HISTCONTROL=ignorespace` in bash or `setopt HIST_IGNORE_SPACE` in zsh), you can set the variable with a leading space:
```bash
export SSH_CHECK_PASSWORD="your_password"
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129
```
#### Option C: Legacy/Direct (Not recommended, leaves history trace)
```bash
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129 "your_password"
```
---
## 4. Firewall (UFW) quick-ref
Used on hosts to manage ports (e.g. 80, 443, 1443).
```bash
# View firewall rules with line numbers
sudo ufw status numbered
# Allow a port to Anywhere
sudo ufw allow 443/tcp
# Delete a rule by rule number
sudo ufw delete <rule_number>
# Restrict port 22 to a specific IP (e.g. Proxy IP)
sudo ufw allow from 43.207.194.92 to any port 22 proto tcp
sudo ufw delete allow 22/tcp
# Reload firewall
sudo ufw reload
```

View File

@ -1,205 +0,0 @@
# yitu-it-series R2 assets
This runbook migrates the local Google Drive `自媒体` directory to Cloudflare R2 for the Docusaurus AI Native knowledge base.
## Architecture
```text
GitHub -> Docusaurus -> Cloudflare Pages -> ebook.svc.plus
Google Drive local folder
-> rclone
-> Cloudflare R2 bucket: yitu-it-series
-> R2 custom domain: img.svc.plus
-> Docusaurus Markdown image URLs
```
## Source and target
```text
Local source:
/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
R2 bucket:
yitu-it-series
Public asset domain:
https://img.svc.plus
```
## Recommended object layout
```text
yitu-it-series/
├── covers/
├── xiaohongshu/
├── observability/
├── storage/
├── networking/
├── ai-native/
├── security/
├── platform-engineering/
└── ebook-assets/
```
Use stable, semantic paths for published content:
```text
covers/season-1/single-machine-to-platform-cover-v1.png
security/least-privilege/root-to-rootless-v1.png
ai-native/agentic-infra/ai-native-platform-v1.png
ebook-assets/diagrams/cloud-native-to-ai-native-v1.png
```
Prefer versioned object names instead of overwriting an already published image. This keeps Cloudflare CDN behavior predictable and preserves old articles.
## Cloudflare API token
Create two token scopes if possible:
```text
Bootstrap token:
- Account: Cloudflare R2: Edit
- Zone: DNS: Edit, Zone: Read for svc.plus
- Used only for bucket/custom-domain setup
Long-running R2 S3 token:
- R2 Object Read & Write
- Scope limited to bucket yitu-it-series
- Used by rclone sync
```
Required environment variables:
```bash
export CF_ACCOUNT_ID="..."
export CF_ZONE_ID="..."
export CLOUDFLARE_API_TOKEN="..."
export R2_ACCESS_KEY_ID="..."
export R2_SECRET_ACCESS_KEY="..."
```
## Commands
From the playbooks directory:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
chmod +x scripts/sync-yitu-it-series-r2.sh
scripts/sync-yitu-it-series-r2.sh doctor
scripts/sync-yitu-it-series-r2.sh create-bucket
scripts/sync-yitu-it-series-r2.sh configure-rclone
scripts/sync-yitu-it-series-r2.sh dry-run
scripts/sync-yitu-it-series-r2.sh copy
scripts/sync-yitu-it-series-r2.sh check
scripts/sync-yitu-it-series-r2.sh tree
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Use `copy` for the first production migration when preserving all historical remote files matters. Use `sync` for steady-state mirroring after the source layout is stable.
## Performance profile
Default large AI image profile:
```bash
export RCLONE_TRANSFERS=16
export RCLONE_CHECKERS=32
export RCLONE_S3_UPLOAD_CUTOFF=128M
export RCLONE_S3_CHUNK_SIZE=128M
```
Many small images:
```bash
export RCLONE_TRANSFERS=32
export RCLONE_CHECKERS=64
```
Large source files such as PSD/video:
```bash
export RCLONE_TRANSFERS=4
export RCLONE_CHECKERS=16
export RCLONE_S3_UPLOAD_CUTOFF=256M
export RCLONE_S3_CHUNK_SIZE=256M
```
## Incremental sync
Install a macOS launchd sync job:
```bash
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
scripts/sync-yitu-it-series-r2.sh install-launchd
launchctl list | grep yitu-it-series
```
Remove it:
```bash
scripts/sync-yitu-it-series-r2.sh uninstall-launchd
```
## R2 custom domain
Target:
```text
img.svc.plus -> R2 bucket yitu-it-series
```
The script calls the Cloudflare R2 custom domain API:
```bash
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
```
Recommended Cloudflare cache rule:
```text
If hostname equals img.svc.plus:
- Cache eligible
- Edge TTL: 30 days or longer
- Browser TTL: 7-30 days, or respect origin
```
## Docusaurus references
Markdown:
```md
![AI Native 基础设施演进](https://img.svc.plus/ai-native/ai-native-infra-cover-v1.png)
![最小权限演进](https://img.svc.plus/security/least-privilege-cover-v1.png)
```
MDX:
```mdx
<img
src="https://img.svc.plus/platform-engineering/platform-engineering-roadmap-v1.png"
alt="Platform Engineering Roadmap"
loading="lazy"
/>
```
Front matter:
```md
---
title: AI Native 基础设施演进
description: 从云原生到 AI Native 的平台工程知识库
image: https://img.svc.plus/covers/ai-native-infra-cover-v1.png
---
```
## AI Native knowledge-base practices
- Keep Docusaurus focused on Markdown, MDX, navigation, SEO, and search.
- Keep heavy generated images and ebook assets in R2.
- Reference published assets with absolute `https://img.svc.plus/...` URLs.
- Keep object names immutable after publication; publish revisions with `-v2`, `-v3`.
- Run `rclone check` before replacing local Markdown image references.
- Keep raw generation artifacts separate from article-ready assets when possible.
- Use topic directories that match the ebook taxonomy so future RAG/vector indexing can attach image context to chapters.

View File

@ -1,7 +0,0 @@
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks && ansible-playbook \
-i "xworkmate-bridge.svc.plus," \
--user ubuntu \
-e "xworkspace_console_hosts=xworkmate-bridge.svc.plus" \
-e "xworkspace_console_local_dashboard_dir=/home/ubuntu/xworkspace/dashboard" \
-e "ansible_become_pass=XXXXXXXXX" \
setup-xworkspace-console.yaml

View File

@ -1,15 +0,0 @@
CF_ACCOUNT_ID=
CF_ZONE_ID=
CLOUDFLARE_API_TOKEN=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=yitu-it-series
R2_REMOTE=cloudflare-r2
R2_CUSTOM_DOMAIN=img.svc.plus
LOCAL_SRC=/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
RCLONE_TRANSFERS=16
RCLONE_CHECKERS=32
RCLONE_S3_UPLOAD_CUTOFF=128M
RCLONE_S3_CHUNK_SIZE=128M

View File

@ -1,7 +0,0 @@
---
- name: Setup minimal GNOME + XRDP desktop
hosts: all
become: true
gather_facts: true
roles:
- roles/vhosts/gnome_xrdp_minimal/

View File

@ -1,8 +0,0 @@
---
- name: Prepare Host Environment
hosts: all
become: true
roles:
- roles/vhosts/common
- roles/vhosts/kernel_tuning
- roles/docker/container_runtime

View File

@ -1,7 +0,0 @@
---
- name: Install Kubernetes via Sealos
hosts: masters
become: true
roles:
- roles/vhosts/sealos_cluster
- roles/vhosts/cni_cilium

View File

@ -1,6 +0,0 @@
---
- name: Install NVIDIA GPU Operator
hosts: masters[0]
become: true
roles:
- roles/charts/nvidia_gpu_operator

View File

@ -1,7 +0,0 @@
---
- name: Deploy Ray Cluster
hosts: masters[0]
become: true
roles:
- roles/charts/ray_cluster
- roles/charts/ray_service

View File

@ -1,7 +0,0 @@
---
- name: Deploy vLLM Inference Service
hosts: masters[0]
become: true
roles:
- roles/charts/vllm_runtime
- roles/charts/vllm_service

View File

@ -1,6 +0,0 @@
---
- import_playbook: gpu_inference_01_prepare.yml
- import_playbook: gpu_inference_02_sealos.yml
- import_playbook: gpu_inference_03_gpu_operator.yml
- import_playbook: gpu_inference_04_ray.yml
- import_playbook: gpu_inference_05_vllm.yml

View File

@ -3,17 +3,3 @@ ansible_ssh_user: root
ansible_ssh_private_key_file: ~/.ssh/id_rsa
ansible_host_key_checking: False
# Global security level for public access.
# Set to 'strict' to disable public Caddy/Ingress access for all roles.
ai_workspace_security_level: standard
# Caddy ingress is enabled by default on Linux where we expect a dedicated box.
# It is disabled on macOS (developer workstation with port conflicts) and Windows
# (Caddy not natively supported in our Windows pipeline).
# Override anytime with -e caddy_enabled=true or -e caddy_enabled=false.
caddy_enabled: "{{ ansible_os_family != 'Darwin' and ansible_os_family != 'Windows' }}"
# Caddy config root. Linux uses the system path /etc/caddy; macOS (Homebrew)
# uses /opt/homebrew/etc/caddy. Roles derive their Caddyfile / conf.d / fragment
# paths from this so a force-enabled Caddy on macOS writes to the brew location.
caddy_config_dir: "{{ '/opt/homebrew/etc/caddy' if ansible_os_family == 'Darwin' else '/etc/caddy' }}"

View File

@ -1,49 +0,0 @@
---
xworkmate_bridge_distributed_topology: dual-node
xworkmate_bridge_distributed_nodes:
- id: xworkmate-bridge
role: primary
public_base_url: https://xworkmate-bridge.svc.plus
bridge_endpoint: http://172.29.10.1:8787
- id: cn-xworkmate-bridge
role: edge
public_base_url: https://cn-xworkmate-bridge.svc.plus
bridge_endpoint: http://172.29.10.2:8787
xworkmate_bridge_distributed_vpn_interface: wg-xwm
xworkmate_bridge_distributed_vpn_wireguard_port: 51820
xworkmate_bridge_distributed_vpn_local_tproxy_port: 51830
xworkmate_bridge_distributed_vpn_vless_port: 2443
xworkmate_bridge_distributed_vpn_forwarder_port: 8787
xworkmate_bridge_distributed_vpn_forwarder_target: 127.0.0.1:8787
xworkmate_bridge_distributed_vpn_vault_addr: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_URL') | default('https://vault.svc.plus', true) }}"
xworkmate_bridge_distributed_vpn_vault_token: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_ROOT_ACCESS_TOKEN') | default(lookup('ansible.builtin.env', 'VAULT_TOKEN'), true) }}"
xworkmate_bridge_distributed_vpn_vault_mount: kv
xworkmate_bridge_distributed_vpn_vault_base_path: xworkmate-bridge/distributed/wireguard-over-vless
xworkmate_bridge_distributed_vpn_nodes:
jp-xhttp-contabo.svc.plus:
node_id: xworkmate-bridge
domain: xworkmate-bridge.svc.plus
wg_ip: 172.29.10.1
public_key: 1staGq8lmHFRFRFNj2QOFx/MPxb/1fFV4tawC6xSi1Q= # gitleaks:allow
peer: cn-xworkmate-bridge.svc.plus
cn-xworkmate-bridge.svc.plus:
node_id: cn-xworkmate-bridge
domain: cn-xworkmate-bridge.svc.plus
wg_ip: 172.29.10.2
public_key: iYlnFaWiMfMelpiN8ZV2SwCDrLihqtJXvHUsM3BN9zU= # gitleaks:allow
peer: jp-xhttp-contabo.svc.plus
xworkmate_bridge_distributed_vpn_clients:
- id: shenlan-macos
wg_ip: 172.29.10.10
public_key: jfHsw1HIqRQzGvfsRfdkS7BLThDbBvWMsAlJRp1kdkw= # gitleaks:allow
attach_to:
- jp-xhttp-contabo.svc.plus
- cn-xworkmate-bridge.svc.plus
- id: shenlan-ios
wg_ip: 172.29.10.11
public_key: I/zCL7gLWrY6FZiLXUs7i/vivU5Xuo8r7EbkNhtv12w= # gitleaks:allow
attach_to:
- jp-xhttp-contabo.svc.plus

View File

@ -1,17 +0,0 @@
---
- name: Harden SSH on all inventory hosts
hosts: all
become: true
gather_facts: true
vars:
sshd_config_path: /etc/ssh/sshd_config
sshd_dropin_dir: /etc/ssh/sshd_config.d
root_authorized_keys_path: /root/.ssh/authorized_keys
local_public_key_path: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa.pub"
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
roles:
- role: harden_ssh_root_key_only

View File

@ -1,16 +0,0 @@
---
xworkmate_bridge_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_domain: cn-xworkmate-bridge.svc.plus
xworkmate_bridge_service_public_base_url: https://cn-xworkmate-bridge.svc.plus
xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-bridge
xworkmate_bridge_service_user: ubuntu
xworkmate_bridge_service_group: ubuntu
xworkmate_bridge_service_home: /home/ubuntu
xworkmate_bridge_required_services: []
xworkmate_bridge_required_listeners:
- host: 127.0.0.1
port: "8787"
name: bridge
xworkmate_bridge_distributed_local_node_id: cn-xworkmate-bridge
xworkmate_bridge_distributed_task_forward_peer_id: xworkmate-bridge

View File

@ -1,2 +0,0 @@
---
gateway_openclaw_acp_enabled: true

View File

@ -1,25 +0,0 @@
---
# LiteLLM Admin UI Credentials
litellm_basic_auth_username: admin
# Database Configuration
litellm_database_host: "127.0.0.1"
litellm_database_port: "15432"
litellm_database_sslmode: "disable"
litellm_database_name: "litellm"
litellm_database_user: "litellm"
litellm_database_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
33303762616661623962386664653533666362393435343830303061613364666238313933626330
6231303463663732313732376238633033386463383134630a313738393762333263653363376266
37323938386331383762363565613361623638353834363735363030363037626666663431613239
3939323036313435360a313833316131663231326162393364616262323763333133336335323837
31336464646334653035646633363164633363353835316466626337633238396130
litellm_master_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
38303433656665323039303561326534636136623766303563313863633133333564343830663032
3038343634323835666430663165343461643338343334330a623764393034356330303263366161
30663735663237653135356663343063353330356137643534313637313062633964383266376263
6432333730393232380a363037333265363462323139306534343563323631616464616132313631
31396232386633656436653966626131663139643539633964633864643930643639

View File

@ -1,3 +0,0 @@
---
xworkmate_bridge_distributed_local_node_id: xworkmate-bridge
xworkmate_bridge_distributed_task_forward_peer_id: ""

View File

@ -1,88 +1,32 @@
# Vhosts
[cn_front_host]
# services: cn-front.svc.plus, cn-homepage.svc.plus
cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root firewall_manage_ufw=false service_domains=cn-front.svc.plus
[cn_homepage_host]
# services: cn-homepage.svc.plus
cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root
[cn_xworkmate_bridge_host]
# services: cn-xworkmate-bridge.svc.plus
cn-xworkmate-bridge.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root service_domains=cn-xworkmate-bridge.svc.plus
[global_homepage_host]
# services: global-homepage.svc.plus
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root
[jp_xhttp_contabo_host]
# services: api.svc.plus, console.svc.plus, docs.svc.plus, accounts.svc.plus, xworkmate-bridge.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, postgresql.svc.plus
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,docs.svc.plus,accounts.svc.plus,xworkmate-bridge.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,postgresql.svc.plus xray_exporter_node_id_custom=jp-xhttp-contabo.svc.plus
[tky_proxy_host]
# services: tky-proxy.svc.plus
tky-proxy.svc.plus ansible_host=43.207.194.92 ansible_user=admin ansible_ssh_user=admin service_domains=tky-proxy.svc.plus
[us_xhttp_host]
# services: tky-proxy.svc.plus
us-xhttp.svc.plus ansible_host=44.251.164.174 ansible_user=ubuntu ansible_ssh_user=admin service_domains=us-xhttp.svc.plus
[jp_k3s_vultr_host]
# services: jp-k3s-vultr.svc.plus
jp-k3s-vultr.svc.plus ansible_host=167.179.110.129 ansible_user=root ansible_ssh_user=root service_domains=jp-k3s-vultr.svc.plus
# Logical service groups
[web]
cn-front.svc.plus
cn-console.svc.plus ansible_host=47.120.61.35
global-console.svc.plus ansible_host=35.220.157.80 ansible_user=root
[agent_svc_plus]
tky-proxy.svc.plus
jp-xhttp-contabo.svc.plus
[deepflow_agents]
192.168.1.101 ansible_user=root ansible_ssh_pass=pass101
192.168.1.102 ansible_user=admin ansible_ssh_pass=pass102
192.168.1.103 ansible_user=root ansible_ssh_pass=pass103 ansible_port=2222
192.168.1.104 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa_ubuntu
[xray_exporter]
tky-proxy.svc.plus
jp-xhttp-contabo.svc.plus
[mail]
smtp.svc.plus ansible_host=45.130.167.90
[xworkmate_bridge]
jp-xhttp-contabo.svc.plus
[cn_xworkmate_bridge]
cn-xworkmate-bridge.svc.plus
[xworkmate_bridge_distributed:children]
xworkmate_bridge
cn_xworkmate_bridge
[billing_service]
jp-xhttp-contabo.svc.plus
[accounts]
jp-xhttp-contabo.svc.plus
[docs]
jp-xhttp-contabo.svc.plus
[apisix]
jp-xhttp-contabo.svc.plus
[apisix]
api.svc.plus ansible_host=46.250.251.132 ansible_user=root
[postgresql]
jp-xhttp-contabo.svc.plus
[k3s]
jp-k3s-vultr.svc.plus
[bootstrap]
auth.svc.plus ansible_host=34.92.122.119 ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_rsa
[all:vars]
ansible_port=22
ansible_user=root
ansible_ssh_transfer_method=piped
ansible_host_key_checking=False
# SSH 密钥或密码(二选一)
ansible_ssh_private_key_file=~/.ssh/id_rsa
k3s_platform_git_private_key=~/.ssh/id_rsa
# ansible_ssh_private_key_file=~/.ssh/id_rsa
# ansible_ssh_pass=your_password
[acp_bridge_host]
acp-bridge.onwalk.net ansible_host=167.179.110.129 ansible_user=root ansible_ssh_user=root
# DeepFlow agent 配置变量
controller_ips=["10.10.10.10", "10.10.10.11"]
vtap_group_id="g-P22vLIMdB6"
# DeepFlow agent 安装包位置
agent_base_dir="deepflow-agent-for-linux"
agent_package_name="deepflow-agent-1.0-5407.systemd.x86_64.rpm"

View File

@ -1,27 +0,0 @@
---
# 全局版本与镜像
kubernetes_version: "v1.28.9"
sealos_version: "5.0.0"
cilium_version: "1.15.5"
gpu_operator_version: "v24.3.0"
kuberay_version: "1.1.0"
ray_version: "2.9.0"
vllm_image: "vllm/vllm-openai:v0.4.2"
# 网络配置
pod_cidr: "10.244.0.0/16"
service_cidr: "10.96.0.0/12"
nccl_socket_ifname: "eth0"
gloo_socket_ifname: "eth0"
# 模型与推理配置
vllm_model: "/models/Llama-3-70B-Instruct"
vllm_tensor_parallel_size: 2
vllm_pipeline_parallel_size: 1
# GPU 驱动策略
driver_enabled: true
driver_version: "535.129.03"
dcgm_exporter_enabled: true
ansible_user: "root"

View File

@ -1 +0,0 @@
---

View File

@ -1 +0,0 @@
---

View File

@ -1 +0,0 @@
---

View File

@ -1,13 +0,0 @@
[masters]
k8s-master-01 ansible_host=10.0.0.10
[gpu_workers]
k8s-gpu-01 ansible_host=10.0.0.21 accelerator=nvidia-h100
k8s-gpu-02 ansible_host=10.0.0.22 accelerator=nvidia-h100
[ray_workers:children]
gpu_workers
[k8s_cluster:children]
masters
gpu_workers

View File

@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""Ansible 动态 inventory —— 数据源为 Terraform 导出的 CMDB。
IAC 联动方式
iac_modules/terraform-hcl-standard/vultr-vps/envs/ai-workspace/ generate.py
`terraform apply` YAML 静态字段与 terraform 运行时输出合并写出
cmdb.json结构化主机事实本脚本把它翻译成 Ansible 动态 inventory
于是 IaC 一变更重跑 `generate.py inventory`inventory 就跟着变
取数优先级
1. 环境变量 AI_WORKSPACE_CMDB_JSON 指向的文件
2. 环境变量 AI_WORKSPACE_TF_DIR或默认 env 目录下的 cmdb.json
用法
ansible-inventory -i inventory/terraform_cmdb.py --list
ansible all -i inventory/terraform_cmdb.py -m ping
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
# playbooks/inventory -> 仓库根 -> terraform env
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
DEFAULT_TF_DIR = os.path.join(
REPO_ROOT,
"iac_modules",
"terraform-hcl-standard",
"vultr-vps",
"envs",
"ai-workspace",
)
def _from_explicit_file():
path = os.environ.get("AI_WORKSPACE_CMDB_JSON")
if path and os.path.isfile(path):
with open(path, encoding="utf-8") as fh:
return json.load(fh)
return None
def _from_default_file(tf_dir):
path = os.path.join(tf_dir, "cmdb.json")
if os.path.isfile(path):
with open(path, encoding="utf-8") as fh:
return json.load(fh)
return None
def load_cmdb():
tf_dir = os.environ.get("AI_WORKSPACE_TF_DIR", DEFAULT_TF_DIR)
for loader in (
_from_explicit_file,
lambda: _from_default_file(tf_dir),
):
data = loader()
if data:
return data
return {}
def build_inventory(cmdb):
inv = {"_meta": {"hostvars": {}}}
groups = {}
for name, host in cmdb.items():
hostvars = {
"ansible_host": host.get("ip"),
"ansible_user": host.get("ansible_user", "root"),
# 云主机 IP 常被回收,放宽 host key 校验避免撞到旧 known_hosts
"ansible_ssh_common_args": (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
),
}
# CMDB 其余字段一并暴露给 playbook 使用
hostvars.update(host.get("host_vars", {}))
hostvars["cmdb_instance_id"] = host.get("instance_id")
hostvars["cmdb_os_id"] = host.get("os_id")
hostvars["cmdb_tags"] = host.get("tags", [])
inv["_meta"]["hostvars"][name] = hostvars
for group in host.get("groups", []) or ["ungrouped"]:
groups.setdefault(group, {"hosts": []})["hosts"].append(name)
inv.update(groups)
inv["all"] = {"children": sorted(list(groups.keys()) + ["ungrouped"])}
return inv
def main():
args = sys.argv[1:]
cmdb = load_cmdb()
if "--host" in args:
# hostvars 已在 _meta 里,单主机查询返回空对象即可
print(json.dumps({}))
return
# 默认与 --list 行为一致
print(json.dumps(build_inventory(cmdb), indent=2))
if __name__ == "__main__":
main()

View File

@ -1,8 +0,0 @@
- name: Addon | single-node k3s platform
hosts: k3s
user: root
become: yes
gather_facts: yes
tasks:
- include_role:
name: vhosts/k3s_platform_addon

View File

@ -1,8 +0,0 @@
- name: Bootstrap single-node k3s GitOps platform
hosts: k3s
user: root
become: yes
gather_facts: yes
tasks:
- include_role:
name: vhosts/k3s_platform_bootstrap

View File

@ -1,10 +0,0 @@
- name: Reset single-node k3s host
hosts: k3s
user: root
become: yes
gather_facts: yes
vars:
cluster_reset: enable
tasks:
- include_role:
name: vhosts/k3s-reset

View File

@ -1,7 +0,0 @@
---
- name: Setup minimal Plasma + XRDP desktop
hosts: all
become: true
gather_facts: true
roles:
- roles/vhosts/plasma_xrdp_minimal/

View File

@ -1,95 +0,0 @@
# Agent Skills
Synchronizes controller skill sources to an Ubuntu runtime user's canonical
skills directory, then exposes the same directory to agent-specific skill
locations.
Default source and target:
- local marketplace source: `~/.agents/skills/`
- local repository source: `../xworkspace-core-skills/skills/`
- remote canonical path: `/home/ubuntu/.agents/skills/`
- default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw`
The repository source is categorized by capability domain, for example
`video-production/`, `image-production/`, `animation/`, and `workspace-core/`.
The role syncs those categories as-is, then creates root-level symlinks for
nested skills so runtimes that scan one directory level can still discover them.
Set `agent_skills_xworkspace_core_enabled=false` to use only the marketplace
source, or `agent_skills_remote_flatten_nested_skills=false` to disable root
symlink materialization.
The role keeps one remote source of truth and links each agent's skills entry to
that canonical directory where the online runtime already uses links. Existing
non-symlink target directories are rejected by default to avoid silently deleting
agent-owned content. The live `ubuntu` Codex runtime on
`xworkmate-bridge.svc.plus` keeps `/home/ubuntu/.codex/skills` as a real
directory, so it is preserved by default through
`agent_skills_preserve_existing_target_dirs`. Set
`agent_skills_replace_existing_target_dirs=true` only when those target
directories should be replaced.
Before syncing, the role can materialize the skills needed by XWorkmate typical
scenario tests into the local canonical source. The default matrix includes:
| Scenario group | Skills |
| --- | --- |
| local document artifacts | `pptx`, `docx`, `xlsx`, `pdf` |
| local image processing | `image-resizer` |
| local browser automation | `browser-automation` |
| online image generation | `image-cog` |
| online image/video editing | `image-video-generation-editting`, `wan-image-video-generation-editting` |
| online video translation | `video-translator` |
| online news/search | `web-search`, `news-fetch`, `find-skills` |
| skill maintenance | `find-skills`, `self-improving`, `skill-vetter`, `skills-security-check` |
Missing local skills are installed on the Ansible controller before rsync. The
installer adapter order is:
1. `clawhub --workdir ~/.agents --dir skills --no-input install <skill>`
2. `find-skills install <skill> --target ~/.agents/skills`
Set `agent_skills_auto_install_enabled=false` to require that all skills are
already present locally. Set
`agent_skills_auto_install_fail_on_missing_installer=false` to skip missing
skills when neither installer is available; the role still fails later if a
required skill cannot be resolved.
Required-skill checks search both the marketplace source and the categorized
repository source recursively. Auto-install still writes only to
`~/.agents/skills/`; repository-owned skills should be changed in
`xworkspace-core-skills`.
After install, optional local quality gates run for each resolved skill when the
command exists:
- `skill-vetter <skill_path>`
- `skills-security-check <skill_path>`
- `self-improving inspect <skill_path>`
The quality gates are enabled by default and fail the play when a present gate
returns an error. Override `agent_skills_quality_gate_enabled=false` or
`agent_skills_quality_gate_fail_on_error=false` only for controlled bootstrap
environments.
Default sync excludes local runtime artifacts such as `.venv/`, `__pycache__/`,
`.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and
references rather than controller-local virtual environments.
The sync defaults to overlay mode (`agent_skills_delete_removed=false`) so it
does not remove skills that already exist on the live runtime catalog. Enable
deletion only for controlled rebuilds of `/home/ubuntu/.agents/skills/`.
Example:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills
```
Bootstrap-only example that keeps the existing local source strict but skips
quality gate failures from newly installed marketplace skills:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills \
-e agent_skills_quality_gate_fail_on_error=false
```

View File

@ -1,126 +0,0 @@
---
agent_skills_user: "{{ ansible_env.USER | default('ubuntu') }}"
agent_skills_group: "{{ 'staff' if ansible_os_family == 'Darwin' else agent_skills_user }}"
agent_skills_home: "{{ ansible_env.HOME | default('/home/' + agent_skills_user) }}"
# 规范化技能落地目录canonical始终在目标主机上。installer 直接装到这里,
# core 技能 clone 后合并进来。本地/pull 与远程 controller 两种模型行为一致。
agent_skills_remote_dir: "{{ agent_skills_home }}/.agents/skills"
# xworkspace-core-skills 以 git clone 获取(最通用、跨平台、双模型一致),
# 在目标主机上 clone不再依赖 controller 端预置目录。
agent_skills_xworkspace_core_enabled: true
agent_skills_xworkspace_core_required: true
agent_skills_xworkspace_core_repo_url: "https://github.com/ai-workspace-lab/xworkspace-core-skills.git"
agent_skills_xworkspace_core_version: "main"
agent_skills_xworkspace_core_clone_dir: "{{ agent_skills_home }}/.local/src/xworkspace-core-skills"
agent_skills_xworkspace_core_source_dir: "{{ agent_skills_xworkspace_core_clone_dir }}/skills"
agent_skills_replace_existing_target_dirs: false
agent_skills_preserve_existing_target_dirs:
- "{{ agent_skills_home }}/.codex/skills"
agent_skills_remote_flatten_nested_skills: true
agent_skills_auto_install_enabled: true
agent_skills_auto_install_fail_on_missing_installer: true
agent_skills_quality_gate_enabled: true
agent_skills_quality_gate_fail_on_error: true
agent_skills_quality_gate_commands:
- name: skill-vetter
argv_prefix:
- skill-vetter
- name: skills-security-check
argv_prefix:
- skills-security-check
- name: self-improving
argv_prefix:
- self-improving
- inspect
agent_skills_typical_scenario_skills:
- name: pptx
scenario_groups: [local-document-artifacts]
aliases: [pptx]
source: local-or-clawhub
- name: docx
scenario_groups: [local-document-artifacts]
aliases: [docx]
source: local-or-clawhub
- name: xlsx
scenario_groups: [local-document-artifacts]
aliases: [xlsx]
source: local-or-clawhub
- name: pdf
scenario_groups: [local-document-artifacts]
aliases: [pdf]
source: local-or-clawhub
- name: image-resizer
scenario_groups: [local-image-processing]
aliases: [image-resizer]
source: clawhub
- name: browser-automation
scenario_groups: [local-browser-automation]
aliases: [browser-automation]
source: clawhub
- name: image-cog
scenario_groups: [online-image-generation]
aliases: [image-cog]
source: acp-descriptor
- name: image-video-generation-editting
scenario_groups: [online-image-video-editing]
aliases:
- image-video-generation-editting
- wan-image-video-generation-editting
source: acp-descriptor
- name: video-translator
scenario_groups: [online-video-translation]
aliases: [video-translator]
source: acp-descriptor
- name: web-search
scenario_groups: [online-news-fetch, online-search]
aliases:
- web-search
- search
- autoglm-websearch
source: clawhub
- name: news-fetch
scenario_groups: [online-news-fetch]
aliases:
- news-fetch
- blogwatcher
source: clawhub
- name: find-skills
scenario_groups: [skill-maintenance, online-news-fetch, online-search]
aliases: [find-skills]
source: local-or-clawhub
- name: self-improving
scenario_groups: [skill-maintenance]
aliases:
- self-improving
- self-improving-1.1.3
source: local-or-clawhub
- name: skill-vetter
scenario_groups: [skill-maintenance]
aliases:
- skill-vetter
- skill-vetter-1.0.0
source: local-or-clawhub
- name: skills-security-check
scenario_groups: [skill-maintenance]
aliases: [skills-security-check]
source: local-or-clawhub
install_force: true
agent_skills_extra_required_skills: []
agent_skills_targets:
- name: codex
paths:
- "{{ agent_skills_home }}/.codex/skills"
- name: gemini
paths:
- "{{ agent_skills_home }}/.gemini/skills"
- name: opencode
paths:
- "{{ agent_skills_home }}/.opencode/skills"
- "{{ agent_skills_home }}/.config/opencode/skills"
- name: openclaw
paths:
- "{{ agent_skills_home }}/.openclaw/skills"

View File

@ -1,348 +0,0 @@
---
# 设计:全程在「目标主机」上执行——没有任何 delegate_to: localhost。
# 因此两种执行模型行为完全一致:
# - 本地/pullcurl|bash → ansible-playbook -c locallocalhost 即主机)
# - 远程 controlleransible-playbook -i <inventory> over ssh任务在主机上跑
# 源以 git clone 获取(最通用、跨平台),不再依赖 controller 端预置目录,
# 合并用 ansible.builtin.copy无裸 rsync、无本地钉死
- name: Validate agent skills input
ansible.builtin.assert:
that:
- agent_skills_user | length > 0
- agent_skills_group | length > 0
- agent_skills_home | length > 0
- agent_skills_remote_dir | length > 0
- agent_skills_targets | length > 0
fail_msg: "agent_skills_user/group/home, remote_dir and targets must be set."
- name: Build required agent skills list
ansible.builtin.set_fact:
agent_skills_required_entries: "{{ agent_skills_typical_scenario_skills + agent_skills_extra_required_skills }}"
- name: Ensure agent skills owner home exists
ansible.builtin.file:
path: "{{ agent_skills_home }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
- name: Ensure canonical agent skills directory exists
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
# --- 源获取:在目标主机 git clone最通用 ---------------------------------
- name: Ensure core skills checkout parent exists
ansible.builtin.file:
path: "{{ agent_skills_xworkspace_core_clone_dir | dirname }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
when: agent_skills_xworkspace_core_enabled | bool
- name: Clone/update xworkspace core skills on the target host
ansible.builtin.git:
repo: "{{ agent_skills_xworkspace_core_repo_url }}"
dest: "{{ agent_skills_xworkspace_core_clone_dir }}"
version: "{{ agent_skills_xworkspace_core_version }}"
depth: 1
force: true
become_user: "{{ agent_skills_user }}"
register: agent_skills_core_clone
when: agent_skills_xworkspace_core_enabled | bool
- name: Inspect core skills directory
ansible.builtin.stat:
path: "{{ agent_skills_xworkspace_core_source_dir }}"
register: agent_skills_core_skills_stat
when: agent_skills_xworkspace_core_enabled | bool
- name: Require core skills directory when enabled and required
ansible.builtin.assert:
that:
- agent_skills_core_skills_stat.stat.isdir | default(false)
fail_msg: "core skills dir missing after clone: {{ agent_skills_xworkspace_core_source_dir }}"
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_xworkspace_core_required | bool
- name: Build skill search dirs (canonical + core checkout)
ansible.builtin.set_fact:
agent_skills_search_dirs: >-
{{
[agent_skills_remote_dir]
+ (
(
agent_skills_xworkspace_core_enabled | bool
and agent_skills_core_skills_stat.stat.isdir | default(false)
)
| ternary([agent_skills_xworkspace_core_source_dir], [])
)
}}
# --- 缺失场景技能:用 installer 适配器装到 canonical主机本地 --------------
- name: Inspect required scenario skills presence
ansible.builtin.shell: |
set -eu
for d in {{ agent_skills_search_dirs | map('quote') | join(' ') }}; do
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
done
done
exit 1
args:
executable: /bin/bash
register: agent_skills_presence
changed_when: false
failed_when: false
check_mode: false
loop: "{{ agent_skills_required_entries }}"
loop_control:
label: "{{ item.name }}"
- name: Build missing scenario skills list
ansible.builtin.set_fact:
agent_skills_missing_entries: >-
{{ agent_skills_presence.results | selectattr('rc', 'ne', 0) | map(attribute='item') | list }}
- name: Install missing scenario skills via installer adapters (clawhub/find-skills)
ansible.builtin.shell: |
set -eu
skill={{ item.name | quote }}
target_dir={{ agent_skills_remote_dir | quote }}
parent="$(dirname "$target_dir")"; base="$(basename "$target_dir")"
rc=1
if command -v clawhub >/dev/null 2>&1; then
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if clawhub --workdir "$parent" --dir "$base" --no-input install {{ (item.install_force | default(false) | bool) | ternary('--force', '') }} "$n"; then rc=0; break; fi
done
exit "$rc"
elif command -v find-skills >/dev/null 2>&1; then
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if find-skills install "$n" --target "$target_dir"; then rc=0; break; fi
done
exit "$rc"
elif [ "{{ agent_skills_auto_install_fail_on_missing_installer | bool | ternary('true', 'false') }}" = "true" ]; then
echo "No installer (clawhub/find-skills) for $skill; preseed $target_dir/$skill." >&2
exit 127
else
echo "Skipped missing $skill (no installer adapter)." >&2
fi
args:
executable: /bin/bash
become_user: "{{ agent_skills_user }}"
register: agent_skills_install_result
changed_when: agent_skills_install_result.rc == 0
loop: "{{ agent_skills_missing_entries }}"
loop_control:
label: "{{ item.name }}"
when:
- agent_skills_auto_install_enabled | bool
- agent_skills_missing_entries | length > 0
# --- 合并 core 技能到 canonical主机本地 copy无 rsync、无 delegate --------
- name: Merge core skills into canonical directory
ansible.builtin.copy:
src: "{{ agent_skills_xworkspace_core_source_dir }}/"
dest: "{{ agent_skills_remote_dir }}/"
remote_src: true
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: preserve
when:
- agent_skills_xworkspace_core_enabled | bool
- agent_skills_core_skills_stat.stat.isdir | default(false)
- name: Re-inspect required scenario skills in canonical dir
ansible.builtin.shell: |
set -eu
d={{ agent_skills_remote_dir | quote }}
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
done
exit 1
args:
executable: /bin/bash
register: agent_skills_presence_final
changed_when: false
failed_when: false
check_mode: false
loop: "{{ agent_skills_required_entries }}"
loop_control:
label: "{{ item.name }}"
- name: Assert required scenario skills are available
ansible.builtin.assert:
that:
- (agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | list | length) == 0
fail_msg: >-
Required scenario skills still missing under {{ agent_skills_remote_dir }}:
{{ agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | map(attribute='item.name') | join(', ') }}.
- name: Build resolved skill paths
ansible.builtin.set_fact:
agent_skills_resolved_paths: >-
{{ agent_skills_presence_final.results | selectattr('rc', 'eq', 0) | map(attribute='stdout') | list | unique }}
- name: Run optional scenario skill quality gates
ansible.builtin.shell: |
set -eu
skill_path={{ item.0 | quote }}
gate_name={{ item.1.name | quote }}
if command -v "$gate_name" >/dev/null 2>&1; then
{{ item.1.argv_prefix | map('quote') | join(' ') }} "$skill_path"
else
echo "Skipped missing quality gate: $gate_name"
fi
args:
executable: /bin/bash
become_user: "{{ agent_skills_user }}"
register: agent_skills_quality_gate_results
changed_when: false
failed_when: agent_skills_quality_gate_fail_on_error | bool and agent_skills_quality_gate_results.rc != 0
loop: "{{ agent_skills_resolved_paths | product(agent_skills_quality_gate_commands) | list }}"
loop_control:
label: "{{ item.1.name }} {{ item.0 | basename }}"
when:
- agent_skills_quality_gate_enabled | bool
- agent_skills_resolved_paths | length > 0
check_mode: false
- name: Set canonical agent skills ownership
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
recurse: true
# --- 把分类嵌套技能在 canonical 根做扁平 symlink主机本地 ------------------
- name: Link nested categorized skills at canonical root
ansible.builtin.shell: |
set -eu
changed=0
while IFS= read -r skill_manifest; do
skill_dir="$(dirname "$skill_manifest")"
skill_name="$(basename "$skill_dir")"
link_path={{ agent_skills_remote_dir | quote }}/"$skill_name"
if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then continue; fi
current_target=""
if [ -L "$link_path" ]; then current_target="$(readlink "$link_path")"; fi
if [ "$current_target" != "$skill_dir" ]; then
if [ "{{ ansible_check_mode | ternary('true', 'false') }}" != "true" ]; then
ln -sfn "$skill_dir" "$link_path"
fi
changed=1
fi
done < <(find {{ agent_skills_remote_dir | quote }} -mindepth 3 -name SKILL.md -type f -print)
if [ "$changed" = "1" ]; then echo "<<CHANGED>>linked nested skills"; fi
args:
executable: /bin/bash
register: agent_skills_flatten_result
changed_when: "'<<CHANGED>>' in agent_skills_flatten_result.stdout"
check_mode: false
when: agent_skills_remote_flatten_nested_skills | bool
- name: Set canonical agent skills ownership after nested links
ansible.builtin.file:
path: "{{ agent_skills_remote_dir }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
recurse: true
when: agent_skills_remote_flatten_nested_skills | bool
# --- 把各 Agent 的 skills 目录 symlink 到 canonical ---------------------------
- name: Flatten agent skills target paths
ansible.builtin.set_fact:
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
- name: Inspect agent skills target paths
ansible.builtin.stat:
path: "{{ item }}"
register: agent_skills_target_path_stats
loop: "{{ agent_skills_target_paths }}"
- name: Reject existing non-link target directories unless replacement is enabled
ansible.builtin.fail:
msg: >-
Agent skills target already exists and is not a symlink: {{ item.item }}.
Set agent_skills_replace_existing_target_dirs=true to replace it with a link
to {{ agent_skills_remote_dir }}.
loop: "{{ agent_skills_target_path_stats.results }}"
when:
- item.stat.exists | default(false)
- not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- not agent_skills_replace_existing_target_dirs | bool
- name: Replace existing non-link target directories when enabled
ansible.builtin.file:
path: "{{ item.item }}"
state: absent
loop: "{{ agent_skills_target_path_stats.results }}"
when:
- item.stat.exists | default(false)
- not item.stat.islnk | default(false)
- item.item not in agent_skills_preserve_existing_target_dirs
- agent_skills_replace_existing_target_dirs | bool
- name: Build agent skills target parent paths
ansible.builtin.set_fact:
agent_skills_target_parent_paths: "{{ agent_skills_target_paths | map('dirname') | list | unique }}"
- name: Inspect agent skills target parent directories
ansible.builtin.stat:
path: "{{ item }}"
register: agent_skills_target_parent_stats
loop: "{{ agent_skills_target_parent_paths }}"
- name: Ensure agent skills target parent directories exist
ansible.builtin.file:
path: "{{ item.item }}"
state: directory
owner: "{{ agent_skills_user }}"
group: "{{ agent_skills_group }}"
mode: "0755"
loop: "{{ agent_skills_target_parent_stats.results }}"
when:
- not item.stat.exists | default(false)
- name: Link agent skills targets to canonical directory
ansible.builtin.file:
src: "{{ agent_skills_remote_dir }}"
dest: "{{ item }}"
state: link
force: true
loop: "{{ agent_skills_target_paths }}"
when: item not in agent_skills_preserve_existing_target_dirs
- name: Verify canonical skill manifests are present
ansible.builtin.find:
paths: "{{ agent_skills_remote_dir }}"
patterns: SKILL.md
recurse: true
file_type: file
register: agent_skills_manifest_files
- name: Assert synced agent skills contain manifests
ansible.builtin.assert:
that:
- agent_skills_manifest_files.matched | int > 0
fail_msg: "No SKILL.md files found under {{ agent_skills_remote_dir }}."
- name: Report synced agent skills
ansible.builtin.debug:
msg: >-
{{ agent_skills_manifest_files.matched }} skill manifests under
{{ agent_skills_remote_dir }}; linked {{ agent_skills_target_paths | length }} agent targets.

View File

@ -1,48 +0,0 @@
# AI Agent Runtime
Provision a Debian-based host for AI agent and AI action execution with one
role entrypoint. The role installs:
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
- Node.js runtime for Playwright-based agents
- Python 3 toolchain for scripts and helpers
- existing system browser, preferring the live `/usr/local/bin/chromium` wrapper
or Google Chrome before installing browser packages
- `pandoc` + XeLaTeX PDF toolchain
- Chinese fonts for document rendering
- shared agent skills via `roles/agent_skills`, including the categorized
`../xworkspace-core-skills/skills/` repository source by default
Design constraints:
- system packages are the primary source of truth
- global npm packages are managed through
`/usr/local/sbin/ai-workspace-manage-npm-global-package` so repeated installs
are idempotent and stale global bin links can be overwritten safely
- Playwright uses the resolved system browser instead of downloading browsers
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
Global npm package actions:
- `install` is the default and only changes the host when a package is missing
or an exact pinned version differs
- `reinstall` forces the configured package set back into place
- `upgrade`, `backup`, `restore`, and `migrate` are reserved action entrypoints
for future runtime lifecycle workflows
Default Playwright environment:
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
- `PLAYWRIGHT_BROWSERS_PATH=0`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/local/bin/chromium` when that live
wrapper exists
Example:
```bash
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml
```
`setup-ai-agent-skills.yml` runs `roles/ai_agent_runtime`, which installs system
dependencies and syncs the current Skill catalog through the embedded
`roles/agent_skills` step in one pass.

View File

@ -1,66 +0,0 @@
---
ai_agent_runtime_base_packages:
- ca-certificates
- curl
- git
- jq
- rsync
- unzip
- wget
ai_agent_runtime_nodejs_enabled: true
ai_agent_runtime_nodejs_version: "24.16.0"
ai_agent_runtime_install_yarn: true
ai_agent_runtime_yarn_version: ""
ai_agent_runtime_npm_global_packages:
- opencode-ai
- "@google/gemini-cli"
- "@openai/codex"
- "@anthropic-ai/claude-code"
ai_agent_runtime_npm_global_package_action: install
ai_agent_runtime_agent_cli_commands:
- opencode
- gemini
- codex
- claude
ai_agent_runtime_playwright_enabled: true
ai_agent_runtime_playwright_version: "1.60.0"
ai_agent_runtime_playwright_skip_browser_download: true
ai_agent_runtime_python_enabled: true
ai_agent_runtime_python_packages:
- python3
- python3-pip
- python3-venv
- python3-dev
- python3-setuptools
- build-essential
- pkg-config
- python-is-python3
ai_agent_runtime_browser_enabled: true
ai_agent_runtime_browser_packages:
- google-chrome-stable
ai_agent_runtime_browser_executable: /usr/local/bin/chromium
ai_agent_runtime_docs_enabled: true
ai_agent_runtime_doc_packages:
- pandoc
- texlive-xetex
- texlive-latex-extra
- texlive-fonts-recommended
- texlive-lang-chinese
- latexmk
ai_agent_runtime_fonts_enabled: true
ai_agent_runtime_font_packages:
- fonts-noto-cjk
- fonts-noto-cjk-extra
- fonts-wqy-zenhei
- fonts-wqy-microhei
ai_agent_runtime_skills_enabled: true
ai_agent_runtime_skills_role_name: agent_skills
ai_agent_runtime_verify_enabled: true
ai_agent_runtime_verify_chinese_fonts: true

View File

@ -1,113 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
action="${1:-install}"
package_spec="${2:-}"
if [ -z "${package_spec}" ]; then
echo "Usage: $0 <install|reinstall|upgrade|backup|restore|migrate> <npm-package-spec>" >&2
exit 2
fi
package_name() {
local spec="$1"
if [[ "${spec}" == @* ]]; then
local rest="${spec#@}"
local scope="${rest%%/*}"
local after_scope="${rest#*/}"
local name="${after_scope%%@*}"
printf '@%s/%s\n' "${scope}" "${name}"
else
printf '%s\n' "${spec%%@*}"
fi
}
desired_version() {
local spec="$1"
if [[ "${spec}" == @* ]]; then
local rest="${spec#@}"
local after_scope="${rest#*/}"
if [[ "${after_scope}" == *"@"* ]]; then
printf '%s\n' "${after_scope#*@}"
fi
elif [[ "${spec}" == *"@"* ]]; then
printf '%s\n' "${spec#*@}"
fi
}
installed_version() {
local name="$1"
local npm_root
npm_root="$(npm root -g)"
node -e '
const fs = require("fs");
const path = require("path");
const pkg = process.argv[1];
const root = process.argv[2];
const packageJson = path.join(root, ...pkg.split("/"), "package.json");
if (!fs.existsSync(packageJson)) process.exit(1);
const parsed = JSON.parse(fs.readFileSync(packageJson, "utf8"));
process.stdout.write(parsed.version || "");
' "${name}" "${npm_root}"
}
is_installed() {
local name="$1"
local want="${2:-}"
local have
have="$(installed_version "${name}" 2>/dev/null || true)"
[ -n "${have}" ] || return 1
[ -z "${want}" ] || [ "${have}" = "${want}" ]
}
install_package() {
local spec="$1"
local name want
name="$(package_name "${spec}")"
want="$(desired_version "${spec}")"
if is_installed "${name}" "${want}"; then
echo "changed=0 action=install package=${spec}"
return
fi
npm install -g --force "${spec}"
echo "changed=1 action=install package=${spec}"
}
reinstall_package() {
local spec="$1"
npm install -g --force "${spec}"
echo "changed=1 action=reinstall package=${spec}"
}
upgrade_package() {
local spec="$1"
npm install -g --force "${spec}"
echo "changed=1 action=upgrade package=${spec}"
}
backup_package() {
echo "changed=0 action=backup package=${1} status=reserved"
}
restore_package() {
echo "changed=0 action=restore package=${1} status=reserved"
}
migrate_package() {
echo "changed=0 action=migrate package=${1} status=reserved"
}
case "${action}" in
install) install_package "${package_spec}" ;;
reinstall) reinstall_package "${package_spec}" ;;
upgrade) upgrade_package "${package_spec}" ;;
backup) backup_package "${package_spec}" ;;
restore) restore_package "${package_spec}" ;;
migrate) migrate_package "${package_spec}" ;;
*)
echo "Unsupported npm package action: ${action}" >&2
exit 2
;;
esac

View File

@ -1,111 +0,0 @@
---
- name: Resolve existing Chromium executable
ansible.builtin.shell: |
set -eu
for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \
chromium-browser \
google-chrome \
google-chrome-stable \
/usr/bin/chromium \
/snap/bin/chromium; do
resolved=""
if command -v "$candidate" >/dev/null 2>&1; then
resolved="$(command -v "$candidate")"
elif [ -x "$candidate" ]; then
resolved="$candidate"
fi
# 必须真正可执行:跳过 xfce 安装的 disabled chromium stub退出码 126
# 否则 resolver 会选中它,后续 --version 校验必失败。
if [ -n "$resolved" ] && "$resolved" --version >/dev/null 2>&1; then
printf '%s\n' "$resolved"
exit 0
fi
done
exit 1
args:
executable: /bin/sh
register: ai_agent_runtime_browser_resolve
changed_when: false
failed_when: false
check_mode: false
- name: Install AI runtime browser packages when no browser exists
ansible.builtin.apt:
name: "{{ ai_agent_runtime_browser_packages }}"
state: present
update_cache: true
install_recommends: false
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when:
- ai_agent_runtime_browser_resolve.rc != 0
- ansible_os_family != 'Darwin'
- name: Resolve Chromium executable
ansible.builtin.shell: |
set -eu
for candidate in \
"{{ ai_agent_runtime_browser_executable }}" \
/usr/local/bin/chromium \
/usr/local/bin/chromium-browser \
/usr/bin/google-chrome \
/usr/bin/google-chrome-stable \
chromium \
chromium-browser \
google-chrome-stable \
/usr/bin/chromium \
/snap/bin/chromium \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; do
resolved=""
if command -v "$candidate" >/dev/null 2>&1; then
resolved="$(command -v "$candidate")"
elif [ -x "$candidate" ]; then
resolved="$candidate"
fi
# 必须真正可执行:跳过 xfce 安装的 disabled chromium stub退出码 126
# 否则 resolver 会选中它,后续 --version 校验必失败。
if [ -n "$resolved" ] && "$resolved" --version >/dev/null 2>&1; then
printf '%s\n' "$resolved"
exit 0
fi
done
exit 1
args:
executable: /bin/sh
register: ai_agent_runtime_browser_resolve
changed_when: false
check_mode: false
- name: Set resolved Chromium executable
ansible.builtin.set_fact:
ai_agent_runtime_browser_resolved_executable: "{{ ai_agent_runtime_browser_resolve.stdout }}"
- name: Ensure AI workspace env directory exists on macOS
ansible.builtin.file:
path: "{{ xworkspace_console_home | default(ansible_env.HOME) }}/.local/state/ai-workspace/env"
state: directory
mode: "0755"
when: ansible_os_family == 'Darwin'
- name: Configure Playwright runtime environment
ansible.builtin.copy:
dest: "{{ '/etc/profile.d' if ansible_os_family != 'Darwin' else (xworkspace_console_home | default(ansible_env.HOME)) + '/.local/state/ai-workspace/env' }}/ai_agent_runtime_playwright.sh"
owner: "{{ 'root' if ansible_os_family != 'Darwin' else (xworkspace_console_user | default(ansible_env.USER)) }}"
group: "{{ 'root' if ansible_os_family != 'Darwin' else ('staff' if ansible_os_family == 'Darwin' else (xworkspace_console_user | default(ansible_env.USER))) }}"
mode: "0644"
content: |
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD={{ '1' if ai_agent_runtime_playwright_skip_browser_download | bool else '0' }}
export PLAYWRIGHT_BROWSERS_PATH=0
export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH={{ ai_agent_runtime_browser_resolved_executable }}
become: "{{ ansible_os_family != 'Darwin' }}"
when: ai_agent_runtime_playwright_enabled | bool

View File

@ -1,14 +0,0 @@
---
- name: Install AI runtime document packages
ansible.builtin.apt:
name: "{{ ai_agent_runtime_doc_packages }}"
state: present
update_cache: true
install_recommends: false
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when: ansible_os_family != 'Darwin'

View File

@ -1,14 +0,0 @@
---
- name: Install AI runtime font packages
ansible.builtin.apt:
name: "{{ ai_agent_runtime_font_packages }}"
state: present
update_cache: true
install_recommends: false
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when: ansible_os_family != 'Darwin'

View File

@ -1,52 +0,0 @@
---
- name: Assert AI agent runtime is supported on Debian or Darwin family
ansible.builtin.assert:
that:
- ansible_facts.os_family in ["Debian", "Darwin", "Windows"]
fail_msg: "roles/ai_agent_runtime currently supports Debian-based, Darwin, and Windows hosts only."
- name: Install AI agent runtime base packages
ansible.builtin.apt:
name: "{{ ai_agent_runtime_base_packages }}"
state: present
update_cache: true
install_recommends: false
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
environment:
DEBIAN_FRONTEND: noninteractive
APT_LISTCHANGES_FRONTEND: none
become: true
when: ansible_os_family == 'Debian'
- name: Configure Node.js runtime
ansible.builtin.include_tasks: nodejs.yml
when: ai_agent_runtime_nodejs_enabled | bool
- name: Configure Python runtime
ansible.builtin.include_tasks: python.yml
when: ai_agent_runtime_python_enabled | bool
- name: Configure browser runtime
ansible.builtin.include_tasks: browser.yml
when: ai_agent_runtime_browser_enabled | bool
- name: Configure document runtime
ansible.builtin.include_tasks: docs.yml
when: ai_agent_runtime_docs_enabled | bool
- name: Configure font runtime
ansible.builtin.include_tasks: fonts.yml
when: ai_agent_runtime_fonts_enabled | bool
- name: Configure shared agent skills
ansible.builtin.include_role:
name: "{{ ai_agent_runtime_skills_role_name }}"
apply:
tags: agent_skills
when: ai_agent_runtime_skills_enabled | bool
tags: [agent_skills]
- name: Verify AI agent runtime
ansible.builtin.include_tasks: verify.yml
when: ai_agent_runtime_verify_enabled | bool

View File

@ -1,49 +0,0 @@
---
- name: Install Node.js role
ansible.builtin.include_role:
name: roles/vhosts/nodejs
vars:
nodejs_version: "{{ ai_agent_runtime_nodejs_version }}"
install_yarn: "{{ ai_agent_runtime_install_yarn }}"
yarn_version: "{{ ai_agent_runtime_yarn_version }}"
- name: Ensure user local bin directory exists on macOS
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/.local/bin"
state: directory
mode: "0755"
when: ansible_os_family == 'Darwin'
- name: Install npm global package manager helper
ansible.builtin.copy:
src: manage_npm_global_package.sh
dest: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package"
owner: "{{ 'root' if ansible_os_family != 'Darwin' else xworkspace_console_user }}"
group: "{{ 'root' if ansible_os_family != 'Darwin' else ('staff' if ansible_os_family == 'Darwin' else xworkspace_console_user) }}"
mode: "0755"
become: "{{ ansible_os_family != 'Darwin' }}"
when: ansible_os_family != 'Windows'
- name: Install global npm packages for AI runtime
ansible.builtin.command:
cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} {{ item }}"
loop: "{{ ai_agent_runtime_npm_global_packages }}"
register: ai_agent_runtime_npm_global_install
changed_when: "'changed=1' in ai_agent_runtime_npm_global_install.stdout"
when:
- ai_agent_runtime_npm_global_packages | length > 0
- ansible_os_family != 'Windows'
- name: Install pinned Playwright package for AI runtime
ansible.builtin.command:
cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} playwright@{{ ai_agent_runtime_playwright_version }}"
register: ai_agent_runtime_playwright_install
changed_when: "'changed=1' in ai_agent_runtime_playwright_install.stdout"
when:
- ai_agent_runtime_playwright_enabled | bool
- ai_agent_runtime_playwright_version | length > 0
- ansible_os_family != 'Windows'
- name: Include Windows specific Node.js package tasks
ansible.builtin.include_tasks: windows.yml
when: ansible_os_family == 'Windows'

View File

@ -1,7 +0,0 @@
---
- name: Install Python 3 role
ansible.builtin.include_role:
name: roles/vhosts/python3
vars:
python3_packages: "{{ ai_agent_runtime_python_packages }}"
python3_install_recommends: false

View File

@ -1,98 +0,0 @@
---
- name: Check node version
ansible.builtin.command: node --version
register: ai_agent_runtime_node_version
changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool
- name: Check npm version
ansible.builtin.command: npm --version
register: ai_agent_runtime_npm_version
changed_when: false
check_mode: false
when: ai_agent_runtime_nodejs_enabled | bool
- name: Check python version
ansible.builtin.command: python3 --version
register: ai_agent_runtime_python_version
changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool
- name: Check pip version
ansible.builtin.command: pip3 --version
register: ai_agent_runtime_pip_version
changed_when: false
check_mode: false
when: ai_agent_runtime_python_enabled | bool
- name: Check chromium version
ansible.builtin.command:
argv:
- "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }}"
- "--version"
register: ai_agent_runtime_chromium_version
changed_when: false
check_mode: false
when: ai_agent_runtime_browser_enabled | bool
- name: Check pandoc version
ansible.builtin.command: pandoc --version
register: ai_agent_runtime_pandoc_version
changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool
- name: Check xelatex version
ansible.builtin.command: xelatex --version
register: ai_agent_runtime_xelatex_version
changed_when: false
check_mode: false
when: ai_agent_runtime_docs_enabled | bool
- name: Check Chinese font inventory
ansible.builtin.command: fc-list :lang=zh family
register: ai_agent_runtime_chinese_fonts
changed_when: false
check_mode: false
when:
- ai_agent_runtime_fonts_enabled | bool
- ai_agent_runtime_verify_chinese_fonts | bool
- name: Assert Chinese fonts are available
ansible.builtin.assert:
that:
- ai_agent_runtime_chinese_fonts.stdout | length > 0
fail_msg: "No Chinese fonts were discovered by fontconfig."
when:
- ai_agent_runtime_fonts_enabled | bool
- ai_agent_runtime_verify_chinese_fonts | bool
- name: Check agent CLI versions
ansible.builtin.command: "{{ item }} --version"
register: ai_agent_runtime_agent_cli_versions
changed_when: false
failed_when: false
check_mode: false
loop: "{{ ai_agent_runtime_agent_cli_commands }}"
when:
- ai_agent_runtime_nodejs_enabled | bool
- ai_agent_runtime_agent_cli_commands | length > 0
- name: Report AI runtime versions
ansible.builtin.debug:
msg:
node: "{{ ai_agent_runtime_node_version.stdout | default('disabled') }}"
npm: "{{ ai_agent_runtime_npm_version.stdout | default('disabled') }}"
python3: "{{ ai_agent_runtime_python_version.stdout | default('disabled') }}"
pip3: "{{ ai_agent_runtime_pip_version.stdout | default('disabled') }}"
chromium: "{{ ai_agent_runtime_chromium_version.stdout | default('disabled') }}"
pandoc: "{{ (ai_agent_runtime_pandoc_version.stdout_lines | default(['disabled']))[0] }}"
xelatex: "{{ (ai_agent_runtime_xelatex_version.stdout_lines | default(['disabled']))[0] }}"
chinese_font_count: "{{ (ai_agent_runtime_chinese_fonts.stdout_lines | default([])) | length }}"
agent_cli: >-
{{
ai_agent_runtime_agent_cli_versions.results | default([])
| items2dict(key_name='item', value_name='stdout')
}}

View File

@ -1,17 +0,0 @@
---
- name: Install global npm packages for AI runtime on Windows
community.windows.win_command:
cmd: "npm install -g {{ item }}"
loop: "{{ ai_agent_runtime_npm_global_packages }}"
register: ai_agent_runtime_npm_global_install_win
changed_when: "'added' in ai_agent_runtime_npm_global_install_win.stdout or 'updated' in ai_agent_runtime_npm_global_install_win.stdout"
when: ai_agent_runtime_npm_global_packages | length > 0
- name: Install pinned Playwright package for AI runtime on Windows
community.windows.win_command:
cmd: "npm install -g playwright@{{ ai_agent_runtime_playwright_version }}"
register: ai_agent_runtime_playwright_install_win
changed_when: "'added' in ai_agent_runtime_playwright_install_win.stdout or 'updated' in ai_agent_runtime_playwright_install_win.stdout"
when:
- ai_agent_runtime_playwright_enabled | bool
- ai_agent_runtime_playwright_version | length > 0

View File

@ -1,232 +0,0 @@
- name: Preview Azure create request
ansible.builtin.debug:
msg:
- "azure_resource_group={{ azure_resource_group }}"
- "azure_vm_name={{ azure_vm_name }}"
- "azure_computer_name={{ azure_computer_name }}"
- "azure_location={{ azure_location }}"
- "allowed_cidrs={{ allowed_cidrs | join(',') }}"
- "allowed_tcp_ports={{ allowed_tcp_ports | join(',') }}"
- "state_file={{ cloud_vm_state_file }}"
- name: Ensure Azure resource group exists
ansible.builtin.command:
argv:
- az
- group
- create
- --subscription
- "{{ azure_subscription_id }}"
- --name
- "{{ azure_resource_group }}"
- --location
- "{{ azure_location }}"
- --tags
- "{{ tags | dict2items | map('join', '=') | join(' ') }}"
changed_when: true
when: not ansible_check_mode
- name: Ensure Azure virtual network exists
ansible.builtin.command:
argv:
- az
- network
- vnet
- create
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_virtual_network }}"
- --address-prefixes
- "{{ azure_vnet_cidr | default('10.42.0.0/16') }}"
- --subnet-name
- "{{ azure_subnet }}"
- --subnet-prefixes
- "{{ azure_subnet_cidr | default('10.42.1.0/24') }}"
changed_when: true
when: not ansible_check_mode
- name: Ensure Azure network security group exists
ansible.builtin.command:
argv:
- az
- network
- nsg
- create
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_network_security_group }}"
- --location
- "{{ azure_location }}"
changed_when: true
when: not ansible_check_mode
- name: Create Azure allowlist rules
ansible.builtin.command:
argv:
- az
- network
- nsg
- rule
- create
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --nsg-name
- "{{ azure_network_security_group }}"
- --name
- "allow-tcp-{{ port }}-{{ '%03d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
- --priority
- "{{ '%d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
- --access
- Allow
- --protocol
- Tcp
- --direction
- Inbound
- --source-address-prefixes
- "{{ cidr }}"
- --source-port-ranges
- "*"
- --destination-port-ranges
- "{{ port | string }}"
loop: "{{ allowed_cidrs | product(allowed_tcp_ports) | list }}"
loop_control:
label: "{{ item.0 }} -> {{ item.1 }}"
index_var: combo_index
changed_when: true
when: not ansible_check_mode
vars:
cidr: "{{ item.0 }}"
port: "{{ item.1 }}"
index: "{{ combo_index % (allowed_cidrs | length) }}"
port_index: "{{ combo_index // (allowed_cidrs | length) }}"
- name: Build Azure VM create command
ansible.builtin.set_fact:
azure_vm_create_command: >-
az vm create
--subscription {{ azure_subscription_id | quote }}
--resource-group {{ azure_resource_group | quote }}
--name {{ azure_vm_name | quote }}
--computer-name {{ azure_computer_name | quote }}
--image {{ (azure_image_publisher ~ ':' ~ azure_image_offer ~ ':' ~ azure_image_sku ~ ':' ~ azure_image_version) | quote }}
--size {{ vm_size | quote }}
--admin-username {{ admin_username | quote }}
--vnet-name {{ azure_virtual_network | quote }}
--subnet {{ azure_subnet | quote }}
--nsg {{ azure_network_security_group | quote }}
--public-ip-sku Standard
--public-ip-address {{ azure_public_ip_name | quote }}
--storage-sku Premium_LRS
--os-disk-size-gb {{ disk_gb | int }}
--tags {{ tags | dict2items | map('join', '=') | join(' ') | quote }}
{% if os_family == 'windows' %}
--admin-password {{ azure_admin_password | quote }}
{% else %}
--ssh-key-values {{ ssh_public_key_path | quote }}
{% endif %}
- name: Create Azure VM
ansible.builtin.shell: "{{ azure_vm_create_command }}"
args:
executable: /bin/bash
changed_when: true
when: not ansible_check_mode
- name: Fetch Azure VM networking facts
ansible.builtin.command:
argv:
- az
- vm
- show
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_vm_name }}"
- --show-details
- --query
- "{publicIp:publicIps,privateIp:privateIps}"
- -o
- json
register: azure_vm_network_json
changed_when: false
when: not ansible_check_mode
- name: Set Azure VM connection facts
ansible.builtin.set_fact:
cloud_vm_public_ip: "{{ (azure_vm_network_json.stdout | from_json).publicIp | default('127.0.0.1') }}"
cloud_vm_private_ip: "{{ (azure_vm_network_json.stdout | from_json).privateIp | default('127.0.0.1') }}"
cloud_vm_admin_user: "{{ admin_username }}"
when: not ansible_check_mode
- name: Set Azure dry-run connection facts
ansible.builtin.set_fact:
cloud_vm_public_ip: "{{ cloud_vm_public_ip | default('198.51.100.10') }}"
cloud_vm_private_ip: "{{ cloud_vm_private_ip | default('10.42.1.10') }}"
cloud_vm_admin_user: "{{ admin_username }}"
when: ansible_check_mode
- name: Prepare Azure Windows VM for initial WinRM bootstrap
ansible.builtin.command:
argv:
- az
- vm
- run-command
- invoke
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_vm_name }}"
- --command-id
- RunPowerShellScript
- --scripts
- |
$ProgressPreference = 'SilentlyContinue'
$profiles = Get-NetConnectionProfile
foreach ($profile in $profiles) {
if ($profile.NetworkCategory -ne 'Private') {
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex -NetworkCategory Private
}
}
winrm quickconfig -quiet
Enable-PSRemoting -Force
Set-Service -Name WinRM -StartupType Automatic
Start-Service -Name WinRM
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true
Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true
netsh advfirewall firewall add rule name="Allow WinRM 5985" dir=in action=allow protocol=TCP localport=5985 | Out-Null
Get-Service -Name WinRM | Select-Object Status, StartType, Name
register: azure_windows_winrm_prep
changed_when: true
when:
- not ansible_check_mode
- os_family == "windows"
- name: Show Azure Windows WinRM prep output
ansible.builtin.debug:
var: azure_windows_winrm_prep.stdout
when:
- not ansible_check_mode
- os_family == "windows"
- name: Wait for Azure Windows WinRM endpoint
ansible.builtin.wait_for:
host: "{{ cloud_vm_public_ip }}"
port: 5985
delay: 10
timeout: 300
state: started
when:
- not ansible_check_mode
- os_family == "windows"

View File

@ -1,96 +0,0 @@
- name: Preview Azure destroy/cleanup request
ansible.builtin.debug:
msg:
- "azure_resource_group={{ azure_resource_group | default('n/a') }}"
- "azure_vm_name={{ azure_vm_name | default(profile_name | default('n/a')) }}"
- "azure_cleanup_mode={{ azure_cleanup_mode | default(false) }}"
- "cloud_vm_destroy_mode={{ cloud_vm_destroy_mode | default('destroy') }}"
- name: Build Azure cleanup query
ansible.builtin.set_fact:
azure_cleanup_query: "[?tags.toolkit_scope=='cloud-dev-desktop' && tags.managed_by=='ansible'].[resourceGroup,name]"
when: azure_cleanup_mode | default(false)
- name: List Azure expired VMs
ansible.builtin.command:
argv:
- az
- vm
- list
- --subscription
- "{{ azure_subscription_id }}"
- --show-details
- --query
- "{{ azure_cleanup_query }}"
- -o
- json
register: azure_expired_vm_list
changed_when: false
when:
- azure_cleanup_mode | default(false)
- not ansible_check_mode
- name: Park Azure VM in lowest-consumption mode
ansible.builtin.command:
argv:
- az
- vm
- deallocate
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_vm_name }}"
changed_when: true
when:
- not azure_cleanup_mode | default(false)
- (cloud_vm_destroy_mode | default('destroy')) == 'park'
- not ansible_check_mode
- name: Delete Azure VM directly
ansible.builtin.command:
argv:
- az
- vm
- delete
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ azure_resource_group }}"
- --name
- "{{ azure_vm_name }}"
- --yes
changed_when: true
when:
- not azure_cleanup_mode | default(false)
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
- not ansible_check_mode
- name: Delete Azure expired VMs
ansible.builtin.command:
argv:
- az
- vm
- delete
- --subscription
- "{{ azure_subscription_id }}"
- --resource-group
- "{{ item[0] }}"
- --name
- "{{ item[1] }}"
- --yes
loop: "{{ (azure_expired_vm_list.stdout | default('[]')) | from_json }}"
changed_when: true
when:
- azure_cleanup_mode | default(false)
- not ansible_check_mode
- name: Remove Azure state file after destroy
ansible.builtin.file:
path: "{{ cloud_vm_state_file }}"
state: absent
when:
- cloud_vm_state_file is defined
- not azure_cleanup_mode | default(false)
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'

View File

@ -1,64 +0,0 @@
- name: Normalize Azure desktop resource names
ansible.builtin.set_fact:
azure_subscription_id: "{{ azure_subscription_id | default(lookup('env', 'AZURE_SUBSCRIPTION_ID')) }}"
azure_resource_group: "{{ azure_resource_group | default('rg-devdesktop-' ~ cloud_vm_owner_slug) }}"
azure_location: "{{ region | default(azure_location | default('japaneast')) }}"
azure_network_security_group: "{{ azure_network_security_group | default('nsg-' ~ cloud_vm_profile_slug) }}"
azure_virtual_network: "{{ azure_virtual_network | default('vnet-devdesktop-' ~ cloud_vm_owner_slug) }}"
azure_subnet: "{{ azure_subnet | default('snet-devdesktop') }}"
azure_public_ip_name: "{{ azure_public_ip_name | default('pip-' ~ cloud_vm_profile_slug) }}"
azure_nic_name: "{{ azure_nic_name | default('nic-' ~ cloud_vm_profile_slug) }}"
azure_vm_name: "{{ azure_vm_name | default('vm-' ~ cloud_vm_profile_slug) }}"
azure_computer_name: >-
{{
azure_computer_name
| default(
('vm' ~ (cloud_vm_profile_slug | regex_replace('[^A-Za-z0-9]', '')))[:15]
)
}}
- name: Assert Azure subscription is available when requested
ansible.builtin.assert:
that:
- azure_subscription_id | length > 0
fail_msg: "AZURE_SUBSCRIPTION_ID or azure_subscription_id is required for Azure operations."
when: not ansible_check_mode
- name: Select Azure image defaults by OS family
ansible.builtin.set_fact:
azure_image_publisher: >-
{{
azure_image_publisher
| default(
{
'windows': 'MicrosoftWindowsDesktop',
'fedora-gnome': 'Fedora',
'debian-kde': 'Debian'
}[os_family]
)
}}
azure_image_offer: >-
{{
image_offer
| default(
{
'windows': 'windows-11',
'fedora-gnome': 'fedora-x86_64',
'debian-kde': 'debian-13'
}[os_family]
)
}}
azure_image_sku: >-
{{
image_sku
| default(
{
'windows': 'win11-24h2-pro',
'fedora-gnome': '43-gen2',
'debian-kde': '13-gen2'
}[os_family]
)
}}
azure_image_version: "{{ image_version | default('latest') }}"
azure_admin_password: "{{ azure_admin_password | default(lookup('env', 'AZURE_WINDOWS_ADMIN_PASSWORD')) }}"
cloud_vm_platform: "azure"

View File

@ -1,16 +0,0 @@
- name: Gather Azure lifecycle facts
ansible.builtin.include_tasks: facts.yml
- name: Run Azure create flow
ansible.builtin.include_tasks: create.yml
when: cloud_lifecycle_action == "create"
- name: Run Azure destroy flow
ansible.builtin.include_tasks: destroy.yml
when: cloud_lifecycle_action == "destroy"
- name: Run Azure cleanup flow
ansible.builtin.include_tasks: destroy.yml
vars:
azure_cleanup_mode: true
when: cloud_lifecycle_action == "cleanup"

View File

@ -1,15 +0,0 @@
---
gpu_operator_namespace: "gpu-operator"
gpu_operator_release_name: "gpu-operator"
gpu_operator_chart_version: "v24.3.0"
# Air-gapped / Private registry support
gpu_operator_repository: "https://helm.ngc.nvidia.com/nvidia"
image_pull_secrets: []
# Operator settings
driver_enabled: true
driver_version: "535.129.03"
toolkit_enabled: true
mig_strategy: "single" # none, single, mixed
dcgm_exporter_enabled: true

View File

@ -1,28 +0,0 @@
---
- name: Create GPU Operator namespace
kubernetes.core.k8s:
api_version: v1
kind: Namespace
name: "{{ gpu_operator_namespace }}"
state: present
when: inventory_hostname == groups['masters'][0]
- name: Add NVIDIA helm repo
kubernetes.core.helm_repository:
name: nvidia
repo_url: "{{ gpu_operator_repository }}"
when: inventory_hostname == groups['masters'][0]
- name: Deploy GPU Operator
kubernetes.core.helm:
name: "{{ gpu_operator_release_name }}"
chart_ref: nvidia/gpu-operator
release_namespace: "{{ gpu_operator_namespace }}"
version: "{{ gpu_operator_chart_version }}"
values: "{{ lookup('template', 'values.yaml.j2') | from_yaml }}"
wait: true
when: inventory_hostname == groups['masters'][0]
- name: Include validation tasks
include_tasks: validate.yml
when: inventory_hostname == groups['masters'][0]

View File

@ -1,15 +0,0 @@
---
- name: Wait for NVIDIA Device Plugin daemonset to be ready
shell: |
kubectl rollout status daemonset/nvidia-device-plugin-daemonset -n {{ gpu_operator_namespace }} --timeout=300s
register: ds_status
changed_when: false
- name: Validate GPU resources are allocatable
shell: |
kubectl get nodes -l nvidia.com/gpu.present=true -o jsonpath='{.items[*].status.allocatable}'
register: gpu_allocatable
until: "'nvidia.com/gpu' in gpu_allocatable.stdout"
retries: 30
delay: 20
changed_when: false

View File

@ -1,15 +0,0 @@
driver:
enabled: {{ driver_enabled }}
version: "{{ driver_version }}"
toolkit:
enabled: {{ toolkit_enabled }}
devicePlugin:
enabled: true
mig:
strategy: "{{ mig_strategy }}"
dcgmExporter:
enabled: {{ dcgm_exporter_enabled }}
{% if image_pull_secrets | length > 0 %}
imagePullSecrets:
{{ image_pull_secrets | to_nice_yaml(indent=2) | indent(2, true) }}
{% endif %}

View File

@ -1,36 +0,0 @@
---
ray_namespace: "ray-system"
ray_cluster_name: "ray-cluster"
ray_image: "rayproject/ray:2.9.0"
ray_version: "2.9.0"
ray_dashboard_enabled: true
ray_head_resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi"
ray_worker_groups:
- groupName: gpu-workers
replicas: 2
minReplicas: 1
maxReplicas: 4
resources:
requests:
cpu: "4"
memory: "32Gi"
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: "64Gi"
nvidia.com/gpu: "1"
nodeSelector:
accelerator: "nvidia-h100"
tolerations: []
volumeMounts:
- mountPath: /dev/shm
name: dshm

View File

@ -1 +0,0 @@
---

View File

@ -1,24 +0,0 @@
---
- name: Create Ray namespace
kubernetes.core.k8s:
name: "{{ ray_namespace }}"
api_version: v1
kind: Namespace
state: present
when: inventory_hostname == groups['masters'][0]
- name: Apply RayCluster CRD
kubernetes.core.k8s:
state: present
definition: "{{ lookup('template', 'raycluster.yaml.j2') | from_yaml }}"
when: inventory_hostname == groups['masters'][0]
- name: Wait for Ray head node to be ready
shell: |
kubectl get pod -n {{ ray_namespace }} -l ray.io/node-type=head -o jsonpath='{.items[0].status.phase}'
register: head_status
until: head_status.stdout == "Running"
retries: 30
delay: 10
changed_when: false
when: inventory_hostname == groups['masters'][0]

View File

@ -1,53 +0,0 @@
apiVersion: ray.io/v1
kind: RayCluster
metadata:
name: {{ ray_cluster_name }}
namespace: {{ ray_namespace }}
spec:
rayVersion: '{{ ray_version }}'
headGroupSpec:
rayStartParams:
dashboard-host: '0.0.0.0'
{% if not ray_dashboard_enabled %}
dashboard-enabled: 'false'
{% endif %}
template:
spec:
containers:
- name: ray-head
image: {{ ray_image }}
resources:
{{ ray_head_resources | to_nice_yaml(indent=4) | indent(12, true) }}
workerGroupSpecs:
{% for group in ray_worker_groups %}
- groupName: {{ group.groupName }}
replicas: {{ group.replicas }}
minReplicas: {{ group.minReplicas }}
maxReplicas: {{ group.maxReplicas }}
rayStartParams: {}
template:
spec:
{% if group.nodeSelector is defined %}
nodeSelector:
{{ group.nodeSelector | to_nice_yaml(indent=2) | indent(10, true) }}
{% endif %}
{% if group.tolerations is defined and group.tolerations | length > 0 %}
tolerations:
{{ group.tolerations | to_nice_yaml(indent=2) | indent(10, true) }}
{% endif %}
containers:
- name: ray-worker
image: {{ ray_image }}
resources:
{{ group.resources | to_nice_yaml(indent=4) | indent(12, true) }}
{% if group.volumeMounts is defined and group.volumeMounts | length > 0 %}
volumeMounts:
{{ group.volumeMounts | to_nice_yaml(indent=2) | indent(10, true) }}
{% endif %}
{% if group.volumeMounts is defined and group.volumeMounts | selectattr('name', 'equalto', 'dshm') | list | length > 0 %}
volumes:
- name: dshm
emptyDir:
medium: Memory
{% endif %}
{% endfor %}

View File

@ -1 +0,0 @@
---

View File

@ -1 +0,0 @@
---

View File

@ -1,31 +0,0 @@
---
vllm_namespace: "vllm-system"
vllm_service_name: "vllm-api"
vllm_image: "vllm/vllm-openai:v0.4.2"
vllm_model: "/models/Llama-3-70B-Instruct"
vllm_tensor_parallel_size: 2
vllm_pipeline_parallel_size: 1
vllm_gpu_memory_utilization: 0.90
vllm_max_model_len: 4096
vllm_max_num_seqs: 256
vllm_port: 8000
vllm_service_type: "ClusterIP"
vllm_ingress_enabled: false
vllm_ingress_host: "vllm.example.com"
# Ray Integration
ray_address: "ray://ray-cluster-head-svc.ray-system.svc.cluster.local:10001"
# Environment Variables
nccl_socket_ifname: "eth0"
gloo_socket_ifname: "eth0"
nccl_ib_disable: "1"
vllm_logging_level: "INFO"
torch_distributed_init_timeout: "300"
huggingface_token: ""
# Model Mount
model_host_path: "/data/models"
model_mount_path: "/models"

View File

@ -1 +0,0 @@
---

View File

@ -1,36 +0,0 @@
---
- name: Create vLLM namespace
kubernetes.core.k8s:
name: "{{ vllm_namespace }}"
api_version: v1
kind: Namespace
state: present
when: inventory_hostname == groups['masters'][0]
- name: Deploy vLLM Deployment
kubernetes.core.k8s:
state: present
definition: "{{ lookup('template', 'deployment.yaml.j2') | from_yaml }}"
when: inventory_hostname == groups['masters'][0]
- name: Deploy vLLM Service
kubernetes.core.k8s:
state: present
definition: "{{ lookup('template', 'service.yaml.j2') | from_yaml }}"
when: inventory_hostname == groups['masters'][0]
- name: Deploy vLLM Ingress
kubernetes.core.k8s:
state: present
definition: "{{ lookup('template', 'ingress.yaml.j2') | from_yaml }}"
when: inventory_hostname == groups['masters'][0] and vllm_ingress_enabled
- name: Wait for vLLM API to be ready
shell: |
kubectl get deploy {{ vllm_service_name }} -n {{ vllm_namespace }} -o jsonpath='{.status.readyReplicas}'
register: vllm_ready
until: vllm_ready.stdout == "1"
retries: 40
delay: 15
changed_when: false
when: inventory_hostname == groups['masters'][0]

Some files were not shown because too many files have changed in this diff Show More