@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.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 (120) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +13 -2
  3. package/SECURITY.md +23 -8
  4. package/bin/fabric-agent-adapter.js +50 -5
  5. package/package.json +1 -1
  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 +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
  17. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  18. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
  19. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  86. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  87. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
  88. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  89. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
  90. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  91. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  92. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  93. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  94. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  95. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  96. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
  97. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  98. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  99. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  100. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  101. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  102. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  103. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  104. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  105. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  106. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  107. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
  108. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  109. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  110. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
@@ -0,0 +1,344 @@
1
+ // The call queue and the load report of DEC-0104 (contract src/agent-network.ts, FAC-SEM-131 to FAC-SEM-133), as the
2
+ // contract's reference answers them: a node that hosts a job-backed agent queues its jobs per (agentRef, context) with
3
+ // these and reports its load as service-load/1 under extensions/load/0.1. Part of the Node service kit (FAA-289, layer
4
+ // L3); the twin of fabric_load.py. Node 20+, no dependency. Nothing here throws on what it reads, except
5
+ // loadExtension, which refuses a declaration a reader would treat as not declared.
6
+ //
7
+ // - jobCallerKey(accepted, credential): the opaque key of a job's caller (an accepted delegation's principal, its
8
+ // outermost actor and a partner organization ending the chain; else the credential the node knows the caller by).
9
+ // - nextQueuedJob(queue, running, limits): the job the node starts next, or undefined (fairness between callers).
10
+ // - admitJob(queue, running, job, limits, retryAfterSeconds): {admit: true}, or the queue-full refusal with its data.
11
+ // - queueStatusMessage(position, length), readQueuePosition(statusMessage): `queued <position> of <length>`.
12
+ // - jobQueueRules(value): FAC-SEM-131 over {queue, running, limits, started}.
13
+ // - loadReportRules(report): FAC-SEM-132 on a service-load/1 report; loadReportProblems(report): its schema.
14
+ // - loadView(wellKnown), loadExtensionRules(wellKnown): the declaration as a reader reads it, and FAC-SEM-133.
15
+ // - loadExtension({ path, windowSeconds, limits }): the `extensions` entry a service declares.
16
+ //
17
+ // The kit has no JSON Schema validator: the two shapes are written here from the schemas as the contract's validator
18
+ // reads them (lengths in code points, date-time as ajv-formats' full mode). Times in the queue are UTC instants
19
+ // (interop-common#/$defs/utcInstant); times in the report are RFC 3339 read strictly (the contract's strictTime). The
20
+ // wording of a schema problem is the kit's, not ajv's.
21
+ import { ServiceError } from './fabric-base.mjs';
22
+ import { dateTime } from './fabric-connections.mjs'; // ajv-formats' date-time, as the contract's validator reads it
23
+ import { isObject, isString, objects, show } from './fabric-jsvalue.mjs';
24
+ import { parseRfc3339Ms } from './fabric-time.mjs'; // the contract's strictTime
25
+
26
+ /**
27
+ * @typedef {{ id: string, caller: string, at: string }} QueuedJob A job waiting at the hosting node: its id, its
28
+ * caller's key (jobCallerKey) and when it arrived (a UTC instant).
29
+ * @typedef {{ id: string, caller?: string }} RunningJob A job running: its id and caller.
30
+ * @typedef {{ concurrency: number, perCaller: number, queueMax: number }} QueueLimits The queue's limits per
31
+ * (agentRef, context), as the load extension declares them.
32
+ * @typedef {{ admit: true } | { admit: false, error: { code: 'queue-full', message: string,
33
+ * data: { retryable: true, retryAfterSeconds: number } } }} Admission
34
+ * @typedef {{ code: string, instancePath: string, message: string }} Finding
35
+ * @typedef {{ declared: boolean, valid: boolean, load: { path: string, windowSeconds: number, limits: QueueLimits } | null,
36
+ * errors: string[] }} LoadView
37
+ */
38
+
39
+ export const LOAD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/load/0.1';
40
+ export const LOAD_REPORT_TYPE = 'service-load/1';
41
+ // The refusal code, the same as fabric-interop-rules.mjs's QUEUE_FULL (a test holds them equal); not imported because
42
+ // the facade fabric-service.mjs, which re-exports this module, comes before the interop rules in the kit's order.
43
+ const QUEUE_FULL = 'queue-full';
44
+ export const QUEUE_FULL_MESSAGE = "the agent's queue for this context is full: start again later";
45
+ /** The longest queue a status message names (`queued <position> of <length>`, six digits). */
46
+ export const QUEUE_LENGTH_MAX = 999999;
47
+ export const DEFAULT_RETRY_AFTER_SECONDS = 60;
48
+
49
+ const UTC_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:[0-5]\d(?:\.\d{1,3})?Z$/;
50
+ const QUEUED = /^queued ([1-9]\d{0,5}) of ([1-9]\d{0,5})$/;
51
+ const ORIGIN_PATH = /^\/(?!\/)[^\s]*$/u;
52
+ const CONTEXT = /^(?:personal|org:[A-Za-z0-9][A-Za-z0-9_-]{0,127})$/;
53
+ const KEY_PATH = `/extensions/${LOAD_EXTENSION_KEY.replace(/~/g, '~0').replace(/\//g, '~1')}`;
54
+ const LIMITS = [['concurrency', 1, 1024], ['perCaller', 1, 1024], ['queueMax', 0, 100000]];
55
+ const ROW_COUNTS = ['queued', 'running', 'completed', 'failed'];
56
+
57
+ /** An own member, or undefined. */
58
+ const member = (value, key) => (isObject(value) && Object.hasOwn(value, key) ? value[key] : undefined);
59
+ /** A UTC instant (`T`, `Z`, at most three fraction digits, a day that exists) in milliseconds, else NaN. */
60
+ const utcMs = (value) => (isString(value) && UTC_INSTANT.test(value) ? parseRfc3339Ms(value) : Number.NaN);
61
+ const strictMs = (value) => (isString(value) ? parseRfc3339Ms(value) : Number.NaN);
62
+ const codePoints = (value) => [...value].length;
63
+
64
+ // --- The call queue (fabric-interop, DEC-0104) ---
65
+
66
+ /**
67
+ * The key of a job's caller at a node that queues (DEC-0104), as the contract's jobCallerKey: for a call whose
68
+ * delegation the node accepted (verifyDelegation's answer), its `principal` and `caller` and the partner organization
69
+ * when the chain ends in one (DEC-0090); for a call with no token, the credential the node knows the caller by (a
70
+ * gate's pass with `principal: null` is that case too). A refused verdict has no key, and neither has an unreadable
71
+ * call: undefined. The key is opaque; only equality matters.
72
+ * @param {unknown} accepted
73
+ * @param {unknown} credential
74
+ * @returns {string | undefined}
75
+ */
76
+ export function jobCallerKey(accepted, credential) {
77
+ if (isObject(accepted) && !(member(accepted, 'ok') === true && member(accepted, 'principal') === null)) {
78
+ const principal = member(accepted, 'principal');
79
+ const caller = member(accepted, 'caller');
80
+ if (member(accepted, 'ok') !== true || !isString(principal) || principal === '' || !isString(caller) || caller === '') return undefined;
81
+ const given = member(accepted, 'chain');
82
+ const chain = Array.isArray(given) ? given : [];
83
+ const last = chain[chain.length - 1];
84
+ const partner = chain.length > 1 && isString(last) && last.startsWith('org:') ? [last] : [];
85
+ return JSON.stringify(['delegated', principal, caller, ...partner]);
86
+ }
87
+ return isString(credential) && credential !== '' ? JSON.stringify(['credential', credential]) : undefined;
88
+ }
89
+
90
+ const limitsOk = (limits) => isObject(limits)
91
+ && [member(limits, 'concurrency'), member(limits, 'perCaller')].every((n) => Number.isSafeInteger(n) && n >= 1)
92
+ && Number.isSafeInteger(member(limits, 'queueMax')) && limits.queueMax >= 0;
93
+ const waitingJob = (job) => isObject(job) && isString(member(job, 'id')) && isString(member(job, 'caller')) && Number.isFinite(utcMs(member(job, 'at')));
94
+
95
+ /**
96
+ * The job the hosting node starts next for one (agentRef, context), or undefined (DEC-0104). Nothing starts while
97
+ * `concurrency` jobs run (every running job holds a slot, its caller readable or not). Of the waiting jobs whose caller
98
+ * runs fewer than `perCaller`, the one whose caller runs the fewest wins; between those callers, the one whose oldest
99
+ * waiting job arrived first; within a caller, its oldest. Arrival ties break by id (UTF-16 order).
100
+ * @param {unknown} queue
101
+ * @param {unknown} running
102
+ * @param {unknown} limits
103
+ * @returns {string | undefined}
104
+ */
105
+ export function nextQueuedJob(queue, running, limits) {
106
+ if (!limitsOk(limits)) return undefined;
107
+ if ((Array.isArray(running) ? running.length : 0) >= limits.concurrency) return undefined;
108
+ const runningOf = new Map();
109
+ for (const job of objects(running)) if (isString(member(job, 'caller'))) runningOf.set(job.caller, (runningOf.get(job.caller) ?? 0) + 1);
110
+ const waiting = objects(queue)
111
+ .filter((job) => waitingJob(job) && (runningOf.get(job.caller) ?? 0) < limits.perCaller)
112
+ .sort((a, b) => utcMs(a.at) - utcMs(b.at) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
113
+ let best;
114
+ for (const job of waiting) if (best === undefined || (runningOf.get(job.caller) ?? 0) < (runningOf.get(best.caller) ?? 0)) best = job;
115
+ return best?.id;
116
+ }
117
+
118
+ /**
119
+ * Whether a start is admitted (DEC-0104): it is when the job would start at once (nextQueuedJob over the queue with it
120
+ * added answers its id), or when fewer than `queueMax` jobs wait; otherwise {admit: false, error} with the queue-full
121
+ * refusal and its `retryAfterSeconds` (1 to 3600, else 60). Unreadable limits or job refuse.
122
+ * @param {unknown} queue
123
+ * @param {unknown} running
124
+ * @param {unknown} job
125
+ * @param {unknown} limits
126
+ * @param {unknown} retryAfterSeconds
127
+ * @returns {Admission}
128
+ */
129
+ export function admitJob(queue, running, job, limits, retryAfterSeconds) {
130
+ const waiting = objects(queue);
131
+ if (limitsOk(limits) && waitingJob(job) && (nextQueuedJob([...waiting, job], running, limits) === job.id || waiting.length < limits.queueMax)) return { admit: true };
132
+ const after = Number.isSafeInteger(retryAfterSeconds) && retryAfterSeconds >= 1 && retryAfterSeconds <= 3600 ? retryAfterSeconds : DEFAULT_RETRY_AFTER_SECONDS;
133
+ return { admit: false, error: { code: QUEUE_FULL, message: QUEUE_FULL_MESSAGE, data: { retryable: true, retryAfterSeconds: after } } };
134
+ }
135
+
136
+ /**
137
+ * The `statusMessage` of a waiting job (DEC-0104): `queued <position> of <length>`, 1 is next, 1 <= position <= length
138
+ * <= 999999; undefined for anything else.
139
+ * @param {unknown} position
140
+ * @param {unknown} length
141
+ * @returns {string | undefined}
142
+ */
143
+ export const queueStatusMessage = (position, length) => (Number.isSafeInteger(position) && Number.isSafeInteger(length)
144
+ && position >= 1 && position <= length && length <= QUEUE_LENGTH_MAX ? `queued ${position} of ${length}` : undefined);
145
+
146
+ /**
147
+ * A waiting job's place, read from its `statusMessage`, or undefined when it is not the queue's grammar.
148
+ * @param {unknown} statusMessage
149
+ * @returns {{ position: number, length: number } | undefined}
150
+ */
151
+ export function readQueuePosition(statusMessage) {
152
+ const match = isString(statusMessage) ? QUEUED.exec(statusMessage) : null;
153
+ if (!match) return undefined;
154
+ const position = Number(match[1]);
155
+ const length = Number(match[2]);
156
+ return position >= 1 && position <= length ? { position, length } : undefined;
157
+ }
158
+
159
+ const finding = (code, instancePath, message) => ({ code, instancePath, message });
160
+
161
+ /**
162
+ * FAC-SEM-131 on {queue, running, limits, started} (`started`: the id the node started, or null): it is nextQueuedJob's.
163
+ * @param {unknown} value
164
+ * @returns {Finding[]}
165
+ */
166
+ export function jobQueueRules(value) {
167
+ const want = nextQueuedJob(member(value, 'queue'), member(value, 'running'), member(value, 'limits')) ?? null;
168
+ if ((member(value, 'started') ?? null) === want) return [];
169
+ return [finding('FAC-SEM-131', '/started', want === null ? 'nothing may start now' : `the job to start is ${show(want)}`)];
170
+ }
171
+
172
+ // --- The load report (DEC-0104) ---
173
+
174
+ /**
175
+ * FAC-SEM-132 on a service-load/1 report: windows in order one `windowSeconds` apart, none after `generatedAt`, one
176
+ * row per context, p50 at most p95, a run time only where a job ended.
177
+ * @param {unknown} report
178
+ * @returns {Finding[]}
179
+ */
180
+ export function loadReportRules(report) {
181
+ const findings = [];
182
+ const flag = (instancePath, message) => findings.push(finding('FAC-SEM-132', instancePath, message));
183
+ const seconds = member(report, 'windowSeconds');
184
+ const step = Number.isSafeInteger(seconds) ? seconds * 1000 : Number.NaN;
185
+ const generated = strictMs(member(report, 'generatedAt'));
186
+ let previous;
187
+ objects(member(report, 'windows')).forEach((window, index) => {
188
+ const start = strictMs(member(window, 'start'));
189
+ if (start > generated) flag(`/windows/${index}/start`, 'a window starts after the report was generated');
190
+ if (previous !== undefined && start - previous !== step) flag(`/windows/${index}/start`, 'windows are not in order one windowSeconds apart');
191
+ previous = start;
192
+ const seen = new Set();
193
+ objects(member(window, 'byContext')).forEach((row, at) => {
194
+ const path = `/windows/${index}/byContext/${at}`;
195
+ const context = member(row, 'context');
196
+ if (isString(context) && seen.has(context)) flag(`${path}/context`, `context ${show(context)} appears twice in one window`);
197
+ if (isString(context)) seen.add(context);
198
+ for (const name of ['waitMs', 'runMs']) {
199
+ const pct = member(row, name);
200
+ if (isObject(pct) && typeof member(pct, 'p50') === 'number' && typeof member(pct, 'p95') === 'number' && pct.p50 > pct.p95) flag(`${path}/${name}`, 'p50 is above p95');
201
+ }
202
+ if (isObject(member(row, 'runMs')) && member(row, 'completed') === 0 && member(row, 'failed') === 0) flag(`${path}/runMs`, 'a run time with no job ended in the window');
203
+ });
204
+ });
205
+ return findings;
206
+ }
207
+
208
+ /** JSON Schema's integer with its bounds, as ajv reads it: a number with no fraction, never a boolean. */
209
+ function integer(value, low, high, at, errors) {
210
+ if (typeof value !== 'number' || !Number.isFinite(value) || value % 1 !== 0) errors.push(`${at} type`);
211
+ else if (value < low) errors.push(`${at} minimum`);
212
+ else if (high !== undefined && value > high) errors.push(`${at} maximum`);
213
+ }
214
+
215
+ function members(value, allowed, required, at, errors) {
216
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) errors.push(`${at}/${key} additionalProperties`);
217
+ for (const key of required) if (!Object.hasOwn(value, key)) errors.push(`${at || '/'} required ${key}`);
218
+ }
219
+
220
+ function timestamp(value, at, errors) {
221
+ if (!isString(value)) errors.push(`${at} type`);
222
+ else if (!dateTime(value)) errors.push(`${at} format`);
223
+ }
224
+
225
+ function percentiles(value, at, errors) {
226
+ if (!isObject(value)) {
227
+ errors.push(`${at} type`);
228
+ return;
229
+ }
230
+ members(value, ['p50', 'p95'], ['p50', 'p95'], at, errors);
231
+ for (const name of ['p50', 'p95']) if (Object.hasOwn(value, name)) integer(value[name], 0, undefined, `${at}/${name}`, errors);
232
+ }
233
+
234
+ function rowProblems(row, at, errors) {
235
+ if (!isObject(row)) {
236
+ errors.push(`${at} type`);
237
+ return;
238
+ }
239
+ members(row, ['context', ...ROW_COUNTS, 'waitMs', 'runMs'], ['context', ...ROW_COUNTS], at, errors);
240
+ if (Object.hasOwn(row, 'context') && !(isString(row.context) && CONTEXT.test(row.context))) errors.push(`${at}/context ${isString(row.context) ? 'pattern' : 'type'}`);
241
+ for (const name of ROW_COUNTS) if (Object.hasOwn(row, name)) integer(row[name], 0, undefined, `${at}/${name}`, errors);
242
+ for (const name of ['waitMs', 'runMs']) if (Object.hasOwn(row, name)) percentiles(row[name], `${at}/${name}`, errors);
243
+ }
244
+
245
+ /**
246
+ * service-load.schema.json: what makes `report` not a service-load/1 report, each as `<path> <keyword>`; empty for a
247
+ * valid one. Counts and durations only: a member the schema does not name (a caller, a job id) is refused.
248
+ * @param {unknown} report
249
+ * @returns {string[]}
250
+ */
251
+ export function loadReportProblems(report) {
252
+ if (!isObject(report)) return ['/ type'];
253
+ const errors = [];
254
+ const names = ['type', 'generatedAt', 'windowSeconds', 'windows'];
255
+ members(report, names, names, '', errors);
256
+ if (Object.hasOwn(report, 'type') && report.type !== LOAD_REPORT_TYPE) errors.push('/type const');
257
+ if (Object.hasOwn(report, 'generatedAt')) timestamp(report.generatedAt, '/generatedAt', errors);
258
+ if (Object.hasOwn(report, 'windowSeconds')) integer(report.windowSeconds, 60, 86400, '/windowSeconds', errors);
259
+ if (Object.hasOwn(report, 'windows') && !Array.isArray(report.windows)) errors.push('/windows type');
260
+ else if (Object.hasOwn(report, 'windows')) {
261
+ if (report.windows.length > 168) errors.push('/windows maxItems');
262
+ report.windows.forEach((window, index) => {
263
+ const at = `/windows/${index}`;
264
+ if (!isObject(window)) {
265
+ errors.push(`${at} type`);
266
+ return;
267
+ }
268
+ members(window, ['start', 'byContext'], ['start', 'byContext'], at, errors);
269
+ if (Object.hasOwn(window, 'start')) timestamp(window.start, `${at}/start`, errors);
270
+ if (Object.hasOwn(window, 'byContext') && !Array.isArray(window.byContext)) errors.push(`${at}/byContext type`);
271
+ else if (Object.hasOwn(window, 'byContext')) {
272
+ if (window.byContext.length > 64) errors.push(`${at}/byContext maxItems`);
273
+ window.byContext.forEach((row, rowIndex) => rowProblems(row, `${at}/byContext/${rowIndex}`, errors));
274
+ }
275
+ });
276
+ }
277
+ return errors;
278
+ }
279
+
280
+ /** service-load-extension.schema.json, each problem as `<path> <keyword>`; empty for a valid declaration. */
281
+ function declarationProblems(value) {
282
+ if (!isObject(value)) return ['/ type'];
283
+ const errors = [];
284
+ members(value, ['path', 'windowSeconds', 'limits'], ['path', 'windowSeconds', 'limits'], '', errors);
285
+ if (Object.hasOwn(value, 'path')) {
286
+ if (!isString(value.path)) errors.push('/path type');
287
+ else {
288
+ if (codePoints(value.path) > 256) errors.push('/path maxLength'); // the originPath's own 2048 is wider
289
+ if (!ORIGIN_PATH.test(value.path)) errors.push('/path pattern');
290
+ }
291
+ }
292
+ if (Object.hasOwn(value, 'windowSeconds')) integer(value.windowSeconds, 60, 86400, '/windowSeconds', errors);
293
+ if (Object.hasOwn(value, 'limits')) {
294
+ const { limits } = value;
295
+ if (!isObject(limits)) errors.push('/limits type');
296
+ else {
297
+ const names = LIMITS.map(([name]) => name);
298
+ members(limits, names, names, '/limits', errors);
299
+ for (const [name, low, high] of LIMITS) if (Object.hasOwn(limits, name)) integer(limits[name], low, high, `/limits/${name}`, errors);
300
+ }
301
+ }
302
+ return errors;
303
+ }
304
+
305
+ /**
306
+ * The load declaration as a reader reads it (DEC-0104): `declared` when the extension key is present, `valid` when its
307
+ * value is the schema's, `load` the declaration when valid (else null) and `errors` its problems. A reader treats an
308
+ * invalid value as not declared and reports it (FAC-SEM-133).
309
+ * @param {unknown} wellKnown
310
+ * @returns {LoadView}
311
+ */
312
+ export function loadView(wellKnown) {
313
+ const extensions = member(wellKnown, 'extensions');
314
+ if (!isObject(extensions) || !Object.hasOwn(extensions, LOAD_EXTENSION_KEY)) return { declared: false, valid: false, load: null, errors: [] };
315
+ const value = extensions[LOAD_EXTENSION_KEY];
316
+ const errors = declarationProblems(value);
317
+ return { declared: true, valid: errors.length === 0, load: errors.length === 0 ? value : null, errors };
318
+ }
319
+
320
+ /**
321
+ * FAC-SEM-133 on a well-known document: a value under the load extension key is valid against
322
+ * service-load-extension.schema.json; one finding at the key otherwise.
323
+ * @param {unknown} wellKnown
324
+ * @returns {Finding[]}
325
+ */
326
+ export function loadExtensionRules(wellKnown) {
327
+ const view = loadView(wellKnown);
328
+ if (!view.declared || view.valid) return [];
329
+ return [finding('FAC-SEM-133', KEY_PATH, `the load declaration is invalid (${view.errors.join('; ')}); a reader treats it as not declared and reports it (DEC-0104)`)];
330
+ }
331
+
332
+ /**
333
+ * The `extensions` entry a service declares (DEC-0104): where it serves its service-load/1 report (an origin path,
334
+ * served under the descriptor token like the usage report), the report's window and the queue's limits per
335
+ * (agentRef, context). Throws ServiceError on a declaration a reader would treat as not declared.
336
+ * @param {{ path: string, windowSeconds: number, limits: QueueLimits }} declaration
337
+ * @returns {Record<string, { path: string, windowSeconds: number, limits: QueueLimits }>}
338
+ */
339
+ export function loadExtension({ path, windowSeconds, limits } = {}) {
340
+ const value = { path, windowSeconds, limits: isObject(limits) ? { ...limits } : limits };
341
+ const problems = declarationProblems(value);
342
+ if (problems.length) throw new ServiceError(`Load declaration: ${problems.join('; ')}.`);
343
+ return { [LOAD_EXTENSION_KEY]: value };
344
+ }
@@ -509,9 +509,71 @@ const FOLDER_MESSAGES = {
509
509
  'provider-manifest': 'provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets fabric.package.json must carry the same costPresets (copy them; pack does not)',
510
510
  };
