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.
Files changed (64) hide show
  1. package/CHANGELOG.md +149 -93
  2. package/CREDITS.md +14 -14
  3. package/LICENSE.md +76 -76
  4. package/LIMITATIONS.md +33 -23
  5. package/PRIVACY.md +30 -30
  6. package/README.md +170 -117
  7. package/SECURITY.md +78 -46
  8. package/action/post-comment.mjs +94 -89
  9. package/action.yml +62 -62
  10. package/bin/knosky.mjs +279 -105
  11. package/core/CONTRACT.md +70 -70
  12. package/core/append-only-checkpoint.mjs +215 -0
  13. package/core/audit-writer.mjs +317 -0
  14. package/core/benchmark-results.mjs +225 -225
  15. package/core/bundle.mjs +178 -178
  16. package/core/churn.mjs +23 -23
  17. package/core/ci.mjs +268 -268
  18. package/core/comparison.mjs +189 -189
  19. package/core/config.mjs +189 -189
  20. package/core/constants.mjs +13 -13
  21. package/core/contract.mjs +123 -123
  22. package/core/cross-repo.mjs +111 -111
  23. package/core/decision-codes.mjs +92 -0
  24. package/core/destination.mjs +161 -161
  25. package/core/district-classification.mjs +111 -0
  26. package/core/doctor-scorecard.mjs +369 -0
  27. package/core/domain-store.mjs +347 -0
  28. package/core/edges.mjs +43 -43
  29. package/core/escalate.mjs +68 -68
  30. package/core/freshness.mjs +198 -194
  31. package/core/fs-indexer.mjs +218 -218
  32. package/core/key-store.mjs +348 -348
  33. package/core/layout.mjs +46 -46
  34. package/core/ledger.mjs +176 -141
  35. package/core/local-ipc-identity.mjs +500 -0
  36. package/core/lod.mjs +155 -155
  37. package/core/mode-b.mjs +410 -0
  38. package/core/multi-model-benchmark.mjs +405 -405
  39. package/core/net-lockdown.mjs +421 -0
  40. package/core/onboarding.mjs +223 -223
  41. package/core/operator-auth.mjs +317 -0
  42. package/core/overlays.mjs +45 -45
  43. package/core/policy-lattice.mjs +142 -0
  44. package/core/pr-comment.mjs +198 -198
  45. package/core/protocol-spec.mjs +460 -460
  46. package/core/provenance.mjs +320 -0
  47. package/core/retrieve.mjs +63 -63
  48. package/core/route.mjs +304 -304
  49. package/core/schema.mjs +275 -275
  50. package/core/signing-tiers.mjs +1265 -0
  51. package/core/swarm-bench.mjs +106 -0
  52. package/core/swarm-coordinator.mjs +867 -0
  53. package/core/trust-root-rekey.mjs +410 -0
  54. package/mcp/server.mjs +264 -108
  55. package/package.json +56 -46
  56. package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
  57. package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
  58. package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
  59. package/renderer/art/kenney/sheet_allCars.xml +545 -545
  60. package/renderer/build-rich.mjs +43 -43
  61. package/renderer/city.template.html +808 -808
  62. package/ssot/decision-codes.json +133 -0
  63. package/ssot/ladder-l0-l3.md +232 -0
  64. package/ssot/tool-menu.json +130 -0
