@passioncode-ai/fabric-agent-adapter 0.81.0 → 0.82.0
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 +78 -0
- package/README.md +5 -3
- package/SECURITY.md +4 -2
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +9 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +239 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +23 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +22 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +11 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +73 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +87 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +29 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +8 -153
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +35 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +77 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +156 -138
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +239 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +117 -19
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +16 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +46 -31
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +18 -16
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.7 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
|
|
2
2
|
Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
|
|
3
3
|
<script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
|
|
4
4
|
data-project="project-a" data-project-name="Project A"></script>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
(function (root) {
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
var VERSION = '1.1.
|
|
14
|
+
var VERSION = '1.1.7';
|
|
15
15
|
var COOKIE = 'fabric_button';
|
|
16
16
|
var GET_FABRIC = 'https://passioncode.ai/fabric';
|
|
17
17
|
var ABSENT_AFTER_MS = 1500;
|
|
@@ -37,8 +37,26 @@
|
|
|
37
37
|
session: ['token', 'id'],
|
|
38
38
|
code: ['login', 'auth', 'access', 'session']
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Whether /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/ matches anywhere, read in one pass (the
|
|
41
|
+
// contract's hasJwt, FAA-313): the expression backtracks from every `eyJ` of a long run, while every `eyJ` of one run
|
|
42
|
+
// shares that run's end, so only its first is tried.
|
|
43
|
+
var URL_BASE64 = /[A-Za-z0-9_-]/;
|
|
44
|
+
function runEnd(text, from) { while (from < text.length && URL_BASE64.test(text.charAt(from))) from += 1; return from; }
|
|
45
|
+
function hasJwt(text) {
|
|
46
|
+
var at = text.indexOf('eyJ');
|
|
47
|
+
while (at >= 0) {
|
|
48
|
+
var one = runEnd(text, at + 3);
|
|
49
|
+
if (one - at - 3 >= 2 && text.charAt(one) === '.') {
|
|
50
|
+
var two = runEnd(text, one + 1);
|
|
51
|
+
if (two - one - 1 >= 2 && text.charAt(two) === '.') return true;
|
|
52
|
+
}
|
|
53
|
+
at = text.indexOf('eyJ', one);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// The contract's SECRET_VALUE at the pin (DEC-0151), as the kit's fabric-secrets.mjs writes it (FAA-314, kit 1.1.7):
|
|
58
|
+
// `Bearer ` only at a value's start, a provider key's prefix in its canonical case after a boundary.
|
|
59
|
+
var SECRET_VALUE = /(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)(?:sk-(?=[A-Za-z0-9_-]{0,255}?\d)(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}))/;
|
|
42
60
|
var LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
|
|
43
61
|
|
|
44
62
|
function nameWords(name) {
|
|
@@ -60,7 +78,7 @@
|
|
|
60
78
|
function longSecret(value) {
|
|
61
79
|
return (value.match(LONG_RUN) || []).some(function (run) { return /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run); });
|
|
62
80
|
}
|
|
63
|
-
function secretValue(value) { return
|
|
81
|
+
function secretValue(value) { return hasJwt(value) || SECRET_VALUE.test(value) || longSecret(value); }
|
|
64
82
|
function decodeKeepingPlus(raw) { try { return decodeURIComponent(raw); } catch (e) { return raw; } }
|
|
65
83
|
function firstKey(text) { var it = new URLSearchParams(text).keys().next(); return it.done ? '' : it.value; }
|
|
66
84
|
function rawValues(query) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.7 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
|
|
3
3
|
reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
|
|
4
4
|
Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.7 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
|
|
3
3
|
eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
|
|
4
4
|
does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
var KIT = 'fabric-dashboard-kit';
|
|
11
|
-
var VERSION = '1.1.
|
|
11
|
+
var VERSION = '1.1.7';
|
|
12
12
|
/* fabric-dashboard-kit:theme-ids:begin */
|
|
13
13
|
var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
14
14
|
/* fabric-dashboard-kit:theme-ids:end */
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-dashboard-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"profile": "fabric-dashboard/0.1",
|
|
5
5
|
"decision": "DEC-0036",
|
|
6
6
|
"declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"contrast"
|
|
16
16
|
],
|
|
17
17
|
"files": {
|
|
18
|
-
"fabric-dashboard.css": "
|
|
19
|
-
"fabric-dashboard.js": "
|
|
20
|
-
"fabric-button.js": "
|
|
18
|
+
"fabric-dashboard.css": "549c1c1546bc9f8d2c1a579e7ad1035d009c94c3324b91d89d892afc437d5800",
|
|
19
|
+
"fabric-dashboard.js": "fa966a8f3b228391c2dae33674887a0d44b102cb58e42dbe6331f08bab57b391",
|
|
20
|
+
"fabric-button.js": "220c5c9719a9f6e35851d8748e2abb1d6606ffdb9fc43f3aff2b01f34637d925",
|
|
21
21
|
"themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
|
|
22
22
|
},
|
|
23
23
|
"history": {
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
|
|
60
60
|
"fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
|
|
61
61
|
"fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6"
|
|
62
|
+
},
|
|
63
|
+
"1.1.7": {
|
|
64
|
+
"fabric-dashboard.css": "549c1c1546bc9f8d2c1a579e7ad1035d009c94c3324b91d89d892afc437d5800",
|
|
65
|
+
"fabric-dashboard.js": "fa966a8f3b228391c2dae33674887a0d44b102cb58e42dbe6331f08bab57b391",
|
|
66
|
+
"fabric-button.js": "220c5c9719a9f6e35851d8748e2abb1d6606ffdb9fc43f3aff2b01f34637d925"
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -37,7 +37,8 @@ refuses it with `invalid_arguments` (FAA-244).
|
|
|
37
37
|
binding credential, shown once, goes into `credential_file` (0600, atomic) and never into the answer, a log or argv.
|
|
38
38
|
Keep the file in the service's data directory or the operator's vault.
|
|
39
39
|
3. Each connection joins with `fabric.board.join {projectId}` (`join_board` / `joinBoard`). It re-reads `hub.json`,
|
|
40
|
-
checks the pid it names is alive
|
|
40
|
+
checks the pid it names is alive (a process of another OS account under that id is `HubError` code `not-a-hub`,
|
|
41
|
+
retryable, saying not verified, as the session hooks say it, FAA-304; none at all is `not-running`), tells the owner of the door's listener from the socket table (DEC-0154 HA-2:
|
|
41
42
|
another OS account's process, or one whose owner cannot be told, is `HubError` code `foreign-user`, retryable, with
|
|
42
43
|
nothing sent; a restricted macOS table, which a retry never clears, is `data.reason` `table-restricted` and not
|
|
43
44
|
retryable, protocol.md, FAA-272), and only then sends the credential (a session already running carries that refusal as `BoardError` code
|
|
@@ -403,7 +404,8 @@ Python (`fabric_hub`): `fabric_root(platform, env)`, `locate_hub(root=None, aliv
|
|
|
403
404
|
`read_door_token(location)`, `read_credential(path)`, `HubClient(root=None, *, alive, timeout, estate, post)` with
|
|
404
405
|
`request_board_access(agent_id, project_id, capabilities, reason)`, `poll_access(request_id, poll_secret,
|
|
405
406
|
credential_file)`, `join_board(project_id, credential_file) -> BoardGrant`, `call_door(tool, args)`,
|
|
406
|
-
`call_binding(tool, args, credential_file)`; `
|
|
407
|
+
`call_binding(tool, args, credential_file)`; `pid_alive(pid)` answers True, False, or None for another OS account's
|
|
408
|
+
process (not verified); `Backoff(first=2, cap=300, jitter=0.2, floor=1)`; `HubError(code,
|
|
407
409
|
message, retryable, data)`.
|
|
408
410
|
|
|
409
411
|
Python (`fabric_board`): `Responder(*, hub, project_id, capabilities, credential_file, checkpoint_file, handler,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md
CHANGED
|
@@ -105,7 +105,8 @@ service. Load it after the stylesheet, with ids only:
|
|
|
105
105
|
Fabric* on a Russian page, or the page's `data-label-open`);
|
|
106
106
|
`--ink` on `--panel` with an `--edge` boundary, the pairs the themes are measured on.
|
|
107
107
|
- **Links (FL-1…FL-6):** built with the kit's port of the contract's `buildFabricLink`; a value outside the grammar
|
|
108
|
-
(or one shaped like a credential
|
|
108
|
+
(or one shaped like a credential, by the contract's DEC-0151 detector as the kit's `fabric-secrets.mjs` reads it, since
|
|
109
|
+
1.1.7) means no button, never a bad link. A press opens a view in Fabric and starts
|
|
109
110
|
nothing (FB-8).
|
|
110
111
|
- **The host (FB-6):** when the host's `fabric_button` cookie says `owner=host`, the page hides its own button.
|
|
111
112
|
- **Fabric absent (FB-5):** when the cookie says `fabric=absent`, or `data-fabric="absent"`, or when the page is still
|
|
@@ -148,6 +149,16 @@ vertically by the viewport or a clipping ancestor; ED-4 is `NOT_RUN` on a page p
|
|
|
148
149
|
the scroll position: say so in the report. A `NOT_RUN` is not a pass. Exit codes: 0 when no rule fails and at least
|
|
149
150
|
one passed, 1 on a `FAIL`, 2 on a usage error, 3 when no rule was measured (every rule `NOT_RUN`, since 0.44.0).
|
|
150
151
|
|
|
152
|
+
Since 0.81.0 the probe opens a loopback page (and asks a local service for its login code) only after it reads the
|
|
153
|
+
page's listener from the socket table as this OS account's (DEC-0154 PL-3, HA-2; FAA-260). On a Mac whose socket
|
|
154
|
+
table is restricted for the runtime that runs the probe (macOS 27.2 shows an ad-hoc-signed Node, such as Homebrew's,
|
|
155
|
+
nothing through `netstat`; DEC-0155 RT-1) the listener reads `unknown` (`table-restricted`), so every rule is
|
|
156
|
+
`NOT_RUN` and the probe exits 3, whatever the page. That is not a pass and not the page's fault: say so in the report,
|
|
157
|
+
with the reason the probe printed. The probe does not vouch for the listener by the service's pid, since only a
|
|
158
|
+
caller that started the process may (DEC-0155 RT-3, the installers' route), and the well-known `process.pid` proves
|
|
159
|
+
nothing. To measure the page, run the probe with a Node whose table reads (an Apple-signed or Developer ID one), or
|
|
160
|
+
on a host without the restriction (FAA-318).
|
|
161
|
+
|
|
151
162
|
## Upgrading an existing dashboard
|
|
152
163
|
|
|
153
164
|
A session that starts (or is cleared) in a repository that owns a service, whose dashboard has no
|
|
@@ -166,7 +177,9 @@ In the middle of an unrelated task, finish that first or record the upgrade as a
|
|
|
166
177
|
3. A page with an older kit: `dashboard_kit.py install <dir>`. A `changed` copy is refused; move the
|
|
167
178
|
edits into the page's own files, then install (or `--force` when the operator says the edits go).
|
|
168
179
|
4. Raise `kit` in the declaration (`dashboard_extension()` does it), restart the service, and run
|
|
169
|
-
both probes above. Report the layout table with every `NOT_RUN`.
|
|
180
|
+
both probes above. Report the layout table with every `NOT_RUN`. On a Mac with a restricted socket
|
|
181
|
+
table the layout probe measures nothing since 0.81.0 (every rule `NOT_RUN`, exit 3, the reason
|
|
182
|
+
`table-restricted`; *Proving it* above): report that as not measured, never as a pass.
|
|
170
183
|
|
|
171
184
|
## When something is missing
|
|
172
185
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md
CHANGED
|
@@ -123,7 +123,15 @@ if_absent policy, the same device and `"listener_owner": "this-account"`: the ki
|
|
|
123
123
|
the host read for the listener on http_url's port (`fabric_peer_owner`'s
|
|
124
124
|
`this-account`, `another-account`, `system`, `unknown`, `none`, or
|
|
125
125
|
`check_service.py`'s `foreign-user`); any other, or none given, refuses the
|
|
126
|
-
browser (DEC-0154 PL-3, FAA-251). The
|
|
126
|
+
browser (DEC-0154 PL-3, FAA-251). The host reads that kind right before it opens
|
|
127
|
+
the page with the adapter's
|
|
128
|
+
`python3 building-fabric-services/scripts/fabric_peer_owner.py listener <port>`
|
|
129
|
+
(or `node building-fabric-services/scripts/fabric-peer-owner.mjs listener <port>`),
|
|
130
|
+
which prints it on one line (`--json` prints the whole owner) and sends nothing
|
|
131
|
+
to the port (FAA-315). Where the socket table is restricted (macOS 27.2 for an
|
|
132
|
+
ad-hoc-signed Python or Node, DEC-0155 RT-4) only this account's listener on
|
|
133
|
+
exactly `127.0.0.1` reads `this-account`; a wildcard or `::1` bind reads
|
|
134
|
+
`unknown` (`table-restricted`), and the browser is refused. The native link does not take it: Fabric
|
|
127
135
|
Dashboards checks its own listener. Contract DEC-0123 PL-3 also allows a browser
|
|
128
136
|
on `unsupported` (a system the host does not ship for); the gate treats
|
|
129
137
|
`unsupported` as no fallback, by the operator's rule that a browser opens only on a
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -59,7 +59,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
59
59
|
| L0 | `fabric_time` | strict RFC 3339, UTC days, formatting, day windows, the clock ([dates](#dates-and-times)) |
|
|
60
60
|
| L0 | `fabric_identifiers` | identifier equality by normal form (DEC-0076) |
|
|
61
61
|
| L0 | `fabric_secrets` | the contract's one credential detector (DEC-0044) |
|
|
62
|
-
| L0 | `fabric_peer_owner` | which OS account owns each end of a loopback connection, from the computer's socket table (DEC-0154 HA-2, FAA-226): `listener`, `server_end`, `peer`, `CheckedHTTPConnection`; the Python file is Project Observatory's `peer_owner.py`, vendored from commit `f86f0622` (1.1.0) and read against `2180235c` (1.2.0, whose Linux rule it carries), with the readings its header names (`1.2.0+kit.
|
|
62
|
+
| L0 | `fabric_peer_owner` | which OS account owns each end of a loopback connection, from the computer's socket table (DEC-0154 HA-2, FAA-226): `listener`, `server_end`, `peer`, `CheckedHTTPConnection`; the Python file is Project Observatory's `peer_owner.py`, vendored from commit `f86f0622` (1.1.0) and read against `2180235c` (1.2.0, whose Linux rule it carries), with the readings its header names (`1.2.0+kit.6`; Observatory's 1.3.x libproc proof is FAA-292), the Node twin reads netstat, `/proc` and the owner SID where Python reads `sysctl`, `/proc` and `GetExtendedTcpTable`; on a restricted macOS table (FAA-267) a caller may vouch with a pid it started, proved by `lsof -p` |
|
|
63
63
|
| L0 | `fabric_tailnet` | who calls an agent behind `tailscale serve` and what that person may open (FAA-297): `Tailscale-User-Login` only on the agent's own serve listener from a loopback peer, groups from a generated `tailnet-perimeter/1` file, a personal SHA-256-stored key for MCP no wider than the groups; `authorize`, `load_perimeter`, `load_keys`, `hash_key`, refusals 401/403 with a code ([surfaces-and-auth.md](surfaces-and-auth.md#behind-tailscale-serve)) |
|
|
64
64
|
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
65
65
|
| L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
@@ -79,7 +79,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
79
79
|
| L3 | `fabric_a2a` | the reading side of an A2A Agent Card: `surfaces.a2a` as the schema reads it, and what a public card must not carry (DEC-0122) |
|
|
80
80
|
| L3 | `fabric_page_links` | a service's pages as Fabric Dashboards' open_link: build and read it, a raw local address, FAC-SEM-143 on a link and on a URL-mode input request (DEC-0123); the Python twin reads URLs as WHATWG does by hand |
|
|
81
81
|
| L3 | `fabric_load` | the call queue and the load report of DEC-0104 (FAA-289): `job_caller_key`, `next_queued_job`, `admit_job`, `queue_status_message`, `read_queue_position`, FAC-SEM-131 to 133, the `service-load/1` and `extensions/load/0.1` shapes, `load_extension`; imports `fabric_connections` for ajv's `date-time` ([interop](interop.md#a-nodes-call-queue)) |
|
|
82
|
-
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
82
|
+
| L3 | `fabric_lock` | the instance lock, its watch where the lock has no path-keyed name (macOS, FAA-303), and the supervisor marker (LC-01, DEC-0057) |
|
|
83
83
|
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
84
84
|
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
85
85
|
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document, the loopback HTTP server that refuses another OS account's peer before reading it (`LoopbackHTTPServer`, Node `loopbackServer`; FAA-227) |
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -106,7 +106,8 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
106
106
|
then poll the well-known document until it answers with this `id.instance` on a listener of this OS account
|
|
107
107
|
(DEC-0154 HA-2: another account's listener, or one whose owner cannot be told, is `foreign-user` and never a
|
|
108
108
|
successful install; the error names it). Where the socket table hides other processes (macOS 27.2 under an
|
|
109
|
-
ad-hoc-signed interpreter), the installer vouches with its own job's pid from `launchctl print
|
|
109
|
+
ad-hoc-signed interpreter), the installer vouches with its own job's pid from `launchctl print`, for a listen on
|
|
110
|
+
exactly `127.0.0.1` alone (DEC-0155 RT-3; a wildcard bind is never vouched for since FAA-312); a listener it
|
|
110
111
|
cannot prove reads `table-restricted` and the error names the way round it (FAA-267). The pid is the job's own, so a
|
|
111
112
|
`ProgramArguments` wrapper that starts the server as a child (`sh -c` without `exec`, `uv run`, `npm start`) cannot
|
|
112
113
|
vouch for it: on such a host start the server with `exec` or run the installer from Apple's `/usr/bin/python3`.
|
|
@@ -381,6 +382,26 @@ holds the same name beside its file lock, so the two kits exclude each other eve
|
|
|
381
382
|
name needs Node 22 or later: Node 18 and early 20 collapse every abstract socket name into one, and later 20 pads it so
|
|
382
383
|
no Python copy or probe sees it, so the Node kit refuses to start there with a sentence naming Node 22 (FAA-72).
|
|
383
384
|
|
|
385
|
+
**A lock that moves under its holder (FAA-303).** `acquire` checks that the inode it locked is the file at the path
|
|
386
|
+
(FAA-293), which closes the race between two copies starting at once. A folder swapped under a copy that already holds
|
|
387
|
+
the lock (a data import replacing the data folder) is a second case. On Linux and Windows the path-keyed name above
|
|
388
|
+
still keeps a second copy out. On macOS the lock is `flock` (Python) or `O_EXLOCK` (Node), both on an inode, so a
|
|
389
|
+
second copy locks the new file and starts. There `hold_single_instance` / `holdSingleInstance` start a watch on the
|
|
390
|
+
held lock (`InstanceLock.watch` / `InstanceLock#watch`): every `LOCK_WATCH_SECONDS` (2 s; one `stat` and one `fstat`)
|
|
391
|
+
the holder compares the inode it locked with the file at the path, and once the path names another file (the imported
|
|
392
|
+
folder's lock, or the one a second copy made) it stops at once with 75 and one sentence naming the path
|
|
393
|
+
(`lost_lock_message` / `lostLockMessage`), so the copy started on the new folder runs alone. The two copies overlap for
|
|
394
|
+
at most one interval. The stop is abrupt on purpose (`stop_lost_holder` / `stopLostHolder`: `os._exit`,
|
|
395
|
+
`process.exit`): a graceful shutdown would write the old copy's state into the folder now at the path, which is not the
|
|
396
|
+
one it read. Under a supervisor the stopped copy is restarted and takes the new folder's lock, or waits as above while
|
|
397
|
+
another copy holds it. A path that is gone or cannot be
|
|
398
|
+
read now (a folder removed, a permission, an I/O error) is read as nothing there yet and tried again next time: a
|
|
399
|
+
second copy has to make a file there to start, and the watch then sees that file. `release` ends the watch. Pass
|
|
400
|
+
`on_lost` / `onLost` to `hold_single_instance` / `holdSingleInstance` (or call `watch` yourself on an `InstanceLock`) to
|
|
401
|
+
react otherwise. A path-keyed name on macOS was weighed and left: it needs a file outside the data folder, in a folder
|
|
402
|
+
the system never cleans and every copy of every user can write, which the standard library cannot promise; and a
|
|
403
|
+
watch also stops a holder that would otherwise go on writing stale state into the imported folder.
|
|
404
|
+
|
|
384
405
|
**Token files on Windows.** The privacy of `service.token` is its ACL (service.md *Windows
|
|
385
406
|
token files*): the kits write a protected list: the user, SYSTEM and Administrators, set before the
|
|
386
407
|
token's bytes go in, any entry already there dropped. On read, `windows_token_acl_problem` /
|
|
@@ -232,15 +232,21 @@ on macOS, the same `/proc` files on Linux, and `netstat -ano` with the owner SID
|
|
|
232
232
|
|
|
233
233
|
macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew Python, measured; a Homebrew Node is ad-hoc-signed too and was not measured) only its own sockets: sysctl lists this
|
|
234
234
|
process alone and netstat prints nothing. The Python calibration therefore also asks for a child process's listener,
|
|
235
|
-
and
|
|
235
|
+
and a restricted table is `unknown` with a reason beginning `table-restricted` (never another account's): DEC-0155 RT-1
|
|
236
|
+
reads so a netstat that prints nothing or only its header, one that fails or exits non-zero, a table holding no row of
|
|
237
|
+
another uid (sysctl) or of another process (netstat), and a macOS table without the reader's own connection (FAA-312;
|
|
238
|
+
Linux and Windows keep `no-row`). Under it `listener(port)` reads this account's listener as its own only on exactly
|
|
239
|
+
`127.0.0.1` (RT-4). `fabric_peer_owner.py listener <port>` and `fabric-peer-owner.mjs listener <port>` print the kind
|
|
240
|
+
(`--json` the whole owner; FAA-315). A caller that started the
|
|
236
241
|
server itself may vouch with that pid: `lsof -p <pid>` asks the process, which the restriction leaves readable, and a
|
|
237
242
|
listener of this account's pid on the port counts as this account's (`CheckedHTTPConnection(vouch_pid=...)`, Node
|
|
238
243
|
`checkedSocket({vouchPid})`). The launchd installers vouch with their own job's pid; elsewhere a restricted table
|
|
239
244
|
refuses (the probe, the hub client and `fabric_http` included, FAA-272), and Apple's `/usr/bin/python3` sees the
|
|
240
|
-
whole table (FAA-267). A vouch is a weaker proof than the table's: it shows the job's pid listens on
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
whole table (FAA-267). A vouch is a weaker proof than the table's: it shows the job's pid listens on exactly `127.0.0.1`
|
|
246
|
+
on the port, not that it accepted this connection. Since FAA-312 every vouch follows DEC-0155 RT-3: a wildcard or `::1`
|
|
247
|
+
listener is never vouched for (`lsof` prints an IPv6-only wildcard as `*:<port>` too), and a reader whose real and
|
|
248
|
+
effective uid differ vouches for nothing. The accepted socket itself (RT-2, libproc or `lsof` of established sockets)
|
|
249
|
+
is not read yet (FAA-292; the repository's `docs/evidence/specs/2026-11-12-faa-292-libproc-decision.md`).
|
|
244
250
|
|
|
245
251
|
Every kit client that sends a credential to loopback checks first: `check_service.py` (rule `listener.owner`, the first that reads the port for
|
|
246
252
|
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`, its
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py
CHANGED
|
@@ -28,6 +28,24 @@ CREDENTIAL_PAIRS = {
|
|
|
28
28
|
'session': {'token', 'id'},
|
|
29
29
|
'code': {'login', 'auth', 'access', 'session'}}
|
|
30
30
|
JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
|
|
31
|
+
URL_BASE64_RUN = re.compile(r'[A-Za-z0-9_-]*')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_jwt(text):
|
|
35
|
+
# Whether JWT_SHAPE matches anywhere, read in one pass (the contract's hasJwt, FAA-313): the expression backtracks
|
|
36
|
+
# from every `eyJ` of a long run, quadratic in it, while every `eyJ` of one run shares that run's end, so only its
|
|
37
|
+
# first is tried. The kit's fabric_secrets.has_jwt reads the same way; this file stands alone, so it has its own.
|
|
38
|
+
at = text.find('eyJ')
|
|
39
|
+
while at >= 0:
|
|
40
|
+
one = URL_BASE64_RUN.match(text, at + 3).end()
|
|
41
|
+
if one - at - 3 >= 2 and text[one:one + 1] == '.':
|
|
42
|
+
two = URL_BASE64_RUN.match(text, one + 1).end()
|
|
43
|
+
if two - one - 1 >= 2 and text[two:two + 1] == '.':
|
|
44
|
+
return True
|
|
45
|
+
at = text.find('eyJ', one)
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
31
49
|
# The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key with its
|
|
32
50
|
# prefix in its canonical case after a boundary (the start, a character that is no ASCII letter or digit, or an escape);
|
|
33
51
|
# an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more.
|
|
@@ -58,7 +76,7 @@ def credential_name(name):
|
|
|
58
76
|
def secret_value(value):
|
|
59
77
|
long_secret = any(re.search(r'[a-z]', run) and re.search(r'[A-Z]', run) and re.search(r'[0-9]', run)
|
|
60
78
|
for run in LONG_RUN.findall(value))
|
|
61
|
-
return bool(
|
|
79
|
+
return bool(has_jwt(value) or SECRET_VALUE.search(value) or long_secret)
|
|
62
80
|
|
|
63
81
|
|
|
64
82
|
def carries_credential(text):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -825,13 +825,15 @@ class Probe:
|
|
|
825
825
|
out = subprocess.run(["launchctl", "print", "%s/%s" % (domain, life.get("label"))], capture_output=True, text=True)
|
|
826
826
|
match = re.search(r"^\s*pid = (\d+)", out.stdout, re.MULTILINE)
|
|
827
827
|
served = ((self.wk or {}).get("process") or {}).get("pid")
|
|
828
|
+
# a LaunchDaemon's leftover LaunchAgent runs in its UserName's gui/<uid> (FAA-317)
|
|
829
|
+
second = other_domain_copy(life.get("label"), domain, plist.get("UserName")) if out.returncode == 0 and match else None
|
|
828
830
|
if out.returncode != 0:
|
|
829
831
|
self.add("lifecycle.one-copy", "FAIL", "launchd job %s is not loaded in %s" % (life.get("label"), domain))
|
|
830
832
|
elif not match:
|
|
831
833
|
self.add("lifecycle.one-copy", "FAIL", "launchd job loaded but not running")
|
|
832
|
-
elif
|
|
834
|
+
elif second: # loaded in both domains: two copies (FAA-286 item 6)
|
|
833
835
|
self.add("lifecycle.one-copy", "FAIL", "launchd job %s is loaded in %s and in %s: two copies; uninstall one"
|
|
834
|
-
% (life.get("label"), domain,
|
|
836
|
+
% (life.get("label"), domain, second))
|
|
835
837
|
else:
|
|
836
838
|
same = int(match.group(1)) == served
|
|
837
839
|
self.add("lifecycle.one-copy", "PASS" if same else "FAIL",
|
|
@@ -18,7 +18,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
18
18
|
|
|
19
19
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
20
20
|
from fabric_jsvalue import indexed_objects, js_keys # noqa: E402
|
|
21
|
-
from fabric_secrets import JS_SPACE,
|
|
21
|
+
from fabric_secrets import JS_SPACE, SECRET_VALUE, has_jwt, raw_values, secret_argument, secret_url, secret_value # noqa: E402
|
|
22
22
|
from fabric_secrets import url_parts as _whatwg_url_parts # noqa: E402 - public since FAA-250
|
|
23
23
|
import check_service_channel_agent_schema as cas # noqa: E402
|
|
24
24
|
|
|
@@ -208,9 +208,9 @@ def read_as_url(read: str) -> bool:
|
|
|
208
208
|
|
|
209
209
|
|
|
210
210
|
def _url_reading(read: str) -> bool:
|
|
211
|
-
"""secretUrl where read_as_url admits the head, secretArgument otherwise.
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
"""secretUrl where read_as_url admits the head, secretArgument otherwise. A URL the WHATWG parser refuses makes the
|
|
212
|
+
contract's `new URL` throw, which finds only what secretArgument finds, so such a URL is read by secret_argument
|
|
213
|
+
alone: url_parts answers None for it here, as the kit's secret_url reads it as an argument only since FAA-309."""
|
|
214
214
|
if not read_as_url(read):
|
|
215
215
|
return secret_argument(read)
|
|
216
216
|
return secret_argument(read) or (url_parts(read) is not None and secret_url(read))
|
|
@@ -222,7 +222,7 @@ def shaped_read(read: str) -> bool:
|
|
|
222
222
|
|
|
223
223
|
def _key_shaped(text: str) -> bool:
|
|
224
224
|
read = head(text)
|
|
225
|
-
return bool(
|
|
225
|
+
return bool(has_jwt(read) or SECRET_VALUE.search(read))
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
def plain_name(name: str) -> bool:
|
|
@@ -397,6 +397,8 @@ def url_parts(text: str) -> Optional[Tuple[str, str]]:
|
|
|
397
397
|
return None
|
|
398
398
|
if at and hostport == "":
|
|
399
399
|
return None
|
|
400
|
+
if host == "" and colon > bracket:
|
|
401
|
+
return None # an empty host ended by `:`, with an empty port or a port: `new URL` throws
|
|
400
402
|
if host == "" and special and lowered != "file":
|
|
401
403
|
return None
|
|
402
404
|
if host and not _host_ok(host, special):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py
CHANGED
|
@@ -135,20 +135,31 @@ import fabric_actions as fa # noqa: E402 - beside this file (DEC-0121)
|
|
|
135
135
|
import fabric_a2a as fa2 # noqa: E402 - beside this file (DEC-0122)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
def other_domain_copy(label: object, domain: str) -> Optional[str]:
|
|
139
|
-
"""The other launchd domain when it also has job `label` loaded, a second copy (FAA-286 item 6), else None.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
def other_domain_copy(label: object, domain: str, user_name: object = None) -> Optional[str]:
|
|
139
|
+
"""The other launchd domain when it also has job `label` loaded, a second copy (FAA-286 item 6), else None. For a
|
|
140
|
+
LaunchDaemon (`system`) that is a gui/<uid> domain: first the one of its plist's UserName (`user_name`), where a
|
|
141
|
+
leftover LaunchAgent of a dedicated account runs (FAA-317), then the probe's own."""
|
|
142
|
+
if domain == "system":
|
|
143
|
+
uids = [_sup._account_uid(user_name)] if isinstance(user_name, str) and user_name else []
|
|
144
|
+
uids.append(getattr(os, "getuid", lambda: 0)())
|
|
145
|
+
others = ["gui/%d" % uid for i, uid in enumerate(uids) if uid is not None and uid not in uids[:i]]
|
|
146
|
+
else:
|
|
147
|
+
others = ["system"]
|
|
148
|
+
for other in others:
|
|
149
|
+
try:
|
|
150
|
+
out = subprocess.run(["launchctl", "print", "%s/%s" % (other, label)], capture_output=True, text=True,
|
|
151
|
+
timeout=10)
|
|
152
|
+
except (OSError, subprocess.SubprocessError):
|
|
153
|
+
continue
|
|
154
|
+
if out.returncode == 0:
|
|
155
|
+
return other
|
|
156
|
+
return None
|
|
146
157
|
|
|
147
158
|
|
|
148
159
|
def in_launch_daemons(plist_path: Path) -> bool:
|
|
149
|
-
"""Whether a plist sits in /Library/LaunchDaemons, where launchd loads it at boot as a LaunchDaemon (FAA-278)
|
|
150
|
-
|
|
151
|
-
return
|
|
160
|
+
"""Whether a plist sits in /Library/LaunchDaemons, where launchd loads it at boot as a LaunchDaemon (FAA-278): the
|
|
161
|
+
supervisors' own rule, imported (FAA-317), which a test may point elsewhere through their SYSTEM_DAEMONS."""
|
|
162
|
+
return _sup._in_daemons(plist_path)
|
|
152
163
|
|
|
153
164
|
|
|
154
165
|
def plist_problems(plist: dict, label: object, token: object = None, *, system: bool = False,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// connectionsExtension builds the `extensions` entry a service declares, `state` from its own checks and its degraded[].
|
|
4
4
|
// Part of the Node service kit (FAA-62, layer L3); the twin of fabric_connections.py. Node 20+, no dependency.
|
|
5
5
|
import { ServiceError } from './fabric-base.mjs';
|
|
6
|
-
import { BEARER_INSIDE,
|
|
6
|
+
import { BEARER_INSIDE, hasJwt, longSecret, secretUrl } from './fabric-secrets.mjs';
|
|
7
7
|
import { isObject } from './fabric-jsvalue.mjs';
|
|
8
8
|
|
|
9
9
|
export const CONNECTIONS_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/connections/0.1';
|
|
@@ -112,8 +112,9 @@ const longRun = (value) =>
|
|
|
112
112
|
const cut = at < 0 ? -1 : piece.indexOf('://', at);
|
|
113
113
|
return longSecret(cut < 0 ? piece : piece.slice(0, cut));
|
|
114
114
|
});
|
|
115
|
+
// hasJwt reads a JWT in one pass, where JWT_SHAPE backtracks from every `eyJ` of a long run (FAA-313).
|
|
115
116
|
const credential = (value, runs) =>
|
|
116
|
-
|
|
117
|
+
hasJwt(value) || KEY_SHAPE.test(value) || BEARER_INSIDE.test(value) || (runs && longRun(value)) || urlsIn(value).some(secretUrl);
|
|
117
118
|
|
|
118
119
|
/** What the schema cannot state: an id named twice, a credential in any string member. */
|
|
119
120
|
function beyondShape(list) {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// effects: { 'example.write': 'draft' }, issuer: wk.auth.delegation.issuer });
|
|
10
10
|
// const verdict = await gate(toolName, req.headers); // before the tool runs
|
|
11
11
|
// if (!verdict.ok) return refusalToolResult(verdict);
|
|
12
|
+
//
|
|
13
|
+
// Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
|
|
12
14
|
|
|
13
15
|
import crypto from 'node:crypto';
|
|
14
16
|
import fs from 'node:fs';
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { ROUTES } from './fabric-usage.mjs';
|
|
|
18
18
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
19
19
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
20
20
|
|
|
21
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
21
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.7';
|
|
22
22
|
|
|
23
23
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
24
24
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs
CHANGED
|
@@ -36,7 +36,10 @@ const instant = (value, name) => {
|
|
|
36
36
|
const reasonOk = (reason, what) => {
|
|
37
37
|
if (!HOLD_REASONS.includes(reason)) throw new ServiceError(`A ${what}'s reason is one of ${HOLD_REASONS.join(', ')}.`);
|
|
38
38
|
};
|
|
39
|
-
|
|
39
|
+
// One run of ECMAScript's whitespace, written out as fabric_secrets.JS_SPACE is, the class both twins squash with
|
|
40
|
+
// (FAA-316): Python's str.split() also takes U+001C to U+001F and U+0085 and not U+FEFF.
|
|
41
|
+
const SPACES = /[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g;
|
|
42
|
+
const squash = (text) => String(text).replace(SPACES, ' ').replace(/^ | $/g, '');
|
|
40
43
|
|
|
41
44
|
/** A held job's statusMessage; the message is shortened, never refused, to fit 500 characters. */
|
|
42
45
|
export function waitingMessage(reason, since, until = null, message = null) {
|
|
@@ -50,7 +53,7 @@ export function waitingMessage(reason, since, until = null, message = null) {
|
|
|
50
53
|
const squashed = squash(message);
|
|
51
54
|
const room = MAX_STATUS_MESSAGE - head - codePoints(text) - ': '.length;
|
|
52
55
|
if (squashed && room > 0) {
|
|
53
|
-
text += ': ' + (codePoints(squashed) <= room ? squashed : [...squashed].slice(0, room - 3).join('').
|
|
56
|
+
text += ': ' + (codePoints(squashed) <= room ? squashed : [...squashed].slice(0, room - 3).join('').replace(/ $/, '') + '...');
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
return pauseMessage(until === null ? { text } : { resetsAt: until, text });
|
|
@@ -86,7 +89,7 @@ export function holdSentence(caller, work, reason, until) {
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
function text(value, name, high = 200) {
|
|
89
|
-
if (typeof value !== 'string' || !value
|
|
92
|
+
if (typeof value !== 'string' || !squash(value) || value.length > high) throw new ServiceError(`${name} is a text of 1 to ${high} characters.`);
|
|
90
93
|
if (secretValue(value)) throw new ServiceError(`${name} looks like a credential; a hold names callers and work, never a secret.`);
|
|
91
94
|
return squash(value);
|
|
92
95
|
}
|
|
@@ -56,8 +56,9 @@ export class HubError extends Error {
|
|
|
56
56
|
/**
|
|
57
57
|
* The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned contract's
|
|
58
58
|
* state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted` when this
|
|
59
|
-
* runtime's socket table hides other processes' sockets,
|
|
60
|
-
* owner's kind or the check's reason
|
|
59
|
+
* runtime's socket table hides other processes' sockets, or, on macOS, holds no row for this connection (DEC-0155
|
|
60
|
+
* RT-1, FAA-312), which never clears, so it is not retryable; otherwise the owner's kind or the check's reason
|
|
61
|
+
* (`another-account`, or `no-row`, which only Linux and Windows give), retryable.
|
|
61
62
|
*/
|
|
62
63
|
export function listenerRefusal(e) {
|
|
63
64
|
const kind = e?.owner?.kind ?? null;
|
|
@@ -75,14 +76,18 @@ export function fabricRoot(platform = process.platform, env = process.env) {
|
|
|
75
76
|
return path.join(env.XDG_DATA_HOME || path.join(home, '.local/share'), 'passioncode-fabric');
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* true: a process of this OS account runs under pid; false: none does. null (falsy): a process of another account does
|
|
81
|
+
* (EPERM), as when the id hub.json names was reused: not verified, never called running (FAA-304, the session hooks'
|
|
82
|
+
* answer since FAA-254). Never signals it.
|
|
83
|
+
*/
|
|
79
84
|
export function pidAlive(pid) {
|
|
80
85
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
81
86
|
try {
|
|
82
87
|
process.kill(pid, 0);
|
|
83
88
|
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return e?.code === 'EPERM' ? null : false;
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -108,7 +113,8 @@ function readSmall(file) {
|
|
|
108
113
|
|
|
109
114
|
/**
|
|
110
115
|
* hub.json read the way an agent must, every call. Answers {origin, mcpUrl, doorTokenFile, pid, startedAt, features} or
|
|
111
|
-
* throws HubError (not-running, retryable; not-a-hub
|
|
116
|
+
* throws HubError (not-running, retryable; not-a-hub, retryable when hub.json names another OS account's process, not
|
|
117
|
+
* verified, FAA-304). Reads no token. `estate` is {record, applied}: with it, a hubKey
|
|
112
118
|
* the document names is checked by hubKeyCertified (DEC-0060); fabric-hub/0.1 names none.
|
|
113
119
|
*/
|
|
114
120
|
export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now } = {}) {
|
|
@@ -142,7 +148,10 @@ export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now }
|
|
|
142
148
|
sameRoot = false;
|
|
143
149
|
}
|
|
144
150
|
if (!sameRoot) throw notHub('names a door token outside the registry root');
|
|
145
|
-
|
|
151
|
+
const live = alive(doc.pid);
|
|
152
|
+
// FAA-304: another account's process under that id, so not this account's Fabric (as FAA-254)
|
|
153
|
+
if (live === null) throw new HubError('not-a-hub', `Fabric's hub.json names process ${doc.pid}, which runs under another OS account: not verified.`, true);
|
|
154
|
+
if (!live) throw new HubError('not-running', `Fabric is not running: hub.json names process ${doc.pid}, which is not running.`, true);
|
|
146
155
|
const features = Array.isArray(doc.features) ? doc.features.filter((f) => typeof f === 'string') : [];
|
|
147
156
|
if (estate && doc.hubKey && typeof doc.hubKey === 'object') {
|
|
148
157
|
const verdict = hubKeyCertified(estate.record, doc.hubKey.kid, doc.hubKey.x, now ?? Date.now() / 1000, estate.applied ?? undefined);
|