511
511
 
512
+ /**
513
+ * The agent's source manifest (contract DEC-0149, `fabric-source/1`): when the folder carries one, `pack` writes
514
+ * `secrets[]` from its secret slots and `state[]` from its store slots' paths, and FAC-SEM-160 holds them equal.
515
+ */
516
+ export const SOURCE_FILE = 'fabric.source.json';
517
+
518
+ function sourceManifest(files) {
519
+ if (!files.has(SOURCE_FILE)) return undefined;
520
+ const source = parseStrict(files.get(SOURCE_FILE), SOURCE_FILE);
521
+ if (source.format !== 'fabric-source/1') throw new PackageError(`${SOURCE_FILE} is not a fabric-source/1 manifest (its format member).`);
522
+ return source;
523
+ }
524
+
525
+ /** `secrets[]` from the secret slots (name, purpose, optional) and `state[]` from the store slots (path, purpose). */
526
+ export function sourceMembers(source) {
527
+ const secrets = [];
528
+ const state = [];
529
+ for (const slot of objects(source.slots)) {
530
+ if (slot.kind === 'secret' && isString(slot.name)) secrets.push({ name: slot.name, purpose: slot.purpose, ...(slot.optional === true ? { optional: true } : {}) });
531
+ else if (slot.kind === 'store' && isString(slot.path)) state.push({ path: slot.path, purpose: slot.purpose });
532
+ }
533
+ return { secrets, state };
534
+ }
535
+
536
+ /**
537
+ * FAC-SEM-160 (contract `sourcePackageFindings`): the package's secrets[] are the source's secret slots, by name and
538
+ * optional; its state[] paths are the store slots' paths when state[] holds any entry.
539
+ */
540
+ export function sourcePackageFindings(source, pkg) {
541
+ if (!isObject(source) || !isObject(pkg)) return [];
542
+ const out = [];
543
+ const flag = (instancePath, message) => out.push({ code: 'FAC-SEM-160', instancePath, message });
544
+ const slots = objects(source.slots);
545
+ const secrets = new Map(slots.filter((s) => s.kind === 'secret' && isString(s.name)).map((s) => [s.name, s.optional === true]));
546
+ const stores = new Set(slots.filter((s) => s.kind === 'store' && isString(s.path)).map((s) => s.path));
547
+ const packaged = (Array.isArray(pkg.secrets) ? pkg.secrets : []).flatMap((e, i) => (isObject(e) ? [[e, i]] : []));
548
+ const names = new Set(packaged.map(([e]) => e.name));
549
+ for (const name of secrets.keys()) if (!names.has(name)) flag('/package/secrets', `the source's secret slot ${name} is not in the package's secrets[]`);
550
+ for (const [entry, index] of packaged) {
551
+ if (!isString(entry.name) || !secrets.has(entry.name)) flag(`/package/secrets/${index}/name`, `secrets[${index}] is no secret slot of the source`);
552
+ else if (secrets.get(entry.name) !== (entry.optional === true)) flag(`/package/secrets/${index}/optional`, `secrets[${index}] is ${secrets.get(entry.name) ? '' : 'not '}optional in the source`);
553
+ }
554
+ if (!Array.isArray(pkg.state) || pkg.state.length === 0) return out;
555
+ const paths = new Set(objects(pkg.state).map((e) => e.path));
556
+ for (const p of stores) if (!paths.has(p)) flag('/package/state', "a store slot's path of the source is not in the package's state[]");
557
+ pkg.state.forEach((entry, index) => {
558
+ if (isObject(entry) && (!isString(entry.path) || !stores.has(entry.path))) flag(`/package/state/${index}/path`, `state[${index}] is no store slot of the source`);
559
+ });
560
+ return out;
561
+ }
562
+
563
+ function sourceProblems(pkg, files) {
564
+ if (!files.has(SOURCE_FILE)) return [];
565
+ try {
566
+ return sourcePackageFindings(sourceManifest(files), pkg).map((p) => ({ ...p, rule: p.code }));
567
+ } catch (error) {
568
+ if (!(error instanceof PackageError)) throw error;
569
+ return [{ rule: 'source', instancePath: '/files', message: error.message }];
570
+ }
571
+ }
572
+
512
573
  function allProblems(pkg, files) {
513
574
  return [
514
575
  ...schemaProblems(pkg).map((p) => ({ ...p, rule: 'schema' })),
576
+ ...sourceProblems(pkg, files),
515
577
  ...pathProblems(pkg).map((p) => ({ ...p, rule: 'path' })),
516
578
  ...packageFindings(pkg).map((p) => ({ ...p, rule: p.code })),
517
579
  ...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: FOLDER_MESSAGES[p.problem] ?? p.problem })),
