knosky 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- package/ssot/tool-menu.json +130 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// KnoSky district classification model, loader, and validator (SAT-505 / KS2-F1-2).
|
|
2
|
+
//
|
|
3
|
+
// Defines the five classification levels for KnoSky district nodes, ordered
|
|
4
|
+
// from least to most restrictive. Any node whose `district_class` field is
|
|
5
|
+
// absent, null, or unrecognised resolves to the most-restrictive level
|
|
6
|
+
// (BLOCKED) — fail-closed by design.
|
|
7
|
+
//
|
|
8
|
+
// Pure module: no I/O, no external dependencies, safe to import anywhere.
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Classification level constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** Least-restrictive level — visible to anyone. */
|
|
15
|
+
export const CLASS_PUBLIC = 'public';
|
|
16
|
+
|
|
17
|
+
/** Visible within the organisation but not externally. */
|
|
18
|
+
export const CLASS_INTERNAL = 'internal';
|
|
19
|
+
|
|
20
|
+
/** Access limited to specific roles or teams. */
|
|
21
|
+
export const CLASS_RESTRICTED = 'restricted';
|
|
22
|
+
|
|
23
|
+
/** Highly sensitive — tightly controlled access. */
|
|
24
|
+
export const CLASS_CONFIDENTIAL = 'confidential';
|
|
25
|
+
|
|
26
|
+
/** Most-restrictive level — node is blocked from general access. */
|
|
27
|
+
export const CLASS_BLOCKED = 'blocked';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Ordered set and default
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* All valid classification levels, ordered from least to most restrictive.
|
|
35
|
+
* Index 0 is the least restrictive; the last index is the most restrictive.
|
|
36
|
+
* @type {readonly string[]}
|
|
37
|
+
*/
|
|
38
|
+
export const CLASSES = Object.freeze([
|
|
39
|
+
CLASS_PUBLIC,
|
|
40
|
+
CLASS_INTERNAL,
|
|
41
|
+
CLASS_RESTRICTED,
|
|
42
|
+
CLASS_CONFIDENTIAL,
|
|
43
|
+
CLASS_BLOCKED,
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The default classification applied when a node carries no recognisable
|
|
48
|
+
* `district_class` value. Fail-closed: most restrictive.
|
|
49
|
+
* @type {string}
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_CLASS = CLASS_BLOCKED;
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// isValidClass
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return `true` if `cls` is a recognised classification level string.
|
|
59
|
+
*
|
|
60
|
+
* @param {unknown} cls
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function isValidClass(cls) {
|
|
64
|
+
return CLASSES.includes(cls);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// validateClass
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate a classification level string.
|
|
73
|
+
* Returns `{ ok: true }` for valid values; `{ ok: false, error: string }`
|
|
74
|
+
* for anything else.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} cls
|
|
77
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
78
|
+
*/
|
|
79
|
+
export function validateClass(cls) {
|
|
80
|
+
if (isValidClass(cls)) return { ok: true };
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: `district_class must be one of ${JSON.stringify(CLASSES)}, got: ${JSON.stringify(cls)}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// loadClass
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the district classification of a node.
|
|
93
|
+
*
|
|
94
|
+
* Reads `node.district_class`. If the value is a recognised classification
|
|
95
|
+
* string it is returned as-is. Otherwise — absent, null, undefined, or any
|
|
96
|
+
* unrecognised string — the most-restrictive default ({@link DEFAULT_CLASS})
|
|
97
|
+
* is returned so that unknown nodes never escape to a permissive class.
|
|
98
|
+
*
|
|
99
|
+
* The function always returns a non-null string from {@link CLASSES}.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} node Any node-like object (may carry a `district_class` field).
|
|
102
|
+
* @returns {string} One of the {@link CLASSES} values.
|
|
103
|
+
*/
|
|
104
|
+
export function loadClass(node) {
|
|
105
|
+
if (node !== null && typeof node === 'object') {
|
|
106
|
+
// Preferred field: district_class. Accept short alias `classification` used by some indexes.
|
|
107
|
+
const cls = node.district_class ?? node.classification;
|
|
108
|
+
if (isValidClass(cls)) return cls;
|
|
109
|
+
}
|
|
110
|
+
return DEFAULT_CLASS;
|
|
111
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// KnoSky doctor scorecard — Mode B + L0–L3 ladder + L3 foundation (DEC-106/108/110/113).
|
|
2
|
+
// Honest, plain-English lines. Never claims Windows kernel lockdown when unsupported.
|
|
3
|
+
// Pure ESM, Node stdlib.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { platform } from 'node:os';
|
|
9
|
+
import { probeNetworkLockdownSupport, doctorLines as lockdownDoctorLines } from './net-lockdown.mjs';
|
|
10
|
+
import { resolveDomainRoot, loadDomain } from './domain-store.mjs';
|
|
11
|
+
import { closedSet, CODES } from './decision-codes.mjs';
|
|
12
|
+
import { verifyAuditChain } from './audit-writer.mjs';
|
|
13
|
+
import { DEFAULT_SWARM_QUOTAS, swarmPaths, readSwarmHeatmap } from './swarm-coordinator.mjs';
|
|
14
|
+
|
|
15
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} ScoreRow
|
|
19
|
+
* @property {string} id
|
|
20
|
+
* @property {'ok'|'warn'|'fail'|'info'} level
|
|
21
|
+
* @property {string} title
|
|
22
|
+
* @property {string} detail
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function mark(level, id, title, detail) {
|
|
26
|
+
return { id, level, title, detail };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function emoji(level) {
|
|
30
|
+
if (level === 'ok') return '🟢';
|
|
31
|
+
if (level === 'warn') return '🟡';
|
|
32
|
+
if (level === 'fail') return '🔴';
|
|
33
|
+
return '⚪';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load SSOT artifacts from repo root.
|
|
38
|
+
*/
|
|
39
|
+
export function inspectSsot(repoRoot = ROOT) {
|
|
40
|
+
const menuPath = join(repoRoot, 'ssot', 'tool-menu.json');
|
|
41
|
+
const codesPath = join(repoRoot, 'ssot', 'decision-codes.json');
|
|
42
|
+
const ladderPath = join(repoRoot, 'ssot', 'ladder-l0-l3.md');
|
|
43
|
+
const out = {
|
|
44
|
+
menuPath,
|
|
45
|
+
codesPath,
|
|
46
|
+
ladderPath,
|
|
47
|
+
menuOk: false,
|
|
48
|
+
codesOk: false,
|
|
49
|
+
ladderOk: false,
|
|
50
|
+
tier1: [],
|
|
51
|
+
closed: [],
|
|
52
|
+
errors: [],
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(menuPath)) throw new Error('missing tool-menu.json');
|
|
56
|
+
const menu = JSON.parse(readFileSync(menuPath, 'utf8'));
|
|
57
|
+
out.menuOk = true;
|
|
58
|
+
out.tier1 = (menu.tiers?.tier1_governed?.tools || []).map((t) => t.name);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
out.errors.push(String(e.message || e));
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (!existsSync(codesPath)) throw new Error('missing decision-codes.json');
|
|
64
|
+
const codes = JSON.parse(readFileSync(codesPath, 'utf8'));
|
|
65
|
+
out.codesOk = true;
|
|
66
|
+
out.closed = codes.closed_set || [];
|
|
67
|
+
} catch (e) {
|
|
68
|
+
out.errors.push(String(e.message || e));
|
|
69
|
+
}
|
|
70
|
+
out.ladderOk = existsSync(ladderPath);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Inspect MCP server source for DEC-108 registration (static honesty).
|
|
76
|
+
*/
|
|
77
|
+
export function inspectMcpMenu(repoRoot = ROOT) {
|
|
78
|
+
const serverPath = join(repoRoot, 'mcp', 'server.mjs');
|
|
79
|
+
const src = existsSync(serverPath) ? readFileSync(serverPath, 'utf8') : '';
|
|
80
|
+
const need = ['kc_search', 'kc_route', 'kc_bundle', 'kc_policy_check', 'createModeBDoor'];
|
|
81
|
+
const missing = need.filter((t) => !src.includes(t));
|
|
82
|
+
return {
|
|
83
|
+
serverPath,
|
|
84
|
+
present: existsSync(serverPath),
|
|
85
|
+
missing,
|
|
86
|
+
hasModeB: src.includes('createModeBDoor'),
|
|
87
|
+
hasSecurityAudit: src.includes('kc_audit_query'),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build structured doctor scorecard.
|
|
93
|
+
* @param {object} [opts]
|
|
94
|
+
* @param {string} [opts.domainRoot]
|
|
95
|
+
* @param {string} [opts.cityPath]
|
|
96
|
+
* @param {string} [opts.repoRoot]
|
|
97
|
+
*/
|
|
98
|
+
export function buildDoctorScorecard(opts = {}) {
|
|
99
|
+
const repoRoot = opts.repoRoot || ROOT;
|
|
100
|
+
const domainRoot = resolveDomainRoot(opts.cityPath, opts.domainRoot);
|
|
101
|
+
/** @type {ScoreRow[]} */
|
|
102
|
+
const rows = [];
|
|
103
|
+
|
|
104
|
+
// --- Platform / F0.5 ---
|
|
105
|
+
const lock = probeNetworkLockdownSupport();
|
|
106
|
+
const plat = platform();
|
|
107
|
+
if (lock.active) {
|
|
108
|
+
rows.push(mark('ok', 'F0.5', 'Network lockdown', 'ACTIVE — process is sandboxed for egress.'));
|
|
109
|
+
} else if (lock.supported && lock.toolAvailable) {
|
|
110
|
+
rows.push(
|
|
111
|
+
mark(
|
|
112
|
+
'warn',
|
|
113
|
+
'F0.5',
|
|
114
|
+
'Network lockdown',
|
|
115
|
+
'Available but NOT active for this process. External egress may still be possible until wrapped.',
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
} else if (plat === 'win32') {
|
|
119
|
+
rows.push(
|
|
120
|
+
mark(
|
|
121
|
+
'warn',
|
|
122
|
+
'F0.5',
|
|
123
|
+
'Network lockdown (Windows)',
|
|
124
|
+
'Not kernel-enforced at runtime on Windows for this CLI. Do NOT claim "no egress guaranteed on Windows". Packaging/AppContainer would be required. ' +
|
|
125
|
+
(lock.unsupportedReason || lock.detail || ''),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
rows.push(
|
|
130
|
+
mark(
|
|
131
|
+
'fail',
|
|
132
|
+
'F0.5',
|
|
133
|
+
'Network lockdown',
|
|
134
|
+
lock.unsupportedReason || lock.detail || 'unsupported',
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- SSOT / DEC-108 ---
|
|
140
|
+
const ssot = inspectSsot(repoRoot);
|
|
141
|
+
if (ssot.menuOk && ssot.codesOk && ssot.ladderOk) {
|
|
142
|
+
rows.push(
|
|
143
|
+
mark(
|
|
144
|
+
'ok',
|
|
145
|
+
'SSOT',
|
|
146
|
+
'Protocol freeze files',
|
|
147
|
+
`tool-menu + decision-codes + ladder present. Tier1: ${ssot.tier1.join(', ') || '(none)'}`,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
rows.push(
|
|
152
|
+
mark(
|
|
153
|
+
'fail',
|
|
154
|
+
'SSOT',
|
|
155
|
+
'Protocol freeze files',
|
|
156
|
+
`Incomplete SSOT under ${join(repoRoot, 'ssot')}: ${ssot.errors.join('; ') || 'ladder missing'}`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let codesRuntimeOk = true;
|
|
162
|
+
try {
|
|
163
|
+
const set = closedSet();
|
|
164
|
+
for (const c of ['ALLOW', 'DENY_IDENTITY', 'ADVISORY_UNAUTH', CODES.DENY_POLICY]) {
|
|
165
|
+
if (!set.includes(c)) codesRuntimeOk = false;
|
|
166
|
+
}
|
|
167
|
+
rows.push(
|
|
168
|
+
codesRuntimeOk
|
|
169
|
+
? mark('ok', 'CODES', 'Decision codes runtime', `Closed set size ${set.length}; core codes present.`)
|
|
170
|
+
: mark('fail', 'CODES', 'Decision codes runtime', 'Closed set missing required codes.'),
|
|
171
|
+
);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
rows.push(mark('fail', 'CODES', 'Decision codes runtime', String(e.message || e)));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const mcp = inspectMcpMenu(repoRoot);
|
|
177
|
+
if (mcp.present && mcp.missing.length === 0 && mcp.hasModeB) {
|
|
178
|
+
rows.push(
|
|
179
|
+
mark(
|
|
180
|
+
'ok',
|
|
181
|
+
'MCP',
|
|
182
|
+
'Mode B front door wiring',
|
|
183
|
+
'mcp/server.mjs registers Tier0+Tier1 and Mode B door. Defaults are non-authorizing until lease+policy ALLOW.',
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
} else if (!mcp.present) {
|
|
187
|
+
rows.push(mark('fail', 'MCP', 'Mode B front door wiring', 'mcp/server.mjs missing'));
|
|
188
|
+
} else {
|
|
189
|
+
rows.push(
|
|
190
|
+
mark(
|
|
191
|
+
'fail',
|
|
192
|
+
'MCP',
|
|
193
|
+
'Mode B front door wiring',
|
|
194
|
+
`Missing markers: ${mcp.missing.join(', ') || 'Mode B'}`,
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- Domain / Mode B readiness ---
|
|
200
|
+
try {
|
|
201
|
+
const domain = loadDomain(domainRoot);
|
|
202
|
+
const leaseN = domain.leaseStore.size;
|
|
203
|
+
const agentN = Object.keys(domain.agents || {}).length;
|
|
204
|
+
const requireId = domain.policy?.require_identity !== false;
|
|
205
|
+
rows.push(
|
|
206
|
+
mark(
|
|
207
|
+
leaseN > 0 ? 'ok' : 'warn',
|
|
208
|
+
'MODEB-LEASES',
|
|
209
|
+
'Local trust domain leases',
|
|
210
|
+
leaseN > 0
|
|
211
|
+
? `Domain ${domainRoot}: ${agentN} agent(s), ${leaseN} lease(s). Mode B routes can bind identity.`
|
|
212
|
+
: `Domain ${domainRoot}: no leases yet. Run: knosky agent-register --agent <id>. Coding profile Mode B will DENY_IDENTITY until then (or use KC_PROFILE=advisory for labeled map-only). require_identity=${requireId}`,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const audit = verifyAuditChain(domainRoot);
|
|
217
|
+
if (audit.ok) {
|
|
218
|
+
rows.push(
|
|
219
|
+
mark(
|
|
220
|
+
audit.count > 0 ? 'ok' : 'info',
|
|
221
|
+
'MODEB-AUDIT',
|
|
222
|
+
'Audit ledger chain',
|
|
223
|
+
audit.count > 0
|
|
224
|
+
? `Hash chain OK (${audit.count} event(s)).`
|
|
225
|
+
: 'No audit events yet — chain OK (empty). First Mode B call will write receipts.',
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
} else {
|
|
229
|
+
rows.push(
|
|
230
|
+
mark('fail', 'MODEB-AUDIT', 'Audit ledger chain', `Verify failed: ${audit.reason || 'unknown'} @${audit.at}`),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} catch (e) {
|
|
234
|
+
rows.push(mark('fail', 'MODEB-DOMAIN', 'Local trust domain', String(e.message || e)));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- L0–L3 ladder honesty ---
|
|
238
|
+
const claimTips = [];
|
|
239
|
+
claimTips.push('L0 local map: OK to claim when city is local-only and default install does not upload source.');
|
|
240
|
+
claimTips.push('L1 share-safe: OK only after share-safe indexer / secret scan path you actually ran.');
|
|
241
|
+
claimTips.push('L2 governed: OK only when Mode B ALLOW path has identity+policy+audit for that install.');
|
|
242
|
+
claimTips.push(
|
|
243
|
+
'L3 swarm: FOUNDATION present (coordinator module). Do NOT market full production “swarm-safe fleet” until remaining polish/benchmarks/red-team gates land.',
|
|
244
|
+
);
|
|
245
|
+
rows.push(mark('info', 'LADDER', 'L0–L3 claim ceiling', claimTips.join(' | ')));
|
|
246
|
+
|
|
247
|
+
// --- L3 foundation ---
|
|
248
|
+
try {
|
|
249
|
+
const sp = swarmPaths(domainRoot);
|
|
250
|
+
const heat = readSwarmHeatmap(domainRoot);
|
|
251
|
+
const hasCoord = existsSync(join(repoRoot, 'core', 'swarm-coordinator.mjs'));
|
|
252
|
+
if (!hasCoord) {
|
|
253
|
+
rows.push(mark('fail', 'L3', 'Swarm coordinator module', 'core/swarm-coordinator.mjs missing'));
|
|
254
|
+
} else {
|
|
255
|
+
rows.push(
|
|
256
|
+
mark(
|
|
257
|
+
'ok',
|
|
258
|
+
'L3-MOD',
|
|
259
|
+
'Swarm coordinator module',
|
|
260
|
+
`Foundation present. Default quotas: claims≤${DEFAULT_SWARM_QUOTAS.maxClaimsPerAgent}, actions/window≤${DEFAULT_SWARM_QUOTAS.maxActionsPerWindow}, antiProbe≥${DEFAULT_SWARM_QUOTAS.antiProbeDenyThreshold}. Fairness=FIFO wait when enableFifoWait.`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
if (heat.ok) {
|
|
264
|
+
rows.push(
|
|
265
|
+
mark(
|
|
266
|
+
'ok',
|
|
267
|
+
'L3-HEAT',
|
|
268
|
+
'Swarm heatmap',
|
|
269
|
+
`Readable at ${sp.heatmapPath}. Use: knosky swarm status`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
rows.push(
|
|
274
|
+
mark(
|
|
275
|
+
'info',
|
|
276
|
+
'L3-HEAT',
|
|
277
|
+
'Swarm heatmap',
|
|
278
|
+
`No heatmap yet under ${sp.heatmapPath}. Run knosky swarm status to write a fresh snapshot.`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (e) {
|
|
284
|
+
rows.push(mark('warn', 'L3', 'Swarm coordinator', String(e.message || e)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Telemetry constitution ---
|
|
288
|
+
rows.push(
|
|
289
|
+
mark(
|
|
290
|
+
'ok',
|
|
291
|
+
'METRICS',
|
|
292
|
+
'Telemetry posture',
|
|
293
|
+
'Default: no always-on product telemetry. Adoption metrics = opt-in / survey / public benchmarks only (DEC-114).',
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// --- Packs presence ---
|
|
298
|
+
const packsRoot = join(repoRoot, 'packs');
|
|
299
|
+
if (existsSync(packsRoot)) {
|
|
300
|
+
rows.push(
|
|
301
|
+
mark(
|
|
302
|
+
'ok',
|
|
303
|
+
'PACKS',
|
|
304
|
+
'Consumer packs tree',
|
|
305
|
+
'packs/ present (Hermes/Claude/Cursor/Codex + P1 stubs). Run pack smoke tests before release.',
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
rows.push(mark('warn', 'PACKS', 'Consumer packs tree', 'packs/ missing'));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const summary = {
|
|
313
|
+
ok: rows.filter((r) => r.level === 'ok').length,
|
|
314
|
+
warn: rows.filter((r) => r.level === 'warn').length,
|
|
315
|
+
fail: rows.filter((r) => r.level === 'fail').length,
|
|
316
|
+
info: rows.filter((r) => r.level === 'info').length,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
version: '1.0.0',
|
|
321
|
+
platform: plat,
|
|
322
|
+
domainRoot,
|
|
323
|
+
repoRoot,
|
|
324
|
+
lockdown: lock,
|
|
325
|
+
summary,
|
|
326
|
+
rows,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Human lines for CLI `knosky doctor`.
|
|
332
|
+
* @param {object} [opts]
|
|
333
|
+
* @returns {string[]}
|
|
334
|
+
*/
|
|
335
|
+
export function doctorScorecardLines(opts = {}) {
|
|
336
|
+
const card = buildDoctorScorecard(opts);
|
|
337
|
+
const lines = [];
|
|
338
|
+
lines.push('KnoSky doctor — Wave 1 scorecard (Mode B · ladder · L3 foundation · sandbox)');
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push(
|
|
341
|
+
`Summary: ${card.summary.ok} ok · ${card.summary.warn} warn · ${card.summary.fail} fail · ${card.summary.info} info`,
|
|
342
|
+
);
|
|
343
|
+
lines.push(`Domain : ${card.domainRoot}`);
|
|
344
|
+
lines.push(`OS : ${card.platform}`);
|
|
345
|
+
lines.push('');
|
|
346
|
+
|
|
347
|
+
// Keep classic F0.5 lines for continuity
|
|
348
|
+
lines.push('--- F0.5 lockdown (classic) ---');
|
|
349
|
+
for (const l of lockdownDoctorLines()) lines.push(l);
|
|
350
|
+
lines.push('');
|
|
351
|
+
|
|
352
|
+
lines.push('--- Scorecard ---');
|
|
353
|
+
for (const r of card.rows) {
|
|
354
|
+
lines.push(`${emoji(r.level)} [${r.id}] ${r.title}`);
|
|
355
|
+
lines.push(` ${r.detail}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push('');
|
|
358
|
+
lines.push('Hints:');
|
|
359
|
+
lines.push(' knosky agent-register --agent <id> # mint Mode B lease');
|
|
360
|
+
lines.push(' knosky swarm status # L3 heatmap snapshot');
|
|
361
|
+
lines.push(' KC_PROFILE=coding|advisory|security # MCP profile');
|
|
362
|
+
lines.push(' Claims: never call Mode A “authorized”; never claim Windows no-egress if scorecard warns.');
|
|
363
|
+
return lines;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** CLI exit code: 0 if no fail rows, 2 if any fail. */
|
|
367
|
+
export function doctorExitCode(card) {
|
|
368
|
+
return card.summary.fail > 0 ? 2 : 0;
|
|
369
|
+
}
|