@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.41.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +18 -6
  3. package/SECURITY.md +27 -6
  4. package/bin/fabric-agent-adapter.js +71 -6
  5. package/package.json +2 -2
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +42 -33
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +104 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +465 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +5 -4
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +129 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +48 -19
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -184
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +288 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +145 -0
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +848 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +7 -18
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +11 -68
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +50 -16
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +235 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +146 -32
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +404 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +408 -0
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +91 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +882 -0
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -39
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +336 -0
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +113 -222
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +44 -405
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +140 -0
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +119 -77
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +285 -109
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +32 -39
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +437 -0
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1621 -0
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
  106. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
  107. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
  108. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  109. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
  110. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +26 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +122 -1392
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +278 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +539 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +324 -0
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +173 -0
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +491 -0
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +11 -1
  121. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
  122. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
  123. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
@@ -0,0 +1,69 @@
1
+ // Where a service's things live (fabric-service/0.1): the services directory, its data, config, logs and run dirs per OS, private dirs, atomic writes.
2
+ // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_paths.py. Node 20+, no dependency.
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { ID, ServiceError, expand } from './fabric-base.mjs';
8
+ import { windowsProtect } from './fabric-platform.mjs';
9
+
10
+ // DEC-0032: LOCALAPPDATA, not APPDATA, so token files never roam with the profile.
11
+ const localAppData = () => process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
12
+
13
+ export function servicesDir(platform = process.platform) {
14
+ if (process.env.FABRIC_SERVICES_DIR) return expand(process.env.FABRIC_SERVICES_DIR);
15
+ const home = os.homedir();
16
+ if (platform === 'darwin') return path.join(home, 'Library/Application Support/ai.passioncode.fabric/services');
17
+ if (platform === 'win32') return path.join(localAppData(), 'passioncode-fabric', 'services');
18
+ return path.join(process.env.XDG_DATA_HOME || path.join(home, '.local/share'), 'passioncode-fabric/services');
19
+ }
20
+
21
+ export function serviceDirs(id, platform = process.platform) {
22
+ if (!ID.test(id)) throw new ServiceError(`Service id ${id} must match ${ID}.`);
23
+ const home = os.homedir();
24
+ if (platform === 'darwin') {
25
+ return {
26
+ data: path.join(home, 'Library/Application Support', id),
27
+ logs: path.join(home, 'Library/Logs', id),
28
+ cache: path.join(home, 'Library/Caches', id),
29
+ };
30
+ }
31
+ if (platform === 'win32') {
32
+ const data = path.join(localAppData(), id);
33
+ return { data, logs: path.join(data, 'Logs'), cache: path.join(data, 'Cache') };
34
+ }
35
+ return {
36
+ data: path.join(process.env.XDG_DATA_HOME || path.join(home, '.local/share'), id),
37
+ logs: path.join(process.env.XDG_STATE_HOME || path.join(home, '.local/state'), id, 'logs'),
38
+ cache: path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), id),
39
+ };
40
+ }
41
+
42
+ export function ensurePrivateDir(dir) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ if (process.platform === 'win32') windowsProtect(dir, { folder: true });
45
+ else fs.chmodSync(dir, 0o700);
46
+ return dir;
47
+ }
48
+
49
+ export function atomicWrite(file, data, mode = 0o600) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}`);
52
+ const fd = fs.openSync(tmp, 'wx', mode);
53
+ try {
54
+ // On Windows the file's ACL is its privacy (DEC-0032): set explicitly and protected — before the bytes go in —
55
+ // so it does not depend on what the folder hands down. A rename keeps it.
56
+ if (process.platform === 'win32') windowsProtect(tmp, { folder: false });
57
+ fs.writeSync(fd, data);
58
+ fs.fsyncSync(fd);
59
+ } finally {
60
+ fs.closeSync(fd);
61
+ }
62
+ try {
63
+ if (process.platform !== 'win32') fs.chmodSync(tmp, mode);
64
+ fs.renameSync(tmp, file);
65
+ } catch (error) {
66
+ fs.rmSync(tmp, { force: true });
67
+ throw error;
68
+ }
69
+ }
@@ -0,0 +1,164 @@
1
+ // Windows, where the kit needs it (DEC-0032): reading a file's ACL to refuse a token file others can read.
2
+ // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_platform.py. Node 20+, no dependency.
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { ServiceError } from './fabric-base.mjs';
9
+
10
+ export const WINDOWS_TRUSTED = Object.freeze(['S-1-5-18', 'S-1-5-32-544']);
11
+
12
+ // Node has no ACL API. Windows PowerShell (5.1, present on every Windows) reads the list through .NET
13
+ // and icacls writes it; both get the path through the environment, never argv, and print no contents.
14
+ export const SYSTEM_SID = 'S-1-5-18';
15
+
16
+ export const ADMINISTRATORS_SID = 'S-1-5-32-544';
17
+
18
+ /** A string SID: revision 1, an identifier authority and up to 15 sub-authorities of 32 bits each. */
19
+ const SID_TEXT = /^S-1-\d{1,15}(-\d{1,10}){0,15}$/i;
20
+
21
+ /** A readable SID in upper case (SIDs compare case-insensitively on their S), or undefined. */
22
+ const sidOf = (value) => (typeof value === 'string' && SID_TEXT.test(value) && value.split('-').slice(3).every((part) => Number(part) <= 0xffffffff) ? value.toUpperCase() : undefined);
23
+
24
+ /**
25
+ * service.md "Windows token files" (DEC-0032, DEC-0033, DEC-0057, FAC-SEM-079) on what a reader has read: `file` is
26
+ * `{isRegularFile, isReparsePoint, realPathInProfile, ownerSid, aces: [{type, sid, mask}]}`, the mask the ACE's unsigned
27
+ * 32-bit access mask. The first refusal `{problem, at, sid?, message}`, or null when the file may be read: a regular
28
+ * file, not a reparse point, inside the profile; owned by the user, SYSTEM or Administrators; every allow ACE with a
29
+ * non-zero mask naming one of those three; deny ACEs ignored; any other ACE type refused; anything unreadable refused.
30
+ * The contract's `windowsTokenAclProblem`; a refusal names the SID, never the contents.
31
+ */
32
+ export function windowsTokenAclProblem(file, currentUserSid) {
33
+ const refuse = (problem, at, message, sid) => (sid === undefined ? { problem, at, message } : { problem, at, sid, message });
34
+ const user = sidOf(currentUserSid);
35
+ if (user === undefined) return refuse('unreadable', '/currentUserSid', "the current user's SID is unreadable");
36
+ if (typeof file !== 'object' || file === null || Array.isArray(file)) return refuse('unreadable', '/file', "the token file's metadata is unreadable");
37
+ for (const flag of ['isRegularFile', 'isReparsePoint', 'realPathInProfile']) {
38
+ if (typeof file[flag] !== 'boolean') return refuse('unreadable', `/file/${flag}`, `${flag} is unreadable`);
39
+ }
40
+ if (file.isReparsePoint) return refuse('reparse-point', '/file/isReparsePoint', 'the token file is a reparse point (a symlink or a junction)');
41
+ if (!file.isRegularFile) return refuse('not-regular-file', '/file/isRegularFile', 'the token file is not a regular file');
42
+ if (!file.realPathInProfile) return refuse('outside-profile', '/file/realPathInProfile', "the token file's real path lies outside the user's profile");
43
+ const trusted = new Set([user, SYSTEM_SID, ADMINISTRATORS_SID]);
44
+ const owner = sidOf(file.ownerSid);
45
+ if (owner === undefined) return refuse('unreadable', '/file/ownerSid', 'the owner SID is unreadable');
46
+ if (!trusted.has(owner)) return refuse('owner', '/file/ownerSid', `the token file is owned by ${owner}, not by the current user, SYSTEM or Administrators`, owner);
47
+ if (!Array.isArray(file.aces)) return refuse('unreadable', '/file/aces', 'the ACL is unreadable');
48
+ for (const [index, ace] of file.aces.entries()) {
49
+ const at = `/file/aces/${index}`;
50
+ if (typeof ace !== 'object' || ace === null || Array.isArray(ace)) return refuse('unreadable', at, `ACE ${index} is unreadable`);
51
+ if (typeof ace.type !== 'string') return refuse('unreadable', `${at}/type`, `ACE ${index} has an unreadable type`);
52
+ const trustee = sidOf(ace.sid);
53
+ if (trustee === undefined) return refuse('unreadable', `${at}/sid`, `ACE ${index} names an unreadable SID`);
54
+ if (typeof ace.mask !== 'number' || !Number.isInteger(ace.mask) || ace.mask < 0 || ace.mask > 0xffffffff) return refuse('unreadable', `${at}/mask`, `ACE ${index} has an unreadable access mask`);
55
+ if (ace.type === 'deny') continue;
56
+ if (ace.type !== 'allow') return refuse('ace-type', `${at}/type`, `ACE ${index} for ${trustee} is of a type the rule does not read`, trustee);
57
+ if (ace.mask === 0) continue;
58
+ if (!trusted.has(trustee)) return refuse('ace-sid', `${at}/sid`, `the token file grants access to ${trustee}; only the current user, SYSTEM and Administrators may hold any`, trustee);
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /** windowsTokenAclProblem as one sentence naming the file and the refusal, or null. */
64
+ export function tokenFileProblem(file, metadata, user) {
65
+ const problem = windowsTokenAclProblem(metadata, user);
66
+ return problem ? `Token file ${file} refused: ${problem.message}.` : null;
67
+ }
68
+
69
+ // The raw descriptor, through .NET: every ACE's type, its trustee's SID and its access mask as read (a signed int;
70
+ // parseAclReport converts it to the unsigned mask). An inherit-only ACE applies to children only and is left out.
71
+ const ACL_SCRIPT = [
72
+ "$ErrorActionPreference = 'Stop'",
73
+ '$acl = [System.IO.File]::GetAccessControl($env:FABRIC_ACL_PATH)',
74
+ "$raw = New-Object System.Security.AccessControl.RawSecurityDescriptor($acl.GetSecurityDescriptorSddlForm('Owner, Access'))",
75
+ '$dacl = $raw.DiscretionaryAcl',
76
+ "$aces = @(if ($dacl) { $dacl | Where-Object { -not ([int]$_.AceFlags -band 8) } | ForEach-Object { @{ type = [string]$_.AceType; sid = $(if ($_.SecurityIdentifier) { $_.SecurityIdentifier.Value } else { '' }); mask = [int64]$_.AccessMask } } })",
77
+ '$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value',
78
+ '[Console]::Out.Write((ConvertTo-Json -Compress -Depth 4 @{ owner = $raw.Owner.Value; aces = $aces; nullDacl = [bool](-not $dacl); user = $user }))',
79
+ ].join('; ');
80
+
81
+ function windowsHelper(command, args, extraEnv = {}) {
82
+ // No PSModulePath: the script uses .NET only, and a module path inherited from PowerShell 7 makes
83
+ // Windows PowerShell load the wrong modules.
84
+ const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.toLowerCase() !== 'psmodulepath'));
85
+ const result = spawnSync(command, args, { env: { ...env, ...extraEnv }, encoding: 'utf8', windowsHide: true, timeout: 30_000 });
86
+ if (result.error || result.status !== 0) {
87
+ throw new ServiceError(`${command} failed: ${(result.error?.message || result.stderr || result.stdout || '').trim().slice(0, 200)}`);
88
+ }
89
+ return result.stdout;
90
+ }
91
+
92
+ /** .NET's AceType names as the rule's ACE type: allow and deny are read, every other type is refused (DEC-0057). */
93
+ const ACE_TYPES = { AccessAllowed: 'allow', AccessDenied: 'deny', AccessAllowedObject: 'allow-object', AccessDeniedObject: 'deny-object',
94
+ AccessAllowedCallback: 'allow-callback', AccessDeniedCallback: 'deny-callback', AccessAllowedCallbackObject: 'allow-object-callback',
95
+ AccessDeniedCallbackObject: 'deny-object-callback' };
96
+
97
+ /**
98
+ * The ACL report PowerShell prints, as (owner SID, ACEs `{type, sid, mask}`, current user). The mask arrives signed
99
+ * (GENERIC_READ 0x80000000 is negative in .NET) and is read as the unsigned 32-bit mask; a NULL DACL grants everyone
100
+ * everything and is read as an allow ACE for Everyone (S-1-1-0).
101
+ */
102
+ export function parseAclReport(text) {
103
+ const report = JSON.parse(text);
104
+ const aces = report.nullDacl ? [{ type: 'allow', sid: 'S-1-1-0', mask: 0x1f01ff }]
105
+ : [].concat(report.aces ?? []).map((ace) => ({ type: ACE_TYPES[ace.type] ?? String(ace.type), sid: ace.sid, mask: Number.isInteger(ace.mask) ? ace.mask >>> 0 : ace.mask }));
106
+ return { owner: report.owner ?? '', aces, user: report.user };
107
+ }
108
+
109
+ /** (owner SID, the ACEs on the file itself, the current user's SID). */
110
+ export function windowsAcl(file) {
111
+ return parseAclReport(windowsHelper('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ACL_SCRIPT], { FABRIC_ACL_PATH: file }));
112
+ }
113
+
114
+ let userSid = null;
115
+
116
+ export function windowsUserSid() {
117
+ if (!userSid) {
118
+ const line = windowsHelper('whoami.exe', ['/user', '/fo', 'csv', '/nh']).trim();
119
+ userSid = (line.match(/"(S-1-[0-9-]+)"\s*$/) ?? [])[1];
120
+ if (!userSid) throw new ServiceError('whoami /user printed no SID.');
121
+ }
122
+ return userSid;
123
+ }
124
+
125
+ /**
126
+ * A protected list (inheritance off): this user, SYSTEM, Administrators, full control; a folder hands it down.
127
+ * `/reset` first drops any explicit entry already there (`/inheritance:r` keeps those), so the three are all that remain.
128
+ */
129
+ export function windowsProtect(target, { folder }) {
130
+ const grant = folder ? '(OI)(CI)F' : 'F';
131
+ windowsHelper('icacls.exe', [target, '/reset']);
132
+ windowsHelper('icacls.exe', [target, '/inheritance:r', '/grant:r', ...[windowsUserSid(), ...WINDOWS_TRUSTED].map((sid) => `*${sid}:${grant}`)]);
133
+ }
134
+
135
+ export function windowsTokenProblem(file, info) {
136
+ const profile = fs.realpathSync.native(process.env.USERPROFILE || os.homedir()).toLowerCase();
137
+ const real = fs.realpathSync.native(file).toLowerCase();
138
+ const inside = path.win32.relative(profile, real);
139
+ const { owner, aces, user } = windowsAcl(file);
140
+ return tokenFileProblem(file, {
141
+ isRegularFile: info.isFile(), isReparsePoint: info.isSymbolicLink(),
142
+ realPathInProfile: Boolean(inside) && !inside.startsWith('..') && !path.win32.isAbsolute(inside), ownerSid: owner, aces,
143
+ }, user);
144
+ }
145
+
146
+ export function tokenMatches(presented, token, scheme = 'Bearer') {
147
+ if (!presented) return false;
148
+ let value = presented;
149
+ if (scheme === 'Bearer') {
150
+ if (!presented.startsWith('Bearer ')) return false;
151
+ value = presented.slice(7);
152
+ }
153
+ const a = Buffer.from(value.trim());
154
+ const b = Buffer.from(token);
155
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
156
+ }
157
+
158
+ export function checkRequest(port, host, origin, secFetchSite) {
159
+ const hosts = new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
160
+ if (!hosts.has(host)) return `Host ${host} is not this service.`;
161
+ if (origin !== undefined && origin !== null && ![...hosts].some((h) => origin === `http://${h}`)) return `Origin ${origin} is not this service.`;
162
+ if (secFetchSite === 'cross-site') return 'Cross-site requests are refused.';
163
+ return null;
164
+ }
@@ -18,43 +18,12 @@ const BARE_BINARY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
18
18
 
