@passioncode-ai/fabric-agent-adapter 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +41 -5
- package/SECURITY.md +14 -0
- package/bin/fabric-agent-adapter.js +87 -43
- package/package.json +10 -5
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 - 2026-09-29
|
|
4
|
+
|
|
5
|
+
- Kit: `LoopbackHTTPServer`, a `ThreadingHTTPServer` whose bind asks no resolver.
|
|
6
|
+
`http.server` calls `socket.getfqdn()` between `bind()` and `listen()`; on a macOS
|
|
7
|
+
CI runner that held a service's port bound but silent for over 20 seconds. The
|
|
8
|
+
sample service and the skill's startup order use it, and the skill names the trap.
|
|
9
|
+
- Kit: a garbled pid file (for example a superscript digit) now reads as "no pid"
|
|
10
|
+
instead of raising `ValueError` while naming the holder of a lock.
|
|
11
|
+
|
|
12
|
+
## 0.4.1 - 2026-09-29
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `building-fabric-services` could not be loaded by a strict YAML reader: its
|
|
17
|
+
front-matter `description` was an unquoted plain scalar holding `: `
|
|
18
|
+
("fabric-service/0.1 extension: which surface", line 3, column 493), which YAML
|
|
19
|
+
rejects with "mapping values are not allowed here". Claude Code read it
|
|
20
|
+
leniently, so the plugin worked, but an agent reading the skill from
|
|
21
|
+
`~/.agents/skills` could drop it. The description is now a folded block
|
|
22
|
+
scalar (`>-`) with the exact same text (927 characters).
|
|
23
|
+
- The validator could not see that class of defect: `test/validate.py` now
|
|
24
|
+
parses every SKILL.md front matter strictly (standard library only) and
|
|
25
|
+
rejects what a YAML reader rejects in an unquoted value — `: ` or a trailing
|
|
26
|
+
`:`, ` #` (a comment that silently truncates the value), a leading indicator
|
|
27
|
+
character, a duplicate key, an unclosed quote, an undeclared multi-line
|
|
28
|
+
value — with the line and column. `test/test_validate.py` plants each case.
|
|
29
|
+
`validate.py --frontmatter <SKILL.md ...>` checks installed copies anywhere.
|
|
30
|
+
- The release smoke test looked for `~/.claude/skills`, where the installer has
|
|
31
|
+
not written since 0.4.0, so it would fail on the first release that ran it.
|
|
32
|
+
It now checks all three skills in `~/.agents/skills`, that `~/.claude/skills`
|
|
33
|
+
stays untouched, and that the installed front matter parses strictly.
|
|
34
|
+
- `SKILL-CARD.md` versions and the release smoke paths joined the validator's
|
|
35
|
+
sync checks.
|
|
36
|
+
|
|
37
|
+
## 0.4.0 - 2026-09-28
|
|
38
|
+
|
|
39
|
+
- Add the `building-fabric-services` skill for the `fabric-service/0.1` local
|
|
40
|
+
service extension (Fabric Agent Contract DEC-0015): surface choice (MCP
|
|
41
|
+
streamable HTTP, CLI, A2A), token and one-time login, state and cache
|
|
42
|
+
directories, one copy per machine, launchd, descriptor, well-known document,
|
|
43
|
+
events feed and notifications.
|
|
44
|
+
- Ship reference kits in Python (`fabric_service.py`) and Node
|
|
45
|
+
(`fabric-service.mjs`) whose instance locks exclude each other, a complete
|
|
46
|
+
`sample_service.py`, and `check_service.py`, a live conformance probe that
|
|
47
|
+
reports PASS, FAIL or NOT_RUN per rule.
|
|
48
|
+
- `creating-fabric-agents` now routes an agent that runs as a service through
|
|
49
|
+
the new skill.
|
|
50
|
+
- Fix the installer shadowing its own plugin: `npx @passioncode-ai/fabric-agent-adapter`
|
|
51
|
+
and `install.sh` now install into the agents hub `~/.agents/skills`; a plain
|
|
52
|
+
copy into `~/.claude/skills` is refused while the plugin is installed, and
|
|
53
|
+
`--prune-shadow` moves existing shadowing copies aside.
|
|
54
|
+
|
|
3
55
|
## 0.3.1 - 2026-08-27
|
|
4
56
|
|
|
5
57
|
- Fix the executable bit on `bin/fabric-agent-adapter.js` and `install.sh`: the
|
package/README.md
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://passioncode.ai/">
|
|
3
|
+
<img src="assets/passioncode-icon-256.png" width="104" height="104" alt="PassionCode.ai passion fruit mark">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
1
7
|
# Fabric Agent Adapter
|
|
2
8
|
|
|
9
|
+
> **PassionCode.ai — The agent-agnostic operating system for AI-native teams.**
|
|
10
|
+
|
|
11
|
+
**From vibe coding to passion coding.** PassionCode.ai moves the control point from
|
|
12
|
+
managing agents one by one to operating Projects. Fabric Agent Adapter is the portable
|
|
13
|
+
on-ramp: it helps an existing repository become a compatible, replaceable Provider
|
|
14
|
+
without moving the Project onto one proprietary agent runtime.
|
|
15
|
+
|
|
3
16
|
[](https://www.npmjs.com/package/@passioncode-ai/fabric-agent-adapter)
|
|
4
17
|
[](https://github.com/passioncode-ai/fabric-agent-adapter/actions/workflows/validate.yml)
|
|
5
18
|
[](LICENSE)
|
|
6
19
|
|
|
7
|
-
Private, portable Agent
|
|
8
|
-
[Fabric Agent Contract](https://github.com/passioncode-ai/fabric-agent-contract)
|
|
20
|
+
Private, portable Agent Skills for the
|
|
21
|
+
[Fabric Agent Contract](https://github.com/passioncode-ai/fabric-agent-contract):
|
|
22
|
+
`adapting-projects-to-fabric`, `creating-fabric-agents` and
|
|
23
|
+
`building-fabric-services`.
|
|
24
|
+
|
|
25
|
+
`building-fabric-services` makes an agent a long-lived local service with a dashboard
|
|
26
|
+
that is always alive, runs as one copy, keeps its state through a reinstall and appears
|
|
27
|
+
in Fabric Dashboards by itself — the `fabric-service/0.1` extension. It ships Python and
|
|
28
|
+
Node reference kits (`scripts/fabric_service.py`, `scripts/fabric-service.mjs`), a
|
|
29
|
+
complete `scripts/sample_service.py`, and `scripts/check_service.py`, a live probe:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
|
|
33
|
+
```
|
|
9
34
|
|
|
10
|
-
|
|
35
|
+
For provider bundles the adapting skill helps an agent author:
|
|
11
36
|
|
|
12
37
|
- inspect the project's stable integration surfaces without executing it;
|
|
13
38
|
- choose A2A `1.0`, MCP `2026-07-28`, or `fabric-local-runner/0.1` per capability;
|
|
@@ -26,7 +51,8 @@ The repository is private; the npm package is public. The npm path needs no GitH
|
|
|
26
51
|
npm — no GitHub access required:
|
|
27
52
|
|
|
28
53
|
```bash
|
|
29
|
-
npx @passioncode-ai/fabric-agent-adapter # installs
|
|
54
|
+
npx @passioncode-ai/fabric-agent-adapter # installs every skill into the agents hub ~/.agents/skills
|
|
55
|
+
npx @passioncode-ai/fabric-agent-adapter --prune-shadow # remove ~/.claude/skills copies that shadow the plugin
|
|
30
56
|
```
|
|
31
57
|
|
|
32
58
|
Generic Agent Skills clients:
|
|
@@ -34,6 +60,7 @@ Generic Agent Skills clients:
|
|
|
34
60
|
```bash
|
|
35
61
|
npx skills add passioncode-ai/fabric-agent-adapter --skill adapting-projects-to-fabric
|
|
36
62
|
npx skills add passioncode-ai/fabric-agent-adapter --skill creating-fabric-agents
|
|
63
|
+
npx skills add passioncode-ai/fabric-agent-adapter --skill building-fabric-services
|
|
37
64
|
```
|
|
38
65
|
|
|
39
66
|
Claude Code plugin marketplace:
|
|
@@ -52,6 +79,12 @@ Adapt this project for Fabric compatibility. Inspect it, choose the profile per
|
|
|
52
79
|
capability, scaffold the provider bundle, and report every conformance gate.
|
|
53
80
|
```
|
|
54
81
|
|
|
82
|
+
Or, for an agent that must keep running with a dashboard:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Make this agent a local service with an always-on dashboard, following fabric-service/0.1.
|
|
86
|
+
```
|
|
87
|
+
|
|
55
88
|
Or, for an agent that does not exist yet:
|
|
56
89
|
|
|
57
90
|
```text
|
|
@@ -86,6 +119,10 @@ claude plugin validate ./plugins/fabric-agent-adapter --strict
|
|
|
86
119
|
claude plugin validate . --strict
|
|
87
120
|
```
|
|
88
121
|
|
|
122
|
+
`test/validate.py` reads every SKILL.md front matter as strictly as a YAML parser does; an
|
|
123
|
+
unquoted value holding `: ` fails it. `python3 test/validate.py --frontmatter <SKILL.md ...>`
|
|
124
|
+
checks copies installed elsewhere, such as `~/.agents/skills/*/SKILL.md`.
|
|
125
|
+
|
|
89
126
|
## Contract pin
|
|
90
127
|
|
|
91
128
|
- version: `0.1.0`
|
|
@@ -93,4 +130,3 @@ claude plugin validate . --strict
|
|
|
93
130
|
|
|
94
131
|
The contract repository is normative. Updating the pin requires a new adapter release,
|
|
95
132
|
fixture review, and a complete validation run.
|
|
96
|
-
|
package/SECURITY.md
CHANGED
|
@@ -8,3 +8,17 @@ The adapter must not write secret values to manifests, fixtures, probes, logs, o
|
|
|
8
8
|
arguments. Generated and external provider outputs are untrusted. Live probes must be
|
|
9
9
|
bounded and non-publishing until explicit scoped grants exist in a project binding.
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
## What the installers and scripts touch
|
|
13
|
+
|
|
14
|
+
- `npx @passioncode-ai/fabric-agent-adapter` and `install.sh` write only
|
|
15
|
+
`~/.agents/skills/<skill>`; `--target claude` writes `~/.claude/skills/<skill>` and is
|
|
16
|
+
refused while the plugin is installed; `--prune-shadow` moves shadowing copies into
|
|
17
|
+
`~/.claude/skills-shadow-backup/`.
|
|
18
|
+
- `building-fabric-services/scripts/check_service.py` reads a descriptor and the token
|
|
19
|
+
file it names, talks only to `127.0.0.1:<port>`, and redeems one login code it asked
|
|
20
|
+
for itself (creating one dashboard session). It never prints the token.
|
|
21
|
+
- The service kits create the service token (0600) and the instance lock in the
|
|
22
|
+
service's own data directory, and write a launchd plist and a descriptor only when an
|
|
23
|
+
installer calls them. The token is readable by any process of the same user; the
|
|
24
|
+
protocol defends against web pages and mistakes, not against hostile local code.
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* fabric-agent-adapter installer CLI.
|
|
4
4
|
*
|
|
5
|
-
* Installs every skill this plugin ships into
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the repository is private; the npm package needs none)
|
|
5
|
+
* Installs every skill this plugin ships into the shared agents hub
|
|
6
|
+
* ~/.agents/skills/<name> (read by Codex, Cursor, Gemini, OpenCode and others).
|
|
7
|
+
* Claude Code gets the skills through the plugin, never through a plain copy:
|
|
8
|
+
* a copy in ~/.claude/skills shadows the plugin and serves its frozen version
|
|
9
|
+
* forever. Idempotent; an existing install is skipped unless --force.
|
|
10
|
+
* Zero dependencies.
|
|
12
11
|
*/
|
|
13
12
|
'use strict';
|
|
14
13
|
|
|
@@ -18,20 +17,25 @@ const os = require('os');
|
|
|
18
17
|
|
|
19
18
|
const ROOT = path.resolve(__dirname, '..');
|
|
20
19
|
const REPO = 'passioncode-ai/fabric-agent-adapter';
|
|
20
|
+
const PLUGIN_ID = 'fabric-agent-adapter@fabric-agent-adapter';
|
|
21
21
|
|
|
22
22
|
function usage() {
|
|
23
23
|
console.log(`fabric-agent-adapter installer
|
|
24
24
|
|
|
25
25
|
Usage:
|
|
26
26
|
npx @passioncode-ai/fabric-agent-adapter [--force]
|
|
27
|
-
install every skill
|
|
28
|
-
|
|
27
|
+
install every skill into the agents hub ~/.agents/skills
|
|
28
|
+
npx @passioncode-ai/fabric-agent-adapter --target claude [--force]
|
|
29
|
+
plain copy into ~/.claude/skills — refused while the plugin is installed
|
|
30
|
+
npx @passioncode-ai/fabric-agent-adapter --prune-shadow
|
|
31
|
+
remove plain ~/.claude/skills copies that shadow the installed plugin
|
|
29
32
|
npx @passioncode-ai/fabric-agent-adapter --help
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
Claude Code (recommended):
|
|
35
|
+
claude plugin marketplace add ${REPO}
|
|
36
|
+
claude plugin install ${PLUGIN_ID}
|
|
37
|
+
Every PassionCode.ai skill for every agent at once:
|
|
38
|
+
npx passioncode@latest update`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function copyDir(src, dest) {
|
|
@@ -44,15 +48,38 @@ function copyDir(src, dest) {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
function pluginInstalled(home) {
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(path.join(home, '.claude/plugins/installed_plugins.json'), 'utf8'));
|
|
54
|
+
const records = (data.plugins || {})[PLUGIN_ID] || [];
|
|
55
|
+
return records.some((r) => r.installPath && fs.existsSync(path.join(r.installPath, 'skills')));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function skillNames() {
|
|
62
|
+
const skillRoot = path.join(ROOT, 'plugins/fabric-agent-adapter/skills');
|
|
63
|
+
if (!fs.existsSync(skillRoot)) return { skillRoot, names: [] };
|
|
64
|
+
// Iterate rather than name one skill: a skill added to the plugin must not
|
|
65
|
+
// require an installer change to reach anybody.
|
|
66
|
+
const names = fs.readdirSync(skillRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
67
|
+
return { skillRoot, names };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pruneShadows(home, names) {
|
|
71
|
+
let removed = 0;
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
const shadow = path.join(home, '.claude', 'skills', name);
|
|
74
|
+
let info;
|
|
75
|
+
try { info = fs.lstatSync(shadow); } catch { continue; }
|
|
76
|
+
const backup = path.join(home, '.claude', 'skills-shadow-backup', `${name}-${Date.now()}`);
|
|
77
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
78
|
+
fs.renameSync(shadow, backup);
|
|
79
|
+
console.log(`Removed shadowing ${info.isSymbolicLink() ? 'link' : 'copy'} ${shadow} (kept at ${backup})`);
|
|
80
|
+
removed += 1;
|
|
51
81
|
}
|
|
52
|
-
|
|
53
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
54
|
-
copyDir(src, dest);
|
|
55
|
-
console.log(`Installed ${label} -> ${dest}`);
|
|
82
|
+
if (!removed) console.log('No shadowing copies found.');
|
|
56
83
|
}
|
|
57
84
|
|
|
58
85
|
function main(argv) {
|
|
@@ -62,39 +89,56 @@ function main(argv) {
|
|
|
62
89
|
return 0;
|
|
63
90
|
}
|
|
64
91
|
const force = args.includes('--force');
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
const prune = args.includes('--prune-shadow');
|
|
93
|
+
let target = 'agents';
|
|
94
|
+
const known = new Set(['--force', '--prune-shadow']);
|
|
95
|
+
const unknown = [];
|
|
96
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
97
|
+
if (args[i] === '--target') {
|
|
98
|
+
target = args[i + 1];
|
|
99
|
+
i += 1;
|
|
100
|
+
} else if (!known.has(args[i])) unknown.push(args[i]);
|
|
101
|
+
}
|
|
102
|
+
if (unknown.length || !['agents', 'claude'].includes(target)) {
|
|
103
|
+
console.error(`unknown argument(s): ${unknown.concat(['agents', 'claude'].includes(target) ? [] : [`--target ${target}`]).join(' ')}`);
|
|
68
104
|
usage();
|
|
69
105
|
return 2;
|
|
70
106
|
}
|
|
71
107
|
|
|
72
|
-
const skillRoot =
|
|
73
|
-
if (!
|
|
74
|
-
console.error(`error:
|
|
108
|
+
const { skillRoot, names } = skillNames();
|
|
109
|
+
if (!names.length) {
|
|
110
|
+
console.error(`error: no skills found under ${skillRoot} — corrupted package?`);
|
|
75
111
|
return 1;
|
|
76
112
|
}
|
|
113
|
+
const home = os.homedir();
|
|
114
|
+
const hasPlugin = pluginInstalled(home);
|
|
77
115
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
if (prune) {
|
|
117
|
+
if (!hasPlugin) {
|
|
118
|
+
console.error(`error: the ${PLUGIN_ID} plugin is not installed, so a copy in ~/.claude/skills is not a shadow — nothing removed.`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
pruneShadows(home, names);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (target === 'claude' && hasPlugin) {
|
|
125
|
+
console.error(`error: the ${PLUGIN_ID} plugin is installed; a plain copy in ~/.claude/skills would shadow it. Update the plugin instead:\n claude plugin marketplace update fabric-agent-adapter && claude plugin update ${PLUGIN_ID}`);
|
|
87
126
|
return 1;
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
const
|
|
129
|
+
const destRoot = target === 'claude' ? path.join(home, '.claude', 'skills') : path.join(home, '.agents', 'skills');
|
|
91
130
|
for (const name of names) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
131
|
+
const dest = path.join(destRoot, name);
|
|
132
|
+
if (fs.existsSync(dest) && !force) {
|
|
133
|
+
console.log(`skip: ${name} already installed at ${dest} (rerun with --force to overwrite)`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
137
|
+
copyDir(path.join(skillRoot, name), dest);
|
|
138
|
+
console.log(`Installed ${name} -> ${dest}`);
|
|
139
|
+
}
|
|
140
|
+
if (target === 'agents' && !hasPlugin) {
|
|
141
|
+
console.log(`Claude Code reads plugins, not the hub:\n claude plugin marketplace add ${REPO} && claude plugin install ${PLUGIN_ID}`);
|
|
98
142
|
}
|
|
99
143
|
console.log('Restart your agent — skills load at session start.');
|
|
100
144
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Create
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q"
|
|
6
|
+
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test test/"
|
|
7
7
|
},
|
|
8
8
|
"bin": {
|
|
9
9
|
"fabric-agent-adapter": "bin/fabric-agent-adapter.js"
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"LICENSE",
|
|
19
19
|
"CHANGELOG.md",
|
|
20
|
-
"SECURITY.md"
|
|
20
|
+
"SECURITY.md",
|
|
21
|
+
"!**/__pycache__/**",
|
|
22
|
+
"!**/*.pyc"
|
|
21
23
|
],
|
|
22
24
|
"repository": "github:passioncode-ai/fabric-agent-adapter",
|
|
23
25
|
"homepage": "https://github.com/passioncode-ai/fabric-agent-adapter",
|
|
@@ -39,6 +41,9 @@
|
|
|
39
41
|
"provider",
|
|
40
42
|
"conformance",
|
|
41
43
|
"claude-code",
|
|
42
|
-
"skill"
|
|
44
|
+
"skill",
|
|
45
|
+
"local-service",
|
|
46
|
+
"dashboard",
|
|
47
|
+
"launchd"
|
|
43
48
|
]
|
|
44
49
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
|
-
"description": "Create new Fabric-compatible agents and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
6
|
-
"version": "0.
|
|
5
|
+
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
6
|
+
"version": "0.4.2",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "passioncode-ai",
|
|
9
9
|
"url": "https://github.com/passioncode-ai"
|
|
@@ -22,4 +22,3 @@
|
|
|
22
22
|
"conformance"
|
|
23
23
|
]
|
|
24
24
|
}
|
|
25
|
-
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned private fabric-agent-contract checkout. Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: passioncode-ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.4.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "20a818e648a4c09a60df0126d11626922e8b9094"
|
|
11
11
|
---
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: building-fabric-services
|
|
3
|
+
description: >-
|
|
4
|
+
Use when building or running an agent as a long-lived local service with a dashboard on the
|
|
5
|
+
operator's Mac — «сделай агенту дашборд», «локальный сервис агента», «дашборд должен всегда
|
|
6
|
+
работать и не плодить копии», «где агенту хранить настройки», «подключи агента к Fabric
|
|
7
|
+
Dashboards», "make this agent a local service", "always-on dashboard", "fabric-service
|
|
8
|
+
protocol", "add the well-known endpoint", "migrate a service to fabric-service". Covers the
|
|
9
|
+
fabric-service/0.1 extension: which surface to expose (MCP streamable HTTP, CLI, A2A), the token
|
|
10
|
+
and one-time login, where state, config, logs and cache live, one copy per machine, launchd, the
|
|
11
|
+
descriptor, the well-known document, the events feed and notifications; ships Python and Node
|
|
12
|
+
reference kits and a live conformance probe. NOT for a one-off script or cron job, a hosted
|
|
13
|
+
SaaS, the provider manifest itself (adapting-projects-to-fabric), or building Fabric Dashboards.
|
|
14
|
+
license: MIT
|
|
15
|
+
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). No network or package install; the contract checkout is optional.
|
|
16
|
+
metadata:
|
|
17
|
+
author: passioncode-ai
|
|
18
|
+
version: "0.4.2"
|
|
19
|
+
contract-version: "0.1.0"
|
|
20
|
+
extension: "fabric-service/0.1"
|
|
21
|
+
extension-commit: "a5a27092ba0dcc5facfbeae8b359146dfb403e9a"
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Building Fabric services
|
|
25
|
+
|
|
26
|
+
A **service** is an agent that keeps running on the operator's computer: it holds
|
|
27
|
+
state, answers other agents, and shows a dashboard. This skill makes one that is
|
|
28
|
+
always alive, comes back after a restart, never runs twice, keeps its state through a
|
|
29
|
+
reinstall, and appears in Fabric Dashboards by itself — the `fabric-service/0.1`
|
|
30
|
+
extension of the Fabric Agent Contract (DEC-0015).
|
|
31
|
+
|
|
32
|
+
Every rule below exists because a real service on this estate broke it. Treat them as
|
|
33
|
+
non-negotiable; the kit implements them so you call a function instead of
|
|
34
|
+
re-deriving a rule.
|
|
35
|
+
|
|
36
|
+
## Boundary
|
|
37
|
+
|
|
38
|
+
Use it to create a service, to bring an existing local server under the protocol, or
|
|
39
|
+
to review one. Do not use it for:
|
|
40
|
+
|
|
41
|
+
- a script, a cron job or a one-shot CLI — nothing stays running, so there is nothing
|
|
42
|
+
to supervise; a launchd `StartInterval` job needs no descriptor;
|
|
43
|
+
- a hosted SaaS — the protocol is loopback-only by design;
|
|
44
|
+
- the provider manifest and admission bundle — that is `adapting-projects-to-fabric`
|
|
45
|
+
(a service that is also a provider does both);
|
|
46
|
+
- the Fabric Dashboards app itself.
|
|
47
|
+
|
|
48
|
+
## Step 0 — is it a service?
|
|
49
|
+
|
|
50
|
+
It is a service when at least one is true: other agents call it while the operator is
|
|
51
|
+
away; it runs jobs longer than one request; it owns a store other tools read; the
|
|
52
|
+
operator watches it on a page. Otherwise build a CLI and stop here.
|
|
53
|
+
|
|
54
|
+
Answer before writing code, and record the answers in the project README:
|
|
55
|
+
|
|
56
|
+
| Question | Answer shape |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `id` | `^[a-z][a-z0-9-]{1,62}$`, stable forever (it names the data directory) |
|
|
59
|
+
| Port | one number; run the claim check below before choosing it |
|
|
60
|
+
| Who calls it | the named agents or people, and through which surface (Step 1) |
|
|
61
|
+
| What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
|
|
62
|
+
| What it tells the operator | the events worth a notification (Step 5) |
|
|
63
|
+
|
|
64
|
+
## Step 1 — choose the surfaces
|
|
65
|
+
|
|
66
|
+
| Caller | Surface | Why |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session; stdio-per-session once left twenty stray servers on this machine |
|
|
69
|
+
| The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard`, `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
|
|
70
|
+
| A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
|
|
71
|
+
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header | the CSRF defence all four existing dashboards use |
|
|
72
|
+
|
|
73
|
+
Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
|
|
74
|
+
MCP registration into a client config, the login flow or the CSRF header.
|
|
75
|
+
|
|
76
|
+
## Step 2 — the non-negotiables
|
|
77
|
+
|
|
78
|
+
1. **Loopback only.** Bind `127.0.0.1`. Refuse any `Host` other than
|
|
79
|
+
`127.0.0.1:<port>`, `localhost:<port>`, `[::1]:<port>`; refuse a foreign `Origin` and
|
|
80
|
+
`Sec-Fetch-Site: cross-site` — on every path, the well-known one included.
|
|
81
|
+
*(A throwaway `python -m http.server` bound to every interface served a source tree to the local network.)*
|
|
82
|
+
2. **One copy, locked before any side effect.** Take the exclusive lock on
|
|
83
|
+
`<data>/service.lock` first — before resuming jobs, starting a scheduler, migrating
|
|
84
|
+
a store or even creating a token. Held → print one sentence naming the holder's pid
|
|
85
|
+
and exit **75**. Binding the port is not a lock. *(A second copy re-queued the first
|
|
86
|
+
copy's running jobs in its startup hook, before its bind failed.)*
|
|
87
|
+
3. **launchd is the only supervisor.** `RunAtLoad` true, `KeepAlive` true,
|
|
88
|
+
`ThrottleInterval` 10, `ExitTimeOut` above your drain time. Never start yourself
|
|
89
|
+
from a CLI with `nohup` or `&`; never let a host spawn you. *(`KeepAlive
|
|
90
|
+
{SuccessfulExit:false}` left a cleanly exited service down; hand-started copies
|
|
91
|
+
raced launchd's.)*
|
|
92
|
+
4. **Build identity.** The well-known document carries the git commit or a package
|
|
93
|
+
digest and the process start time. *(A service ran on stale code after an edit and
|
|
94
|
+
burned budget; nothing showed which build was answering.)*
|
|
95
|
+
5. **State outside code.** Data and config in `~/Library/Application Support/<id>/`,
|
|
96
|
+
logs in `~/Library/Logs/<id>/`, cache in `~/Library/Caches/<id>/`; secrets in a 0600
|
|
97
|
+
file. Never inside the service's own code checkout or a release directory; a
|
|
98
|
+
repository that versions the data itself is a store and is fine — declare
|
|
99
|
+
`source.repository` so the probe can tell them apart. *(Deleting a checkout left a
|
|
100
|
+
plist launchd retried every 10 s, and the data went with it.)*
|
|
101
|
+
6. **Tokens stay in files and headers.** Mode 0600, owner-checked, never a symlink;
|
|
102
|
+
never in a query string, an argument vector or a plist. The dashboard gets a
|
|
103
|
+
one-time login code, never the token.
|
|
104
|
+
7. **`degraded` is always present.** An empty list asserts full health; `ready` with a
|
|
105
|
+
non-empty list is a lie the kit corrects to `degraded`.
|
|
106
|
+
8. **Atomic writes.** Temporary file, fsync, rename — for the store's side files, the
|
|
107
|
+
descriptor and the token.
|
|
108
|
+
|
|
109
|
+
## Step 3 — build it with the kit
|
|
110
|
+
|
|
111
|
+
Copy the kit into the service (each is one self-contained file): Python
|
|
112
|
+
[`scripts/fabric_service.py`](scripts/fabric_service.py), Node
|
|
113
|
+
[`scripts/fabric-service.mjs`](scripts/fabric-service.mjs). Both share file formats, and
|
|
114
|
+
their locks exclude each other. The complete worked example is
|
|
115
|
+
[`scripts/sample_service.py`](scripts/sample_service.py) — read it before writing a
|
|
116
|
+
handler.
|
|
117
|
+
|
|
118
|
+
Startup order is the part that goes wrong; keep it exactly:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import fabric_service as fs
|
|
122
|
+
|
|
123
|
+
dirs = fs.service_dirs("example-agent")
|
|
124
|
+
lock = fs.hold_single_instance(dirs["data"]) # 1. lock — exits 75 if held
|
|
125
|
+
token = fs.ensure_token(dirs["data"] / "service.token") # 2. only now touch state
|
|
126
|
+
log = fs.JsonlEventLog(dirs["data"] / "events.jsonl") # (or a view over your own log)
|
|
127
|
+
codes = fs.LoginCodes(dirs["data"] / "auth")
|
|
128
|
+
resume_jobs() # 3. side effects after the lock
|
|
129
|
+
fs.LoopbackHTTPServer(("127.0.0.1", port), Handler) # 4. bind loopback, no resolver
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Node: `const lock = await holdSingleInstance(dirs.data)` first, then the same order.
|
|
133
|
+
|
|
134
|
+
On every request call `check_request(port, host, origin, sec_fetch_site)` and answer
|
|
135
|
+
403 with its sentence when it returns one. Serve the four protocol routes:
|
|
136
|
+
|
|
137
|
+
| Route | Auth | Kit |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `GET /.well-known/fabric-service` | none, guard still applies | `build_well_known(...)` — from memory, under 100 ms |
|
|
140
|
+
| `GET /fabric/v1/events?after=&limit=` | service token | `events_page(fetch, after, parse_limit(limit))` |
|
|
141
|
+
| `POST /fabric/v1/login-code` | service token | `LoginCodes.issue()` |
|
|
142
|
+
| `GET /fabric/v1/login?code=` | the code | `LoginCodes.redeem(code)` → `Set-Cookie: session_cookie_header(...)`, 302 to the dashboard |
|
|
143
|
+
|
|
144
|
+
`SIGTERM` drains in-flight work within `ExitTimeOut`, then exits; interrupted work
|
|
145
|
+
resumes on the next start.
|
|
146
|
+
|
|
147
|
+
## Step 4 — install it
|
|
148
|
+
|
|
149
|
+
The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
150
|
+
|
|
151
|
+
1. `write_descriptor(descriptor)` — refuses a port or `id.instance` another descriptor
|
|
152
|
+
claims. A preview or branch copy is a second **instance**, never a second id.
|
|
153
|
+
2. `launchd_plist(...)` then `launchd_install(...)` — writes and lints the plist,
|
|
154
|
+
`bootout` and waits for the unload, `bootstrap` with retries on the transient I/O
|
|
155
|
+
error, then polls the well-known document until it answers with **this** identity.
|
|
156
|
+
3. Uninstall: `launchd_uninstall`, `remove_descriptor`; keep data unless the operator
|
|
157
|
+
asks to purge.
|
|
158
|
+
|
|
159
|
+
Code runs from an immutable release directory; an upgrade writes a new release,
|
|
160
|
+
rewrites the plist and restarts. Read [the lifecycle reference](references/lifecycle.md)
|
|
161
|
+
for the plist fields, the release layout, log rotation and the Linux case.
|
|
162
|
+
|
|
163
|
+
## Step 5 — the dashboard and the events
|
|
164
|
+
|
|
165
|
+
The dashboard is a same-origin page: no inline script, strict CSP, polling that pauses
|
|
166
|
+
while the operator types or a dialog is open, one panel's failure never blanks the
|
|
167
|
+
others, and every error is a sentence with the next action. Read
|
|
168
|
+
[the dashboard reference](references/dashboard.md) before building the page.
|
|
169
|
+
|
|
170
|
+
The events feed is a **view over the log you already keep** — a jobs table, a JSONL
|
|
171
|
+
journal — not a second store. Each event is one sentence a person reads; set
|
|
172
|
+
`notify: true` only for what the operator must act on or would want to hear about
|
|
173
|
+
unprompted, with a `link` to the page that resolves it. Read
|
|
174
|
+
[the events reference](references/events-and-notifications.md) for mapping an existing
|
|
175
|
+
log, retention and notification policy.
|
|
176
|
+
|
|
177
|
+
## Step 6 — verify
|
|
178
|
+
|
|
179
|
+
Run the probe against the live service — it reads the descriptor, then checks every
|
|
180
|
+
rule it can reach and says which it could not:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, well-known
|
|
187
|
+
shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
|
|
188
|
+
events auth and shape, single-use login, state outside code, instance lock, plist,
|
|
189
|
+
launchd pid equals answering pid) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass —
|
|
190
|
+
name it in the report.
|
|
191
|
+
|
|
192
|
+
Then prove the lock: start a second copy by hand against the same data directory; it
|
|
193
|
+
must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
|
|
194
|
+
the well-known document must answer again with a new pid within about 15 seconds.
|
|
195
|
+
|
|
196
|
+
## Migrating an existing service
|
|
197
|
+
|
|
198
|
+
Read [the migration reference](references/migrating-a-service.md) — the order of
|
|
199
|
+
changes that keeps the service answering throughout, and what each existing service on
|
|
200
|
+
this estate needs.
|
|
201
|
+
|
|
202
|
+
## When something is missing
|
|
203
|
+
|
|
204
|
+
- **Not macOS / no launchd:** declare `lifecycle.manager: "none"`; hosts show the
|
|
205
|
+
service read-only. Keep every other rule.
|
|
206
|
+
- **No Python:** use the Node kit; run the probe from any machine that has Python and
|
|
207
|
+
can reach the port, or mark the probe `NOT_RUN` with the reason.
|
|
208
|
+
- **Neither kit fits the framework:** implement the four routes and the lock by hand
|
|
209
|
+
against [the protocol reference](references/protocol.md); the probe is the judge.
|
|
210
|
+
- **No contract checkout:** the protocol reference is pinned to the extension commit
|
|
211
|
+
in this skill's metadata; do not reconstruct fields from memory.
|
|
212
|
+
- **Fabric Dashboards not installed:** nothing changes — the service is complete
|
|
213
|
+
without a host; the descriptor waits for one.
|
|
214
|
+
|
|
215
|
+
## Gotchas
|
|
216
|
+
|
|
217
|
+
- `lsof` shows `*:<port>` or `0.0.0.0` → the bind is wrong even if the Host check works.
|
|
218
|
+
- A health probe that only checks HTTP 200 accepts any program on the port. Compare
|
|
219
|
+
`service.id` and `service.instance` — two services here once defaulted to ports
|
|
220
|
+
another service already held.
|
|
221
|
+
- `launchctl bootstrap` right after `bootout` fails with I/O error 5 until the unload
|
|
222
|
+
finishes; wait, then retry.
|
|
223
|
+
- Under launchd `PATH` is bare. Pin the interpreter by absolute path; set `PATH`
|
|
224
|
+
explicitly in the plist; never copy the whole shell environment into it.
|
|
225
|
+
- A plist pointing at a worktree or a checkout breaks the day it moves. Point it at a
|
|
226
|
+
release.
|
|
227
|
+
- `http.server.HTTPServer` asks the resolver for its FQDN between `bind()` and
|
|
228
|
+
`listen()`. With a slow resolver (a macOS CI runner, a Mac offline) the port is bound
|
|
229
|
+
but silent: connects time out instead of being refused. Use `fs.LoopbackHTTPServer`.
|
|
230
|
+
- `logging.basicConfig` called twice: the second call is a no-op, so a server's own
|
|
231
|
+
log file stays empty. Configure logging once, in the entry point.
|
|
232
|
+
- Hand-made `.plist.bak` files in `~/Library/LaunchAgents` are never cleaned up; keep
|
|
233
|
+
backups elsewhere.
|
|
234
|
+
|
|
235
|
+
## Completion format
|
|
236
|
+
|
|
237
|
+
Report: the `id`, port and surfaces with the Step 0 answers; files created or changed;
|
|
238
|
+
the probe table verbatim with every `NOT_RUN` explained; the lock and kill-9 results
|
|
239
|
+
with pids; and the one next command. Never call a service conformant while the probe
|
|
240
|
+
reports a `FAIL`.
|