@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
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
// One copy of a service, locked before any side effect (LC-01, DEC-0057): the instance lock and the supervisor marker.
|
|
2
2
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_lock.py. Node 20+ (22+ for the Linux lock), no dependency.
|
|
3
|
+
//
|
|
4
|
+
// A lock that moves under its holder (FAA-303). Linux and Windows hold a name keyed by the lock's path (an abstract
|
|
5
|
+
// unix socket, a named pipe), so a folder swapped under a running copy cannot let a second copy in. macOS has no such
|
|
6
|
+
// name: O_EXLOCK (and the Python twin's flock) lock an inode, so after a data import swaps the data folder a second
|
|
7
|
+
// copy locks the new file and starts. There the holder watches its lock instead (InstanceLock#watch, started by
|
|
8
|
+
// holdSingleInstance): every LOCK_WATCH_SECONDS it compares the inode it locked with the file at the path, and once
|
|
9
|
+
// the path names another file it stops with 75 and one sentence naming the path, before writing anything more into a
|
|
10
|
+
// folder that is no longer the one it read. Two copies then overlap for at most one interval. A path-keyed name on
|
|
11
|
+
// macOS was weighed and left: it needs a file in a folder outside the data folder that the system never cleans and
|
|
12
|
+
// every copy can write, which Node alone cannot promise.
|
|
3
13
|
import crypto from 'node:crypto';
|
|
4
14
|
import fs from 'node:fs';
|
|
5
15
|
import net from 'node:net';
|
|
@@ -15,6 +25,12 @@ export const LOCK_BACKOFF_FIRST_SECONDS = 0.5;
|
|
|
15
25
|
|
|
16
26
|
export const LOCK_BACKOFF_MAX_SECONDS = 30;
|
|
17
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How often a holder that has no path-keyed name (macOS) compares the inode it locked with the file at the lock's path
|
|
30
|
+
* (FAA-303): one stat and one fstat; two copies overlap for at most this long after a folder swap.
|
|
31
|
+
*/
|
|
32
|
+
export const LOCK_WATCH_SECONDS = 2;
|
|
33
|
+
|
|
18
34
|
const O_EXLOCK = 0x20; // BSD/macOS: open(2) takes flock(LOCK_EX); absent from fs.constants
|
|
19
35
|
|
|
20
36
|
export class AlreadyRunning extends ServiceError {
|
|
@@ -34,6 +50,7 @@ export class InstanceLock {
|
|
|
34
50
|
this.path = path.join(dataDir, 'service.lock');
|
|
35
51
|
this.fd = null;
|
|
36
52
|
this.server = null;
|
|
53
|
+
this.timer = null;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
async acquire() {
|
|
@@ -124,14 +141,65 @@ export class InstanceLock {
|
|
|
124
141
|
return true;
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Re-check, every `intervalSeconds` on an unref'd timer, that the file this copy locked is still the file at the path
|
|
146
|
+
* (FAA-303); the first time the path names another file (a swapped folder's lock, or the one a second copy made),
|
|
147
|
+
* call `onLost(lock)` once and stop watching. The default stops the process with 75 and one sentence
|
|
148
|
+
* (stopLostHolder). A path that is gone or cannot be read now is read as nothing there yet and tried again next time:
|
|
149
|
+
* a second copy has to make a file there to start, and the watch then sees that file. `release` ends the watch. Only
|
|
150
|
+
* a lock held as a file is watched (macOS): elsewhere the descriptor is null and the path-keyed name keeps a second
|
|
151
|
+
* copy out, so this is a no-op, as for a lock not held.
|
|
152
|
+
*/
|
|
153
|
+
watch({ onLost, intervalSeconds = LOCK_WATCH_SECONDS } = {}) {
|
|
154
|
+
if (this.fd === null || this.timer !== null) return this;
|
|
155
|
+
const fd = this.fd;
|
|
156
|
+
const react = onLost ?? stopLostHolder;
|
|
157
|
+
this.timer = setInterval(() => {
|
|
158
|
+
let here;
|
|
159
|
+
let held;
|
|
160
|
+
try {
|
|
161
|
+
here = fs.statSync(this.path);
|
|
162
|
+
held = fs.fstatSync(fd);
|
|
163
|
+
} catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (here.dev === held.dev && here.ino === held.ino) return;
|
|
167
|
+
clearInterval(this.timer);
|
|
168
|
+
this.timer = null;
|
|
169
|
+
react(this);
|
|
170
|
+
}, intervalSeconds * 1000);
|
|
171
|
+
this.timer.unref();
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
release() {
|
|
176
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
128
177
|
if (this.fd !== null) fs.closeSync(this.fd);
|
|
129
178
|
if (this.server) this.server.close();
|
|
179
|
+
this.timer = null;
|
|
130
180
|
this.fd = null;
|
|
131
181
|
this.server = null;
|
|
132
182
|
}
|
|
133
183
|
}
|
|
134
184
|
|
|
185
|
+
/** The one sentence a holder whose lock moved under it stops with (FAA-303). */
|
|
186
|
+
export function lostLockMessage(lockPath) {
|
|
187
|
+
return `${lockPath} is no longer the file this copy locked (the data folder moved or was swapped?): stopping, so a copy started on the folder now at that path runs alone.`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* InstanceLock#watch's default: write lostLockMessage and end the process with 75 at once. Not a graceful stop: a
|
|
192
|
+
* shutdown would write this copy's state into the folder now at the path, which is not the one it read; the kit's
|
|
193
|
+
* journals make an abrupt stop recoverable (LC-03).
|
|
194
|
+
*/
|
|
195
|
+
export function stopLostHolder(lock) {
|
|
196
|
+
try {
|
|
197
|
+
fs.writeSync(2, `${lostLockMessage(lock.path)}\n`);
|
|
198
|
+
} finally {
|
|
199
|
+
process.exit(EXIT_ALREADY_RUNNING);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
135
203
|
let abstractNamesSeen = null;
|
|
136
204
|
|
|
137
205
|
/**
|
|
@@ -253,10 +321,12 @@ const sleepSeconds = (s) => new Promise((resolve) => setTimeout(resolve, s * 100
|
|
|
253
321
|
* supervisor that restarts on exit (launchd's KeepAlive) an immediate exit is a respawn every ThrottleInterval,
|
|
254
322
|
* forever; so a supervised copy first backs off in-process (0.5 s doubling to 30 s,
|
|
255
323
|
* LOCK_WAIT_SUPERVISED_SECONDS in all), takes over if the holder leaves, and exits 75 only when
|
|
256
|
-
* the wait runs out. `waitSeconds` overrides the choice (0 = never wait).
|
|
324
|
+
* the wait runs out. `waitSeconds` overrides the choice (0 = never wait). Where the lock is held as a file (macOS)
|
|
325
|
+
* the held lock is watched (InstanceLock#watch, FAA-303): a folder swapped under the running copy stops it with 75;
|
|
326
|
+
* `onLost` replaces that reaction.
|
|
257
327
|
*/
|
|
258
328
|
export async function holdSingleInstance(dataDir, {
|
|
259
|
-
waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv,
|
|
329
|
+
waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv, onLost,
|
|
260
330
|
} = {}) {
|
|
261
331
|
// DEC-0057: markers that disagree exit 75 at once, before the lock, naming them.
|
|
262
332
|
const marker = supervisorMarker({ ...(env === undefined ? {} : { env }), ...(argv === undefined ? {} : { argv }) });
|
|
@@ -270,7 +340,7 @@ export async function holdSingleInstance(dataDir, {
|
|
|
270
340
|
let announced = false;
|
|
271
341
|
for (;;) {
|
|
272
342
|
try {
|
|
273
|
-
return await new InstanceLock(dataDir).acquire();
|
|
343
|
+
return (await new InstanceLock(dataDir).acquire()).watch({ onLost });
|
|
274
344
|
} catch (error) {
|
|
275
345
|
if (!(error instanceof AlreadyRunning)) throw error;
|
|
276
346
|
const remaining = deadline - clock();
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs
CHANGED
|
@@ -40,12 +40,14 @@
|
|
|
40
40
|
// privilege. The parsers are pure functions over captured text (test/fixtures/peer-owner/).
|
|
41
41
|
|
|
42
42
|
import { execFile } from 'node:child_process';
|
|
43
|
+
import { realpathSync } from 'node:fs';
|
|
43
44
|
import { open } from 'node:fs/promises';
|
|
44
45
|
import http from 'node:http';
|
|
45
46
|
import net from 'node:net';
|
|
46
47
|
import os from 'node:os';
|
|
48
|
+
import { fileURLToPath } from 'node:url';
|
|
47
49
|
|
|
48
|
-
export const PEER_OWNER_VERSION = '1.2.0+kit.
|
|
50
|
+
export const PEER_OWNER_VERSION = '1.2.0+kit.6'; // the Python twin's, Observatory's 1.2.0 with the kit's differences
|
|
49
51
|
export const THIS_ACCOUNT = 'this-account';
|
|
50
52
|
export const ANOTHER_ACCOUNT = 'another-account';
|
|
51
53
|
export const SYSTEM = 'system';
|
|
@@ -62,6 +64,8 @@ export const LSOF = '/usr/sbin/lsof';
|
|
|
62
64
|
// The reason a socket table hides other processes' sockets from this one (FAA-267): macOS 27.2 gives an ad-hoc-signed
|
|
63
65
|
// runtime an empty netstat.
|
|
64
66
|
export const TABLE_RESTRICTED = 'table-restricted';
|
|
67
|
+
// A macOS table that lists sockets but not the reader's own connection (DEC-0155 RT-1, FAA-312).
|
|
68
|
+
export const NO_ROW_RESTRICTED = `${TABLE_RESTRICTED}: no row for this connection`;
|
|
65
69
|
export const SERVER_NOT_OWN = 'server-not-own';
|
|
66
70
|
export const SERVER_UNVERIFIED = 'server-unverified';
|
|
67
71
|
export const SERVER_NOT_THIS_WORKSPACE = 'server-not-this-workspace';
|
|
@@ -290,10 +294,10 @@ export function runProgram(file, args, timeout = TIMEOUT_MS) {
|
|
|
290
294
|
});
|
|
291
295
|
}
|
|
292
296
|
|
|
293
|
-
export function method() {
|
|
294
|
-
if (
|
|
295
|
-
if (
|
|
296
|
-
if (
|
|
297
|
+
export function method(platform = process.platform) {
|
|
298
|
+
if (platform === 'darwin') return 'netstat';
|
|
299
|
+
if (platform === 'linux') return 'proc';
|
|
300
|
+
if (platform === 'win32') return 'netstat-windows';
|
|
297
301
|
return 'none';
|
|
298
302
|
}
|
|
299
303
|
|
|
@@ -319,9 +323,18 @@ async function readProc(name) {
|
|
|
319
323
|
return Buffer.concat(chunks);
|
|
320
324
|
}
|
|
321
325
|
|
|
322
|
-
export async function readTable({ run = runProgram } = {}) {
|
|
323
|
-
const how = method();
|
|
324
|
-
if (how === 'netstat')
|
|
326
|
+
export async function readTable({ run = runProgram, platform = process.platform } = {}) {
|
|
327
|
+
const how = method(platform);
|
|
328
|
+
if (how === 'netstat') {
|
|
329
|
+
let text;
|
|
330
|
+
try {
|
|
331
|
+
text = await run(NETSTAT, ['-anv', '-p', 'tcp']);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
// A netstat that fails, exits non-zero or times out is a restricted table (DEC-0155 RT-1, FAA-312).
|
|
334
|
+
throw new TableUnreadable(`${TABLE_RESTRICTED}: ${err instanceof TableUnreadable ? err.message : `netstat: ${err?.name ?? 'error'}`}`);
|
|
335
|
+
}
|
|
336
|
+
return { rows: parseNetstat(text), how };
|
|
337
|
+
}
|
|
325
338
|
if (how === 'proc') {
|
|
326
339
|
const rows = [];
|
|
327
340
|
for (const [name, family] of [['/proc/net/tcp', 4], ['/proc/net/tcp6', 6]]) {
|
|
@@ -387,6 +400,21 @@ export async function resolve(row, how, { run = runProgram, ownUid: mine, ownSid
|
|
|
387
400
|
|
|
388
401
|
// -- the questions ----------------------------------------------------------------------------------------------------
|
|
389
402
|
|
|
403
|
+
// DEC-0155 RT-1 (FAA-312): a macOS table holding no row of another uid (sysctl, the Python twin's, which carries uids)
|
|
404
|
+
// or of another process (netstat, which carries pids) is restricted, and a missing row proves nothing. Never on Linux
|
|
405
|
+
// or Windows, which carry the owner on each row.
|
|
406
|
+
export function restricted(rows, how, { ownUid: mine, ownPid } = {}) {
|
|
407
|
+
if (how === 'sysctl') {
|
|
408
|
+
const own = mine ?? ownUid();
|
|
409
|
+
return !rows.some((r) => r.uid !== null && r.uid !== undefined && r.uid !== own);
|
|
410
|
+
}
|
|
411
|
+
if (how === 'netstat') {
|
|
412
|
+
const own = ownPid ?? process.pid;
|
|
413
|
+
return !rows.some((r) => r.pid !== null && r.pid !== undefined && r.pid !== own);
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
390
418
|
// The live connected rows with exactly this local and remote address: one, or the caller cannot tell.
|
|
391
419
|
// A row printed twice is one socket: Linux resumes a /proc read that sockets came and went under by position, and can
|
|
392
420
|
// print the same row again (0.74.0 seam reading 6); rows equal in every field count once.
|
|
@@ -414,7 +442,8 @@ async function ownerOf(local, remote, opts = {}, server = false) {
|
|
|
414
442
|
// A /proc read under churn can skip a row or print one twice; one more read before answering unknown.
|
|
415
443
|
if (table.how !== 'proc' || found.length === 1) break;
|
|
416
444
|
}
|
|
417
|
-
|
|
445
|
+
// A macOS table without the reader's own connection is restricted (DEC-0155 RT-1, FAA-312).
|
|
446
|
+
if (found.length === 0) return owner(UNKNOWN, { method: table.how, reason: ['sysctl', 'netstat'].includes(table.how) ? NO_ROW_RESTRICTED : 'no-row' });
|
|
418
447
|
if (found.length > 1) return owner(UNKNOWN, { method: table.how, reason: 'several-rows' });
|
|
419
448
|
if (table.how === 'proc' && server && found[0].uid === null) {
|
|
420
449
|
// Linux: a connection not yet accept()ed is printed with uid 0 and inode 0; it belongs to the listener on its local
|
|
@@ -491,23 +520,28 @@ export async function listener(port, opts = {}) {
|
|
|
491
520
|
const at = now();
|
|
492
521
|
const hit = injected ? undefined : listeners.get(port);
|
|
493
522
|
if (hit && at - hit[0] < LISTENER_TTL_MS) return hit[1];
|
|
494
|
-
let found;
|
|
495
|
-
let how;
|
|
496
523
|
let table;
|
|
497
524
|
try {
|
|
498
525
|
table = await (read ?? readTable)(opts);
|
|
499
|
-
how = table.how;
|
|
500
|
-
found = pickListener(table.rows, port);
|
|
501
526
|
} catch (err) {
|
|
502
527
|
const who = owner(UNKNOWN, { method: method(), reason: err instanceof TableUnreadable ? err.message : `table: ${err.name}` });
|
|
503
528
|
if (!injected) listeners.set(port, [at, who]);
|
|
504
529
|
return who;
|
|
505
530
|
}
|
|
506
|
-
const who = await
|
|
531
|
+
const who = restricted(table.rows, table.how, opts) ? await restrictedListener(table.rows, port, table.how, opts)
|
|
532
|
+
: await oneOwner(pickListener(table.rows, port), table.how, opts);
|
|
507
533
|
if (!injected) listeners.set(port, [at, who]);
|
|
508
534
|
return who;
|
|
509
535
|
}
|
|
510
536
|
|
|
537
|
+
// DEC-0155 RT-4 (FAA-312): under a restricted table only this account's own bind on exactly 127.0.0.1 is its own; its
|
|
538
|
+
// wildcard or ::1 listener, root's, and a port with no row are an owner that cannot be told.
|
|
539
|
+
async function restrictedListener(rows, port, how, opts) {
|
|
540
|
+
const who = await oneOwner(rows.filter((r) => r.listening && r.local[0] === '127.0.0.1' && r.local[1] === port), how, opts);
|
|
541
|
+
if (who.kind === THIS_ACCOUNT) return who;
|
|
542
|
+
return owner(UNKNOWN, { method: how, reason: `${TABLE_RESTRICTED}: no listener of this account on exactly 127.0.0.1:${port}` });
|
|
543
|
+
}
|
|
544
|
+
|
|
511
545
|
// [code, why] when a client must not send on a connection whose server end is `who`, else null.
|
|
512
546
|
export function refusal(who, port, expectPid = null) {
|
|
513
547
|
if (who.kind === ANOTHER_ACCOUNT || who.kind === SYSTEM) return [SERVER_NOT_OWN, `the server on port ${port} belongs to ${who.kind}`];
|
|
@@ -518,16 +552,20 @@ export function refusal(who, port, expectPid = null) {
|
|
|
518
552
|
return null;
|
|
519
553
|
}
|
|
520
554
|
|
|
521
|
-
// macOS, when the socket table is restricted (FAA-267): whether process `pid`, this account's, listens on
|
|
522
|
-
//
|
|
523
|
-
// `
|
|
524
|
-
|
|
555
|
+
// macOS, when the socket table is restricted (FAA-267, DEC-0155 RT-3): whether process `pid`, this account's, listens on
|
|
556
|
+
// exactly 127.0.0.1:<port> (never the IPv4 wildcard, which lsof prints as it prints an IPv6-only one, nor ::1; FAA-312).
|
|
557
|
+
// `lsof -p` asks the process itself, which the restriction does not hide. Only a caller that started `pid` itself (a
|
|
558
|
+
// launchd job it installed) may vouch with it, and never a set-uid one (its real and effective uid differ, DEC-0155).
|
|
559
|
+
export async function pidListens(pid, port, { run = runProgram, platform = process.platform, ownUid: mine, ownEuid } = {}) {
|
|
525
560
|
if (platform !== 'darwin' || !Number.isInteger(pid) || pid <= 1) return false;
|
|
561
|
+
const real = mine ?? ownUid();
|
|
562
|
+
const effective = ownEuid ?? (typeof process.geteuid === 'function' ? process.geteuid() : real);
|
|
563
|
+
if (effective !== real) return false;
|
|
526
564
|
try {
|
|
527
565
|
const uid = parsePsUid(await run(PS, ['-o', 'uid=', '-p', String(pid)]));
|
|
528
|
-
if (uid === null || uid !==
|
|
566
|
+
if (uid === null || uid !== real) return false;
|
|
529
567
|
const out = await run(LSOF, ['-nP', '-a', '-p', String(pid), '-i4TCP', '-sTCP:LISTEN', '-Fn']);
|
|
530
|
-
return parseLsofListeners(out).some(([host, p]) =>
|
|
568
|
+
return parseLsofListeners(out).some(([host, p]) => host === '127.0.0.1' && p === port);
|
|
531
569
|
} catch {
|
|
532
570
|
return false;
|
|
533
571
|
}
|
|
@@ -587,3 +625,32 @@ export class CheckedAgent extends http.Agent {
|
|
|
587
625
|
expectPid: this.expectPid, vouchPid: this.vouchPid, ...this.ask }).then((socket) => callback(null, socket), (err) => callback(err));
|
|
588
626
|
}
|
|
589
627
|
}
|
|
628
|
+
|
|
629
|
+
const USAGE = 'usage: fabric-peer-owner.mjs listener <port> [--json]\n';
|
|
630
|
+
|
|
631
|
+
// `listener <port> [--json]`: the kind of who listens on 127.0.0.1:<port>, which the dashboard link gate takes as
|
|
632
|
+
// listener_owner (FAA-315), or the whole owner as JSON; 2 on a usage error. `listener` and `write` are the tests'.
|
|
633
|
+
export async function main(argv, { listener: ask = listener, write = (text) => process.stdout.write(text) } = {}) {
|
|
634
|
+
const asJson = argv.length === 3 && argv[2] === '--json';
|
|
635
|
+
const port = Number(argv[1]);
|
|
636
|
+
if (argv.length !== 2 + Number(asJson) || argv[0] !== 'listener' || !/^[0-9]+$/.test(argv[1] ?? '') || port < 1 || port > 65535) {
|
|
637
|
+
process.stderr.write(USAGE);
|
|
638
|
+
return 2;
|
|
639
|
+
}
|
|
640
|
+
const who = await ask(port);
|
|
641
|
+
write(asJson ? `${JSON.stringify({ kind: who.kind, uid: who.uid, pid: who.pid, method: who.method, reason: who.reason })}\n` : `${who.kind}\n`);
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Whether node was started on this file, by real paths (a symlinked install folder, as check_dashboard_layout.mjs).
|
|
646
|
+
function startedDirectly() {
|
|
647
|
+
try {
|
|
648
|
+
return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
649
|
+
} catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (startedDirectly()) {
|
|
655
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (err) => { process.stderr.write(`${err?.message ?? err}\n`); process.exitCode = 1; });
|
|
656
|
+
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs
CHANGED
|
@@ -38,6 +38,31 @@ export function credentialName(name) {
|
|
|
38
38
|
|
|
39
39
|
/** A JWT or JWS anywhere in a value. */
|
|
40
40
|
export const JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/;
|
|
41
|
+
const isUrlBase64 = (code) => (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || (code >= 0x30 && code <= 0x39)
|
|
42
|
+
|| code === 0x2d || code === 0x5f;
|
|
43
|
+
const runEnd = (text, from) => { let end = from; while (end < text.length && isUrlBase64(text.charCodeAt(end))) end += 1; return end; };
|
|
44
|
+
/** The spans JWT_SHAPE matches in a text, leftmost and not overlapping, read in one pass (the contract's jwtSpans,
|
|
45
|
+
* FAA-313): the regular expression backtracks from every `eyJ` of a long run, quadratic in it, while every `eyJ` of one
|
|
46
|
+
* run shares that run's end, so only its first is tried, and a match ends where its third segment's run ends. `first`
|
|
47
|
+
* stops at the first match. */
|
|
48
|
+
export function jwtSpans(text, first = false) {
|
|
49
|
+
const spans = [];
|
|
50
|
+
let from = 0;
|
|
51
|
+
for (let at = text.indexOf('eyJ', from); at >= 0; at = text.indexOf('eyJ', from)) {
|
|
52
|
+
const one = runEnd(text, at + 3);
|
|
53
|
+
from = one;
|
|
54
|
+
if (one - at - 3 < 2 || text[one] !== '.') continue;
|
|
55
|
+
const two = runEnd(text, one + 1);
|
|
56
|
+
if (two - one - 1 < 2 || text[two] !== '.') continue;
|
|
57
|
+
const three = runEnd(text, two + 1);
|
|
58
|
+
spans.push([at, three]);
|
|
59
|
+
if (first) break;
|
|
60
|
+
from = three;
|
|
61
|
+
}
|
|
62
|
+
return spans;
|
|
63
|
+
}
|
|
64
|
+
/** Whether JWT_SHAPE matches anywhere in a text, read in one pass (the contract's hasJwt, FAA-313). */
|
|
65
|
+
export const hasJwt = (text) => jwtSpans(text, true).length > 0;
|
|
41
66
|
/** A value that says it is a credential (`Bearer ` at its start, in any case), or a provider key's shape with its prefix
|
|
42
67
|
* in its canonical case only, at the start of a value, after a character that is no ASCII letter or digit, or after an
|
|
43
68
|
* escape; an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more,
|
|
@@ -48,7 +73,7 @@ const LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
|
|
|
48
73
|
export const longSecret = (value) => (value.match(LONG_RUN) ?? []).some((run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run));
|
|
49
74
|
|
|
50
75
|
/** Whether a value could be a credential: a JWT anywhere, a provider key or `Bearer …`, or a long secret-shaped run. */
|
|
51
|
-
export const secretValue = (value) =>
|
|
76
|
+
export const secretValue = (value) => hasJwt(value) || SECRET_VALUE.test(value) || longSecret(value);
|
|
52
77
|
|
|
53
78
|
export const BEARER_INSIDE = /\bbearer\s+(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{12,}=*/i;
|
|
54
79
|
const PATH_LIKE = /^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*:\/\/)/i;
|
|
@@ -58,7 +83,7 @@ const pathLike = (piece) =>
|
|
|
58
83
|
|
|
59
84
|
/** The same for a launch argument or a server's command and URL: a long run is looked for between path separators. */
|
|
60
85
|
export const secretArgument = (value) =>
|
|
61
|
-
|
|
86
|
+
hasJwt(value) || SECRET_VALUE.test(value) || BEARER_INSIDE.test(value) ||
|
|
62
87
|
value.split(/[\s=]/).some((piece) => (pathLike(piece) ? piece.split(/[\\/]/).some((part) => longSecret(part)) : longSecret(piece)));
|
|
63
88
|
|
|
64
89
|
const decodeKeepingPlus = (raw) => { try { return decodeURIComponent(raw); } catch { return raw; } };
|
|
@@ -122,7 +147,8 @@ export function repositoryPassword(value) {
|
|
|
122
147
|
|
|
123
148
|
// A repository's parameters, read as fabric_secrets.secret_url reads them, by hand: `new URL`
|
|
124
149
|
// refuses a host it cannot parse and a special scheme written without `//` (`https:host/r?access_token=...`), where
|
|
125
|
-
// the Python twin
|
|
150
|
+
// the Python twin's repository_credential reads them too (0.62.0 readings 8 and 9, FAA-200 (17)); its secret_url, like
|
|
151
|
+
// secretUrl, refuses such a URL since FAA-309.
|
|
126
152
|
function repositoryParts(head, tail) {
|
|
127
153
|
const text = tail.replace(/[\x00-\x20]+$/, '').replace(/[\t\n\r]/g, '');
|
|
128
154
|
const hash = text.indexOf('#');
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py
CHANGED
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
`degraded[]`. Part of the service kit (FAA-62, layer L3). Standard library only."""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import bisect
|
|
8
|
-
import ipaddress
|
|
9
7
|
import re
|
|
10
|
-
import unicodedata
|
|
11
8
|
import urllib.parse
|
|
12
9
|
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
|
13
10
|
|
|
@@ -18,7 +15,12 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
18
15
|
|
|
19
16
|
from fabric_base import ServiceError # noqa: E402
|
|
20
17
|
from fabric_secrets import ( # noqa: E402
|
|
21
|
-
|
|
18
|
+
_BEARER_INSIDE as _BEARER, _long_secret, credential_name, has_jwt, raw_values, secret_argument, secret_value,
|
|
19
|
+
)
|
|
20
|
+
# The WHATWG host reading and its IDNA tables (FAA-98) live in fabric_secrets since FAA-309, where secret_url reads a
|
|
21
|
+
# URL's host with them; the names stay here for code and tests that reached them.
|
|
22
|
+
from fabric_secrets import ( # noqa: E402,F401
|
|
23
|
+
_FORBIDDEN_DOMAIN, _FORBIDDEN_HOST, _IDNA_DISALLOWED, _IDNA_IGNORED, _domain_ok, _idna_host, _ipv4_number, _ipv6_ok,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
CONNECTIONS_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/connections/0.1"
|
|
@@ -171,153 +173,6 @@ _credential_name, _secret_value, _secret_argument = credential_name, secret_valu
|
|
|
171
173
|
|
|
172
174
|
|
|
173
175
|
_SPECIAL = ("http", "https", "ws", "wss", "ftp", "file")
|
|
174
|
-
_FORBIDDEN_HOST = set("\x00\t\n\r #/:<>?@[\\]^|")
|
|
175
|
-
_FORBIDDEN_DOMAIN = _FORBIDDEN_HOST | set(chr(c) for c in range(0x20)) | {"%", "\x7f"}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# FAA-98: UTS46 narrowed, not ported. A non-ASCII host is refused only where every Node from 20 to 26 refuses it, so
|
|
179
|
-
# the doubt stays on the closed side (its URL's credentials are read). The two tables are measured by
|
|
180
|
-
# test/fixtures/idna/make_idna_tables.mjs (idna-tables.json, held equal by test_the_idna_tables_are_the_measured_ones):
|
|
181
|
-
# code points refused in every context, and code points removed between letters. The contextual rules read Unicode 3.2
|
|
182
|
-
# classes (`unicodedata.ucd_3_2_0`) and skip what that Unicode does not know, since Node's own UTS46 data lags its
|
|
183
|
-
# Unicode: an Arabic letter of Unicode 14 is no right-to-left letter to it.
|
|
184
|
-
_IDNA_DISALLOWED = (
|
|
185
|
-
(0x80, 0xa0), (0xa8, 0xa8), (0xaf, 0xaf), (0xb4, 0xb4), (0xb8, 0xb8), (0x2d8, 0x2dd), (0x37a, 0x37a),
|
|
186
|
-
(0x384, 0x385), (0x600, 0x605), (0x61c, 0x61c), (0x6dd, 0x6dd), (0x70f, 0x70f), (0x890, 0x891), (0x8e2, 0x8e2),
|
|
187
|
-
(0x1680, 0x1680), (0x1fbd, 0x1fbd), (0x1fbf, 0x1fc1), (0x1fcd, 0x1fcf), (0x1fdd, 0x1fdf), (0x1fed, 0x1fee),
|
|
188
|
-
(0x1ffd, 0x1ffe), (0x2000, 0x200a), (0x200e, 0x200f), (0x2017, 0x2017), (0x2024, 0x2026), (0x2028, 0x202f),
|
|
189
|
-
(0x203e, 0x203e), (0x2047, 0x2049), (0x205f, 0x205f), (0x2066, 0x2069), (0x2100, 0x2101), (0x2105, 0x2106),
|
|
190
|
-
(0x2488, 0x249b), (0x2a74, 0x2a74), (0x2ff0, 0x3000), (0x309b, 0x309c), (0x31ef, 0x31ef), (0x33c2, 0x33c2),
|
|
191
|
-
(0x33c7, 0x33c7), (0x33d8, 0x33d8), (0xe000, 0xf8ff), (0xfc5e, 0xfc63), (0xfdfa, 0xfdfb), (0xfe12, 0xfe13),
|
|
192
|
-
(0xfe16, 0xfe16), (0xfe19, 0xfe19), (0xfe30, 0xfe30), (0xfe47, 0xfe4c), (0xfe52, 0xfe52), (0xfe55, 0xfe56),
|
|
193
|
-
(0xfe5f, 0xfe5f), (0xfe64, 0xfe65), (0xfe68, 0xfe68), (0xfe6a, 0xfe6b), (0xfe70, 0xfe70), (0xfe72, 0xfe72),
|
|
194
|
-
(0xfe74, 0xfe74), (0xfe76, 0xfe76), (0xfe78, 0xfe78), (0xfe7a, 0xfe7a), (0xfe7c, 0xfe7c), (0xfe7e, 0xfe7e),
|
|
195
|
-
(0xff03, 0xff03), (0xff05, 0xff05), (0xff0f, 0xff0f), (0xff1a, 0xff1a), (0xff1c, 0xff1c), (0xff1e, 0xff20),
|
|
196
|
-
(0xff3b, 0xff3e), (0xff5c, 0xff5c), (0xffe3, 0xffe3), (0xfff9, 0xfffd), (0x110bd, 0x110bd), (0x110cd, 0x110cd),
|
|
197
|
-
(0x13430, 0x1343f), (0x1f100, 0x1f100), (0xe0001, 0xe0001), (0xe0020, 0xe007f), (0xf0000, 0xffffd),
|
|
198
|
-
(0x100000, 0x10fffd),
|
|
199
|
-
)
|
|
200
|
-
_IDNA_IGNORED = (
|
|
201
|
-
(0xad, 0xad), (0x34f, 0x34f), (0x180b, 0x180d), (0x180f, 0x180f), (0x200b, 0x200b), (0x2060, 0x2060),
|
|
202
|
-
(0x2064, 0x2064), (0xfe00, 0xfe0f), (0xfeff, 0xfeff), (0x1bca0, 0x1bca3), (0xe0100, 0xe01ef),
|
|
203
|
-
)
|
|
204
|
-
_IDNA_DOTS = re.compile("[.\u3002\uff0e\uff61]")
|
|
205
|
-
_U32 = unicodedata.ucd_3_2_0
|
|
206
|
-
_RTL_OK = {"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}
|
|
207
|
-
_JOINERS = "\u200c\u200d"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _in_ranges(ranges: Sequence[tuple], cp: int) -> bool:
|
|
211
|
-
i = bisect.bisect_right(ranges, (cp, 0x10ffff)) - 1
|
|
212
|
-
return i >= 0 and ranges[i][0] <= cp <= ranges[i][1]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _known(c: str) -> bool:
|
|
216
|
-
return _U32.category(c) != "Cn" and unicodedata.category(c) != "Cn"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _joiner_refused(label: str, i: int) -> bool:
|
|
220
|
-
"""CheckJoiners as Node 20 to 26 all read it: they find a joining context, or a virama, anywhere in the label
|
|
221
|
-
(`\u0628a\u200c\u0627` and `\u0628\u200c\u0627\u200d` parse), so only a joiner with nothing but ASCII beside it
|
|
222
|
-
in its label has none for certain."""
|
|
223
|
-
return all(c.isascii() for j, c in enumerate(label) if j != i)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def _bidi_refused(label: str) -> bool:
|
|
227
|
-
"""RFC 5893 as Node 20 to 26 all read it: only a label holding R, AL or AN is judged, and its first code point is
|
|
228
|
-
not (Node 26 alone refuses a label opening with a digit or a hyphen)."""
|
|
229
|
-
if not label or not all(_known(c) for c in label) or any(c in _JOINERS for c in label):
|
|
230
|
-
return False # Node 20 to 26 pass a label holding a joiner over (`\u05d0a\u094d\u200d` parses)
|
|
231
|
-
k = [_U32.bidirectional(c) for c in label]
|
|
232
|
-
if not any(x in ("R", "AL", "AN") for x in k):
|
|
233
|
-
return False
|
|
234
|
-
body = list(k)
|
|
235
|
-
while body and body[-1] == "NSM":
|
|
236
|
-
body.pop()
|
|
237
|
-
if not body or body[-1] not in ("R", "AL", "EN", "AN"):
|
|
238
|
-
return True
|
|
239
|
-
return k[0] in ("R", "AL") and (any(x not in _RTL_OK for x in k) or ("EN" in k and "AN" in k))
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _idna_host(host: str) -> Optional[str]:
|
|
243
|
-
"""None where WHATWG's domain-to-ASCII refuses a non-ASCII host for certain; else the host as UTS46 maps it, read
|
|
244
|
-
as NFKC with case folding (exact enough for the ASCII rules that follow, which read digits and dots)."""
|
|
245
|
-
if any(_in_ranges(_IDNA_DISALLOWED, ord(c)) for c in host):
|
|
246
|
-
return None
|
|
247
|
-
kept = "".join(c for c in host if not _in_ranges(_IDNA_IGNORED, ord(c)))
|
|
248
|
-
if kept == "":
|
|
249
|
-
return None # nothing left: an empty host
|
|
250
|
-
if not all(_known(c) for c in kept):
|
|
251
|
-
return host # a code point this Python's Unicode does not know: the contextual rules are not read
|
|
252
|
-
mapped = unicodedata.normalize("NFC", unicodedata.normalize("NFKC", unicodedata.normalize("NFKC", kept).casefold()))
|
|
253
|
-
for label in _IDNA_DOTS.split(mapped):
|
|
254
|
-
first = label[:1]
|
|
255
|
-
if first and first not in "\u17b4\u17b5" and _U32.category(first)[0] == "M" and unicodedata.category(first)[0] == "M":
|
|
256
|
-
return None # a label opening with a mark (U+17B4 and U+17B5 are ignored by the newer Nodes)
|
|
257
|
-
if any(c in _JOINERS and _joiner_refused(label, i) for i, c in enumerate(label)):
|
|
258
|
-
return None
|
|
259
|
-
if _bidi_refused(label):
|
|
260
|
-
return None
|
|
261
|
-
return _IDNA_DOTS.sub(".", mapped)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _ipv4_number(part: str) -> Optional[int]:
|
|
265
|
-
if part == "":
|
|
266
|
-
return None # an empty part fails WHATWG's IPv4 parser (`1..2`); `0x` alone is 0
|
|
267
|
-
base = 10
|
|
268
|
-
if part[:2].lower() == "0x":
|
|
269
|
-
part, base = part[2:], 16
|
|
270
|
-
elif len(part) > 1 and part[0] == "0":
|
|
271
|
-
part, base = part[1:], 8
|
|
272
|
-
if part == "":
|
|
273
|
-
return 0
|
|
274
|
-
digits = {10: "0123456789", 16: "0123456789abcdefABCDEF", 8: "01234567"}[base]
|
|
275
|
-
return int(part, base) if all(c in digits for c in part) else None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _domain_ok(host: str) -> bool:
|
|
279
|
-
"""The WHATWG host parser on a special scheme's host, enough to know when `new URL` refuses it."""
|
|
280
|
-
if host.startswith("["):
|
|
281
|
-
return host.endswith("]") and _ipv6_ok(host[1:-1])
|
|
282
|
-
if re.search(r"%(?![0-9A-Fa-f]{2})", host) is None:
|
|
283
|
-
try:
|
|
284
|
-
host = urllib.parse.unquote_to_bytes(host).decode("utf-8")
|
|
285
|
-
except UnicodeDecodeError:
|
|
286
|
-
return False
|
|
287
|
-
if any(c in _FORBIDDEN_DOMAIN for c in host):
|
|
288
|
-
return False
|
|
289
|
-
# A non-ASCII host goes through UTS46 in WHATWG; this module reads it narrowly (_idna_host, FAA-98). When unsure,
|
|
290
|
-
# the host is taken as valid, so its URL's credentials are read: the doubt then hides a list rather than publish one
|
|
291
|
-
# (round-2 reading). A host UTS46 maps to ASCII is read by the ASCII rules below (`127.0.0.1` is an address).
|
|
292
|
-
if any(ord(c) > 0x7f for c in host):
|
|
293
|
-
mapped = _idna_host(host)
|
|
294
|
-
if mapped is None:
|
|
295
|
-
return False
|
|
296
|
-
if not mapped.isascii() or any(c in _FORBIDDEN_DOMAIN for c in mapped):
|
|
297
|
-
return True
|
|
298
|
-
host = mapped
|
|
299
|
-
labels = host.split(".")
|
|
300
|
-
if labels and labels[-1] == "" and len(labels) > 1:
|
|
301
|
-
labels = labels[:-1]
|
|
302
|
-
last = labels[-1] if labels else ""
|
|
303
|
-
if not (last and (last.isdigit() and last.isascii() or re.fullmatch(r"0[xX][0-9A-Fa-f]*", last))):
|
|
304
|
-
return host != ""
|
|
305
|
-
numbers = [_ipv4_number(label) for label in labels]
|
|
306
|
-
if len(numbers) > 4 or any(n is None for n in numbers):
|
|
307
|
-
return False
|
|
308
|
-
return all(n <= 255 for n in numbers[:-1]) and numbers[-1] < 256 ** (5 - len(numbers))
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _ipv6_ok(text: str) -> bool:
|
|
312
|
-
if "%" in text:
|
|
313
|
-
return False # a zone id: `ipaddress` takes one, WHATWG does not
|
|
314
|
-
try:
|
|
315
|
-
ipaddress.IPv6Address(text)
|
|
316
|
-
return True
|
|
317
|
-
except ValueError:
|
|
318
|
-
return False
|
|
319
|
-
|
|
320
|
-
|
|
321
176
|
_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
322
177
|
_QUERY_ENCODE = set(chr(c) for c in range(0x21)) | set('"#<>')
|
|
323
178
|
_FRAGMENT_ENCODE = set(chr(c) for c in range(0x21)) | set('"<>`')
|
|
@@ -388,7 +243,7 @@ def _whatwg_parts(value: str) -> Optional[Dict[str, str]]:
|
|
|
388
243
|
return None
|
|
389
244
|
elif any(c in _FORBIDDEN_HOST for c in host):
|
|
390
245
|
return None
|
|
391
|
-
elif host == "" and ("@" in authority or
|
|
246
|
+
elif host == "" and ("@" in authority or ":" in hostport):
|
|
392
247
|
return None
|
|
393
248
|
return {"password": password, "query": query, "fragment": fragment}
|
|
394
249
|
|
|
@@ -424,7 +279,7 @@ def _secret_url(value: str) -> bool:
|
|
|
424
279
|
|
|
425
280
|
|
|
426
281
|
def _credential(value: str, runs: bool) -> bool:
|
|
427
|
-
return bool(
|
|
282
|
+
return bool(has_jwt(value) or _KEY_SHAPE.search(value) or _BEARER.search(value) # has_jwt: one pass (FAA-313)
|
|
428
283
|
or (runs and _long_run(value)) or any(_secret_url(url) for url in _urls_in(value)))
|
|
429
284
|
|
|
430
285
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py
CHANGED
|
@@ -55,7 +55,7 @@ from fabric_usage import (
|
|
|
55
55
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.
|
|
58
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.7"
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py
CHANGED
|
@@ -55,6 +55,14 @@ LIFT_ACTION = "limit.lift"
|
|
|
55
55
|
_MAX_STATUS_MESSAGE = 500
|
|
56
56
|
_WAITING = re.compile(r"^waiting (daily-quota|caller-share|concurrency|money) since ([0-9][0-9TZ:.-]*?)(?:: ([\s\S]+))?\Z")
|
|
57
57
|
_SIZES = ("jobs", "usd", "limit")
|
|
58
|
+
#: One run of ECMAScript's whitespace, the class both twins squash with (FAA-316): Python's str.split() also takes
|
|
59
|
+
#: U+001C to U+001F and U+0085 and not U+FEFF, so the twins wrote different statusMessages.
|
|
60
|
+
_SPACES = re.compile("[%s]+" % fabric_secrets.JS_SPACE)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _squash(text: str) -> str:
|
|
64
|
+
"""Runs of ECMAScript whitespace as one space, none at either end (the Node twin's `squash`)."""
|
|
65
|
+
return _SPACES.sub(" ", text).strip(" ")
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
def _instant(value: Any, name: str) -> str:
|
|
@@ -75,10 +83,10 @@ def waiting_message(reason: str, since: str, until: Optional[str] = None, messag
|
|
|
75
83
|
text = "waiting %s since %s" % (reason, since)
|
|
76
84
|
head = len(QUOTA_EXHAUSTED) + (0 if until is None else len(" until ") + len(until)) + len(": ")
|
|
77
85
|
if message is not None:
|
|
78
|
-
message =
|
|
86
|
+
message = _squash(str(message))
|
|
79
87
|
room = _MAX_STATUS_MESSAGE - head - len(text) - len(": ")
|
|
80
88
|
if message and room > 0:
|
|
81
|
-
text += ": " + (message if len(message) <= room else message[:room - 3].rstrip() + "...")
|
|
89
|
+
text += ": " + (message if len(message) <= room else message[:room - 3].rstrip(" ") + "...")
|
|
82
90
|
return pause_message(until, text)
|
|
83
91
|
|
|
84
92
|
|
|
@@ -124,11 +132,11 @@ def hold_sentence(caller: str, work: str, reason: str, until: Optional[str]) ->
|
|
|
124
132
|
|
|
125
133
|
|
|
126
134
|
def _text(value: Any, name: str, high: int = 200) -> str:
|
|
127
|
-
if not isinstance(value, str) or not value
|
|
135
|
+
if not isinstance(value, str) or not _squash(value) or len(value) > high:
|
|
128
136
|
raise ServiceError("%s is a text of 1 to %d characters." % (name, high))
|
|
129
137
|
if fabric_secrets.secret_value(value):
|
|
130
138
|
raise ServiceError("%s looks like a credential; a hold names callers and work, never a secret." % name)
|
|
131
|
-
return
|
|
139
|
+
return _squash(value)
|
|
132
140
|
|
|
133
141
|
|
|
134
142
|
class HoldBook:
|
|
@@ -73,8 +73,9 @@ class HttpError(Exception):
|
|
|
73
73
|
self.retry_after_s = retry_after_s
|
|
74
74
|
self.body = bytes(body[:2048])
|
|
75
75
|
self.kind = kind # http (an answer), connection, timeout, too-large, refused, foreign-user
|
|
76
|
-
#: For foreign-user, why (FAA-272): `table-restricted` (this process sees no other process's sockets,
|
|
77
|
-
#: never clears), the owner's kind (`another-account`,
|
|
76
|
+
#: For foreign-user, why (FAA-272): `table-restricted` (this process sees no other process's sockets, or on
|
|
77
|
+
#: macOS no row for this connection, DEC-0155 RT-1; it never clears), the owner's kind (`another-account`,
|
|
78
|
+
#: `system`) or the check's reason (`no-row`, from Linux and Windows only).
|
|
78
79
|
self.reason = reason
|
|
79
80
|
|
|
80
81
|
|