19
19
  export class ProfileError extends Error {}
20
20
 
21
- const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
22
- const list = (value) => (Array.isArray(value) ? value.filter(isObject) : []);
21
+ import { isObject, objects as list, indexed, show, jsonEqual, toNumber } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
23
22
  const strings = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []);
24
- /** The object items of an array with their index as written, so an instancePath names the item even after a non-object. */
25
- const indexed = (value) => (Array.isArray(value) ? value.flatMap((item, index) => (isObject(item) ? [[item, index]] : [])) : []);
26
23
  /** DEC-0083: the model class of a model whose vendor publishes its weights openly — descriptive: a policy admits and
27
24
  * excludes nothing by it — and what a lane reads when it reads what a member, a client or the public sends. */
28
25
  export const OPEN_VENDOR = 'open-vendor';
29
26
  export const USER_CONTENT = 'user-content';
30
- /** The contract's `show`: String(value), or its JSON where String() throws (an object whose own `toString` is data). */
31
- const show = (value) => {
32
- if (typeof value === 'string') return value;
33
- try {
34
- return String(value);
35
- } catch {
36
- return JSON.stringify(value) ?? typeof value;
37
- }
38
- };
39
- /** The contract's jsonEqual: arrays by item, objects by own member whatever the order, anything else by ===, so 1 and
40
- * true differ and NaN equals nothing (a JSON.stringify comparison would call NaN and null equal). */
41
- function jsonEqual(a, b) {
42
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => jsonEqual(item, b[i]));
43
- if (isObject(a) || isObject(b)) {
44
- if (!isObject(a) || !isObject(b)) return false;
45
- const keys = Object.keys(a);
46
- return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
47
- }
48
- return a === b;
49
- }
50
- /** The contract's toNumber: Number(value), or NaN where Number() throws (an object whose own toString is data). */
51
- const toNumber = (value) => {
52
- try {
53
- return Number(value);
54
- } catch {
55
- return Number.NaN;
56
- }
57
- };
58
27
 
