@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.1
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 +169 -3
- package/README.md +7 -3
- package/SECURITY.md +4 -3
- 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 +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
2
|
+
// A service's operations digests (contract DEC-0153, docs/specification/ops-digest.md at the pin): every record of its
|
|
3
|
+
// operations log counted by operation and outcome in the hour it happened, one ops.digest.hourly@1 line for each hour
|
|
4
|
+
// the service observed itself running in (an empty one included), and one ops.digest.daily@1 line for each closed UTC
|
|
5
|
+
// day that has hourly lines. Counts only: no log line, no path, no identifier (OD-7). foldHour and foldDay are the
|
|
6
|
+
// contract's reference folds (src/ops-digest.ts, its vectors in test/fixtures/ops-digest/); opsDigestRules is
|
|
7
|
+
// FAC-SEM-166 and FAC-SEM-167; OpsDigest keeps the files (OD-1, OD-5, OD-6); opsDigestExtension writes the
|
|
8
|
+
// descriptor's OD-9 declaration. Part of the Node service kit (FAA-206 part 2, layer L3); the twin of
|
|
9
|
+
// fabric_ops_digest.py. Node 20+, no dependency.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { ServiceError } from './fabric-base.mjs';
|
|
13
|
+
import { indexed, isObject } from './fabric-jsvalue.mjs';
|
|
14
|
+
import { atomicWrite, ensurePrivateDir } from './fabric-paths.mjs';
|
|
15
|
+
import { secretValue } from './fabric-secrets.mjs';
|
|
16
|
+
import { DAY_MS } from './fabric-time.mjs';
|
|
17
|
+
|
|
18
|
+
export const OPS_DIGEST_HOURLY = 'ops.digest.hourly@1';
|
|
19
|
+
export const OPS_DIGEST_DAILY = 'ops.digest.daily@1';
|
|
20
|
+
/** The descriptor's extension key (OD-9), protocol fabric-ops-digest/0.1. */
|
|
21
|
+
export const OPS_DIGEST_EXTENSION = 'https://fabric.passioncode.ai/agent-contract/extensions/ops-digest/0.1';
|
|
22
|
+
export const RISE_MIN_FAILED = 10;
|
|
23
|
+
export const RISE_FACTOR = 2;
|
|
24
|
+
/** An operation's name (OD-2), the contract's OP_NAME, and its length cap OP_NAME_MAX. */
|
|
25
|
+
export const OP_NAME = /^[a-z][A-Za-z0-9_]*(?:[.:/-][A-Za-z0-9_]+)*$/;
|
|
26
|
+
export const OP_NAME_MAX = 64;
|
|
27
|
+
export const OPS_MAX = 4096;
|
|
28
|
+
/** The overflow name the kit counts the rest under once a day holds 4095 named operations (OD-3). */
|
|
29
|
+
export const OVERFLOW_OP = 'ops.overflow';
|
|
30
|
+
export const OUTCOMES = Object.freeze(['ok', 'failed', 'unknown']);
|
|
31
|
+
const HOUR_MS = 3_600_000;
|
|
32
|
+
const WHOLE_HOUR = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):00:00(?:\.000)?Z$/;
|
|
33
|
+
const DAY = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
|
|
34
|
+
const SLASH = String.raw`(?:[\/\\]|%(?:25)?2[fF])`;
|
|
35
|
+
/** The contract's hasHomePath (src/activity-rules.ts at the pin). */
|
|
36
|
+
export const HOME_PATH = Object.freeze([
|
|
37
|
+
new RegExp(String.raw`(?:^|[^A-Za-z0-9]|%(?:25)?2[fF])(?:[Uu]sers|home)${SLASH}[A-Za-z0-9_]`),
|
|
38
|
+
/(?:^|[^A-Za-z0-9])-(?:[Uu]sers|home)-[A-Za-z0-9]/,
|
|
39
|
+
/(?:^|[\s"'=\/\\])~(?:[A-Za-z_][A-Za-z0-9_.-]*)?(?:[\/\\]|$)/,
|
|
40
|
+
]);
|
|
41
|
+
const PRINTABLE = /^[\x20-\x7e]+$/;
|
|
42
|
+
const SOURCE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
43
|
+
const FILE = /^(hourly|daily)-([0-9]{4})-([0-9]{2})\.jsonl$/;
|
|
44
|
+
|
|
45
|
+
/** An input no digest holds; its message names a credential-shaped or home-folder value only by its place. */
|
|
46
|
+
export class OpsDigestError extends ServiceError {}
|
|
47
|
+
|
|
48
|
+
export const hasHomePath = (value) => typeof value === 'string' && HOME_PATH.some((p) => p.test(value));
|
|
49
|
+
export const isRise = (failed, previous) => failed >= RISE_MIN_FAILED && failed >= RISE_FACTOR * Math.max(1, previous);
|
|
50
|
+
|
|
51
|
+
/** The instant of a whole UTC hour in the schema's form, or null (a 25th hour, a 30 February). */
|
|
52
|
+
export function wholeHour(value) {
|
|
53
|
+
const match = typeof value === 'string' ? WHOLE_HOUR.exec(value) : null;
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const [year, month, day, hour] = match.slice(1).map(Number);
|
|
56
|
+
const at = new Date(0);
|
|
57
|
+
at.setUTCFullYear(year, month - 1, day);
|
|
58
|
+
at.setUTCHours(hour);
|
|
59
|
+
return at.getUTCFullYear() === year && at.getUTCMonth() === month - 1 && at.getUTCDate() === day && at.getUTCHours() === hour ? at.getTime() : null;
|
|
60
|
+
}
|
|
61
|
+
export const realDay = (value) => typeof value === 'string' && DAY.test(value) && wholeHour(`${value}T00:00:00Z`) !== null;
|
|
62
|
+
export const isoHour = (at) => new Date(at).toISOString();
|
|
63
|
+
export const dayOf = (at) => isoHour(at).slice(0, 10);
|
|
64
|
+
|
|
65
|
+
const count = (n) => (typeof n === 'number' && Number.isSafeInteger(n) && n >= 0 ? n : null);
|
|
66
|
+
const hidden = (value) => secretValue(value) || hasHomePath(value);
|
|
67
|
+
const shown = (value, instead) => (hidden(value) || !PRINTABLE.test(value) ? instead : value);
|
|
68
|
+
const opShown = (op) => (op.length <= OP_NAME_MAX && OP_NAME.test(op) ? shown(op, 'this operation') : 'this operation');
|
|
69
|
+
const byName = (a, b) => (a.op < b.op ? -1 : 1);
|
|
70
|
+
|
|
71
|
+
/** What OD-2 refuses in an operation's name (its grammar, OP_NAME_MAX included, or FAC-SEM-167); null when lawful. */
|
|
72
|
+
export function nameRefusal(op) {
|
|
73
|
+
if (typeof op !== 'string' || op.length > OP_NAME_MAX || !OP_NAME.test(op)) return "fails OD-2's name grammar";
|
|
74
|
+
return hidden(op) ? 'is refused by FAC-SEM-167' : null;
|
|
75
|
+
}
|
|
76
|
+
function allObjects(list, where) {
|
|
77
|
+
if (!Array.isArray(list)) throw new OpsDigestError(`${where} is not an array`);
|
|
78
|
+
for (let i = 0; i < list.length; i += 1) if (!isObject(list[i])) throw new OpsDigestError(`the item at ${i} of ${where} is not an object`);
|
|
79
|
+
}
|
|
80
|
+
function once(ops, where) {
|
|
81
|
+
const names = new Set();
|
|
82
|
+
ops.forEach((c, i) => {
|
|
83
|
+
if (typeof c.op !== 'string') throw new OpsDigestError(`the operation at ${i} of ${where} has no string name`);
|
|
84
|
+
if (names.has(c.op)) throw new OpsDigestError(`${shown(c.op, `the operation at ${i}`)} is named twice in ${where}`);
|
|
85
|
+
names.add(c.op);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const sum = (ops) => ops.reduce((t, c) => ({ ok: t.ok + c.ok, failed: t.failed + c.failed, unknown: t.unknown + c.unknown }), { ok: 0, failed: 0, unknown: 0 });
|
|
89
|
+
function safe(digest) {
|
|
90
|
+
const counts = [digest.total, ...digest.ops].flatMap((c) => [c.ok, c.failed, c.unknown])
|
|
91
|
+
.concat(digest.risen.flatMap((r) => ('previous' in r ? [r.failed, r.previous] : [r.failed])));
|
|
92
|
+
if (!counts.every((n) => count(n) !== null)) throw new OpsDigestError('a sum is beyond 2^53 - 1');
|
|
93
|
+
return digest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The digest of the hour starting at from (the contract's foldHour); throws OpsDigestError where it throws. */
|
|
97
|
+
export function foldHour(counts, from, previous) {
|
|
98
|
+
const start = wholeHour(from);
|
|
99
|
+
if (start === null) throw new OpsDigestError(`${typeof from === 'string' ? shown(from, 'from') : 'from'} is not a whole UTC hour`);
|
|
100
|
+
allObjects(counts, 'counts');
|
|
101
|
+
if (wholeHour(isoHour(start + HOUR_MS)) === null) throw new OpsDigestError(`the hour after ${shown(from, 'from')} is not a whole UTC hour the schema writes`);
|
|
102
|
+
const merged = new Map();
|
|
103
|
+
counts.forEach((c, i) => {
|
|
104
|
+
const refused = nameRefusal(c.op);
|
|
105
|
+
if (refused !== null) throw new OpsDigestError(`the operation at ${i} ${refused}: no hourly digest (OD-3)`);
|
|
106
|
+
if (![c.ok, c.failed, c.unknown].every((n) => count(n) !== null)) throw new OpsDigestError(`a count of ${shown(c.op, `the operation at ${i}`)} is not an integer from 0 to 2^53 - 1`);
|
|
107
|
+
const m = merged.get(c.op) ?? { op: c.op, ok: 0, failed: 0, unknown: 0 };
|
|
108
|
+
m.ok += c.ok; m.failed += c.failed; m.unknown += c.unknown;
|
|
109
|
+
merged.set(c.op, m);
|
|
110
|
+
});
|
|
111
|
+
if (merged.size > OPS_MAX) throw new OpsDigestError(`the hour counts ${merged.size} operations, more than ${OPS_MAX}: no hourly digest (OD-3)`);
|
|
112
|
+
const ops = [...merged.values()].sort(byName);
|
|
113
|
+
const absent = previous === null || previous === undefined;
|
|
114
|
+
if (!absent && !(isObject(previous) && typeof previous.to === 'string')) throw new OpsDigestError('the previous hour is neither absent nor a digest with a string to');
|
|
115
|
+
const adjoining = !absent && wholeHour(previous.to) === start ? previous.ops : [];
|
|
116
|
+
allObjects(adjoining, "the previous hour's ops");
|
|
117
|
+
once(adjoining, "the previous hour's ops");
|
|
118
|
+
const before = new Map(adjoining.map((c) => [c.op, c.failed]));
|
|
119
|
+
for (const c of ops) if (before.has(c.op) && count(before.get(c.op)) === null) throw new OpsDigestError(`the previous hour's failures of ${shown(c.op, 'an operation')} are not an integer from 0 to 2^53 - 1`);
|
|
120
|
+
const risen = ops.filter((c) => isRise(c.failed, before.get(c.op) ?? 0)).map((c) => ({ op: c.op, failed: c.failed, previous: before.get(c.op) ?? 0 }));
|
|
121
|
+
return safe({ schema: OPS_DIGEST_HOURLY, from: isoHour(start), to: isoHour(start + HOUR_MS), ops, total: sum(ops), risen });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The day's hourly digests folded into one (the contract's foldDay), or null for a day with no hour of its own. */
|
|
125
|
+
export function foldDay(day, hourly) {
|
|
126
|
+
if (!realDay(day)) throw new OpsDigestError(`${typeof day === 'string' ? shown(day, 'day') : 'day'} is not a real UTC day`);
|
|
127
|
+
allObjects(hourly, 'hourly');
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const mine = hourly.filter((h) => {
|
|
130
|
+
const at = wholeHour(h.from);
|
|
131
|
+
if (at === null || dayOf(at) !== day || seen.has(at)) return false;
|
|
132
|
+
seen.add(at);
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
if (mine.length === 0) return null;
|
|
136
|
+
const merged = new Map();
|
|
137
|
+
const rose = new Map();
|
|
138
|
+
for (const h of mine) {
|
|
139
|
+
for (const where of ['ops', 'risen']) {
|
|
140
|
+
allObjects(h[where], `${h.from}'s ${where}`);
|
|
141
|
+
h[where].forEach((c, i) => {
|
|
142
|
+
const refused = nameRefusal(c.op);
|
|
143
|
+
if (refused !== null) throw new OpsDigestError(`the operation at ${i} of ${h.from}'s ${where} ${refused}: no daily digest (OD-3)`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const c of h.ops) {
|
|
147
|
+
if (![c.ok, c.failed, c.unknown].every((n) => count(n) !== null)) throw new OpsDigestError(`a count of ${shown(c.op, 'an operation')} in ${h.from} is not an integer from 0 to 2^53 - 1`);
|
|
148
|
+
const m = merged.get(c.op) ?? { op: c.op, ok: 0, failed: 0, unknown: 0 };
|
|
149
|
+
m.ok += c.ok; m.failed += c.failed; m.unknown += c.unknown;
|
|
150
|
+
merged.set(c.op, m);
|
|
151
|
+
}
|
|
152
|
+
once(h.risen, `the risen of ${h.from}`);
|
|
153
|
+
for (const r of h.risen) {
|
|
154
|
+
if (count(r.failed) === null) throw new OpsDigestError(`the rise of ${shown(r.op, 'an operation')} in ${h.from} is not an integer from 0 to 2^53 - 1`);
|
|
155
|
+
if (!h.ops.some((c) => c.op === r.op)) throw new OpsDigestError(`${shown(r.op, 'an operation')} rose in ${h.from} but that hour's ops does not count it`);
|
|
156
|
+
const x = rose.get(r.op) ?? { hours: 0, failed: 0 };
|
|
157
|
+
x.hours += 1; x.failed += r.failed;
|
|
158
|
+
rose.set(r.op, x);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (merged.size > OPS_MAX) throw new OpsDigestError(`the day folds ${merged.size} operations, more than ${OPS_MAX}: no daily digest (OD-3)`);
|
|
162
|
+
const ops = [...merged.values()].sort(byName);
|
|
163
|
+
const risen = [...rose.entries()].map(([op, x]) => ({ op, ...x })).sort((a, b) => b.failed - a.failed || byName(a, b));
|
|
164
|
+
return safe({ schema: OPS_DIGEST_DAILY, day, hours: mine.length, ops, total: sum(ops), risen });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** FAC-SEM-166 and FAC-SEM-167 on one digest of either kind; undefined for any other kind. */
|
|
168
|
+
export function opsDigestRules(kind, value) {
|
|
169
|
+
if (!(kind === 'ops-digest-hourly' || kind === 'ops-digest-daily')) return undefined;
|
|
170
|
+
const findings = [];
|
|
171
|
+
const window = (instancePath, message) => findings.push({ code: 'FAC-SEM-166', instancePath, message });
|
|
172
|
+
const named = (instancePath, op) => {
|
|
173
|
+
if (secretValue(op)) findings.push({ code: 'FAC-SEM-167', instancePath, message: "an operation's name is shaped like a credential; it names an operation, never a value" });
|
|
174
|
+
else if (hasHomePath(op)) findings.push({ code: 'FAC-SEM-167', instancePath, message: "an operation's name holds a home folder; it names an operation, never a path" });
|
|
175
|
+
};
|
|
176
|
+
if (kind === 'ops-digest-hourly') {
|
|
177
|
+
const from = wholeHour(value.from);
|
|
178
|
+
const to = wholeHour(value.to);
|
|
179
|
+
if (from === null) window('/from', 'from is a whole UTC hour of a real day');
|
|
180
|
+
if (to === null) window('/to', 'to is a whole UTC hour of a real day');
|
|
181
|
+
if (from !== null && to !== null && to - from !== HOUR_MS) window('/to', 'to is exactly one hour after from');
|
|
182
|
+
} else if (!realDay(value.day)) {
|
|
183
|
+
window('/day', 'day is a real UTC day, YYYY-MM-DD');
|
|
184
|
+
}
|
|
185
|
+
const byOp = new Map();
|
|
186
|
+
const totals = { ok: 0, failed: 0, unknown: 0 };
|
|
187
|
+
for (const [entry, index] of indexed(value.ops)) {
|
|
188
|
+
const op = entry.op;
|
|
189
|
+
if (typeof op === 'string') {
|
|
190
|
+
if (byOp.has(op)) window(`/ops/${index}/op`, `${opShown(op)} is counted once`);
|
|
191
|
+
else byOp.set(op, entry);
|
|
192
|
+
named(`/ops/${index}/op`, op);
|
|
193
|
+
}
|
|
194
|
+
for (const k of OUTCOMES) {
|
|
195
|
+
const n = count(entry[k]);
|
|
196
|
+
totals[k] = n === null || totals[k] === null ? null : totals[k] + n;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const total = isObject(value.total) ? value.total : null;
|
|
200
|
+
if (total !== null && Array.isArray(value.ops)) {
|
|
201
|
+
for (const k of OUTCOMES) {
|
|
202
|
+
const want = totals[k];
|
|
203
|
+
if (want === null || count(total[k]) === null) continue;
|
|
204
|
+
if (!Number.isSafeInteger(want)) window(`/total/${k}`, `the sum of ops' ${k} is beyond 2^53 - 1, so no total.${k} can be it`);
|
|
205
|
+
else if (total[k] !== want) window(`/total/${k}`, `total.${k} is the sum of ops' ${k}, ${want}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const rose = new Set();
|
|
209
|
+
const hours = count(value.hours);
|
|
210
|
+
for (const [entry, index] of indexed(value.risen)) {
|
|
211
|
+
const op = entry.op;
|
|
212
|
+
if (typeof op !== 'string') continue;
|
|
213
|
+
const at = `/risen/${index}`;
|
|
214
|
+
named(`${at}/op`, op);
|
|
215
|
+
if (rose.has(op)) window(`${at}/op`, `${opShown(op)} is named once among the rises`);
|
|
216
|
+
rose.add(op);
|
|
217
|
+
const counted = byOp.get(op);
|
|
218
|
+
if (counted === undefined) { window(`${at}/op`, `${opShown(op)} rose, so ops counts it`); continue; }
|
|
219
|
+
const failed = count(entry.failed);
|
|
220
|
+
const countedFailed = count(counted.failed);
|
|
221
|
+
if (kind === 'ops-digest-hourly') {
|
|
222
|
+
const previous = count(entry.previous);
|
|
223
|
+
if (failed !== null && countedFailed !== null && failed !== countedFailed) window(`${at}/failed`, `a rise names the hour's failures of ${opShown(op)}, ${countedFailed}`);
|
|
224
|
+
if (failed !== null && previous !== null && !isRise(failed, previous)) window(at, `${failed} failures against ${previous} is no rise: at least ${RISE_MIN_FAILED} and ${RISE_FACTOR} times the hour before (none counts as one)`);
|
|
225
|
+
} else {
|
|
226
|
+
const risenHours = count(entry.hours);
|
|
227
|
+
if (risenHours !== null && hours !== null && risenHours > hours) window(`${at}/hours`, `${opShown(op)} rose in ${risenHours} hours; the day folds ${hours}`);
|
|
228
|
+
if (failed !== null && risenHours !== null && failed < RISE_MIN_FAILED * risenHours) window(`${at}/failed`, `${risenHours} hours of rises hold at least ${RISE_MIN_FAILED * risenHours} failures`);
|
|
229
|
+
if (failed !== null && countedFailed !== null && failed > countedFailed) window(`${at}/failed`, `the rises' failures are at most the day's failures of ${opShown(op)}, ${countedFailed}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return findings;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sameKeys = (o, keys) => Object.keys(o).sort().join(',') === [...keys].sort().join(',');
|
|
236
|
+
function shapeOk(line, daily) {
|
|
237
|
+
if (!isObject(line) || !sameKeys(line, daily ? ['schema', 'day', 'hours', 'ops', 'total', 'risen'] : ['schema', 'from', 'to', 'ops', 'total', 'risen'])) return false;
|
|
238
|
+
if (line.schema !== (daily ? OPS_DIGEST_DAILY : OPS_DIGEST_HOURLY)) return false;
|
|
239
|
+
if (!Array.isArray(line.ops) || !Array.isArray(line.risen) || !isObject(line.total)) return false;
|
|
240
|
+
if (line.ops.length > OPS_MAX || line.risen.length > OPS_MAX) return false;
|
|
241
|
+
for (const c of line.ops) {
|
|
242
|
+
if (!(isObject(c) && sameKeys(c, ['failed', 'ok', 'op', 'unknown']) && nameRefusal(c.op) === null && OUTCOMES.every((k) => count(c[k]) !== null))) return false;
|
|
243
|
+
}
|
|
244
|
+
const want = daily ? ['failed', 'hours', 'op'] : ['failed', 'op', 'previous'];
|
|
245
|
+
for (const r of line.risen) {
|
|
246
|
+
if (!(isObject(r) && sameKeys(r, want) && nameRefusal(r.op) === null && want.filter((k) => k !== 'op').every((k) => count(r[k]) !== null))) return false;
|
|
247
|
+
}
|
|
248
|
+
return sameKeys(line.total, ['failed', 'ok', 'unknown']) && OUTCOMES.every((k) => count(line.total[k]) !== null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The digests of kind (hourly or daily) in folder as a reader reads them (OD-6); the first line of an hour or day kept. */
|
|
252
|
+
export function readDigests(folder, kind) {
|
|
253
|
+
const daily = kind === 'daily';
|
|
254
|
+
const out = [];
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
let names = [];
|
|
257
|
+
try { names = fs.readdirSync(folder).filter((n) => n.startsWith(`${kind}-`) && FILE.test(n)).sort(); } catch { return out; }
|
|
258
|
+
for (const name of names) {
|
|
259
|
+
let text;
|
|
260
|
+
try { text = fs.readFileSync(path.join(folder, name)).toString('utf8'); } catch { continue; }
|
|
261
|
+
for (const raw of text.split('\n')) {
|
|
262
|
+
let line;
|
|
263
|
+
try { line = JSON.parse(raw); } catch { continue; }
|
|
264
|
+
if (!shapeOk(line, daily) || opsDigestRules(daily ? 'ops-digest-daily' : 'ops-digest-hourly', line).length) continue;
|
|
265
|
+
const key = daily ? line.day : wholeHour(line.from);
|
|
266
|
+
if (seen.has(key)) continue;
|
|
267
|
+
seen.add(key);
|
|
268
|
+
out.push(line);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lineOf = (digest) => `${JSON.stringify(digest)}\n`;
|
|
275
|
+
function append(file, data) {
|
|
276
|
+
const fd = fs.openSync(file, 'a', 0o600);
|
|
277
|
+
try { fs.writeSync(fd, data); } finally { fs.closeSync(fd); }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The files of one service's operations digests in folder; see fabric_ops_digest.OpsDigest. */
|
|
281
|
+
export class OpsDigest {
|
|
282
|
+
constructor(folder, { overflow = OVERFLOW_OP, clock } = {}) {
|
|
283
|
+
if (nameRefusal(overflow) !== null) throw new OpsDigestError(`the overflow name ${nameRefusal(overflow)}`);
|
|
284
|
+
this.folder = folder;
|
|
285
|
+
this.overflow = overflow;
|
|
286
|
+
this.clock = clock ?? (() => Date.now());
|
|
287
|
+
this.hour = null;
|
|
288
|
+
this.counts = new Map();
|
|
289
|
+
this.names = new Set();
|
|
290
|
+
this.namesDay = null;
|
|
291
|
+
this.started = false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
get kept() { return path.join(this.folder, 'kept-hour.json'); }
|
|
295
|
+
now(at) { return Math.trunc(at ?? this.clock()); }
|
|
296
|
+
|
|
297
|
+
start(at) {
|
|
298
|
+
ensurePrivateDir(this.folder);
|
|
299
|
+
const now = this.now(at);
|
|
300
|
+
const written = { hours: 0, days: 0 };
|
|
301
|
+
const kept = this.readKept();
|
|
302
|
+
if (kept !== null) {
|
|
303
|
+
if (kept.start + HOUR_MS <= now) { written.hours += this.writeHour(kept.start, kept.counts); this.dropKept(); }
|
|
304
|
+
else { this.hour = kept.start; this.counts = kept.counts; }
|
|
305
|
+
}
|
|
306
|
+
if (this.hour === null) { this.hour = now - (now % HOUR_MS); this.counts = new Map(); }
|
|
307
|
+
this.rebuildNames(now);
|
|
308
|
+
written.days += this.foldClosedDays(now);
|
|
309
|
+
this.started = true;
|
|
310
|
+
return written;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
record(op, outcome, at) {
|
|
314
|
+
const refused = nameRefusal(op);
|
|
315
|
+
if (refused !== null) throw new OpsDigestError(`the operation's name ${refused} (OD-2): name the operation, never a value of it`);
|
|
316
|
+
const now = this.advance(this.now(at));
|
|
317
|
+
let name = op;
|
|
318
|
+
if (!this.names.has(name) && name !== this.overflow) {
|
|
319
|
+
if (this.names.size >= OPS_MAX - 1) name = this.overflow;
|
|
320
|
+
else this.names.add(name);
|
|
321
|
+
}
|
|
322
|
+
this.namesDay = dayOf(now);
|
|
323
|
+
const row = this.counts.get(name) ?? [0, 0, 0];
|
|
324
|
+
row[outcome === 'ok' ? 0 : outcome === 'failed' ? 1 : 2] += 1;
|
|
325
|
+
this.counts.set(name, row);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
tick(at) { this.advance(this.now(at)); }
|
|
329
|
+
|
|
330
|
+
flush(at) {
|
|
331
|
+
const now = this.now(at);
|
|
332
|
+
const written = { hours: 0, days: 0 };
|
|
333
|
+
if (this.hour !== null && this.hour + HOUR_MS <= now) {
|
|
334
|
+
written.hours += this.writeHour(this.hour, this.counts);
|
|
335
|
+
this.hour = null; this.counts = new Map();
|
|
336
|
+
this.dropKept();
|
|
337
|
+
}
|
|
338
|
+
written.days += this.foldClosedDays(now);
|
|
339
|
+
this.keep();
|
|
340
|
+
return written;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
stop(at) { return this.flush(at); }
|
|
344
|
+
|
|
345
|
+
advance(now) {
|
|
346
|
+
if (!this.started) throw new OpsDigestError('call start() before recording (OD-1: the kept hour is written first)');
|
|
347
|
+
const hour = now - (now % HOUR_MS);
|
|
348
|
+
if (this.hour !== null && hour < this.hour) return this.hour;
|
|
349
|
+
if (this.hour !== null && hour > this.hour) {
|
|
350
|
+
this.writeHour(this.hour, this.counts);
|
|
351
|
+
this.counts = new Map();
|
|
352
|
+
this.foldClosedDays(now);
|
|
353
|
+
}
|
|
354
|
+
if (dayOf(hour) !== this.namesDay) { this.names = new Set(); this.namesDay = dayOf(hour); }
|
|
355
|
+
this.hour = hour;
|
|
356
|
+
return now;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
previous(start) {
|
|
360
|
+
const before = start - HOUR_MS;
|
|
361
|
+
const lines = readDigests(this.folder, 'hourly');
|
|
362
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) if (wholeHour(lines[i].from) === before) return lines[i];
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
writeHour(start, counts) {
|
|
367
|
+
ensurePrivateDir(this.folder);
|
|
368
|
+
const from = isoHour(start);
|
|
369
|
+
if (readDigests(this.folder, 'hourly').some((line) => wholeHour(line.from) === start)) return 0;
|
|
370
|
+
const rows = [...counts.entries()].map(([op, c]) => ({ op, ok: c[0], failed: c[1], unknown: c[2] }));
|
|
371
|
+
const digest = foldHour(rows, from, this.previous(start));
|
|
372
|
+
append(path.join(this.folder, `hourly-${from.slice(0, 7)}.jsonl`), lineOf(digest));
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
foldClosedDays(now) {
|
|
377
|
+
const hourly = readDigests(this.folder, 'hourly');
|
|
378
|
+
const done = new Set(readDigests(this.folder, 'daily').map((line) => line.day));
|
|
379
|
+
const today = dayOf(now);
|
|
380
|
+
let folded = 0;
|
|
381
|
+
for (const day of [...new Set(hourly.map((line) => line.from.slice(0, 10)))].sort()) {
|
|
382
|
+
if (day >= today || done.has(day)) continue;
|
|
383
|
+
const digest = foldDay(day, hourly);
|
|
384
|
+
if (digest !== null) { append(path.join(this.folder, `daily-${day.slice(0, 7)}.jsonl`), lineOf(digest)); folded += 1; }
|
|
385
|
+
}
|
|
386
|
+
return folded;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
rebuildNames(now) {
|
|
390
|
+
const today = dayOf(now);
|
|
391
|
+
const names = new Set();
|
|
392
|
+
for (const line of readDigests(this.folder, 'hourly')) if (line.from.slice(0, 10) === today) for (const c of line.ops) names.add(c.op);
|
|
393
|
+
if (this.hour !== null && dayOf(this.hour) === today) for (const op of this.counts.keys()) names.add(op);
|
|
394
|
+
names.delete(this.overflow);
|
|
395
|
+
this.names = names; this.namesDay = today;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
keep() {
|
|
399
|
+
if (this.hour === null) return;
|
|
400
|
+
ensurePrivateDir(this.folder);
|
|
401
|
+
const ops = [...this.counts.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([op, c]) => [op, ...c]);
|
|
402
|
+
atomicWrite(this.kept, `${JSON.stringify({ from: isoHour(this.hour), ops })}\n`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
readKept() {
|
|
406
|
+
try {
|
|
407
|
+
const body = JSON.parse(fs.readFileSync(this.kept, 'utf8'));
|
|
408
|
+
const start = wholeHour(body.from);
|
|
409
|
+
if (start === null) return null;
|
|
410
|
+
const counts = new Map();
|
|
411
|
+
for (const row of body.ops ?? []) {
|
|
412
|
+
if (Array.isArray(row) && row.length === 4 && nameRefusal(row[0]) === null && row.slice(1).every((n) => count(n) !== null)) counts.set(row[0], row.slice(1));
|
|
413
|
+
}
|
|
414
|
+
return { start, counts };
|
|
415
|
+
} catch { return null; }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
dropKept() { try { fs.unlinkSync(this.kept); } catch { /* already gone */ } }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A log's own outcome word as OD-3 maps it; any word the service's published mapping does not place is unknown. */
|
|
422
|
+
export function outcomeOf(word, mapping = {}) {
|
|
423
|
+
if (word === 'ok' || word === 'failed') return word;
|
|
424
|
+
const mapped = typeof word === 'string' && Object.hasOwn(mapping ?? {}, word) ? mapping[word] : undefined;
|
|
425
|
+
return OUTCOMES.includes(mapped) ? mapped : 'unknown';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// String.prototype.isWellFormed is Node 20; the kit's floor is Node 18 (package.json engines).
|
|
429
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
430
|
+
function dirProblem(d) {
|
|
431
|
+
if (typeof d !== 'string' || !d || d.length > 256) return 'dir is 1 to 256 characters';
|
|
432
|
+
if (d.startsWith('/') || d.startsWith('~') || d.endsWith('/') || d.includes('//') || d.includes('\\') || d.includes(':')) return 'dir is relative to the data folder: forward slashes, no leading / or ~, no drive or :, no trailing /';
|
|
433
|
+
if ([...d].some((ch) => ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f) || LONE_SURROGATE.test(d)) return 'dir holds no control character and no lone surrogate';
|
|
434
|
+
if (d.split('/').some((seg) => seg === '.' || seg === '..' || !seg.replace(/[. ]+$/, ''))) return 'dir has no ., .. or empty segment';
|
|
435
|
+
if (hasHomePath(d) || hasHomePath(d.replaceAll('./', '/'))) return 'dir holds a home folder';
|
|
436
|
+
if (secretValue(d)) return 'dir is shaped like a credential (a run of 40 letters, digits and / mixing cases and a digit counts; a space or a dot inside a segment breaks it)';
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The descriptor's extensions member that says where a collector reads the daily digests (OD-9). */
|
|
441
|
+
export function opsDigestExtension(folders = [{ dir: 'digests', source: 'app' }]) {
|
|
442
|
+
const items = [...folders].map((f) => ({ ...f }));
|
|
443
|
+
if (items.length < 1 || items.length > 8) throw new OpsDigestError('the ops-digest declaration holds 1 to 8 folders');
|
|
444
|
+
const dirs = new Set();
|
|
445
|
+
const sources = new Set();
|
|
446
|
+
items.forEach((item, i) => {
|
|
447
|
+
if (!sameKeys(item, ['dir', 'source'])) throw new OpsDigestError(`folder ${i} is {dir, source} and nothing else`);
|
|
448
|
+
const problem = dirProblem(item.dir);
|
|
449
|
+
if (problem) throw new OpsDigestError(`folder ${i}: ${problem} (FAC-SEM-168)`);
|
|
450
|
+
if (typeof item.source !== 'string' || !SOURCE.test(item.source) || secretValue(item.source)) throw new OpsDigestError(`folder ${i}: source is a label matching ^[a-z][a-z0-9-]{0,31}$`);
|
|
451
|
+
const key = item.dir.toLowerCase().split('/').map((seg) => seg.replace(/[. ]+$/, '')).join('/');
|
|
452
|
+
if (dirs.has(key) || sources.has(item.source)) throw new OpsDigestError(`folder ${i}: each dir and each source is named once (FAC-SEM-168)`);
|
|
453
|
+
dirs.add(key); sources.add(item.source);
|
|
454
|
+
});
|
|
455
|
+
return { [OPS_DIGEST_EXTENSION]: items };
|
|
456
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// fabric-package/1, the schema reading: the package schema bundled from the contract at the pin, read by a JSON
|
|
2
3
|
// Schema 2020-12 subset with JavaScript's patterns, lengths in code points and the formats the contract uses.
|
|
3
4
|
// fabric-package.mjs checks, packs, signs and verifies on it and re-exports schemaProblems. Part of the Node service
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
2
3
|
// fabric-package/1 (contract DEC-0050, DEC-0051, DEC-0071, DEC-0074; FAA-27a): pack an agent or a skill set as one folder
|
|
3
4
|
// with fabric.package.json, sign it, and verify it. The twin of fabric_package.py; both write the same bytes.
|
|
4
5
|
//
|
|
@@ -269,10 +270,25 @@ export const packageFindings = (pkg) => [...whole(pkg), ...noSecret(pkg), ...age
|
|
|
269
270
|
|
|
270
271
|
// --- the folder check and the verifier -------------------------------------------------------------------------------
|
|
271
272
|
|
|
273
|
+
const replacingUtf8 = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
274
|
+
// How deep the arrays and objects of a JSON text nest, strings skipped (the Python kit's _nesting).
|
|
275
|
+
const nestingOf = (text) => {
|
|
276
|
+
let depth = 0;
|
|
277
|
+
let deepest = 0;
|
|
278
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
279
|
+
const c = text[i];
|
|
280
|
+
if (c === '"') { i += 1; while (i < text.length && text[i] !== '"') i += text[i] === '\\' ? 2 : 1; } else if (c === '{' || c === '[') { depth += 1; deepest = Math.max(deepest, depth); } else if (c === '}' || c === ']') depth -= 1;
|
|
281
|
+
}
|
|
282
|
+
return deepest;
|
|
283
|
+
};
|
|
284
|
+
// A plugin or provider manifest as the contract's checkPackageFolder reads it: decoded with replacement characters
|
|
285
|
+
// (FAA-194 (29)); nesting past MAX_NESTING, the limit both kits share, is no manifest (FAA-194 (27)).
|
|
272
286
|
const parsed = (bytes) => {
|
|
273
287
|
if (bytes === undefined) return undefined;
|
|
288
|
+
const text = replacingUtf8.decode(bytes);
|
|
289
|
+
if (nestingOf(text) > MAX_NESTING) return undefined;
|
|
274
290
|
try {
|
|
275
|
-
const value = JSON.parse(
|
|
291
|
+
const value = JSON.parse(text);
|
|
276
292
|
return isObject(value) ? value : undefined;
|
|
277
293
|
} catch {
|
|
278
294
|
return undefined;
|
|
@@ -437,7 +453,7 @@ export function readFolder(folder) {
|
|
|
437
453
|
}
|
|
438
454
|
};
|
|
439
455
|
walk(folder, '');
|
|
440
|
-
if (refused.length) throw new PackageError(`Not packed:
|
|
456
|
+
if (refused.length) throw new PackageError(`Not packed: pack a clean copy (what Git would commit, references/package.md); delete only litter and credential files in place: ${refused.sort().join('; ')}`);
|
|
441
457
|
return files;
|
|
442
458
|
}
|
|
443
459
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// A service's pages as Fabric Dashboards' open_link (Fabric Agent Contract DEC-0123, PL-1 to PL-4, FAC-SEM-143;
|
|
2
3
|
// `src/page-link-rules.ts` at the pin): build and read the open_link, tell a raw local address, and the findings on a
|
|
3
4
|
// link a document hands a person and on a URL-mode input request. The twin of fabric_page_links.py, held to the
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// 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
3
|
// Part of the Node service kit (FAA-65, layer L1); the twin of fabric_paths.py. Node 20+, no dependency.
|
|
3
4
|
import crypto from 'node:crypto';
|
|
@@ -18,25 +19,34 @@ export function servicesDir(platform = process.platform) {
|
|
|
18
19
|
return path.join(process.env.XDG_DATA_HOME || path.join(home, '.local/share'), 'passioncode-fabric/services');
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// FABRIC_DATA_HOME (FAA-192): an absolute folder on a durable volume holds the data folder, <home>/<id>, on a host whose
|
|
23
|
+
// own disk does not persist; logs and cache stay where the OS puts them. A relative value is refused.
|
|
24
|
+
function durable(id, dirs) {
|
|
25
|
+
const value = process.env.FABRIC_DATA_HOME;
|
|
26
|
+
if (!value) return dirs;
|
|
27
|
+
if (!path.isAbsolute(value)) throw new ServiceError('FABRIC_DATA_HOME is an absolute folder on a durable volume.');
|
|
28
|
+
return { ...dirs, data: path.join(value, id) };
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
export function serviceDirs(id, platform = process.platform) {
|
|
22
32
|
if (!ID.test(id)) throw new ServiceError(`Service id ${id} must match ${ID}.`);
|
|
23
33
|
const home = os.homedir();
|
|
24
34
|
if (platform === 'darwin') {
|
|
25
|
-
return {
|
|
35
|
+
return durable(id, {
|
|
26
36
|
data: path.join(home, 'Library/Application Support', id),
|
|
27
37
|
logs: path.join(home, 'Library/Logs', id),
|
|
28
38
|
cache: path.join(home, 'Library/Caches', id),
|
|
29
|
-
};
|
|
39
|
+
});
|
|
30
40
|
}
|
|
31
41
|
if (platform === 'win32') {
|
|
32
42
|
const data = path.join(localAppData(), id);
|
|
33
|
-
return { data, logs: path.join(data, 'Logs'), cache: path.join(data, 'Cache') };
|
|
43
|
+
return durable(id, { data, logs: path.join(data, 'Logs'), cache: path.join(data, 'Cache') });
|
|
34
44
|
}
|
|
35
|
-
return {
|
|
45
|
+
return durable(id, {
|
|
36
46
|
data: path.join(process.env.XDG_DATA_HOME || path.join(home, '.local/share'), id),
|
|
37
47
|
logs: path.join(process.env.XDG_STATE_HOME || path.join(home, '.local/state'), id, 'logs'),
|
|
38
48
|
cache: path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), id),
|
|
39
|
-
};
|
|
49
|
+
});
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
/**
|
|
@@ -50,6 +60,10 @@ export function instanceDirs(id, instance = 'default', platform = process.platfo
|
|
|
50
60
|
const folder = `${id}.${instance}`;
|
|
51
61
|
if (platform === 'win32') {
|
|
52
62
|
const data = path.join(path.dirname(personal.data), folder);
|
|
63
|
+
if (process.env.FABRIC_DATA_HOME) { // logs and cache stay on the local disk, beside the default's
|
|
64
|
+
const local = path.join(path.dirname(path.dirname(personal.logs)), folder);
|
|
65
|
+
return { data, logs: path.join(local, 'Logs'), cache: path.join(local, 'Cache') };
|
|
66
|
+
}
|
|
53
67
|
return { data, logs: path.join(data, 'Logs'), cache: path.join(data, 'Cache') };
|
|
54
68
|
}
|
|
55
69
|
if (platform === 'darwin') return Object.fromEntries(Object.entries(personal).map(([key, dir]) => [key, path.join(path.dirname(dir), folder)]));
|