@passioncode-ai/passioncode 0.2.45 → 0.2.47
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 +23 -0
- package/README.md +1 -1
- package/SECURITY.md +2 -1
- package/family.json +1 -1
- package/lib/doctor.js +25 -3
- package/lib/launcher.js +34 -5
- package/lib/releases.js +3 -2
- package/lib/setup.js +2 -1
- package/lib/windows-apps.js +74 -0
- 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/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.47 - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`update` on Windows moves `current` to the new release** (PC-85, the Windows acceptance of 2026-10-28): Windows
|
|
8
|
+
refuses a rename over a directory link (`EPERM ... rename 'current.<pid>.lnk' -> 'current'`), so an update stopped
|
|
9
|
+
halfway, kept the old release and left a `current.<pid>.lnk` per attempt. There the old link is now removed first
|
|
10
|
+
(the link itself, never what it points at) and put back if the rename fails again; the temporary link never outlives
|
|
11
|
+
the call, and the ones a failed update left are removed by the next one.
|
|
12
|
+
- **`doctor` checks the apps on Windows** (PC-86): it reads each app's uninstall entry (`reg query`, the name and
|
|
13
|
+
version its installer wrote) or finds it in `%LOCALAPPDATA%\Programs\<name>`, `%LOCALAPPDATA%\<name>` or Program
|
|
14
|
+
Files, and says which version is installed and whether a newer one is out, instead of "app checks run on macOS".
|
|
15
|
+
Setup still installs apps on macOS only.
|
|
16
|
+
|
|
17
|
+
## 0.2.46 - 2026-10-28
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Fabric Agent Adapter is pinned at `v0.56.0`** (it was `v0.55.0`): stability and Windows. working-with-fabric names
|
|
22
|
+
the repositories a Windows turn touched; the board responder recovers from a refused slot, leaves within 3 s when
|
|
23
|
+
idle and logs its notices at `warning` with a subject; the reference service can take a moved slot back; a spend
|
|
24
|
+
gate fails closed.
|
|
25
|
+
|
|
3
26
|
## 0.2.45 - 2026-10-27
|
|
4
27
|
|
|
5
28
|
### Changed
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ secret into a product's own hidden prompt, starting Docker for Fabric, and resta
|
|
|
47
47
|
one question per app; `--yes` accepts the plan, `--with`/`--without <ids>` choose apps
|
|
48
48
|
(`switchboard`, `dashboards`, `observatory`, `inbox`, `fabric`), `--no-apps` sets up the skills
|
|
49
49
|
and MCP only, `--dry-run` shows the plan. Apps are installed on macOS; on other systems setup
|
|
50
|
-
names each download page. Skills only: `npx @passioncode-ai/passioncode@latest update`.
|
|
50
|
+
names each download page, and on Windows `doctor` reads which version of each app is installed. Skills only: `npx @passioncode-ai/passioncode@latest update`.
|
|
51
51
|
2. **Configure:** nothing to set, no account and no key. Background updates are on by default;
|
|
52
52
|
`npx @passioncode-ai/passioncode@latest config set update.auto off` turns them off. Each app's
|
|
53
53
|
own onboarding (sign-ins, Fabric Inbox's server, Observatory's projects) is listed by `setup`.
|
package/SECURITY.md
CHANGED
|
@@ -32,7 +32,8 @@ returns a nonzero exit code. See the [CLI recovery contract](docs/reference/cli.
|
|
|
32
32
|
`doctor` reads: `~/.claude.json`, `~/.codex/config.toml`, `~/.gemini/settings.json`,
|
|
33
33
|
`~/.kimi-code/mcp.json` (entry names, commands and arguments; environment values are passed to
|
|
34
34
|
the server it starts and never printed), the Fabric service descriptors (never their token
|
|
35
|
-
files), the apps' `Info.plist
|
|
35
|
+
files), the apps' `Info.plist` (on Windows, the apps' uninstall entries through `reg query` and their install folders),
|
|
36
|
+
and the GitHub releases API. It starts each registered stdio MCP
|
|
36
37
|
server for one proving call and stops it (never Fabric's, which enrols a session as it starts), and writes only `~/.passioncode/doctor.json` and a
|
|
37
38
|
release cache.
|
|
38
39
|
|
package/family.json
CHANGED
package/lib/doctor.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* `passioncode doctor`: reads the whole PassionCode.ai setup of this machine and names, for every
|
|
4
4
|
* check, its state, the exact fix and what setup may repair (ADR-0004). It changes nothing: the only
|
|
5
5
|
* processes it starts are the registered stdio MCP servers for one proving call each (never Fabric's,
|
|
6
|
-
* which enrols a session when it starts), stopped when the call returns
|
|
6
|
+
* which enrols a session when it starts), stopped when the call returns, and on Windows three
|
|
7
|
+
* read-only `reg query` calls for the apps' uninstall entries (PC-86). `doctor --fix` is setup's
|
|
7
8
|
* repair half (lib/setup.js), not this module.
|
|
8
9
|
*/
|
|
9
10
|
const fs = require('fs');
|
|
@@ -12,6 +13,7 @@ const path = require('path');
|
|
|
12
13
|
const L = require('./launcher');
|
|
13
14
|
const catalog = require('./catalog');
|
|
14
15
|
const releases = require('./releases');
|
|
16
|
+
const W = require('./windows-apps');
|
|
15
17
|
const { probe } = require('./mcp-probe');
|
|
16
18
|
|
|
17
19
|
const PKG = (() => { try { return require('../package.json').version; } catch (_) { return '0.0.0'; } })();
|
|
@@ -123,12 +125,32 @@ async function collect({ home, platform, claude, online }) {
|
|
|
123
125
|
const cacheFile = path.join(p.state, 'releases-cache.json');
|
|
124
126
|
const probes = new Map();
|
|
125
127
|
let docker = null;
|
|
128
|
+
let windowsEntries = null; // the uninstall entries, read once (three `reg query` calls)
|
|
126
129
|
for (const pr of catalog.PRODUCTS) {
|
|
127
130
|
const at = catalog.appPath(pr);
|
|
128
131
|
const installed = platform === 'darwin' && fs.existsSync(at);
|
|
129
132
|
const label = `${pr.name}${pr.preview ? ' (early preview)' : ''}`;
|
|
130
|
-
if (platform
|
|
131
|
-
|
|
133
|
+
if (platform === 'win32') {
|
|
134
|
+
// PC-86: presence and version from the installer's own records; setup installs nothing here.
|
|
135
|
+
if (!windowsEntries) {
|
|
136
|
+
windowsEntries = W.readUninstallEntries(releases.run);
|
|
137
|
+
for (const u of windowsEntries.unread) add({ id: `apps.registry.${u.hive.split('\\')[0].toLowerCase()}${u.hive.includes('WOW6432Node') ? '-wow64' : ''}`, area: 'apps', state: 'warn', detail: `could not read ${u.hive} (${u.why}); an app installed for every user may show as not installed` });
|
|
138
|
+
}
|
|
139
|
+
const found = W.findWindowsApp(pr, { entries: windowsEntries.entries, home });
|
|
140
|
+
if (!found) {
|
|
141
|
+
add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: not installed`, fix: `download it from ${pr.page}` });
|
|
142
|
+
} else {
|
|
143
|
+
let latest = null, latestErr = null;
|
|
144
|
+
if (online) { try { latest = await releases.latestRelease(pr, { cacheFile }); } catch (error) { latestErr = error.message; } }
|
|
145
|
+
const outdated = latest && found.version && newer(latest.version, found.version);
|
|
146
|
+
add({
|
|
147
|
+
id: `app.${pr.id}`, area: 'apps', product: pr.id, state: outdated || (!latest && online) ? 'warn' : 'ok',
|
|
148
|
+
detail: `${label} ${found.version || '(version unknown)'}${found.at ? ` in ${found.at}` : ''}${latest ? (outdated ? `; ${latest.version} is out` : found.version ? ' (latest)' : `; the latest is ${latest.version}`) : online ? `; latest unknown (${latestErr})` : '; latest not checked (--offline)'}`,
|
|
149
|
+
fix: outdated ? `install ${latest.version} from ${pr.page} (or accept the update ${pr.name} offers)` : null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} else if (platform !== 'darwin') {
|
|
153
|
+
add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: app checks run on macOS and Windows; on this system download it from ${pr.page}` });
|
|
132
154
|
} else if (!installed) {
|
|
133
155
|
add({ id: `app.${pr.id}`, area: 'apps', product: pr.id, state: 'info', detail: `${label}: not installed`, fix: `${NPX} setup --with ${pr.id}` });
|
|
134
156
|
} else {
|
package/lib/launcher.js
CHANGED
|
@@ -65,13 +65,42 @@ function readState(file, { tolerant = false } = {}) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Replace a symlink atomically (create beside it, rename over it). Windows refuses any rename over
|
|
70
|
+
* a directory link (EPERM, PC-85), so there the old link is removed first: unlink takes the link
|
|
71
|
+
* itself, never what it points at, and the old link is put back if the rename fails again. The
|
|
72
|
+
* temporary link never outlives the call, and the ones an earlier failed update left are removed.
|
|
73
|
+
* On Windows the swap is therefore not atomic: between the unlink and the rename `at` is absent,
|
|
74
|
+
* and a run killed there leaves no `current` until the next update (status reports it missing).
|
|
75
|
+
*/
|
|
76
|
+
function symlinkAtomic(target, at, { platform = process.platform, rename = fs.renameSync, unlink = fs.unlinkSync } = {}) {
|
|
70
77
|
fs.mkdirSync(path.dirname(at), { recursive: true });
|
|
71
78
|
const tmp = `${at}.${process.pid}.lnk`;
|
|
72
|
-
try { fs.unlinkSync(tmp); } catch (_) { /* none */ }
|
|
79
|
+
const dropTmp = () => { try { fs.unlinkSync(tmp); } catch (_) { /* none */ } };
|
|
80
|
+
dropTmp();
|
|
73
81
|
fs.symlinkSync(target, tmp);
|
|
74
|
-
|
|
82
|
+
try {
|
|
83
|
+
rename(tmp, at);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const old = lstat(at);
|
|
86
|
+
if (platform !== 'win32' || !['EPERM', 'EACCES', 'EEXIST'].includes(error.code) || !old || !old.isSymbolicLink()) { dropTmp(); throw error; }
|
|
87
|
+
const previous = linkTarget(at);
|
|
88
|
+
try { unlink(at); } catch (unlinkError) { dropTmp(); throw unlinkError; }
|
|
89
|
+
try { rename(tmp, at); } catch (again) {
|
|
90
|
+
dropTmp();
|
|
91
|
+
try { fs.symlinkSync(previous, at); } catch (back) { throw new Error(`${again.message}; and putting back ${at} → ${previous} failed (${back.message})`); }
|
|
92
|
+
throw again;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (platform !== 'win32') return; // only a refused rename on Windows ever left one (PC-85)
|
|
96
|
+
const base = path.basename(at);
|
|
97
|
+
let siblings = [];
|
|
98
|
+
try { siblings = fs.readdirSync(path.dirname(at)); } catch (_) { /* nothing to sweep */ }
|
|
99
|
+
for (const name of siblings) {
|
|
100
|
+
if (!name.startsWith(`${base}.`) || !/^\.\d+\.lnk$/.test(name.slice(base.length))) continue;
|
|
101
|
+
const left = path.join(path.dirname(at), name);
|
|
102
|
+
if (lstat(left)?.isSymbolicLink()) { try { fs.unlinkSync(left); } catch (_) { /* the next update sweeps it */ } }
|
|
103
|
+
}
|
|
75
104
|
}
|
|
76
105
|
|
|
77
106
|
// --- one update at a time (lifecycle contract LC-03) ---------------------------------------
|
|
@@ -854,5 +883,5 @@ module.exports = {
|
|
|
854
883
|
restore: (opts = {}) => withUpdateLock(opts.home, () => restore(opts)),
|
|
855
884
|
uninstall: (opts = {}) => withUpdateLock(opts.home, () => uninstall(opts)),
|
|
856
885
|
status, setConfig, paths, CHANNELS, MARKETPLACE, payloadManifest, marketplaceAddArg,
|
|
857
|
-
acquireUpdateLock, UpdateRunning, staleReleases, claudeRunner, bundledMcpServers, mcpFileTargets, readSettings, tomlTableRange, tomlTableCommand, tomlTableArgs, tomlMentions,
|
|
886
|
+
acquireUpdateLock, UpdateRunning, staleReleases, symlinkAtomic, claudeRunner, bundledMcpServers, mcpFileTargets, readSettings, tomlTableRange, tomlTableCommand, tomlTableArgs, tomlMentions,
|
|
858
887
|
};
|
package/lib/releases.js
CHANGED
|
@@ -16,9 +16,10 @@ const catalog = require('./catalog');
|
|
|
16
16
|
const API = () => (process.env.PASSIONCODE_GITHUB_API || 'https://api.github.com').replace(/\/$/, '');
|
|
17
17
|
const CACHE_TTL_MS = 30 * 60 * 1000;
|
|
18
18
|
|
|
19
|
-
function run(cmd, args, { timeout = 60000, input } = {}) {
|
|
20
|
-
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout, input });
|
|
19
|
+
function run(cmd, args, { timeout = 60000, input, maxBuffer = 1024 * 1024 } = {}) {
|
|
20
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout, input, maxBuffer });
|
|
21
21
|
if (r.error && r.error.code === 'ENOENT') return { code: 127, out: '', err: `${cmd} not found` };
|
|
22
|
+
if (r.error && r.error.code === 'ENOBUFS') return { code: 1, out: '', err: `${cmd} wrote more than ${maxBuffer} bytes` };
|
|
22
23
|
if (r.error && r.error.code === 'ETIMEDOUT') return { code: 124, out: r.stdout || '', err: `${cmd} timed out after ${Math.round(timeout / 1000)} s` };
|
|
23
24
|
return { code: r.status === null ? 1 : r.status, out: r.stdout || '', err: r.stderr || '' };
|
|
24
25
|
}
|
package/lib/setup.js
CHANGED
|
@@ -76,7 +76,8 @@ async function setup({
|
|
|
76
76
|
const claude = L.claudeRunner();
|
|
77
77
|
const before = D.sortChecks(await D.collect({ home, platform, claude, online }));
|
|
78
78
|
const fresh = !before.some((c) => c.id === 'skills.installed' && c.state !== 'fail')
|
|
79
|
-
|
|
79
|
+
// An app doctor found (ok or warn: on Windows from the installer's records, PC-86) makes it existing.
|
|
80
|
+
&& !before.some((c) => /^app\.[^.]+$/.test(c.id) && (c.state === 'ok' || c.state === 'warn'));
|
|
80
81
|
const items = plan(before, { platform, withIds, withoutIds, noApps, repairOnly });
|
|
81
82
|
const result = {
|
|
82
83
|
version: D.PKG, platform, mode: fresh ? 'new' : 'existing', at: new Date().toISOString(), plan: items,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Where a PassionCode.ai app is installed on Windows and which version it is, read without
|
|
4
|
+
* starting it (PC-86): the uninstall entry its installer wrote (electron-builder and Tauri both
|
|
5
|
+
* write DisplayName and DisplayVersion; Tauri also InstallLocation), else the folder a per-user
|
|
6
|
+
* installer uses (%LOCALAPPDATA%\Programs\<name> for electron-builder, %LOCALAPPDATA%\<name> for
|
|
7
|
+
* Tauri) or a per-machine one under Program Files, with the program in it.
|
|
8
|
+
*/
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const HIVES = [
|
|
13
|
+
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
14
|
+
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
15
|
+
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** `reg query <hive> /s` output → [{ key, DisplayName, DisplayVersion, InstallLocation, UninstallString }]. */
|
|
19
|
+
function parseUninstall(text) {
|
|
20
|
+
const entries = [];
|
|
21
|
+
let entry = null;
|
|
22
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
23
|
+
if (/^HKEY_/.test(line)) { entry = { key: line.trim() }; entries.push(entry); continue; }
|
|
24
|
+
const m = /^\s+(\S+)\s+REG_(?:SZ|EXPAND_SZ)\s+(.*)$/.exec(line);
|
|
25
|
+
if (m && entry) entry[m[1]] = m[2].trim();
|
|
26
|
+
}
|
|
27
|
+
return entries.filter((e) => e.DisplayName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every uninstall entry on this machine, and the hives `reg` could not read (a missing hive is
|
|
32
|
+
* normal: HKLM's WOW6432Node exists only on 64-bit Windows), so a reader can say an app it did not
|
|
33
|
+
* find may still be there.
|
|
34
|
+
*/
|
|
35
|
+
function readUninstallEntries(run) {
|
|
36
|
+
const entries = [], unread = [];
|
|
37
|
+
for (const hive of HIVES) {
|
|
38
|
+
const r = run('reg', ['query', hive, '/s'], { timeout: 15000, maxBuffer: 32 * 1024 * 1024 });
|
|
39
|
+
if (r.code === 0) entries.push(...parseUninstall(r.out));
|
|
40
|
+
else if (!/unable to find/i.test(`${r.err || ''}${r.out || ''}`)) unread.push({ hive, why: (r.err || r.out || `exit ${r.code}`).trim().split(/\r?\n/)[0] });
|
|
41
|
+
}
|
|
42
|
+
return { entries, unread };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** electron-builder names its entry "<name> <version>", Tauri "<name>"; "Fabric" never matches "Fabric Inbox". */
|
|
46
|
+
function names(entry, name) {
|
|
47
|
+
return entry.DisplayName === name || (!!entry.DisplayVersion && entry.DisplayName === `${name} ${entry.DisplayVersion}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The install folder an entry names, or null: an MSI entry's `MsiExec.exe /X{...}` names none. */
|
|
51
|
+
function folderOf(entry) {
|
|
52
|
+
if (entry.InstallLocation) return entry.InstallLocation.replace(/^"|"$/g, '').replace(/\\$/, '');
|
|
53
|
+
const m = /^"([^"]+)"|^(\S+\.exe)/i.exec(entry.UninstallString || '');
|
|
54
|
+
const exe = m && (m[1] || m[2]);
|
|
55
|
+
if (!exe || !exe.includes('\\') || /(^|\\)msiexec\.exe$/i.test(exe)) return null;
|
|
56
|
+
return exe.slice(0, exe.lastIndexOf('\\'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function folders(name, { env, home }) {
|
|
60
|
+
const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
61
|
+
return [path.join(local, 'Programs', name), path.join(local, name), env.ProgramFiles && path.join(env.ProgramFiles, name)].filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasProgram(dir) { try { return fs.readdirSync(dir).some((n) => /\.exe$/i.test(n)); } catch (_) { return false; } }
|
|
65
|
+
|
|
66
|
+
/** { at, version, source: 'registry' | 'folder' } or null when the app is not installed. */
|
|
67
|
+
function findWindowsApp(product, { entries, env = process.env, home }) {
|
|
68
|
+
const dir = folders(product.name, { env, home }).find(hasProgram);
|
|
69
|
+
const hit = entries.find((e) => names(e, product.name));
|
|
70
|
+
if (hit) return { at: folderOf(hit) || dir || null, version: hit.DisplayVersion || null, source: 'registry' };
|
|
71
|
+
return dir ? { at: dir, version: null, source: 'folder' } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { parseUninstall, readUninstallEntries, findWindowsApp, HIVES };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
4
4
|
"description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
|
|
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.56.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; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
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.2.
|
|
38
|
+
"version": "0.2.47",
|
|
39
39
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), 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.2.
|
|
3
|
+
"version": "0.2.47",
|
|
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.56.0",
|
|
11
|
+
"commit": "250d5d7a18980bacdfc7ba3bb449794995d4c07d",
|
|
12
|
+
"version": "0.56.0",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"legacyMarketplaces": [
|
|
24
24
|
"fabric-agent-adapter"
|
|
25
25
|
],
|
|
26
|
-
"contentHash": "sha256:
|
|
26
|
+
"contentHash": "sha256:a27c7b99d9b29039b28aea11680bf1acce43a6c7792b8e80b16310559d424856",
|
|
27
27
|
"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; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"name": "passioncode",
|
|
63
63
|
"displayName": "PassionCode.ai",
|
|
64
64
|
"repo": "passioncode-ai/passioncode",
|
|
65
|
-
"ref": "v0.2.
|
|
65
|
+
"ref": "v0.2.47",
|
|
66
66
|
"commit": null,
|
|
67
|
-
"version": "0.2.
|
|
67
|
+
"version": "0.2.47",
|
|
68
68
|
"via": "this repository",
|
|
69
69
|
"skills": [
|
|
70
70
|
"setting-up-passioncode",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"legacyPluginIds": [],
|
|
74
74
|
"legacyMarketplaces": [],
|
|
75
|
-
"contentHash": "sha256:
|
|
75
|
+
"contentHash": "sha256:4fbc28ab1c059e892eb3998c42946149e777a76d54eca40842bfe66927342ccb",
|
|
76
76
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), 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.",
|
|
77
77
|
"author": {
|
|
78
78
|
"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; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.56.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (
|
|
2
|
+
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -5,7 +5,7 @@ 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.56.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
11
11
|
---
|
|
@@ -47,7 +47,11 @@ ENV_MAX = 64
|
|
|
47
47
|
# Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
|
|
48
48
|
_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
|
|
49
49
|
_ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
|
|
50
|
-
|
|
50
|
+
# A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
|
|
51
|
+
# `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
|
|
52
|
+
_URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
|
|
53
|
+
# any other scheme: a backslash belongs to the user information (seam r8)
|
|
54
|
+
r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
|
|
51
55
|
_SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
|
|
52
56
|
_LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
|
|
53
57
|
_URL = re.compile(URL_PATTERN)
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
"""The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
|
|
3
3
|
that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
|
|
4
4
|
(LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
|
|
5
|
-
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
5
|
+
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
6
|
+
|
|
7
|
+
It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
|
|
8
|
+
U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
12
|
import re
|
|
10
|
-
from typing import Dict, List
|
|
13
|
+
from typing import Dict, List, Tuple
|
|
11
14
|
import urllib.parse
|
|
12
15
|
|
|
13
16
|
CREDENTIAL_WORDS = (
|
|
@@ -22,20 +25,28 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
22
25
|
"session": ("token", "id"),
|
|
23
26
|
"code": ("login", "auth", "access", "session"),
|
|
24
27
|
}
|
|
28
|
+
#: ECMAScript's \s, as a character class body.
|
|
29
|
+
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
30
|
+
_NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
25
31
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
26
|
-
SECRET_VALUE = re.compile(r"(^bearer
|
|
27
|
-
re.IGNORECASE | re.
|
|
32
|
+
SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
|
|
33
|
+
r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
|
|
28
34
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
29
|
-
_BEARER_INSIDE = re.compile(r"
|
|
30
|
-
|
|
35
|
+
_BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
36
|
+
re.IGNORECASE | re.ASCII)
|
|
37
|
+
_PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
|
|
38
|
+
_WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
|
|
39
|
+
_ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
|
|
31
40
|
_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
41
|
+
#: WHATWG's special schemes that carry an authority with user information (file has none).
|
|
42
|
+
_SPECIAL = ("ftp", "http", "https", "ws", "wss")
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
def name_words(name: str) -> List[str]:
|
|
35
46
|
"""A name's words: split at `.`, `_`, `-` and camelCase humps, lowercased (`APIKey` is `api key`)."""
|
|
36
47
|
spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
|
|
37
48
|
spaced = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", spaced)
|
|
38
|
-
return [w.lower() for w in
|
|
49
|
+
return [w.lower() for w in _WORD_SPLIT.split(spaced) if w]
|
|
39
50
|
|
|
40
51
|
|
|
41
52
|
def _with_singulars(words: List[str]) -> List[str]:
|
|
@@ -73,7 +84,7 @@ def secret_argument(value: str) -> bool:
|
|
|
73
84
|
so a folder path holding a session id is not taken for a key."""
|
|
74
85
|
if JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
|
|
75
86
|
return True
|
|
76
|
-
for piece in
|
|
87
|
+
for piece in _ARGUMENT_SPLIT.split(value):
|
|
77
88
|
if _path_like(piece):
|
|
78
89
|
if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
79
90
|
return True
|
|
@@ -93,7 +104,8 @@ def _decode_keeping_plus(raw: str) -> str:
|
|
|
93
104
|
|
|
94
105
|
|
|
95
106
|
def _form_name(text: str) -> str:
|
|
96
|
-
|
|
107
|
+
# URLSearchParams drops one leading `?` from the text it is given (0.56.0 unit r7).
|
|
108
|
+
pairs = urllib.parse.parse_qsl(text[1:] if text.startswith("?") else text, keep_blank_values=True)
|
|
97
109
|
return pairs[0][0] if pairs else ""
|
|
98
110
|
|
|
99
111
|
|
|
@@ -109,23 +121,66 @@ def raw_values(query: str) -> Dict[str, List[str]]:
|
|
|
109
121
|
return values
|
|
110
122
|
|
|
111
123
|
|
|
124
|
+
#: The C0 controls and space, U+0000 to U+0020: what WHATWG URL parsing trims from a URL's ends.
|
|
125
|
+
_C0_OR_SPACE = "".join(map(chr, range(0x21)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_LONE_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _percent_encode(text: str, also: str) -> str:
|
|
132
|
+
"""WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
|
|
133
|
+
above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
|
|
134
|
+
`new URL` hands the contract's secretUrl the text in this form, so a malformed escape left in it is read the same
|
|
135
|
+
way by both ports (0.56.0 unit r8)."""
|
|
136
|
+
out = []
|
|
137
|
+
for char in _LONE_SURROGATE.sub("\ufffd", text):
|
|
138
|
+
if ord(char) <= 0x20 or ord(char) > 0x7E or char in '"<>' or char in also:
|
|
139
|
+
out.append("".join("%%%02X" % byte for byte in char.encode("utf-8")))
|
|
140
|
+
else:
|
|
141
|
+
out.append(char)
|
|
142
|
+
return "".join(out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _url_parts(value: str) -> Tuple[str, str, str]:
|
|
146
|
+
"""The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
|
|
147
|
+
refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
|
|
148
|
+
r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
|
|
149
|
+
after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
|
|
150
|
+
next slash, backslash, `?` or `#`, so `https:u:p@h` and `https:\\u:p@h` carry the password p; any other scheme
|
|
151
|
+
has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
|
|
152
|
+
last `@`, and the password follows its first `:`. A host `new URL` would refuse is not checked: such a URL is read
|
|
153
|
+
for its password and parameters all the same, the safe direction for a detector."""
|
|
154
|
+
scheme, _, rest = value.partition(":")
|
|
155
|
+
rest, _, fragment = rest.partition("#")
|
|
156
|
+
rest, _, query = rest.partition("?")
|
|
157
|
+
lowered = scheme.lower()
|
|
158
|
+
if lowered in _SPECIAL:
|
|
159
|
+
authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
|
|
160
|
+
elif lowered != "file" and rest.startswith("//"):
|
|
161
|
+
authority = rest[2:].partition("/")[0]
|
|
162
|
+
else:
|
|
163
|
+
authority = ""
|
|
164
|
+
userinfo, at, _ = authority.rpartition("@")
|
|
165
|
+
return ((userinfo.partition(":")[2] if at else ""), _percent_encode(query, "'" if lowered in _SPECIAL or lowered == "file" else ""), # file is special too
|
|
166
|
+
_percent_encode(fragment, "`"))
|
|
167
|
+
|
|
168
|
+
|
|
112
169
|
def secret_url(value: str) -> bool:
|
|
113
170
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
114
171
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
115
172
|
a value, or whose value is shaped like one. A string that is not an absolute URL is checked as an argument only."""
|
|
116
173
|
if secret_argument(value):
|
|
117
174
|
return True
|
|
118
|
-
|
|
175
|
+
# What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
|
|
176
|
+
# refuses), then remove every tab, CR and LF.
|
|
177
|
+
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
178
|
+
if not _SCHEME.match(value):
|
|
119
179
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
except ValueError:
|
|
123
|
-
return False
|
|
124
|
-
if not parts.scheme:
|
|
125
|
-
return False
|
|
126
|
-
if parts.password:
|
|
180
|
+
password, query, fragment = _url_parts(value)
|
|
181
|
+
if password:
|
|
127
182
|
return True
|
|
128
|
-
for part in (
|
|
183
|
+
for part in (query, fragment):
|
|
129
184
|
for name, raws in raw_values(part).items():
|
|
130
185
|
if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
|
|
131
186
|
return True
|
|
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
14
14
|
compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); 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.
|
|
15
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.56.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
@@ -104,8 +104,8 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
|
|
|
104
104
|
every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
|
|
105
105
|
The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
|
|
106
106
|
session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
|
|
107
|
-
stays moved for good (
|
|
108
|
-
or its elicitation). Read [the board reference](references/board.md).
|
|
107
|
+
stays moved for good (the operator reaches it through an agent that relays their decision in `_meta`, the service's
|
|
108
|
+
page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
|
|
109
109
|
|
|
110
110
|
## Step 2: the non-negotiables
|
|
111
111
|
|
|
@@ -260,8 +260,9 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
|
|
|
260
260
|
`extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
|
|
261
261
|
`_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
|
|
262
262
|
A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
|
|
263
|
-
answer as today, except the board responder's take-back action, declared `relayed` now
|
|
264
|
-
|
|
263
|
+
answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
|
|
264
|
+
`_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
|
|
265
|
+
([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
|
|
265
266
|
refusals.
|
|
266
267
|
|
|
267
268
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|