@passioncode-ai/passioncode 0.1.16 → 0.1.17
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 +7 -0
- package/family.json +1 -1
- 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 +13 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +92 -0
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +86 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +92 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +86 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +13 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +92 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +86 -0
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.17 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Pin Fabric Agent Adapter v0.5.6 so the installed set carries its dashboard deep-link procedure and portable handoff checker. The Observatory Log and working-in-passioncode members keep their existing behavior.
|
|
8
|
+
- Dashboard routing evidence belongs to the [adapter handoff](https://github.com/passioncode-ai/fabric-agent-adapter/blob/v0.5.6/docs/handoffs/2026-10-01-dashboard-links.md); the launcher installs these bytes through its existing plugin and agents-hub channels.
|
|
9
|
+
|
|
3
10
|
## 0.1.16 - 2026-10-01
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/family.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
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.5.
|
|
14
|
+
"version": "0.5.6",
|
|
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.17",
|
|
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.17",
|
|
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.5.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.5.
|
|
10
|
+
"ref": "v0.5.6",
|
|
11
|
+
"commit": "1b473987feeeb04ee67a1122d6baadc83bc03816",
|
|
12
|
+
"version": "0.5.6",
|
|
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:bf48b5d4c4163d43485e85c73e393a71b31842ae70bbd7133c1d5a769f8bc9eb",
|
|
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.17",
|
|
65
65
|
"commit": null,
|
|
66
|
-
"version": "0.1.
|
|
66
|
+
"version": "0.1.17",
|
|
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:b8f988dc498dbd00ef9e1d0e3fb93fe9069d8d8758623ff47bd225d3a63fffa4",
|
|
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.5.
|
|
6
|
+
"version": "0.5.6",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -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.5.
|
|
8
|
+
version: "0.5.6"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "74d3852f122f5ca5cbc4138a201483531dfa5006"
|
|
11
11
|
---
|
|
@@ -213,3 +213,15 @@ Report:
|
|
|
213
213
|
|
|
214
214
|
Never summarize the outcome as “Fabric-compatible” unless all required admission gates
|
|
215
215
|
passed against a real provider and the project binding is valid.
|
|
216
|
+
|
|
217
|
+
## Dashboard handoff — also for consumers
|
|
218
|
+
|
|
219
|
+
When handing a registered service dashboard to a person, or writing the generated
|
|
220
|
+
agent's completion/notification instructions, read and apply
|
|
221
|
+
[dashboard links](references/dashboard-links.md): use the host's `open_link`,
|
|
222
|
+
check the target device and host, and run the bundled output gate before delivery.
|
|
223
|
+
An installed host failure never means browser fallback. This applies in Claude
|
|
224
|
+
Code, Codex and other providers; it needs no host-specific hook. MCP unavailable:
|
|
225
|
+
report the unresolved capability or use an approved project resolver; no automatic
|
|
226
|
+
client configuration. Python unavailable: perform the reference's manual checks
|
|
227
|
+
and report the executable gate NOT_RUN. Headless agents need no dashboard.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Dashboard links — producer and consumer rule
|
|
2
|
+
|
|
3
|
+
Read when an agent creates a dashboard, implements `<tool> dashboard`, or hands
|
|
4
|
+
an existing Fabric service dashboard to a person. This also applies to generated
|
|
5
|
+
agent instructions and completion messages. Headless agents need no dashboard;
|
|
6
|
+
API origins, external docs and OAuth links are not dashboard actions.
|
|
7
|
+
|
|
8
|
+
## Resolve, then hand over
|
|
9
|
+
|
|
10
|
+
1. Discover the target computer and exact registered `service.id.instance`.
|
|
11
|
+
Never guess a key from a project name or port. `default` and `preview` differ.
|
|
12
|
+
2. Discover Fabric Dashboards tools by their descriptions; provider tool prefixes
|
|
13
|
+
differ. Use `link` with service + path (including the page's query/fragment),
|
|
14
|
+
or pass a known local service URL to it. For a root, `list_services.open_link`
|
|
15
|
+
is also authoritative. Hand back **the returned `open_link` as the primary
|
|
16
|
+
dashboard action**. Do not rebuild its URI or double-encode `path`.
|
|
17
|
+
3. Use `host_status`, where offered, to distinguish available, not_installed,
|
|
18
|
+
unknown, incompatible and handler_mismatch. It is a read, not a GUI launch.
|
|
19
|
+
With an older server lacking that tool, host readiness is unverified: use a
|
|
20
|
+
compatible host resolver or report the limitation, not presumed absence.
|
|
21
|
+
4. A request for a link opens nothing. A request to open uses the host `open`
|
|
22
|
+
tool/approved opener, which starts or focuses the existing Fabric Dashboards
|
|
23
|
+
and reuses the service view. `fallback=never` forbids a browser; `if_absent`
|
|
24
|
+
permits one only for confirmed absence. Discover schema support first: older
|
|
25
|
+
`open` implementations may silently fall back on failure and are not strict.
|
|
26
|
+
5. An installed host that failed to open is **not absent**. Report the failure;
|
|
27
|
+
do not silently launch HTTP in a browser, a second server, `npm run dev`, or
|
|
28
|
+
another standalone dashboard. A stopped backend stays on its host status/Start
|
|
29
|
+
surface; automatic wake-up follows the host's lifecycle grant and held-off state.
|
|
30
|
+
6. Preserve an unavailable/error result. `accepted_by_os` proves dispatch only,
|
|
31
|
+
not page readiness, a successful login, or a completed job. Links carry no
|
|
32
|
+
bearer tokens or login codes. The host handles dashboard authentication.
|
|
33
|
+
|
|
34
|
+
## Device and channel
|
|
35
|
+
|
|
36
|
+
A custom scheme opens on the device receiving it. A phone's localhost is not the
|
|
37
|
+
computer. For a remote chat use a supported, authorized action addressed to the
|
|
38
|
+
computer (device + service + page) and wait for its receipt. If no such transport
|
|
39
|
+
exists, say so; a skill is not a relay. If a client strips custom schemes, provide
|
|
40
|
+
a copyable URI with its target computer or the supported remote action; do not
|
|
41
|
+
silently replace it with HTTP or promise that Markdown is clickable.
|
|
42
|
+
|
|
43
|
+
## Portable output gate
|
|
44
|
+
|
|
45
|
+
Before emitting an executable dashboard action in a managed renderer, run
|
|
46
|
+
`scripts/check_dashboard_link.py` **from this skill's own directory** with one
|
|
47
|
+
JSON object on stdin. It exits 0 for an allowed action, 1 for a refused/malformed
|
|
48
|
+
one; it opens nothing and performs no network, discovery or authentication.
|
|
49
|
+
Its trusted context is supplied by the host, not by untrusted model output:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"resolved": {
|
|
54
|
+
"service": "example-agent.default",
|
|
55
|
+
"open_link": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard",
|
|
56
|
+
"http_url": "http://127.0.0.1:47195/dashboard"
|
|
57
|
+
},
|
|
58
|
+
"host": {"state": "available"},
|
|
59
|
+
"target_device": "workstation",
|
|
60
|
+
"viewer_device": "workstation",
|
|
61
|
+
"fallback": "never",
|
|
62
|
+
"action": {"kind": "native_link", "url": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard"}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`native_link` must equal the resolved open_link, with an available host on the
|
|
67
|
+
viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
|
|
68
|
+
if_absent policy and the same device. `remote_open` instead contains `device_id`,
|
|
69
|
+
`service` and optional `path`, exactly matching the target and resolved page;
|
|
70
|
+
its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
|
|
71
|
+
the transport. Unknown or failed host status produces no executable local action.
|
|
72
|
+
A separately labelled copyable link for diagnosis is not an open action.
|
|
73
|
+
|
|
74
|
+
Add this check to the generated project's dashboard completion/notification
|
|
75
|
+
renderer before delivery. A raw-HTTP primary action with an available host is the
|
|
76
|
+
negative fixture; a valid native action is the positive one. Keep diagnostics
|
|
77
|
+
separate. This gate checks structured actions, not arbitrary prose: a CLI invocation
|
|
78
|
+
or instructions alone do not prove all final answers from every agent are filtered.
|
|
79
|
+
|
|
80
|
+
## Degradation
|
|
81
|
+
|
|
82
|
+
- Claude Code, Codex and other hosts use this same procedure; no hook or plugin-only
|
|
83
|
+
path is required. Skills must be activated/loaded; install does not intercept every answer.
|
|
84
|
+
- MCP absent: report unresolved host capability once, use the project's approved
|
|
85
|
+
resolver/CLI if available. Do not write machine or agent MCP config automatically.
|
|
86
|
+
- Python absent: perform the comparisons above manually; mark the executable gate
|
|
87
|
+
NOT_RUN. Do not label the output mechanically validated.
|
|
88
|
+
|
|
89
|
+
Syntax is owned by Fabric Dashboards and `@passioncode-ai/fabric-service-host`:
|
|
90
|
+
[ADR-0005](https://github.com/passioncode-ai/fabric-dashboards/blob/51a7a80acf42781d7bec304cc87c655d48cc309b/docs/adr/0005-service-links.md).
|
|
91
|
+
The gate accepts canonical links and the legacy `open?service=` form returned by
|
|
92
|
+
older hosts; it does not generate either dialect or redefine the Fabric wire contract.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check one structured dashboard handoff from stdin. No I/O effects; exit 0/1.
|
|
3
|
+
|
|
4
|
+
The caller supplies a trusted host resolver result and observed device/host context.
|
|
5
|
+
This is an output correctness gate, not authentication, discovery, or a relay.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from urllib.parse import parse_qs, urlsplit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require(condition, reason):
|
|
14
|
+
if not condition:
|
|
15
|
+
raise ValueError(reason)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_path(path):
|
|
19
|
+
return (isinstance(path, str) and path.startswith('/') and not path.startswith('//')
|
|
20
|
+
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(data):
|
|
24
|
+
require(isinstance(data, dict), 'invalid_input')
|
|
25
|
+
resolved, host, action = (data.get(k) for k in ('resolved', 'host', 'action'))
|
|
26
|
+
require(all(isinstance(v, dict) for v in (resolved, host, action)), 'missing_context')
|
|
27
|
+
service, native, http = (resolved.get(k) for k in ('service', 'open_link', 'http_url'))
|
|
28
|
+
require(isinstance(service, str) and re.fullmatch(r'[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*', service), 'invalid_service')
|
|
29
|
+
require(isinstance(native, str) and isinstance(http, str), 'missing_resolved_links')
|
|
30
|
+
require(not re.search(r'[\\\x00-\x20\x7f]', native + http), 'unsafe_url')
|
|
31
|
+
deep, plain = urlsplit(native), urlsplit(http)
|
|
32
|
+
require(deep.scheme == 'fabric-dashboards' and not deep.username and not deep.password and not deep.port and not deep.fragment, 'invalid_deep_link')
|
|
33
|
+
query = parse_qs(deep.query, keep_blank_values=True, strict_parsing=True)
|
|
34
|
+
if deep.netloc == 'service':
|
|
35
|
+
require(deep.path in ('/' + service, '/' + service + '/') and set(query) <= {'path'}, 'wrong_service_or_parameter')
|
|
36
|
+
else:
|
|
37
|
+
require(deep.netloc == 'open' and deep.path in ('', '/') and query.get('service') == [service] and set(query) <= {'service', 'path'}, 'invalid_legacy_link')
|
|
38
|
+
require('path' not in query or len(query['path']) == 1, 'duplicate_path')
|
|
39
|
+
page = query.get('path', [None])[0]
|
|
40
|
+
require(page is None or safe_path(page), 'unsafe_path')
|
|
41
|
+
require(plain.scheme == 'http' and plain.hostname in ('127.0.0.1', 'localhost', '::1') and plain.port is not None
|
|
42
|
+
and not plain.username and not plain.password, 'invalid_http_fallback')
|
|
43
|
+
http_page = plain.path + ('?' + plain.query if plain.query else '') + ('#' + plain.fragment if plain.fragment else '')
|
|
44
|
+
require(safe_path(http_page) and http_page == (page or '/'), 'mismatched_page')
|
|
45
|
+
sensitive = {'token', 'access_token', 'refresh_token', 'password', 'secret', 'login_code'}
|
|
46
|
+
page_query = parse_qs(urlsplit(http_page).query)
|
|
47
|
+
require(not sensitive.intersection(k.lower() for k in page_query), 'credential_query')
|
|
48
|
+
require(urlsplit(http_page).path != '/fabric/v1/login', 'login_link_is_not_dashboard')
|
|
49
|
+
state = host.get('state')
|
|
50
|
+
require(state in ('available', 'not_installed', 'unknown', 'incompatible', 'handler_mismatch', 'unsupported'), 'unknown_host_state')
|
|
51
|
+
target, viewer = data.get('target_device'), data.get('viewer_device')
|
|
52
|
+
require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
|
|
53
|
+
fallback = data.get('fallback', 'if_absent')
|
|
54
|
+
require(fallback in ('if_absent', 'never'), 'invalid_fallback')
|
|
55
|
+
kind = action.get('kind')
|
|
56
|
+
if kind == 'remote_open':
|
|
57
|
+
require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
|
|
58
|
+
require(action.get('device_id') == target and action.get('service') == service and action.get('path') == page, 'wrong_remote_target')
|
|
59
|
+
require(viewer != target, 'remote_action_on_local_device')
|
|
60
|
+
elif kind == 'native_link':
|
|
61
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == native, 'not_host_provided_link')
|
|
62
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
63
|
+
require(state == 'available', 'host_unavailable')
|
|
64
|
+
elif kind == 'browser_link':
|
|
65
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
|
|
66
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
67
|
+
require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError('unsupported_dashboard_action')
|
|
70
|
+
return {'ok': True, 'kind': kind}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
try:
|
|
75
|
+
raw = sys.stdin.read(65537)
|
|
76
|
+
require(len(raw) <= 65536, 'input_too_large')
|
|
77
|
+
result = check(json.loads(raw))
|
|
78
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
79
|
+
# Fixed reasons only: never echo a submitted URL, token or JSON blob.
|
|
80
|
+
result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
|
|
81
|
+
print(json.dumps(result))
|
|
82
|
+
return 0 if result['ok'] else 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
sys.exit(main())
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: building-fabric-services
|
|
3
3
|
description: >-
|
|
4
|
-
Use when
|
|
4
|
+
Use when handing out or opening a Fabric service dashboard, or building a local agent service on
|
|
5
5
|
a macOS machine — «сделай агенту дашборд», «локальный сервис агента», «дашборд должен всегда
|
|
6
6
|
работать и не плодить копии», «где агенту хранить настройки», «подключи агента к Fabric
|
|
7
7
|
Dashboards», "make this agent a local service", "always-on dashboard", "fabric-service
|
|
@@ -12,10 +12,10 @@ description: >-
|
|
|
12
12
|
reference kits and a live conformance probe. NOT for a one-off script or cron job, a hosted
|
|
13
13
|
SaaS, the provider manifest itself (adapting-projects-to-fabric), or building Fabric Dashboards.
|
|
14
14
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
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).
|
|
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). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
16
16
|
metadata:
|
|
17
17
|
author: PassionCode.ai
|
|
18
|
-
version: "0.5.
|
|
18
|
+
version: "0.5.6"
|
|
19
19
|
contract-version: "0.1.0"
|
|
20
20
|
extension: "fabric-service/0.1"
|
|
21
21
|
extension-commit: "74d3852f122f5ca5cbc4138a201483531dfa5006"
|
|
@@ -275,3 +275,15 @@ Report: the `id`, port and surfaces with the Step 0 answers; files created or ch
|
|
|
275
275
|
the probe table verbatim with every `NOT_RUN` explained; the lock and kill-9 results
|
|
276
276
|
with pids; and the one next command. Never call a service conformant while the probe
|
|
277
277
|
reports a `FAIL`.
|
|
278
|
+
|
|
279
|
+
## Dashboard handoff — also for consumers
|
|
280
|
+
|
|
281
|
+
When handing a registered service dashboard to a person, or writing the generated
|
|
282
|
+
agent's completion/notification instructions, read and apply
|
|
283
|
+
[dashboard links](references/dashboard-links.md): use the host's `open_link`,
|
|
284
|
+
check the target device and host, and run the bundled output gate before delivery.
|
|
285
|
+
An installed host failure never means browser fallback. This applies in Claude
|
|
286
|
+
Code, Codex and other providers; it needs no host-specific hook. MCP unavailable:
|
|
287
|
+
report the unresolved capability or use an approved project resolver; no automatic
|
|
288
|
+
client configuration. Python unavailable: perform the reference's manual checks
|
|
289
|
+
and report the executable gate NOT_RUN. Headless agents need no dashboard.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Dashboard links — producer and consumer rule
|
|
2
|
+
|
|
3
|
+
Read when an agent creates a dashboard, implements `<tool> dashboard`, or hands
|
|
4
|
+
an existing Fabric service dashboard to a person. This also applies to generated
|
|
5
|
+
agent instructions and completion messages. Headless agents need no dashboard;
|
|
6
|
+
API origins, external docs and OAuth links are not dashboard actions.
|
|
7
|
+
|
|
8
|
+
## Resolve, then hand over
|
|
9
|
+
|
|
10
|
+
1. Discover the target computer and exact registered `service.id.instance`.
|
|
11
|
+
Never guess a key from a project name or port. `default` and `preview` differ.
|
|
12
|
+
2. Discover Fabric Dashboards tools by their descriptions; provider tool prefixes
|
|
13
|
+
differ. Use `link` with service + path (including the page's query/fragment),
|
|
14
|
+
or pass a known local service URL to it. For a root, `list_services.open_link`
|
|
15
|
+
is also authoritative. Hand back **the returned `open_link` as the primary
|
|
16
|
+
dashboard action**. Do not rebuild its URI or double-encode `path`.
|
|
17
|
+
3. Use `host_status`, where offered, to distinguish available, not_installed,
|
|
18
|
+
unknown, incompatible and handler_mismatch. It is a read, not a GUI launch.
|
|
19
|
+
With an older server lacking that tool, host readiness is unverified: use a
|
|
20
|
+
compatible host resolver or report the limitation, not presumed absence.
|
|
21
|
+
4. A request for a link opens nothing. A request to open uses the host `open`
|
|
22
|
+
tool/approved opener, which starts or focuses the existing Fabric Dashboards
|
|
23
|
+
and reuses the service view. `fallback=never` forbids a browser; `if_absent`
|
|
24
|
+
permits one only for confirmed absence. Discover schema support first: older
|
|
25
|
+
`open` implementations may silently fall back on failure and are not strict.
|
|
26
|
+
5. An installed host that failed to open is **not absent**. Report the failure;
|
|
27
|
+
do not silently launch HTTP in a browser, a second server, `npm run dev`, or
|
|
28
|
+
another standalone dashboard. A stopped backend stays on its host status/Start
|
|
29
|
+
surface; automatic wake-up follows the host's lifecycle grant and held-off state.
|
|
30
|
+
6. Preserve an unavailable/error result. `accepted_by_os` proves dispatch only,
|
|
31
|
+
not page readiness, a successful login, or a completed job. Links carry no
|
|
32
|
+
bearer tokens or login codes. The host handles dashboard authentication.
|
|
33
|
+
|
|
34
|
+
## Device and channel
|
|
35
|
+
|
|
36
|
+
A custom scheme opens on the device receiving it. A phone's localhost is not the
|
|
37
|
+
computer. For a remote chat use a supported, authorized action addressed to the
|
|
38
|
+
computer (device + service + page) and wait for its receipt. If no such transport
|
|
39
|
+
exists, say so; a skill is not a relay. If a client strips custom schemes, provide
|
|
40
|
+
a copyable URI with its target computer or the supported remote action; do not
|
|
41
|
+
silently replace it with HTTP or promise that Markdown is clickable.
|
|
42
|
+
|
|
43
|
+
## Portable output gate
|
|
44
|
+
|
|
45
|
+
Before emitting an executable dashboard action in a managed renderer, run
|
|
46
|
+
`scripts/check_dashboard_link.py` **from this skill's own directory** with one
|
|
47
|
+
JSON object on stdin. It exits 0 for an allowed action, 1 for a refused/malformed
|
|
48
|
+
one; it opens nothing and performs no network, discovery or authentication.
|
|
49
|
+
Its trusted context is supplied by the host, not by untrusted model output:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"resolved": {
|
|
54
|
+
"service": "example-agent.default",
|
|
55
|
+
"open_link": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard",
|
|
56
|
+
"http_url": "http://127.0.0.1:47195/dashboard"
|
|
57
|
+
},
|
|
58
|
+
"host": {"state": "available"},
|
|
59
|
+
"target_device": "workstation",
|
|
60
|
+
"viewer_device": "workstation",
|
|
61
|
+
"fallback": "never",
|
|
62
|
+
"action": {"kind": "native_link", "url": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard"}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`native_link` must equal the resolved open_link, with an available host on the
|
|
67
|
+
viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
|
|
68
|
+
if_absent policy and the same device. `remote_open` instead contains `device_id`,
|
|
69
|
+
`service` and optional `path`, exactly matching the target and resolved page;
|
|
70
|
+
its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
|
|
71
|
+
the transport. Unknown or failed host status produces no executable local action.
|
|
72
|
+
A separately labelled copyable link for diagnosis is not an open action.
|
|
73
|
+
|
|
74
|
+
Add this check to the generated project's dashboard completion/notification
|
|
75
|
+
renderer before delivery. A raw-HTTP primary action with an available host is the
|
|
76
|
+
negative fixture; a valid native action is the positive one. Keep diagnostics
|
|
77
|
+
separate. This gate checks structured actions, not arbitrary prose: a CLI invocation
|
|
78
|
+
or instructions alone do not prove all final answers from every agent are filtered.
|
|
79
|
+
|
|
80
|
+
## Degradation
|
|
81
|
+
|
|
82
|
+
- Claude Code, Codex and other hosts use this same procedure; no hook or plugin-only
|
|
83
|
+
path is required. Skills must be activated/loaded; install does not intercept every answer.
|
|
84
|
+
- MCP absent: report unresolved host capability once, use the project's approved
|
|
85
|
+
resolver/CLI if available. Do not write machine or agent MCP config automatically.
|
|
86
|
+
- Python absent: perform the comparisons above manually; mark the executable gate
|
|
87
|
+
NOT_RUN. Do not label the output mechanically validated.
|
|
88
|
+
|
|
89
|
+
Syntax is owned by Fabric Dashboards and `@passioncode-ai/fabric-service-host`:
|
|
90
|
+
[ADR-0005](https://github.com/passioncode-ai/fabric-dashboards/blob/51a7a80acf42781d7bec304cc87c655d48cc309b/docs/adr/0005-service-links.md).
|
|
91
|
+
The gate accepts canonical links and the legacy `open?service=` form returned by
|
|
92
|
+
older hosts; it does not generate either dialect or redefine the Fabric wire contract.
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
## CLI for people and scripts
|
|
18
18
|
|
|
19
19
|
`<tool> service status|start|stop|restart` (launchctl on the label, then the
|
|
20
|
-
well-known document), `<tool> dashboard` (
|
|
21
|
-
|
|
20
|
+
well-known document), `<tool> dashboard` (resolve the exact service and use the Fabric Dashboards host
|
|
21
|
+
opener first; installed-but-failed never falls back to a browser; only confirmed
|
|
22
|
+
absence permits explicit browser fallback and its one-time login flow), `<tool> doctor --json`
|
|
22
23
|
(the same checks the probe runs, plus provider keys), `--json` on every command. Print
|
|
23
24
|
a sign-in link only when stdout is a terminal.
|
|
24
25
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check one structured dashboard handoff from stdin. No I/O effects; exit 0/1.
|
|
3
|
+
|
|
4
|
+
The caller supplies a trusted host resolver result and observed device/host context.
|
|
5
|
+
This is an output correctness gate, not authentication, discovery, or a relay.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from urllib.parse import parse_qs, urlsplit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require(condition, reason):
|
|
14
|
+
if not condition:
|
|
15
|
+
raise ValueError(reason)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_path(path):
|
|
19
|
+
return (isinstance(path, str) and path.startswith('/') and not path.startswith('//')
|
|
20
|
+
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(data):
|
|
24
|
+
require(isinstance(data, dict), 'invalid_input')
|
|
25
|
+
resolved, host, action = (data.get(k) for k in ('resolved', 'host', 'action'))
|
|
26
|
+
require(all(isinstance(v, dict) for v in (resolved, host, action)), 'missing_context')
|
|
27
|
+
service, native, http = (resolved.get(k) for k in ('service', 'open_link', 'http_url'))
|
|
28
|
+
require(isinstance(service, str) and re.fullmatch(r'[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*', service), 'invalid_service')
|
|
29
|
+
require(isinstance(native, str) and isinstance(http, str), 'missing_resolved_links')
|
|
30
|
+
require(not re.search(r'[\\\x00-\x20\x7f]', native + http), 'unsafe_url')
|
|
31
|
+
deep, plain = urlsplit(native), urlsplit(http)
|
|
32
|
+
require(deep.scheme == 'fabric-dashboards' and not deep.username and not deep.password and not deep.port and not deep.fragment, 'invalid_deep_link')
|
|
33
|
+
query = parse_qs(deep.query, keep_blank_values=True, strict_parsing=True)
|
|
34
|
+
if deep.netloc == 'service':
|
|
35
|
+
require(deep.path in ('/' + service, '/' + service + '/') and set(query) <= {'path'}, 'wrong_service_or_parameter')
|
|
36
|
+
else:
|
|
37
|
+
require(deep.netloc == 'open' and deep.path in ('', '/') and query.get('service') == [service] and set(query) <= {'service', 'path'}, 'invalid_legacy_link')
|
|
38
|
+
require('path' not in query or len(query['path']) == 1, 'duplicate_path')
|
|
39
|
+
page = query.get('path', [None])[0]
|
|
40
|
+
require(page is None or safe_path(page), 'unsafe_path')
|
|
41
|
+
require(plain.scheme == 'http' and plain.hostname in ('127.0.0.1', 'localhost', '::1') and plain.port is not None
|
|
42
|
+
and not plain.username and not plain.password, 'invalid_http_fallback')
|
|
43
|
+
http_page = plain.path + ('?' + plain.query if plain.query else '') + ('#' + plain.fragment if plain.fragment else '')
|
|
44
|
+
require(safe_path(http_page) and http_page == (page or '/'), 'mismatched_page')
|
|
45
|
+
sensitive = {'token', 'access_token', 'refresh_token', 'password', 'secret', 'login_code'}
|
|
46
|
+
page_query = parse_qs(urlsplit(http_page).query)
|
|
47
|
+
require(not sensitive.intersection(k.lower() for k in page_query), 'credential_query')
|
|
48
|
+
require(urlsplit(http_page).path != '/fabric/v1/login', 'login_link_is_not_dashboard')
|
|
49
|
+
state = host.get('state')
|
|
50
|
+
require(state in ('available', 'not_installed', 'unknown', 'incompatible', 'handler_mismatch', 'unsupported'), 'unknown_host_state')
|
|
51
|
+
target, viewer = data.get('target_device'), data.get('viewer_device')
|
|
52
|
+
require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
|
|
53
|
+
fallback = data.get('fallback', 'if_absent')
|
|
54
|
+
require(fallback in ('if_absent', 'never'), 'invalid_fallback')
|
|
55
|
+
kind = action.get('kind')
|
|
56
|
+
if kind == 'remote_open':
|
|
57
|
+
require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
|
|
58
|
+
require(action.get('device_id') == target and action.get('service') == service and action.get('path') == page, 'wrong_remote_target')
|
|
59
|
+
require(viewer != target, 'remote_action_on_local_device')
|
|
60
|
+
elif kind == 'native_link':
|
|
61
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == native, 'not_host_provided_link')
|
|
62
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
63
|
+
require(state == 'available', 'host_unavailable')
|
|
64
|
+
elif kind == 'browser_link':
|
|
65
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
|
|
66
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
67
|
+
require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError('unsupported_dashboard_action')
|
|
70
|
+
return {'ok': True, 'kind': kind}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
try:
|
|
75
|
+
raw = sys.stdin.read(65537)
|
|
76
|
+
require(len(raw) <= 65536, 'input_too_large')
|
|
77
|
+
result = check(json.loads(raw))
|
|
78
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
79
|
+
# Fixed reasons only: never echo a submitted URL, token or JSON blob.
|
|
80
|
+
result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
|
|
81
|
+
print(json.dumps(result))
|
|
82
|
+
return 0 if result['ok'] else 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
sys.exit(main())
|
|
@@ -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. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.5.
|
|
8
|
+
version: "0.5.6"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "74d3852f122f5ca5cbc4138a201483531dfa5006"
|
|
11
11
|
---
|
|
@@ -160,3 +160,15 @@ and which traps became fixtures; created files; contract version and commit; the
|
|
|
160
160
|
table with `PASS`, `FAIL`, `NOT_RUN`, or `NOT_VERIFIED`; remaining placeholders; and the
|
|
161
161
|
exact next command. Never summarize the outcome as "Fabric-compatible" unless every
|
|
162
162
|
required admission gate passed against the real provider.
|
|
163
|
+
|
|
164
|
+
## Dashboard handoff — also for consumers
|
|
165
|
+
|
|
166
|
+
When handing a registered service dashboard to a person, or writing the generated
|
|
167
|
+
agent's completion/notification instructions, read and apply
|
|
168
|
+
[dashboard links](references/dashboard-links.md): use the host's `open_link`,
|
|
169
|
+
check the target device and host, and run the bundled output gate before delivery.
|
|
170
|
+
An installed host failure never means browser fallback. This applies in Claude
|
|
171
|
+
Code, Codex and other providers; it needs no host-specific hook. MCP unavailable:
|
|
172
|
+
report the unresolved capability or use an approved project resolver; no automatic
|
|
173
|
+
client configuration. Python unavailable: perform the reference's manual checks
|
|
174
|
+
and report the executable gate NOT_RUN. Headless agents need no dashboard.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Dashboard links — producer and consumer rule
|
|
2
|
+
|
|
3
|
+
Read when an agent creates a dashboard, implements `<tool> dashboard`, or hands
|
|
4
|
+
an existing Fabric service dashboard to a person. This also applies to generated
|
|
5
|
+
agent instructions and completion messages. Headless agents need no dashboard;
|
|
6
|
+
API origins, external docs and OAuth links are not dashboard actions.
|
|
7
|
+
|
|
8
|
+
## Resolve, then hand over
|
|
9
|
+
|
|
10
|
+
1. Discover the target computer and exact registered `service.id.instance`.
|
|
11
|
+
Never guess a key from a project name or port. `default` and `preview` differ.
|
|
12
|
+
2. Discover Fabric Dashboards tools by their descriptions; provider tool prefixes
|
|
13
|
+
differ. Use `link` with service + path (including the page's query/fragment),
|
|
14
|
+
or pass a known local service URL to it. For a root, `list_services.open_link`
|
|
15
|
+
is also authoritative. Hand back **the returned `open_link` as the primary
|
|
16
|
+
dashboard action**. Do not rebuild its URI or double-encode `path`.
|
|
17
|
+
3. Use `host_status`, where offered, to distinguish available, not_installed,
|
|
18
|
+
unknown, incompatible and handler_mismatch. It is a read, not a GUI launch.
|
|
19
|
+
With an older server lacking that tool, host readiness is unverified: use a
|
|
20
|
+
compatible host resolver or report the limitation, not presumed absence.
|
|
21
|
+
4. A request for a link opens nothing. A request to open uses the host `open`
|
|
22
|
+
tool/approved opener, which starts or focuses the existing Fabric Dashboards
|
|
23
|
+
and reuses the service view. `fallback=never` forbids a browser; `if_absent`
|
|
24
|
+
permits one only for confirmed absence. Discover schema support first: older
|
|
25
|
+
`open` implementations may silently fall back on failure and are not strict.
|
|
26
|
+
5. An installed host that failed to open is **not absent**. Report the failure;
|
|
27
|
+
do not silently launch HTTP in a browser, a second server, `npm run dev`, or
|
|
28
|
+
another standalone dashboard. A stopped backend stays on its host status/Start
|
|
29
|
+
surface; automatic wake-up follows the host's lifecycle grant and held-off state.
|
|
30
|
+
6. Preserve an unavailable/error result. `accepted_by_os` proves dispatch only,
|
|
31
|
+
not page readiness, a successful login, or a completed job. Links carry no
|
|
32
|
+
bearer tokens or login codes. The host handles dashboard authentication.
|
|
33
|
+
|
|
34
|
+
## Device and channel
|
|
35
|
+
|
|
36
|
+
A custom scheme opens on the device receiving it. A phone's localhost is not the
|
|
37
|
+
computer. For a remote chat use a supported, authorized action addressed to the
|
|
38
|
+
computer (device + service + page) and wait for its receipt. If no such transport
|
|
39
|
+
exists, say so; a skill is not a relay. If a client strips custom schemes, provide
|
|
40
|
+
a copyable URI with its target computer or the supported remote action; do not
|
|
41
|
+
silently replace it with HTTP or promise that Markdown is clickable.
|
|
42
|
+
|
|
43
|
+
## Portable output gate
|
|
44
|
+
|
|
45
|
+
Before emitting an executable dashboard action in a managed renderer, run
|
|
46
|
+
`scripts/check_dashboard_link.py` **from this skill's own directory** with one
|
|
47
|
+
JSON object on stdin. It exits 0 for an allowed action, 1 for a refused/malformed
|
|
48
|
+
one; it opens nothing and performs no network, discovery or authentication.
|
|
49
|
+
Its trusted context is supplied by the host, not by untrusted model output:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"resolved": {
|
|
54
|
+
"service": "example-agent.default",
|
|
55
|
+
"open_link": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard",
|
|
56
|
+
"http_url": "http://127.0.0.1:47195/dashboard"
|
|
57
|
+
},
|
|
58
|
+
"host": {"state": "available"},
|
|
59
|
+
"target_device": "workstation",
|
|
60
|
+
"viewer_device": "workstation",
|
|
61
|
+
"fallback": "never",
|
|
62
|
+
"action": {"kind": "native_link", "url": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard"}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`native_link` must equal the resolved open_link, with an available host on the
|
|
67
|
+
viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
|
|
68
|
+
if_absent policy and the same device. `remote_open` instead contains `device_id`,
|
|
69
|
+
`service` and optional `path`, exactly matching the target and resolved page;
|
|
70
|
+
its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
|
|
71
|
+
the transport. Unknown or failed host status produces no executable local action.
|
|
72
|
+
A separately labelled copyable link for diagnosis is not an open action.
|
|
73
|
+
|
|
74
|
+
Add this check to the generated project's dashboard completion/notification
|
|
75
|
+
renderer before delivery. A raw-HTTP primary action with an available host is the
|
|
76
|
+
negative fixture; a valid native action is the positive one. Keep diagnostics
|
|
77
|
+
separate. This gate checks structured actions, not arbitrary prose: a CLI invocation
|
|
78
|
+
or instructions alone do not prove all final answers from every agent are filtered.
|
|
79
|
+
|
|
80
|
+
## Degradation
|
|
81
|
+
|
|
82
|
+
- Claude Code, Codex and other hosts use this same procedure; no hook or plugin-only
|
|
83
|
+
path is required. Skills must be activated/loaded; install does not intercept every answer.
|
|
84
|
+
- MCP absent: report unresolved host capability once, use the project's approved
|
|
85
|
+
resolver/CLI if available. Do not write machine or agent MCP config automatically.
|
|
86
|
+
- Python absent: perform the comparisons above manually; mark the executable gate
|
|
87
|
+
NOT_RUN. Do not label the output mechanically validated.
|
|
88
|
+
|
|
89
|
+
Syntax is owned by Fabric Dashboards and `@passioncode-ai/fabric-service-host`:
|
|
90
|
+
[ADR-0005](https://github.com/passioncode-ai/fabric-dashboards/blob/51a7a80acf42781d7bec304cc87c655d48cc309b/docs/adr/0005-service-links.md).
|
|
91
|
+
The gate accepts canonical links and the legacy `open?service=` form returned by
|
|
92
|
+
older hosts; it does not generate either dialect or redefine the Fabric wire contract.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check one structured dashboard handoff from stdin. No I/O effects; exit 0/1.
|
|
3
|
+
|
|
4
|
+
The caller supplies a trusted host resolver result and observed device/host context.
|
|
5
|
+
This is an output correctness gate, not authentication, discovery, or a relay.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from urllib.parse import parse_qs, urlsplit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def require(condition, reason):
|
|
14
|
+
if not condition:
|
|
15
|
+
raise ValueError(reason)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_path(path):
|
|
19
|
+
return (isinstance(path, str) and path.startswith('/') and not path.startswith('//')
|
|
20
|
+
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(data):
|
|
24
|
+
require(isinstance(data, dict), 'invalid_input')
|
|
25
|
+
resolved, host, action = (data.get(k) for k in ('resolved', 'host', 'action'))
|
|
26
|
+
require(all(isinstance(v, dict) for v in (resolved, host, action)), 'missing_context')
|
|
27
|
+
service, native, http = (resolved.get(k) for k in ('service', 'open_link', 'http_url'))
|
|
28
|
+
require(isinstance(service, str) and re.fullmatch(r'[a-z0-9][a-z0-9-]*\.[a-z0-9][a-z0-9-]*', service), 'invalid_service')
|
|
29
|
+
require(isinstance(native, str) and isinstance(http, str), 'missing_resolved_links')
|
|
30
|
+
require(not re.search(r'[\\\x00-\x20\x7f]', native + http), 'unsafe_url')
|
|
31
|
+
deep, plain = urlsplit(native), urlsplit(http)
|
|
32
|
+
require(deep.scheme == 'fabric-dashboards' and not deep.username and not deep.password and not deep.port and not deep.fragment, 'invalid_deep_link')
|
|
33
|
+
query = parse_qs(deep.query, keep_blank_values=True, strict_parsing=True)
|
|
34
|
+
if deep.netloc == 'service':
|
|
35
|
+
require(deep.path in ('/' + service, '/' + service + '/') and set(query) <= {'path'}, 'wrong_service_or_parameter')
|
|
36
|
+
else:
|
|
37
|
+
require(deep.netloc == 'open' and deep.path in ('', '/') and query.get('service') == [service] and set(query) <= {'service', 'path'}, 'invalid_legacy_link')
|
|
38
|
+
require('path' not in query or len(query['path']) == 1, 'duplicate_path')
|
|
39
|
+
page = query.get('path', [None])[0]
|
|
40
|
+
require(page is None or safe_path(page), 'unsafe_path')
|
|
41
|
+
require(plain.scheme == 'http' and plain.hostname in ('127.0.0.1', 'localhost', '::1') and plain.port is not None
|
|
42
|
+
and not plain.username and not plain.password, 'invalid_http_fallback')
|
|
43
|
+
http_page = plain.path + ('?' + plain.query if plain.query else '') + ('#' + plain.fragment if plain.fragment else '')
|
|
44
|
+
require(safe_path(http_page) and http_page == (page or '/'), 'mismatched_page')
|
|
45
|
+
sensitive = {'token', 'access_token', 'refresh_token', 'password', 'secret', 'login_code'}
|
|
46
|
+
page_query = parse_qs(urlsplit(http_page).query)
|
|
47
|
+
require(not sensitive.intersection(k.lower() for k in page_query), 'credential_query')
|
|
48
|
+
require(urlsplit(http_page).path != '/fabric/v1/login', 'login_link_is_not_dashboard')
|
|
49
|
+
state = host.get('state')
|
|
50
|
+
require(state in ('available', 'not_installed', 'unknown', 'incompatible', 'handler_mismatch', 'unsupported'), 'unknown_host_state')
|
|
51
|
+
target, viewer = data.get('target_device'), data.get('viewer_device')
|
|
52
|
+
require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
|
|
53
|
+
fallback = data.get('fallback', 'if_absent')
|
|
54
|
+
require(fallback in ('if_absent', 'never'), 'invalid_fallback')
|
|
55
|
+
kind = action.get('kind')
|
|
56
|
+
if kind == 'remote_open':
|
|
57
|
+
require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
|
|
58
|
+
require(action.get('device_id') == target and action.get('service') == service and action.get('path') == page, 'wrong_remote_target')
|
|
59
|
+
require(viewer != target, 'remote_action_on_local_device')
|
|
60
|
+
elif kind == 'native_link':
|
|
61
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == native, 'not_host_provided_link')
|
|
62
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
63
|
+
require(state == 'available', 'host_unavailable')
|
|
64
|
+
elif kind == 'browser_link':
|
|
65
|
+
require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
|
|
66
|
+
require(viewer == target, 'wrong_viewer_device')
|
|
67
|
+
require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError('unsupported_dashboard_action')
|
|
70
|
+
return {'ok': True, 'kind': kind}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
try:
|
|
75
|
+
raw = sys.stdin.read(65537)
|
|
76
|
+
require(len(raw) <= 65536, 'input_too_large')
|
|
77
|
+
result = check(json.loads(raw))
|
|
78
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
79
|
+
# Fixed reasons only: never echo a submitted URL, token or JSON blob.
|
|
80
|
+
result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
|
|
81
|
+
print(json.dumps(result))
|
|
82
|
+
return 0 if result['ok'] else 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
sys.exit(main())
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "passioncode",
|
|
4
4
|
"displayName": "PassionCode.ai",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.17",
|
|
6
6
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|