@passioncode-ai/passioncode 0.1.44 → 0.1.46
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 +21 -0
- package/README.md +6 -0
- package/SECURITY.md +6 -1
- package/family.json +1 -1
- package/lib/launcher.js +212 -3
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +5 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +14 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +90 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +26 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +443 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +32 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +826 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +7 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.46 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`update` registers the Fabric Dashboards MCP server for every agent that is present**
|
|
8
|
+
(`fabric-dashboards`, stdio, the app bundle's `bin/fabric-dashboards-mcp`): Claude Code
|
|
9
|
+
through `claude mcp add --scope user`, Codex CLI as a `["mcp_servers"."fabric-dashboards"]`
|
|
10
|
+
table appended to `~/.codex/config.toml`, Gemini CLI and Kimi Code in the `mcpServers` of
|
|
11
|
+
`~/.gemini/settings.json` and `$KIMI_CODE_HOME/mcp.json`. A machine without the app skips
|
|
12
|
+
the step; an entry that already points at the same command is left alone, and one that
|
|
13
|
+
points elsewhere, or is declared in a form the launcher does not edit, is reported and
|
|
14
|
+
kept. Each registration is journalled before the file changes and removed again by
|
|
15
|
+
`restore` and `uninstall` only while it is still exactly what the launcher wrote
|
|
16
|
+
([ADR-0003](docs/adr/0003-fabric-dashboards-mcp-registration.md)).
|
|
17
|
+
|
|
18
|
+
## 0.1.45 - 2026-10-12
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Fabric Agent Adapter is pinned at `v0.21.0`** (it was `v0.20.0`). Fabric Agent Adapter 0.21.0 (FAA-31): both service kits verify the operator's signed estate record (estate-record/1, contract DEC-0055) — the chain of revisions, the passkey's WebAuthn assertion checked offline, the 30-day window and the record's rules — and a delegation gate given the current record trusts a token's key only while that record certifies it. The adapter now pins contract b291bf0.
|
|
23
|
+
|
|
3
24
|
## 0.1.44 - 2026-10-12
|
|
4
25
|
|
|
5
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -55,6 +55,12 @@ out of it, even when they implement Fabric protocols.
|
|
|
55
55
|
`~/.passioncode/quarantine/` — `npx @passioncode-ai/passioncode@latest restore` puts them back.
|
|
56
56
|
- Each version is an immutable release under `~/.passioncode/releases/<version>`; an update
|
|
57
57
|
keeps the release it replaced for rollback and removes older ones.
|
|
58
|
+
- When the Fabric Dashboards app is installed, `update` also registers its MCP server
|
|
59
|
+
(`fabric-dashboards`) for every agent that is present — Claude Code (through
|
|
60
|
+
`claude mcp add --scope user`), Codex CLI (`~/.codex/config.toml`), Gemini CLI
|
|
61
|
+
(`~/.gemini/settings.json`) and Kimi Code (`~/.kimi-code/mcp.json`). An entry that
|
|
62
|
+
already points at the same command is left alone; one that points elsewhere is reported
|
|
63
|
+
and kept. `restore` and `uninstall` remove what the launcher added.
|
|
58
64
|
|
|
59
65
|
## Staying current
|
|
60
66
|
|
package/SECURITY.md
CHANGED
|
@@ -11,7 +11,12 @@ that replaces it is verified. For a member's old single-member marketplace
|
|
|
11
11
|
(`legacyMarketplaces` in `family.json`), once that plugin is verified and nothing else
|
|
12
12
|
installed comes from it, it also moves the `extraKnownMarketplaces` entry out of
|
|
13
13
|
`~/.claude/settings.json` (a copy of the file is kept in the quarantine) and runs
|
|
14
|
-
`claude plugin marketplace remove <name>`.
|
|
14
|
+
`claude plugin marketplace remove <name>`. When the Fabric Dashboards app is installed it
|
|
15
|
+
also registers a `fabric-dashboards` MCP entry for every agent that is present: through
|
|
16
|
+
`claude mcp add --scope user` for Claude Code (it never writes `~/.claude.json` itself),
|
|
17
|
+
and in `~/.codex/config.toml`, `~/.gemini/settings.json` and `~/.kimi-code/mcp.json`,
|
|
18
|
+
copying each existing file into the quarantine first; an entry that already exists is
|
|
19
|
+
left alone and reported. Displaced files are moved rather than
|
|
15
20
|
deleted: a recovery record is written before the move in
|
|
16
21
|
`~/.passioncode/quarantine/<run>/moved.json`. `restore` processes the latest batch,
|
|
17
22
|
checkpoints completed entries and refuses to overwrite a conflicting user replacement.
|
package/family.json
CHANGED
package/lib/launcher.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The PassionCode.ai launcher: installs the vendored payload as one release,
|
|
4
4
|
* gives Claude Code the plugins through a local marketplace, gives every other
|
|
5
|
-
* agent the same skills through the ~/.agents/skills hub,
|
|
6
|
-
*
|
|
5
|
+
* agent the same skills through the ~/.agents/skills hub, registers the Fabric
|
|
6
|
+
* Dashboards MCP server for every agent that is present, and removes what would
|
|
7
|
+
* shadow them — reversibly. Every step is recorded; nothing is deleted.
|
|
7
8
|
*
|
|
8
9
|
* One channel per agent: Claude Code reads plugins, so nothing is written to
|
|
9
10
|
* ~/.claude/skills (a plain copy there shadows the plugin and serves its frozen
|
|
@@ -47,6 +48,12 @@ function writeJson(file, value) {
|
|
|
47
48
|
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n');
|
|
48
49
|
fs.renameSync(tmp, file);
|
|
49
50
|
}
|
|
51
|
+
function writeText(file, text) {
|
|
52
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
53
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
54
|
+
fs.writeFileSync(tmp, text);
|
|
55
|
+
fs.renameSync(tmp, file);
|
|
56
|
+
}
|
|
50
57
|
function lstat(p) { try { return fs.lstatSync(p); } catch (_) { return null; } }
|
|
51
58
|
function linkTarget(p) { try { return fs.readlinkSync(p); } catch (_) { return null; } }
|
|
52
59
|
function pointsTo(at, target) { const link = linkTarget(at); return link !== null && path.resolve(path.dirname(at), link) === target; }
|
|
@@ -187,6 +194,103 @@ function marketplaceAddArg(source) {
|
|
|
187
194
|
return source.url || source.repo || source.path || null;
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
const MCP_NAME = 'fabric-dashboards';
|
|
198
|
+
const MCP_COMMAND = () => process.env.PASSIONCODE_FABRIC_DASHBOARDS_MCP
|
|
199
|
+
|| '/Applications/Fabric Dashboards.app/Contents/Resources/bin/fabric-dashboards-mcp';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The agents besides Claude Code the Fabric Dashboards MCP server is registered for, and
|
|
203
|
+
* the file each keeps its MCP configuration in. An agent is present when its directory
|
|
204
|
+
* is; a missing file under a present agent is created. Claude Code is registered through
|
|
205
|
+
* its own CLI instead: every running session rewrites ~/.claude.json, so a second writer
|
|
206
|
+
* there would lose one side's change.
|
|
207
|
+
*/
|
|
208
|
+
function mcpFileTargets(home) {
|
|
209
|
+
const kimiHome = process.env.KIMI_CODE_HOME || path.join(home, '.kimi-code');
|
|
210
|
+
return [
|
|
211
|
+
{ agent: 'Codex CLI', dir: path.join(home, '.codex'), file: path.join(home, '.codex', 'config.toml'), kind: 'toml' },
|
|
212
|
+
{ agent: 'Gemini CLI', dir: path.join(home, '.gemini'), file: path.join(home, '.gemini', 'settings.json'), kind: 'json' },
|
|
213
|
+
{ agent: 'Kimi Code', dir: kimiHome, file: path.join(kimiHome, 'mcp.json'), kind: 'json' },
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The line range of the `["mcp_servers"."<name>"]` table in a TOML text, or null. */
|
|
218
|
+
function tomlTableRange(text, name) {
|
|
219
|
+
const lines = text.split('\n');
|
|
220
|
+
const header = new RegExp(`^\\s*\\[\\s*["']?mcp_servers["']?\\s*\\.\\s*["']?${name}["']?\\s*\\]\\s*(?:#.*)?$`);
|
|
221
|
+
for (let i = 0; i < lines.length; i++) {
|
|
222
|
+
if (!header.test(lines[i])) continue;
|
|
223
|
+
let end = lines.length;
|
|
224
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
225
|
+
if (/^\s*\[/.test(lines[j])) { end = j; break; }
|
|
226
|
+
}
|
|
227
|
+
return { start: i, end, lines };
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Any other way a TOML text can already define the server — a sub-table such as
|
|
234
|
+
* `[mcp_servers.<name>.env]` or an inline `<name> = { … }` under `[mcp_servers]`.
|
|
235
|
+
* Appending a table then would declare the key twice and Codex would refuse the file.
|
|
236
|
+
*/
|
|
237
|
+
function tomlMentions(text, name) {
|
|
238
|
+
return new RegExp(`^\\s*\\[\\s*["']?mcp_servers["']?\\s*\\.\\s*["']?${name}["']?\\s*\\.`, 'm').test(text)
|
|
239
|
+
|| new RegExp(`^\\s*["']?${name}["']?\\s*=`, 'm').test(text);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The `command` of a TOML table range: the string, null when absent, undefined when unreadable. */
|
|
243
|
+
function tomlTableCommand(range) {
|
|
244
|
+
for (let i = range.start + 1; i < range.end; i++) {
|
|
245
|
+
const m = /^\s*command\s*=\s*("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/.exec(range.lines[i]);
|
|
246
|
+
if (m) { try { return JSON.parse(m[1]); } catch (_) { return undefined; } }
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function mcpTomlTable(command) {
|
|
252
|
+
return `["mcp_servers"."${MCP_NAME}"]\ncommand = ${JSON.stringify(command)}\nargs = []\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Two MCP entries are the same when their JSON lines up at every depth, key order aside. */
|
|
256
|
+
function sameEntry(a, b) {
|
|
257
|
+
const canon = (v) => (v && typeof v === 'object' && !Array.isArray(v)
|
|
258
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, canon(v[k])]))
|
|
259
|
+
: Array.isArray(v) ? v.map(canon) : v);
|
|
260
|
+
return JSON.stringify(canon(a)) === JSON.stringify(canon(b));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Remove an MCP entry the launcher added, only while it is still exactly that entry.
|
|
265
|
+
* Returns a note, or null when the entry is already gone. Never deletes the file.
|
|
266
|
+
*/
|
|
267
|
+
function removeMcpRegistration(record, claude) {
|
|
268
|
+
const kept = (why) => `${record.file}: ${record.name} for ${record.agent} ${why}; kept`;
|
|
269
|
+
const removed = `${record.name} for ${record.agent} removed from ${record.file}`;
|
|
270
|
+
if (record.file.endsWith('.toml')) {
|
|
271
|
+
let text;
|
|
272
|
+
try { text = fs.readFileSync(record.file, 'utf8'); } catch (_) { return null; }
|
|
273
|
+
const range = tomlTableRange(text, record.name);
|
|
274
|
+
if (!range) return null;
|
|
275
|
+
if (tomlTableCommand(range) !== record.entry.command) return kept('was changed');
|
|
276
|
+
writeText(record.file, range.lines.slice(0, range.start).concat(range.lines.slice(range.end)).join('\n'));
|
|
277
|
+
return removed;
|
|
278
|
+
}
|
|
279
|
+
let cfg;
|
|
280
|
+
try { cfg = readSettings(record.file); } catch (error) { return kept(`could not be read (${error.message})`); }
|
|
281
|
+
const servers = cfg.doc.mcpServers;
|
|
282
|
+
if (!servers || !Object.prototype.hasOwnProperty.call(servers, record.name)) return null;
|
|
283
|
+
if (!sameEntry(servers[record.name], record.entry)) return kept('was changed');
|
|
284
|
+
if (record.via === 'claude') {
|
|
285
|
+
if (!claude.available()) return kept('is registered with Claude Code, whose CLI was not found');
|
|
286
|
+
const r = claude.run(['mcp', 'remove', '--scope', 'user', record.name]);
|
|
287
|
+
return r.code === 0 ? removed : kept(`could not be removed (${r.out || 'claude mcp remove failed'})`);
|
|
288
|
+
}
|
|
289
|
+
delete servers[record.name];
|
|
290
|
+
writeJson(record.file, cfg.doc);
|
|
291
|
+
return removed;
|
|
292
|
+
}
|
|
293
|
+
|
|
190
294
|
function payloadManifest(dir = PAYLOAD()) {
|
|
191
295
|
const m = readJson(path.join(dir, 'manifest.json'), null);
|
|
192
296
|
if (!m || !Array.isArray(m.members)) throw new Error(`No payload in ${dir}: the package is incomplete (run scripts/vendor.mjs).`);
|
|
@@ -391,7 +495,93 @@ function update({ home, dryRun = false, claude = claudeRunner(), log = () => {}
|
|
|
391
495
|
}
|
|
392
496
|
}
|
|
393
497
|
|
|
394
|
-
// 5. The
|
|
498
|
+
// 5. The Fabric Dashboards MCP server, for every agent that is present. Entries a
|
|
499
|
+
// person or another tool set are never rewritten: a matching entry is left, a
|
|
500
|
+
// different one is reported. What the launcher adds is journalled before it is
|
|
501
|
+
// written, so restore and uninstall can take it out again.
|
|
502
|
+
const registered = [];
|
|
503
|
+
const mcpCommand = MCP_COMMAND();
|
|
504
|
+
const mcpBackup = (file, agent) => {
|
|
505
|
+
if (!lstat(file)) return null;
|
|
506
|
+
fs.mkdirSync(quarantineDir, { recursive: true });
|
|
507
|
+
const backup = path.join(quarantineDir, `${path.basename(file)}.before-mcp-${agent.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
|
|
508
|
+
fs.copyFileSync(file, backup);
|
|
509
|
+
return backup;
|
|
510
|
+
};
|
|
511
|
+
const register = (record, write) => step('mcp', `register ${MCP_NAME} for ${record.agent} in ${record.file}`, () => {
|
|
512
|
+
recordMove(record);
|
|
513
|
+
write();
|
|
514
|
+
registered.push({ agent: record.agent, file: record.file, name: record.name, entry: record.entry, ...(record.via ? { via: record.via } : {}) });
|
|
515
|
+
});
|
|
516
|
+
const existingNote = (agent, existing) => existing === mcpCommand
|
|
517
|
+
? { kind: 'mcp', detail: `${agent} already runs ${MCP_NAME} from ${mcpCommand}`, outcome: 'skipped' }
|
|
518
|
+
: { kind: 'mcp', detail: `${agent} has its own ${MCP_NAME} entry (${existing || 'not a stdio server'}); it was kept`, outcome: 'skipped' };
|
|
519
|
+
if (!lstat(mcpCommand)) {
|
|
520
|
+
steps.push({ kind: 'mcp', detail: `no Fabric Dashboards MCP at ${mcpCommand}; nothing was registered`, outcome: 'skipped' });
|
|
521
|
+
} else {
|
|
522
|
+
const claudeJson = path.join(p.home, '.claude.json');
|
|
523
|
+
if (!hasClaude) {
|
|
524
|
+
steps.push({ kind: 'mcp', detail: `Claude Code CLI not found; ${MCP_NAME} was not registered for it`, outcome: 'skipped' });
|
|
525
|
+
} else {
|
|
526
|
+
let servers = null;
|
|
527
|
+
try { servers = readSettings(claudeJson).doc.mcpServers || {}; } catch (error) {
|
|
528
|
+
steps.push({ kind: 'mcp', detail: `keep ${claudeJson}: it could not be read`, outcome: 'failed', error: error.message });
|
|
529
|
+
}
|
|
530
|
+
const existing = servers && servers[MCP_NAME];
|
|
531
|
+
if (existing) steps.push(existingNote('Claude Code', typeof existing === 'object' ? existing.command || existing.url : null));
|
|
532
|
+
else if (servers) {
|
|
533
|
+
const entry = { type: 'stdio', command: mcpCommand, args: [], env: {} };
|
|
534
|
+
register({ kind: 'mcp', via: 'claude', agent: 'Claude Code', file: claudeJson, name: MCP_NAME, entry, backup: null }, () => {
|
|
535
|
+
const r = claude.run(['mcp', 'add', '--scope', 'user', MCP_NAME, '--', mcpCommand], { cwd: p.state });
|
|
536
|
+
if (r.code !== 0) throw new Error(r.out || 'claude mcp add failed');
|
|
537
|
+
const now = readSettings(claudeJson).doc.mcpServers || {};
|
|
538
|
+
if (!now[MCP_NAME] || now[MCP_NAME].command !== mcpCommand) throw new Error(`Claude Code did not register ${MCP_NAME} in ${claudeJson}`);
|
|
539
|
+
// What the CLI wrote is what restore and uninstall compare against.
|
|
540
|
+
entry.type = now[MCP_NAME].type; entry.args = now[MCP_NAME].args; entry.env = now[MCP_NAME].env;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (const target of mcpFileTargets(p.home)) {
|
|
545
|
+
if (!isDirectory(target.dir)) {
|
|
546
|
+
steps.push({ kind: 'mcp', detail: `${target.agent} not found; ${MCP_NAME} was not registered for it`, outcome: 'skipped' });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (target.kind === 'json') {
|
|
550
|
+
let cfg;
|
|
551
|
+
try { cfg = readSettings(target.file); } catch (error) {
|
|
552
|
+
steps.push({ kind: 'mcp', detail: `keep ${target.file}: it could not be read`, outcome: 'failed', error: error.message });
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const existing = (cfg.doc.mcpServers || {})[MCP_NAME];
|
|
556
|
+
if (existing) { steps.push(existingNote(target.agent, typeof existing === 'object' ? existing.command || existing.url : null)); continue; }
|
|
557
|
+
const entry = { command: mcpCommand, args: [], env: {} };
|
|
558
|
+
register({ kind: 'mcp', agent: target.agent, file: target.file, name: MCP_NAME, entry, backup: mcpBackup(target.file, target.agent) }, () => {
|
|
559
|
+
cfg.doc.mcpServers = { ...(cfg.doc.mcpServers || {}), [MCP_NAME]: entry };
|
|
560
|
+
writeJson(target.file, cfg.doc);
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
let text = '';
|
|
564
|
+
try { text = fs.readFileSync(target.file, 'utf8'); } catch (error) {
|
|
565
|
+
if (error.code !== 'ENOENT') {
|
|
566
|
+
steps.push({ kind: 'mcp', detail: `keep ${target.file}: it could not be read`, outcome: 'failed', error: error.message });
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const range = tomlTableRange(text, MCP_NAME);
|
|
571
|
+
if (range) { const c = tomlTableCommand(range); steps.push(existingNote(target.agent, c == null ? null : c)); continue; }
|
|
572
|
+
if (tomlMentions(text, MCP_NAME)) {
|
|
573
|
+
steps.push({ kind: 'mcp', detail: `${target.agent} declares ${MCP_NAME} in ${target.file} in a form the launcher does not edit; it was kept`, outcome: 'skipped' });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const entry = { command: mcpCommand, args: [] };
|
|
577
|
+
register({ kind: 'mcp', agent: target.agent, file: target.file, name: MCP_NAME, entry, backup: mcpBackup(target.file, target.agent) }, () => {
|
|
578
|
+
writeText(target.file, text ? `${text.endsWith('\n') ? text : `${text}\n`}\n${mcpTomlTable(mcpCommand)}` : mcpTomlTable(mcpCommand));
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 6. The launcher's own releases: keep this one and the one it replaced, remove the rest.
|
|
395
585
|
// They are its own output, reinstallable from npm — not the person's files, which are
|
|
396
586
|
// only ever moved to the quarantine.
|
|
397
587
|
const stale = staleReleases(p, version, state.installed || null);
|
|
@@ -410,6 +600,9 @@ function update({ home, dryRun = false, claude = claudeRunner(), log = () => {}
|
|
|
410
600
|
fresh.members = manifest.members.map((m) => ({ name: m.name, version: m.version, ref: m.ref, commit: m.commit }));
|
|
411
601
|
fresh.quarantines = [...new Set([...(fresh.quarantines || []), ...(moved.length ? [quarantineDir] : [])])];
|
|
412
602
|
if (state.legacyRemoved) fresh.legacyRemoved = state.legacyRemoved;
|
|
603
|
+
if (registered.length) {
|
|
604
|
+
fresh.mcpRegistrations = [...(fresh.mcpRegistrations || []).filter((r) => !registered.some((n) => n.file === r.file && n.name === r.name)), ...registered];
|
|
605
|
+
}
|
|
413
606
|
delete fresh.updatingSince;
|
|
414
607
|
writeJson(p.stateFile, fresh);
|
|
415
608
|
}
|
|
@@ -430,6 +623,15 @@ function restore({ home, which, claude = claudeRunner() } = {}) {
|
|
|
430
623
|
if (entry.restoredAt) continue;
|
|
431
624
|
if (entry.kind === 'marketplace') {
|
|
432
625
|
restored.push(restoreMarketplace(p, entry, claude));
|
|
626
|
+
} else if (entry.kind === 'mcp') {
|
|
627
|
+
const note = removeMcpRegistration(entry, claude);
|
|
628
|
+
if (note) restored.push(note);
|
|
629
|
+
const state = readState(p.stateFile);
|
|
630
|
+
if (state.mcpRegistrations) {
|
|
631
|
+
state.mcpRegistrations = state.mcpRegistrations.filter((r) => !(r.file === entry.file && r.name === entry.name));
|
|
632
|
+
if (!state.mcpRegistrations.length) delete state.mcpRegistrations;
|
|
633
|
+
writeJson(p.stateFile, state);
|
|
634
|
+
}
|
|
433
635
|
} else if (lstat(entry.to)) {
|
|
434
636
|
if (lstat(entry.from)) {
|
|
435
637
|
// Old journals did not record the replacement. Only recognise a link
|
|
@@ -561,6 +763,13 @@ function uninstall({ home, claude = claudeRunner() } = {}) {
|
|
|
561
763
|
fs.unlinkSync(p.current);
|
|
562
764
|
}
|
|
563
765
|
const state = readState(p.stateFile);
|
|
766
|
+
// The MCP registrations this launcher added — only while they are still exactly what
|
|
767
|
+
// it wrote; an entry a person changed since stays.
|
|
768
|
+
for (const record of state.mcpRegistrations || []) {
|
|
769
|
+
const note = removeMcpRegistration(record, claude);
|
|
770
|
+
if (note) removed.push(note);
|
|
771
|
+
}
|
|
772
|
+
delete state.mcpRegistrations;
|
|
564
773
|
state.installed = null;
|
|
565
774
|
state.members = [];
|
|
566
775
|
state.config = { ...(state.config || {}), auto: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "The PassionCode.ai agent skill set \u2014 Fabric Agent Adapter, Observatory Log and the organisation's working rules \u2014 for Claude Code and every other agent on the machine, updated as one set.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.21.0",
|
|
15
15
|
"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.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.1.
|
|
38
|
+
"version": "0.1.46",
|
|
39
39
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.
|
|
10
|
+
"ref": "v0.21.0",
|
|
11
|
+
"commit": "7dfbb0a7af36dc1a28e266c0ef248168dabc904f",
|
|
12
|
+
"version": "0.21.0",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"legacyMarketplaces": [
|
|
23
23
|
"fabric-agent-adapter"
|
|
24
24
|
],
|
|
25
|
-
"contentHash": "sha256:
|
|
25
|
+
"contentHash": "sha256:c849515947a8e3ccb28b9bcc570e5cb7a3b417eb5c61e5ab9e1d2a7e656c78ef",
|
|
26
26
|
"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.",
|
|
27
27
|
"author": {
|
|
28
28
|
"name": "PassionCode.ai",
|
|
@@ -61,16 +61,16 @@
|
|
|
61
61
|
"name": "passioncode",
|
|
62
62
|
"displayName": "PassionCode.ai",
|
|
63
63
|
"repo": "passioncode-ai/passioncode",
|
|
64
|
-
"ref": "v0.1.
|
|
64
|
+
"ref": "v0.1.46",
|
|
65
65
|
"commit": null,
|
|
66
|
-
"version": "0.1.
|
|
66
|
+
"version": "0.1.46",
|
|
67
67
|
"via": "this repository",
|
|
68
68
|
"skills": [
|
|
69
69
|
"working-in-passioncode"
|
|
70
70
|
],
|
|
71
71
|
"legacyPluginIds": [],
|
|
72
72
|
"legacyMarketplaces": [],
|
|
73
|
-
"contentHash": "sha256:
|
|
73
|
+
"contentHash": "sha256:eceb409d950b38814a892a29ffe028c41a3000c3323af46458ec345f1faa37f3",
|
|
74
74
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
75
75
|
"author": {
|
|
76
76
|
"name": "PassionCode.ai",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
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.
|
|
6
|
+
"version": "0.21.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). 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.21.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -70,7 +70,7 @@ Use exactly:
|
|
|
70
70
|
|
|
71
71
|
- contract version `0.1.0`;
|
|
72
72
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
73
|
-
- commit `
|
|
73
|
+
- commit `b291bf09e0a9f9fd3c16cf540832cd021203b7d1`.
|
|
74
74
|
|
|
75
75
|
This is the default for new bundles. When checking an issued legacy bundle, preserve its
|
|
76
76
|
explicit supported lock and use that exact clean checkout. Never silently upgrade or
|
|
@@ -21,10 +21,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
|
|
|
21
21
|
|
|
22
22
|
CONTRACT_VERSION = "0.1.0"
|
|
23
23
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
24
|
-
CONTRACT_COMMIT = "
|
|
24
|
+
CONTRACT_COMMIT = "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
|
|
25
25
|
# #region supported-contract-revisions — docs: README.md#contract-pin
|
|
26
26
|
# Only this declaration may name a legacy revision in live files.
|
|
27
|
-
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
27
|
+
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
28
28
|
# #endregion supported-contract-revisions
|
|
29
29
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
30
30
|
MCP_REVISION = "2026-07-28"
|
|
@@ -16,10 +16,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
16
16
|
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
17
17
|
metadata:
|
|
18
18
|
author: PassionCode.ai
|
|
19
|
-
version: "0.
|
|
19
|
+
version: "0.21.0"
|
|
20
20
|
contract-version: "0.1.0"
|
|
21
21
|
extension: "fabric-service/0.1"
|
|
22
|
-
extension-commit: "
|
|
22
|
+
extension-commit: "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
# Building Fabric services
|
|
@@ -83,7 +83,9 @@ are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
|
|
|
83
83
|
[`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
|
|
84
84
|
service checks each call's delegation token (DEC-0039): read [the delegation reference](references/delegation.md);
|
|
85
85
|
the verifier is [`scripts/fabric_delegation.py`](scripts/fabric_delegation.py) /
|
|
86
|
-
[`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs).
|
|
86
|
+
[`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs). A service on the operator's machines that
|
|
87
|
+
applies the estate record (DEC-0055) verifies each revision first and trusts only the keys it certifies: read
|
|
88
|
+
[the estate record reference](references/estate-record.md). When Fabric chooses the service's runner and models
|
|
87
89
|
(DEC-0046), or a project routes its work through an ordered list of runners (DEC-0026: the doctor's closed probe
|
|
88
90
|
results, the route checks FAC-SEM-028…034), read [the profiles reference](references/profiles.md).
|
|
89
91
|
|
|
@@ -16,6 +16,7 @@ contract's own verifier does.
|
|
|
16
16
|
- Approvals
|
|
17
17
|
- What a refusal looks like
|
|
18
18
|
- Keys and the cache
|
|
19
|
+
- Keys the estate record certifies
|
|
19
20
|
- What the kit does not do
|
|
20
21
|
|
|
21
22
|
## Declaring it
|
|
@@ -92,6 +93,19 @@ Python kit verifies Ed25519 itself (RFC 8032, standard library only, strict as O
|
|
|
92
93
|
canonical points, no small-order key); it follows no redirect when it reads the key set; the Node kit uses `node:crypto` and refuses small-order keys
|
|
93
94
|
itself (`isSmallOrderKey`), because Node before 26 lets OpenSSL accept a signature under one.
|
|
94
95
|
|
|
96
|
+
## Keys the estate record certifies
|
|
97
|
+
|
|
98
|
+
A verifier that also reads the estate's record (DEC-0055, [the estate record reference](estate-record.md)) trusts a
|
|
99
|
+
`kid` only while a current revision certifies it for a machine and does not revoke it (DL-14). Pass the current
|
|
100
|
+
verified revision as `estate=` / `{ estate }`, as a value or as a function returning it, to `delegation_gate`,
|
|
101
|
+
`verify_delegation` or `verify_approval`.
|
|
102
|
+
- An uncertified `kid` is refused `unauthenticated` (`approval-invalid` for an approval), with detail
|
|
103
|
+
`kid-not-certified`.
|
|
104
|
+
- An unreadable record is refused `issuer-unavailable`, with detail `estate-unreadable`.
|
|
105
|
+
- Without `estate`, nothing changes.
|
|
106
|
+
- Pass only a revision `verify_estate_record` accepted, and the highest one the machine has applied. The rule trusts
|
|
107
|
+
what it is given, so an unverified or older revision would certify a key the current one revoked.
|
|
108
|
+
|
|
95
109
|
## What the kit does not do
|
|
96
110
|
|
|
97
111
|
It does not mint tokens, check attenuation against a parent (the issuer's rule, DL-5), check a grant or a spend
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# The estate record — what the operator's machines are, signed by their passkey (DEC-0055)
|
|
2
|
+
|
|
3
|
+
Fabric Agent Contract DEC-0055, `docs/specification/estate-record.md`, schema `schemas/estate-record.schema.json`,
|
|
4
|
+
rules `FAC-SEM-072…076`. One revision of one person's estate lists:
|
|
5
|
+
- the passkeys that sign it;
|
|
6
|
+
- the machines, with their device keys and a certificate of each machine's hub key;
|
|
7
|
+
- the packages placed on each machine;
|
|
8
|
+
- where each secret slot is filled from (by reference only);
|
|
9
|
+
- share grants and revocations.
|
|
10
|
+
|
|
11
|
+
A revision names its parent's digest and is signed by a passkey assertion (WebAuthn Level 3) over a challenge
|
|
12
|
+
derived from its own digest. Python: [`scripts/fabric_estate.py`](../scripts/fabric_estate.py). Node:
|
|
13
|
+
[`scripts/fabric-estate.mjs`](../scripts/fabric-estate.mjs). Both answer the contract's own vectors.
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- Applying a revision
|
|
18
|
+
- Trusting a delegation key
|
|
19
|
+
- Checking a record you produce
|
|
20
|
+
- What the kit does not do
|
|
21
|
+
|
|
22
|
+
## Applying a revision
|
|
23
|
+
|
|
24
|
+
A machine applies a revision only when it is valid against `estate-record.schema.json` **and** the verifier accepts
|
|
25
|
+
it. The verifier does not check the schema.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
verdict = fe.verify_estate_record(record, at=datetime.now(timezone.utc), parent=current) # revision 2 and later
|
|
29
|
+
verdict = fe.verify_estate_record(record, at=now, authorities=pinned_passkeys) # revision 1
|
|
30
|
+
if verdict["ok"]:
|
|
31
|
+
current = record # verdict: digest, revision, signedBy, signCount
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const verdict = verifyEstateRecord(record, { at: new Date(), parent: current });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It checks, in the order of the ER table:
|
|
39
|
+
1. **The clock.** An unreadable one is `no-clock`, and it comes first.
|
|
40
|
+
2. **The shape.**
|
|
41
|
+
3. **The digest.** SHA-256 over the RFC 8785 form, without `digest` and `signature`.
|
|
42
|
+
4. **The chain to the parent.** Same estate, the next revision number, the parent's digest, never issued before the
|
|
43
|
+
parent, and no revocation dropped.
|
|
44
|
+
5. **The signer.** The signing passkey must be among the trusted ones and not revoked by the parent. A revision may
|
|
45
|
+
revoke its own signer: that is rotation.
|
|
46
|
+
6. **The assertion.**
|
|
47
|
+
- Client data: type, challenge, origin, no cross-origin use.
|
|
48
|
+
- The RP ID hash, and the user-presence and user-verification flags.
|
|
49
|
+
- The signature: ES256 or EdDSA, under a key validated first (a valid P-256 point; an Ed25519 key of full order).
|
|
50
|
+
7. **The window.** At most 30 days.
|
|
51
|
+
8. **The record's own rules.** A violation is `rule-violation` with its `codes`.
|
|
52
|
+
|
|
53
|
+
Dates are strict RFC 3339: a `T`, an offset, and a day that exists. `:60` and a space are refused. Passkey JWK `x`
|
|
54
|
+
and `y` must be canonical base64url. The Python kit verifies ES256 in pure Python (P-256, strict DER), and the Node
|
|
55
|
+
kit through `node:crypto`.
|
|
56
|
+
|
|
57
|
+
## Trusting a delegation key
|
|
58
|
+
|
|
59
|
+
A delegation verifier that reads the estate record trusts a token's `kid` only while a current revision certifies it
|
|
60
|
+
for a machine and does not revoke it (DL-14, contract CT-20). Give the gate the current verified revision, or a
|
|
61
|
+
function that returns it:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., estate=lambda: current)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- A `kid` the record does not certify is refused like an unknown key: `unauthenticated` (for an approval,
|
|
68
|
+
`approval-invalid`) with detail `kid-not-certified`. The same holds when the record names another key under that
|
|
69
|
+
kid, revokes it, or is outside its window.
|
|
70
|
+
- A record that cannot be read certifies nothing: `issuer-unavailable` with detail `estate-unreadable`.
|
|
71
|
+
- **What you pass in.** Pass only a revision that `verify_estate_record` accepted, and the highest revision the
|
|
72
|
+
machine has applied. The rule trusts the record it is given. A record nobody verified, or an older revision still
|
|
73
|
+
inside its window, would certify a key the current revision revoked (a rollback).
|
|
74
|
+
- `hub_key_certified(record, kid, x, at)` / `hubKeyCertified(...)` answers the question on its own.
|
|
75
|
+
|
|
76
|
+
## Checking a record you produce
|
|
77
|
+
|
|
78
|
+
`estate_record_rules(kind, value)` / `estateRecordRules(kind, value)` are the contract's rules, finding for finding:
|
|
79
|
+
- `estate-record` (FAC-SEM-072…076);
|
|
80
|
+
- `estate-record-revision` `{parent, next}` (072);
|
|
81
|
+
- `estate-placement` `{record, machine, package}` (074, 075).
|
|
82
|
+
|
|
83
|
+
`placement_trust(record, machine, agentRef)` names the publisher keys a package verifier trusts for a placed package.
|
|
84
|
+
|
|
85
|
+
## What the kit does not do
|
|
86
|
+
|
|
87
|
+
- It does not sign: it holds no passkey and no private key.
|
|
88
|
+
- It does not check the schema or verify packages.
|
|
89
|
+
- It does not attest the authenticator (an assertion carries no attestation).
|
|
90
|
+
- It does not track the signature counter: passkeys sync between devices.
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fabric-service/0.1 — wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `b291bf09e0a9f9fd3c16cf540832cd021203b7d1`
|
|
4
4
|
(`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
|
|
5
5
|
DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
|
|
6
6
|
delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
|