59
28
  /** FAC-SEM-060: the request's profile, else the agent's, else the binding's or project's, else the estate default. */
60
29
  export function resolveModelProfile({ catalogue, profiles, choices = {}, policy } = {}) {
@@ -0,0 +1,122 @@
1
+ // Protocol revisions (DEC-0067) and an event's machine-readable data (DEC-0070, the feed-data protocols).
2
+ // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_protocols.py. Node 20+, no dependency.
3
+ import { credentialName, secretArgument, secretUrl, secretValue } from './fabric-secrets.mjs';
4
+
5
+ // An event about traced work carries traceId and spanId together (fabric-interop/0.1 C3.4 c);
6
+ // traceIds(traceparent) in fabric-interop.mjs gives both.
7
+
8
+ /** Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`); a test holds it equal to the contract's. */
9
+ export const PROTOCOL_REVISIONS = new Map(Object.entries({
10
+ 'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'coding-agents': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
11
+ 'fabric-activity': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
12
+ 'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-operator-channel': ['0.1'],
13
+ 'fabric-package': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
14
+ 'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'],
15
+ memory: ['0.1'], 'pairing-message': ['1'], pipeline: ['0.1'], 'revocation-delta': ['1'], 'session-handoff': ['1'],
16
+ 'share-registration': ['1'], 'share-registration-receipt': ['1'], 'support-quality-daily': ['1'], 'window-lease': ['1'],
17
+ }));
18
+
19
+ export const MAX_PROTOCOLS = 64;
20
+
21
+ const REVISION = /^([a-z][a-z0-9.-]{0,63})\/([0-9]{1,4}(?:\.[0-9]{1,4}){0,2})$/;
22
+
23
+ /** `<name>/<revision>` split, or undefined. */
24
+ export function parseRevision(value) {
25
+ const m = typeof value === 'string' ? REVISION.exec(value) : null;
26
+ return m ? { name: m[1], revision: m[2] } : undefined;
27
+ }
28
+
29
+ /** The revisions a receiver implementing `name` up to `newest` accepts: N and, when the register has one, N−1. */
30
+ export function acceptedRevisions(name, newest) {
31
+ const known = PROTOCOL_REVISIONS.get(name);
32
+ if (!known?.length) return [];
33
+ const top = newest === undefined ? known.length - 1 : known.indexOf(newest);
34
+ if (top < 0) return [];
35
+ return known.slice(Math.max(0, top - 1), top + 1).reverse().map((r) => `${name}/${r}`);
36
+ }
37
+
38
+ /**
39
+ * A receiver's answer to a document naming `received` (DEC-0067, the contract's `negotiateRevision`): `{ok: true}`, the
40
+ * `schema-unsupported` refusal naming what was received (at most 80 characters) and the accepted revisions of that
41
+ * protocol (all of them when it names none the receiver reads), or `{fault: 'no-accepted-revisions'}`.
42
+ */
43
+ export function negotiateRevision(received, accepted) {
44
+ const usable = [...new Set((Array.isArray(accepted) ? accepted : []).filter((e) => parseRevision(e) !== undefined))].slice(0, MAX_PROTOCOLS);
45
+ if (!usable.length) return { fault: 'no-accepted-revisions' };
46
+ if (typeof received === 'string' && usable.includes(received)) return { ok: true };
47
+ const parsed = parseRevision(received);
48
+ const same = parsed ? usable.filter((e) => parseRevision(e)?.name === parsed.name) : [];
49
+ return { error: { code: 'schema-unsupported', message: 'this receiver does not read that protocol revision',
50
+ data: { protocol: typeof received === 'string' ? Array.from(received).slice(0, 80).join('') : '', accepted: same.length ? same : usable } } };
51
+ }
52
+
53
+ /** FAC-SEM-099 (DEC-0067): 1 to 64 entries, each once; at most N and N−1 of one protocol, consecutive in the register. */
54
+ export function protocolsFindings(protocols, prefix = '/protocols') {
55
+ const flag = (instancePath, message) => ({ code: 'FAC-SEM-099', instancePath, message });
56
+ if (!Array.isArray(protocols)) return [flag(prefix, 'protocols is a list of protocol revisions')];
57
+ if (protocols.length === 0 || protocols.length > MAX_PROTOCOLS) return [flag(prefix, `protocols lists 1 to ${MAX_PROTOCOLS} revisions`)];
58
+ const findings = [];
59
+ const byName = new Map();
60
+ const seen = new Set();
61
+ protocols.forEach((entry, i) => {
62
+ const at = `${prefix}/${i}`;
63
+ const parsed = parseRevision(entry);
64
+ if (!parsed) { findings.push(flag(at, 'a protocol revision is <name>/<revision>, such as fabric-service/0.1')); return; }
65
+ if (seen.has(entry)) { findings.push(flag(at, 'each protocol revision once')); return; }
66
+ seen.add(entry);
67
+ const known = PROTOCOL_REVISIONS.get(parsed.name);
68
+ if (known && !known.includes(parsed.revision)) { findings.push(flag(at, `${parsed.name} has no published revision ${parsed.revision}`)); return; }
69
+ const indices = byName.get(parsed.name) ?? [];
70
+ indices.push(known ? known.indexOf(parsed.revision) : -1);
71
+ byName.set(parsed.name, indices);
72
+ if (indices.length > 2) findings.push(flag(at, 'at most 2 revisions of one protocol: N and N−1'));
73
+ else if (known && indices.length === 2 && Math.abs(indices[0] - indices[1]) !== 1) findings.push(flag(at, 'two revisions of one protocol are N and N−1, consecutive in the register'));
74
+ });
75
+ return findings;
76
+ }
77
+
78
+ const SCHEMA_URI = /^[A-Za-z][A-Za-z0-9+.-]*:\S+$/;
79
+
80
+ export const scalar = (v) => v === null || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v)) || (typeof v === 'string' && Array.from(v).length <= 500);
81
+
82
+ const isPlain = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
83
+
84
+ const pointerToken = (name) => String(name).replace(/~/g, '~0').replace(/\//g, '~1');
85
+
86
+ /** FAC-SEM-101 over an events page (the contract's `feedDataFindings`): no credential's name or shape in an event's data. */
87
+ export function feedDataFindings(page) {
88
+ const findings = [];
89
+ const check = (named, member, at) => {
90
+ if (typeof member !== 'string') return;
91
+ if (named) findings.push({ code: 'FAC-SEM-101', instancePath: at, message: "an event's data puts no string under a credential's name" });
92
+ else if (secretValue(member) || secretUrl(member)) findings.push({ code: 'FAC-SEM-101', instancePath: at, message: "an event's data carries no value shaped like a credential" });
93
+ };
94
+ (Array.isArray(page?.events) ? page.events : []).forEach((event, i) => {
95
+ if (!isPlain(event) || !isPlain(event.data) || !isPlain(event.data.value)) return;
96
+ for (const [key, member] of Object.entries(event.data.value)) {
97
+ const at = `/events/${i}/data/value/${pointerToken(key)}`;
98
+ const named = credentialName(key);
99
+ check(named, member, at);
100
+ if (Array.isArray(member)) member.forEach((item, j) => check(named, item, `${at}/${j}`));
101
+ else if (isPlain(member)) for (const [inner, leaf] of Object.entries(member)) check(named || credentialName(inner), leaf, `${at}/${pointerToken(inner)}`);
102
+ }
103
+ });
104
+ return findings;
105
+ }
106
+
107
+ /** DEC-0070's `data {schema, value}` as the schema bounds it, then FAC-SEM-101. */
108
+ export function feedDataProblems(data) {
109
+ if (!isPlain(data) || Object.keys(data).length !== 2 || !('schema' in data) || !('value' in data)) return ['data is {schema, value}'];
110
+ const problems = [];
111
+ if (!(typeof data.schema === 'string' && SCHEMA_URI.test(data.schema))) problems.push('data.schema is the absolute URI of a payload schema the service publishes');
112
+ const value = data.value;
113
+ if (!isPlain(value) || Object.keys(value).length > 32) return [...problems, 'data.value is an object of at most 32 members'];
114
+ for (const [key, member] of Object.entries(value)) {
115
+ if (scalar(member)) continue;
116
+ if (Array.isArray(member) && member.length <= 32 && member.every(scalar)) continue;
117
+ if (isPlain(member) && Object.keys(member).length <= 32 && Object.values(member).every(scalar)) continue;
118
+ problems.push(`data.value.${key} is a scalar (a string of at most 500 characters), a list of at most 32 scalars or an object of at most 32 scalar members`);
119
+ }
120
+ if (problems.length) return problems;
121
+ return feedDataFindings({ events: [{ data }] }).map((f) => `FAC-SEM-101: ${f.instancePath.replace('/events/0', '')} ${f.message}`);
122
+ }