@@ -525,6 +587,21 @@ function writeAtomic(file, text) {
525
587
  fs.renameSync(temp, file);
526
588
  }
527
589
 
590
+ /** The names a hand-written secrets[] declares that the source manifest has no secret slot for, in their order. */
591
+ function secretsLacking(draft, secrets) {
592
+ const have = new Set(secrets.map((s) => s.name));
593
+ const lacking = [];
594
+ for (const entry of Array.isArray(draft.secrets) ? draft.secrets : []) {
595
+ const name = isObject(entry) ? entry.name : undefined;
596
+ if (isString(name) && !have.has(name) && !lacking.includes(name)) lacking.push(name);
597
+ }
598
+ return lacking;
599
+ }
600
+
601
+ function lackingMessage(lacking) {
602
+ return `${SOURCE_FILE} declares no secret slot for ${lacking.join(', ')}, which ${PACKAGE_FILE}'s secrets[] names: pack writes secrets[] from the manifest's slots and never drops a declared secret. Add each as a slot of kind secret to ${SOURCE_FILE} (or take it out of secrets[]), then pack again`;
603
+ }
604
+
528
605
  /** Pack `folder`; with `apply` and no problem, fabric.package.json is rewritten. */
529
606
  export function pack(folder, { apply = false } = {}) {
530
607
  const files = readFolder(folder);
@@ -532,8 +609,28 @@ export function pack(folder, { apply = false } = {}) {
532
609
  throw new PackageError(`Write ${PACKAGE_FILE} in the folder first: schema, kind, id, version, publisher, licence and, for an agent, resultSignal and evals with its receipt. pack fills in agentRef, files[] and digest.`);
533
610
  }
534
611
  const draft = parseStrict(files.get(PACKAGE_FILE), PACKAGE_FILE);
612
+ const source = sourceManifest(files);
613
+ const written = [];
614
+ const refused = [];
615
+ if (source !== undefined) {
616
+ // DEC-0149: secrets[] and state[] come from the source manifest's slots; an empty state[] is a copy without state.
617
+ const members = sourceMembers(source);
618
+ const lacking = secretsLacking(draft, members.secrets);
619
+ if (lacking.length > 0) {
620
+ // a declared secret is never dropped: the manifest gains its slot first
621
+ refused.push({ rule: 'source', instancePath: '/secrets', message: lackingMessage(lacking) });
622
+ } else if (members.secrets.length > 0 || Object.hasOwn(draft, 'secrets')) {
623
+ Object.defineProperty(draft, 'secrets', { value: members.secrets, enumerable: true, writable: true, configurable: true });
624
+ written.push('secrets[]');
625
+ }
626
+ if (members.state.length > 0 && !(Array.isArray(draft.state) && draft.state.length === 0)) {
627
+ Object.defineProperty(draft, 'state', { value: members.state, enumerable: true, writable: true, configurable: true });
628
+ written.push('state[]');
629
+ }
630
+ }
535
631
  const { pkg, notes } = compose(draft, files);
536
- const problems = allProblems(pkg, files);
632
+ if (written.length > 0) notes.push(`${written.join(' and ')} written from ${SOURCE_FILE}`);
633
+ const problems = [...refused, ...allProblems(pkg, files)];
537
634
  const suite = isObject(pkg.evals) ? pkg.evals.suite : undefined;
538
635
  if (isString(suite) && files.has(suite) && problems.some((p) => p.instancePath === '/evals/receipt/suite')) notes.push(`the suite packed is ${sha256Of(files.get(suite))}: run it again and write that receipt`);
539
636
  const answer = { ok: problems.length === 0, applied: false, digest: pkg.digest, files: pkg.files.length, problems, notes };
@@ -45,7 +45,7 @@ import http from 'node:http';
45
45
  import net from 'node:net';
46
46
  import os from 'node:os';
47
47
 
48
- export const PEER_OWNER_VERSION = '1.1.0+kit.3'; // the Python twin's, Observatory's 1.1.0 with the kit's differences
48
+ export const PEER_OWNER_VERSION = '1.2.0+kit.5'; // the Python twin's, Observatory's 1.2.0 with the kit's differences
49
49
  export const THIS_ACCOUNT = 'this-account';
50
50
  export const ANOTHER_ACCOUNT = 'another-account';
51
51
  export const SYSTEM = 'system';
@@ -154,6 +154,10 @@ export function parseNetstat(text) {
154
154
  const at = names.indexOf('process:pid');
155
155
  const leading = at - names.slice(0, at).filter((w) => w === 'Address').length;
156
156
  const trailing = names.length - at - 1;
157
+ // The caller's own connection is in any table it can see: a header and no tcp row is the restricted table (FAA-273).
158
+ if (!lines.slice(header + 1).some((line) => (line.split(/\s+/).filter(Boolean)[0] ?? '').startsWith('tcp'))) {
159
+ throw new TableUnreadable(`${TABLE_RESTRICTED}: netstat printed only its header to this process`);
160
+ }
157
161
  const rows = [];
158
162
  for (const line of lines.slice(header + 1)) {
159
163
  const tok = line.split(/\s+/).filter(Boolean);
@@ -416,7 +420,7 @@ async function ownerOf(local, remote, opts = {}, server = false) {
416
420
  // Linux: a connection not yet accept()ed is printed with uid 0 and inode 0; it belongs to the listener on its local
417
421
  // address and dies with it, so that listener answers. An accepted connection's own row carries the uid of the
418
422
  // process that accepted it: it answers itself, so a listener swapped after another account accepted the client
419
- // cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's pending CT-254, FAA-258).
423
+ // cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's CT-254, released).
420
424
  // A server row its owner already closed (an orphan) also has inode 0 and reads so: no process holds it, so nothing
421
425
  // sent to it reaches anyone (0.74.0 unit reading 6).
422
426
  return oneOwner(listenersOf(table.rows, found[0].local), table.how, opts, 'not-accepted');
@@ -460,7 +464,7 @@ export function peer(socket, opts) {
460
464
  // For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
461
465
  // address is this socket's own, the server process's accepted socket, so a listener swapped after listen cannot pass
462
466
  // for the one that answers. On Linux a connection not yet accept()ed has no owner of its own (inode 0): its listener
463
- // answers (the contract's pending CT-254, FAA-258).
467
+ // answers (the contract's CT-254, HA-2's Linux source).
464
468
  export function serverEnd(socket, opts) {
465
469
  if (!socket.remoteAddress || !socket.localAddress) return Promise.resolve(owner(UNKNOWN, { reason: 'socket: not connected' }));
466
470
  return ownerOf(address(socket.remoteAddress, socket.remotePort), address(socket.localAddress, socket.localPort), opts, true);
@@ -12,7 +12,7 @@ export const PROTOCOL_REVISIONS = new Map(Object.entries({
12
12
  'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-machine-report': ['1'], 'fabric-ops-digest': ['0.1'], 'fabric-operator-channel': ['0.1'],
13
13
  'fabric-package': ['1'],
14
14
  'fabric-peer-share': ['1'], 'fabric-peer-share-envelope': ['1'], 'fabric-peer-share-report': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
15
- 'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
15
+ 'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], 'fabric-source': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
16
16
  memory: ['0.1'], 'node-summary': ['1'], 'operator-source': ['1'], 'org-adaptation': ['1'], 'org-adaptation-receipt': ['1'], 'org-authority': ['1'], 'org-authority-draft': ['1'],
17
17
  'org-key-set': ['1'], 'org-key-set-draft': ['1'], 'org-membership': ['1'], 'org-root-registration': ['1'], 'pairing-message': ['1'],
18
18
  'peer-share-deletion': ['1'], 'peer-share-frame': ['1'], 'peer-share-revocation': ['1'], pipeline: ['0.1'], 'repair-request': ['1'], 'repair-result': ['1'],
@@ -38,8 +38,12 @@ 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
- /** A value that says it is a credential, or a provider key's shape. */
42
- export const SECRET_VALUE = /(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|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,})/i;
41
+ /** 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
+ * 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
+ * escape; an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more,
44
+ * within 256 characters (the contract's SECRET_VALUE at the pin, DEC-0151). A match includes its boundary character:
45
+ * callers test, never read the matched text or its index. */
46
+ export const 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,}))/;
43
47
  const LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
44
48
  export const longSecret = (value) => (value.match(LONG_RUN) ?? []).some((run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run));
45
49
 
@@ -91,7 +95,10 @@ export function secretUrl(value) {
91
95
  const REPOSITORY_URI = /^([A-Za-z][A-Za-z0-9+.-]*:(?:\/\/[^/?#]*)?)([^?#]*)([\s\S]*)$/;
92
96
  const REPOSITORY_NAME = /^(?:[a-z0-9._-]+|[A-Za-z._-]+)$/; // lower case, or letters with no digit (readings 8)
93
97
  const REPOSITORY_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
94
- const KEY_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))(?:${SECRET_VALUE.source})`, 'i');
98
+ // A repository name's own key shapes, boundary-free and case-sensitive (DEC-0151): SECRET_VALUE takes a consuming
99
+ // boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
100
+ const REPOSITORY_KEYS = 'sk-[A-Za-z0-9_-]{16,}|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,}';
101
+ const KEY_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))(?:${REPOSITORY_KEYS})`);
95
102
  const JWT_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))${JWT_SHAPE.source}`);
96
103
  const SPECIAL_QUERY = /^(?:https?|wss?|ftp|file):/i; // whose query also encodes `'`, as the URL standard does
97
104
 
@@ -43,6 +43,10 @@ export {
43
43
  } from './fabric-protocols.mjs';
44
44
  export { ACTIONS_EXTENSION_KEY, actionDecision, actionRules, actionsExtension, actionsExtensionRules, actionsView, decisionShape } from './fabric-actions.mjs';
45
45
  export { CONNECTIONS_EXTENSION_KEY, connectionsExtension, connectionsRules, connectionsView } from './fabric-connections.mjs';
46
+ export { // the call queue and the load report (DEC-0104, FAA-289)
47
+ DEFAULT_RETRY_AFTER_SECONDS, LOAD_EXTENSION_KEY, LOAD_REPORT_TYPE, QUEUE_FULL_MESSAGE, QUEUE_LENGTH_MAX, admitJob, jobCallerKey, jobQueueRules,
48
+ loadExtension, loadExtensionRules, loadReportProblems, loadReportRules, loadView, nextQueuedJob, queueStatusMessage, readQueuePosition,
49
+ } from './fabric-load.mjs';
46
50
  export {
47
51
  EXIT_ALREADY_RUNNING,
48
52
  LOCK_WAIT_SUPERVISED_SECONDS,
@@ -103,6 +107,10 @@ export {
103
107
  STATUSES,
104
108
  PLACEMENTS,
105
109
  MANAGERS,
110
+ PEER_CHECKS,
111
+ connectionRefusal,
112
+ connectionOwner,
113
+ loopbackServer,
106
114
  descriptorPath,
107
115
  readDescriptors,
108
116
  validateDescriptor,
@@ -118,7 +126,9 @@ export {
118
126
  fabricButtonPresence,
119
127
  placementOf,
120
128
  remoteOriginProblems,
129
+ portListenerRefusal,
121
130
  portOf,
131
+ portOwner,
122
132
  NO_SOURCE_REPOSITORY,
123
133
  namesRepository,
124
134
  sourceRefusal,
@@ -179,6 +189,7 @@ export {
179
189
  taskUninstall,
180
190
  launchdPlist,
181
191
  SYSTEM_DAEMONS,
192
+ USER_NAME_RULE,
182
193
  launchdDomain,
183
194
  wellKnownOf,
184
195
  jobWellKnownOf,