@@ -0,0 +1,410 @@
1
+ // KnoSky Mode B evaluator (DEC-106/108).
2
+ // identity → policy lattice → audit receipt → ALLOW subgraph | DENY_* / ADVISORY_UNAUTH
3
+
4
+ import { evaluate, DENY, ALLOW, NOT_APPLICABLE } from './policy-lattice.mjs';
5
+ import { resolveLeaseIdentity } from './local-ipc-identity.mjs';
6
+ import { writeDecisionReceipt } from './audit-writer.mjs';
7
+ import { CODES, envelope } from './decision-codes.mjs';
8
+ import {
9
+ loadDomain,
10
+ policyRulesFromDomain,
11
+ inferRouteClass,
12
+ resolveDomainRoot,
13
+ } from './domain-store.mjs';
14
+ import { kcRoute } from './route.mjs';
15
+ import { kcBundle } from './bundle.mjs';
16
+ import { loadClass, DEFAULT_CLASS } from './district-classification.mjs';
17
+
18
+ /**
19
+ * @typedef {object} ModeBRequest
20
+ * @property {'route'|'bundle'|'policy_check'} tool
21
+ * @property {string} [destination]
22
+ * @property {string[]} [nodeIds]
23
+ * @property {string} [leaseId]
24
+ * @property {string|null} [agentId] payload claim — never trusted alone
25
+ * @property {boolean} [advisory] force Mode A
26
+ * @property {number} [limit]
27
+ * @property {string} [root] repo root for bundle secret scan
28
+ */
29
+
30
+ /**
31
+ * Create a Mode B front-door bound to a city ctx + domain.
32
+ * @param {object} opts
33
+ * @param {object} opts.cityCtx from retrieve.load
34
+ * @param {string} opts.cityPath
35
+ * @param {string} [opts.domainRoot]
36
+ * @param {'coding'|'security'|'advisory'} [opts.profile]
37
+ */
38
+ export function createModeBDoor(opts) {
39
+ const cityCtx = opts.cityCtx;
40
+ // Normalize byId so route/filter code never assumes Map vs plain object.
41
+ if (cityCtx && cityCtx.byId && typeof cityCtx.byId.get !== 'function') {
42
+ cityCtx.byId = new Map(Object.entries(cityCtx.byId));
43
+ }
44
+ const cityPath = opts.cityPath;
45
+ const domainRoot = resolveDomainRoot(cityPath, opts.domainRoot);
46
+ // Reload domain on each handle() so lease revoke / policy updates are visible
47
+ // without process restart (stale-state window closed at request boundary).
48
+ let domain = loadDomain(domainRoot);
49
+ const profile = opts.profile || process.env.KC_PROFILE || 'coding';
50
+
51
+ function refreshDomain() {
52
+ domain = loadDomain(domainRoot);
53
+ return domain;
54
+ }
55
+
56
+ function bindIdentity(req) {
57
+ // Always re-read domain so revocations / policy edits apply mid-process.
58
+ refreshDomain();
59
+ // Advisory path may skip identity
60
+ if (req.advisory || profile === 'advisory') {
61
+ return { ok: true, advisory: true, agentId: null, lease: null };
62
+ }
63
+ const requireId = domain.policy.require_identity !== false;
64
+ if (!requireId) {
65
+ return { ok: true, advisory: false, agentId: req.agentId || 'anonymous-local', lease: null };
66
+ }
67
+ if (!req.leaseId) {
68
+ return { ok: false, code: CODES.DENY_IDENTITY, hint: 'Provide leaseId from knosky agent register (or set KC_MODE=advisory).' };
69
+ }
70
+ const resolved = resolveLeaseIdentity(domain.leaseStore, req.leaseId, req.agentId ?? null);
71
+ if (!resolved.ok) {
72
+ return {
73
+ ok: false,
74
+ code: CODES.DENY_IDENTITY,
75
+ hint: `Identity bind failed (${resolved.reason}). Register agent + lease in domain.`,
76
+ };
77
+ }
78
+ return { ok: true, advisory: false, agentId: resolved.agentId, lease: resolved.lease };
79
+ }
80
+
81
+ function decidePolicy(agentId, className, tool) {
82
+ const rules = policyRulesFromDomain(domain.policy);
83
+ const subject = { agentId, class: className, tool };
84
+ const { decision, reasons } = evaluate(rules, subject);
85
+ if (decision === DENY) {
86
+ return { ok: false, code: CODES.DENY_POLICY, reasons };
87
+ }
88
+ if (decision === NOT_APPLICABLE && domain.policy.require_identity !== false) {
89
+ // No allow found — fail closed when identity required
90
+ return { ok: false, code: CODES.DENY_POLICY, reasons: reasons.concat(['no_allow_rule']) };
91
+ }
92
+ return { ok: true, decision, reasons };
93
+ }
94
+
95
+ function receipt(decision_code, agentId, tool, destination, meta) {
96
+ const wr = writeDecisionReceipt({
97
+ domainRoot,
98
+ decision_code,
99
+ agent_id: agentId,
100
+ tool,
101
+ destination,
102
+ mode: decision_code === CODES.ADVISORY_UNAUTH ? 'A' : 'B',
103
+ meta,
104
+ });
105
+ if (!wr.ok) {
106
+ return { ok: false, code: CODES.DENY_AUDIT, hint: wr.reason };
107
+ }
108
+ return { ok: true, receipt_id: wr.receipt_id, ledger_seq: wr.ledger_seq };
109
+ }
110
+
111
+ function filterRouteByPolicy(routeDoc, agentId) {
112
+ // Drop waypoints whose node class is denied for agent
113
+ const rules = policyRulesFromDomain(domain.policy);
114
+ const getNodeById = (id) => {
115
+ if (!id || !cityCtx?.byId) return null;
116
+ if (typeof cityCtx.byId.get === 'function') return cityCtx.byId.get(id) || null;
117
+ return cityCtx.byId[id] || null;
118
+ };
119
+ const filterList = (arr) => {
120
+ if (!Array.isArray(arr)) return [];
121
+ return arr.filter((e) => {
122
+ const id = e.id || (e.path ? `fs:${e.path}` : null);
123
+ let cls = DEFAULT_CLASS;
124
+ const node = getNodeById(id);
125
+ if (node) cls = loadClass(node);
126
+ const { decision } = evaluate(rules, { agentId, class: cls, tool: 'route' });
127
+ return decision !== DENY;
128
+ });
129
+ };
130
+ return {
131
+ ...routeDoc,
132
+ route: filterList(routeDoc.route),
133
+ alternates: filterList(routeDoc.alternates),
134
+ tests: filterList(routeDoc.tests),
135
+ docs: filterList(routeDoc.docs),
136
+ authorized: true,
137
+ mode: 'B',
138
+ };
139
+ }
140
+
141
+ /**
142
+ * @param {ModeBRequest} req
143
+ */
144
+ function handle(req) {
145
+ const request_id = `req_${Date.now().toString(36)}`;
146
+ try {
147
+ if (!cityCtx) {
148
+ return envelope({
149
+ code: CODES.ERROR_INDEX,
150
+ mode: 'B',
151
+ request_id,
152
+ next_action: 'Set KC_CITY to a valid city-data.json',
153
+ });
154
+ }
155
+
156
+ const tool = req.tool || 'route';
157
+ const identity = bindIdentity(req);
158
+
159
+ // Mode A advisory
160
+ if (identity.advisory || req.advisory === true || profile === 'advisory') {
161
+ if (tool === 'policy_check') {
162
+ return envelope({
163
+ code: CODES.ADVISORY_UNAUTH,
164
+ mode: 'A',
165
+ request_id,
166
+ payload: { note: 'policy_check in advisory mode does not authorize', decision: 'ADVISORY' },
167
+ });
168
+ }
169
+ if (tool === 'route') {
170
+ if (!req.destination || typeof req.destination !== 'string') {
171
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'A', request_id });
172
+ }
173
+ const doc = kcRoute(cityCtx, String(req.destination).slice(0, 400), {
174
+ limit: req.limit ? Math.min(Math.max(1, req.limit), 20) : 8,
175
+ });
176
+ const labeled = {
177
+ ...doc,
178
+ authorized: false,
179
+ mode: 'A',
180
+ advisory: true,
181
+ caveats: [...(doc.caveats || []), 'ADVISORY_UNAUTH: not policy-certified; verify before acting'],
182
+ };
183
+ // optional receipt for advisory trail
184
+ const rc = receipt(CODES.ADVISORY_UNAUTH, null, tool, req.destination, { advisory: true });
185
+ return envelope({
186
+ code: CODES.ADVISORY_UNAUTH,
187
+ mode: 'A',
188
+ request_id,
189
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
190
+ payload: labeled,
191
+ });
192
+ }
193
+ if (tool === 'bundle') {
194
+ return envelope({
195
+ code: CODES.ADVISORY_UNAUTH,
196
+ mode: 'A',
197
+ request_id,
198
+ next_action: 'Bundle under Mode B requires identity; switch profile=coding with leaseId',
199
+ });
200
+ }
201
+ }
202
+
203
+ if (!identity.ok) {
204
+ return envelope({
205
+ code: identity.code || CODES.DENY_IDENTITY,
206
+ mode: 'B',
207
+ request_id,
208
+ next_action: identity.hint,
209
+ });
210
+ }
211
+
212
+ const agentId = identity.agentId;
213
+
214
+ // Build structural artifact first (metadata), then authorize
215
+ let raw = null;
216
+ let destination = req.destination || null;
217
+ let className = DEFAULT_CLASS;
218
+ /** @type {string[]} */
219
+ let bundleNodeIds = Array.isArray(req.nodeIds) ? req.nodeIds.map(String) : [];
220
+
221
+ if (tool === 'route' || tool === 'policy_check') {
222
+ if (!req.destination || typeof req.destination !== 'string') {
223
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'B', request_id });
224
+ }
225
+ raw = kcRoute(cityCtx, String(req.destination).slice(0, 400), {
226
+ limit: req.limit ? Math.min(Math.max(1, req.limit), 20) : 8,
227
+ });
228
+ className = inferRouteClass(cityCtx, raw);
229
+ } else if (tool === 'bundle') {
230
+ if (!bundleNodeIds.length && req.destination) {
231
+ raw = kcRoute(cityCtx, String(req.destination).slice(0, 400), { limit: 8 });
232
+ // Policy-filter route waypoints BEFORE bundling (don't leak restricted ids)
233
+ const filtered = filterRouteByPolicy(raw, agentId);
234
+ bundleNodeIds = (filtered.route || []).map((e) => e.id).filter(Boolean);
235
+ className = inferRouteClass(cityCtx, filtered);
236
+ } else if (bundleNodeIds.length) {
237
+ // Explicit nodeIds: derive class from allowed nodes; drop denied nodes
238
+ const kept = [];
239
+ let best = null;
240
+ const order = ['public', 'internal', 'restricted', 'confidential', 'blocked'];
241
+ for (const id of bundleNodeIds) {
242
+ const node =
243
+ typeof cityCtx.byId?.get === 'function' ? cityCtx.byId.get(id) : cityCtx.byId?.[id];
244
+ const cls = node ? loadClass(node) : DEFAULT_CLASS;
245
+ const polN = decidePolicy(agentId, cls, 'bundle');
246
+ if (polN.ok) {
247
+ kept.push(id);
248
+ if (best == null || order.indexOf(cls) < order.indexOf(best)) best = cls;
249
+ }
250
+ }
251
+ bundleNodeIds = kept;
252
+ className = best || DEFAULT_CLASS;
253
+ } else {
254
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'B', request_id });
255
+ }
256
+ if (!bundleNodeIds.length) {
257
+ return envelope({
258
+ code: CODES.DENY_POLICY,
259
+ mode: 'B',
260
+ request_id,
261
+ next_action: 'No authorized nodes remain for bundle under current agent classes',
262
+ });
263
+ }
264
+ } else {
265
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'B', request_id });
266
+ }
267
+
268
+ const pol = decidePolicy(agentId, className, tool);
269
+ if (!pol.ok) {
270
+ const rc = receipt(pol.code, agentId, tool, destination, { reasons: pol.reasons, class: className });
271
+ return envelope({
272
+ code: pol.code,
273
+ mode: 'B',
274
+ request_id,
275
+ receipt_id: rc.ok ? rc.receipt_id : undefined,
276
+ next_action: rc.ok
277
+ ? 'Adjust agent class allowlist in .knosky/policy.json or pick another destination'
278
+ : `Audit write failed: ${rc.hint}`,
279
+ });
280
+ }
281
+
282
+ if (tool === 'policy_check') {
283
+ const rc = receipt(CODES.ALLOW, agentId, tool, destination, { class: className });
284
+ if (!rc.ok) {
285
+ return envelope({
286
+ code: CODES.DENY_AUDIT,
287
+ mode: 'B',
288
+ request_id,
289
+ next_action: rc.hint,
290
+ });
291
+ }
292
+ return envelope({
293
+ code: CODES.ALLOW,
294
+ mode: 'B',
295
+ request_id,
296
+ receipt_id: rc.receipt_id,
297
+ payload: {
298
+ would_allow: true,
299
+ class: className,
300
+ agent_id: agentId,
301
+ reasons: pol.reasons,
302
+ },
303
+ });
304
+ }
305
+
306
+ if (tool === 'route') {
307
+ // Audit BEFORE allow payload (DEC-106)
308
+ const rc = receipt(CODES.ALLOW, agentId, tool, destination, { class: className });
309
+ if (!rc.ok) {
310
+ return envelope({
311
+ code: CODES.DENY_AUDIT,
312
+ mode: 'B',
313
+ request_id,
314
+ next_action: rc.hint,
315
+ });
316
+ }
317
+ const authorized = filterRouteByPolicy(raw, agentId);
318
+ authorized.receipt_id = rc.receipt_id;
319
+ authorized.agent_id = agentId;
320
+ return envelope({
321
+ code: CODES.ALLOW,
322
+ mode: 'B',
323
+ request_id,
324
+ receipt_id: rc.receipt_id,
325
+ payload: authorized,
326
+ });
327
+ }
328
+
329
+ if (tool === 'bundle') {
330
+ const ids = bundleNodeIds;
331
+ if (!ids.length) {
332
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'B', request_id });
333
+ }
334
+ try {
335
+ const manifest = kcBundle(cityCtx, ids, {
336
+ root: req.root || null,
337
+ expiry: req.expiry ?? null,
338
+ });
339
+ if (manifest.secret_scan && manifest.secret_scan.status === 'blocked') {
340
+ // Final decision is DENY — audit the denial, never an ALLOW receipt
341
+ const rcDeny = receipt(CODES.DENY_EVIDENCE, agentId, tool, destination, {
342
+ class: className,
343
+ reason: 'secret_scan_blocked',
344
+ });
345
+ return envelope({
346
+ code: CODES.DENY_EVIDENCE,
347
+ mode: 'B',
348
+ request_id,
349
+ receipt_id: rcDeny.ok ? rcDeny.receipt_id : undefined,
350
+ next_action: 'Bundle blocked by fail-closed secret scan',
351
+ });
352
+ }
353
+ // Audit ALLOW only after evidence checks pass
354
+ const rc = receipt(CODES.ALLOW, agentId, tool, destination, { class: className });
355
+ if (!rc.ok) {
356
+ return envelope({
357
+ code: CODES.DENY_AUDIT,
358
+ mode: 'B',
359
+ request_id,
360
+ next_action: rc.hint,
361
+ });
362
+ }
363
+ return envelope({
364
+ code: CODES.ALLOW,
365
+ mode: 'B',
366
+ request_id,
367
+ receipt_id: rc.receipt_id,
368
+ payload: { ...manifest, authorized: true, agent_id: agentId },
369
+ });
370
+ } catch (err) {
371
+ return envelope({
372
+ code: CODES.ERROR_INVALID_INPUT,
373
+ mode: 'B',
374
+ request_id,
375
+ next_action: err && err.message ? err.message : String(err),
376
+ });
377
+ }
378
+ }
379
+
380
+ return envelope({ code: CODES.ERROR_INVALID_INPUT, mode: 'B', request_id });
381
+ } catch (err) {
382
+ const msg = err && err.message ? err.message : String(err);
383
+ // Do not mask security-path failures as "bad input"
384
+ const securityish =
385
+ /audit|ledger|hwm|policy|identity|lease|operator|deny|fsync|permission|EPERM|ENOSPC/i.test(
386
+ msg,
387
+ );
388
+ return envelope({
389
+ code: securityish ? CODES.DENY_AUDIT : CODES.ERROR_INVALID_INPUT,
390
+ mode: 'B',
391
+ request_id,
392
+ next_action: securityish
393
+ ? `Security path error (fail-closed): ${msg.slice(0, 200)}`
394
+ : msg.slice(0, 200),
395
+ });
396
+ }
397
+ }
398
+
399
+ return {
400
+ get domainRoot() {
401
+ return domainRoot;
402
+ },
403
+ get domain() {
404
+ return domain;
405
+ },
406
+ refreshDomain,
407
+ profile,
408
+ handle,
409
+ };
410
+ }