@meetless/mla 0.1.4
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/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RULES_PUBLISH_USAGE = exports.RULES_USAGE = void 0;
|
|
37
|
+
exports.runRulesList = runRulesList;
|
|
38
|
+
exports.runRulesActivity = runRulesActivity;
|
|
39
|
+
exports.runRulesAttest = runRulesAttest;
|
|
40
|
+
exports.runRulesRevoke = runRulesRevoke;
|
|
41
|
+
exports.runRulesPublish = runRulesPublish;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const config_1 = require("../lib/config");
|
|
45
|
+
const http_1 = require("../lib/http");
|
|
46
|
+
const attest_notes_location_1 = require("../lib/rules/attest-notes-location");
|
|
47
|
+
const attest_code_rule_version_1 = require("../lib/rules/attest-code-rule-version");
|
|
48
|
+
const attest_rule_version_1 = require("../lib/rules/attest-rule-version");
|
|
49
|
+
const ce0_store_1 = require("../lib/rules/ce0-store");
|
|
50
|
+
const code_rule_registry_1 = require("../lib/rules/code-rule-registry");
|
|
51
|
+
const interception_store_1 = require("../lib/rules/interception-store");
|
|
52
|
+
const local_rule_version_repo_1 = require("../lib/rules/local-rule-version-repo");
|
|
53
|
+
const rule_version_hash_1 = require("../lib/rules/rule-version-hash");
|
|
54
|
+
const rule_activity_1 = require("../lib/rules/rule-activity");
|
|
55
|
+
const runtime_scope_1 = require("../lib/rules/runtime-scope");
|
|
56
|
+
const ulid_1 = require("../lib/rules/ulid");
|
|
57
|
+
const evidence_1 = require("./evidence");
|
|
58
|
+
// `mla rules list`: the read-only window onto what R0 has durably observed in the ACTIVE runtime
|
|
59
|
+
// scope (proposal §10.2 R0 readiness). The runtime hooks only RECORD; this command never mutates a
|
|
60
|
+
// row, never calls the backend, and never crosses into another scope. It lists one entry per distinct
|
|
61
|
+
// observed rule: the observed-rule-v1 hash, the scanned directive text, the latest verdict and when
|
|
62
|
+
// it was last seen, how many times it has been observed, and whether a local attested version derives
|
|
63
|
+
// from that observed hash. It is a thin IO shell over listObservedRulesInScope; the runtime-scope
|
|
64
|
+
// resolver and the store path are injectable so the workflow is testable end to end.
|
|
65
|
+
const USAGE = "usage: mla rules <list|activity|attest|revoke|publish> [...]";
|
|
66
|
+
exports.RULES_USAGE = USAGE;
|
|
67
|
+
const PUBLISH_USAGE = "usage: mla rules publish [--json]";
|
|
68
|
+
exports.RULES_PUBLISH_USAGE = PUBLISH_USAGE;
|
|
69
|
+
const ATTEST_USAGE = "usage: mla rules attest --from-observed <observedRuleHash> " +
|
|
70
|
+
"[--new-rule <ruleId> | --rule <ruleId>] [--agent-on-user-request --yes] [--no-publish]";
|
|
71
|
+
const CODE_RULE_ATTEST_USAGE = "usage: mla rules attest --from-code-rule <name> [--agent-on-user-request --yes] [--no-publish] " +
|
|
72
|
+
"(the rule id is pinned by the registry; --from-observed / --new-rule / --rule are not allowed here)";
|
|
73
|
+
const REVOKE_USAGE = "usage: mla rules revoke [--rule <ruleId>] [--yes] [--no-publish]";
|
|
74
|
+
/** `mla rules list [--json]`: list the observed rules R0 has recorded in the active runtime scope. */
|
|
75
|
+
async function runRulesList(argv, deps = {}) {
|
|
76
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
77
|
+
const json = argv.includes("--json");
|
|
78
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
79
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
80
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
81
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
82
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
83
|
+
const store = open(dbPath);
|
|
84
|
+
try {
|
|
85
|
+
const rules = (0, interception_store_1.listObservedRulesInScope)(store, runtimeScopeId);
|
|
86
|
+
out(json ? JSON.stringify({ runtimeScopeId, rules }) : formatRulesText(runtimeScopeId, rules));
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Render the listing as a compact, stable text block (one record per observed rule). */
|
|
94
|
+
function formatRulesText(runtimeScopeId, rules) {
|
|
95
|
+
const lines = [`runtime scope: ${runtimeScopeId}`];
|
|
96
|
+
if (rules.length === 0) {
|
|
97
|
+
lines.push("no observed rules recorded in this scope");
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
lines.push(`${rules.length} observed rule(s)`, "");
|
|
101
|
+
for (const r of rules) {
|
|
102
|
+
lines.push(r.observedRuleHash);
|
|
103
|
+
lines.push(` text: ${r.ruleText}`);
|
|
104
|
+
lines.push(` latest: ${r.latestResult} at ${r.latestObservedAt} (${r.observationCount} observation(s))`);
|
|
105
|
+
lines.push(` version: ${r.hasLocalVersion ? "attested" : "none"}`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n").trimEnd();
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// mla rules activity: the R2-LOCAL accountability projection (proposal §2.6 / §3.7 "still local")
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
/**
|
|
114
|
+
* `mla rules activity [--json]`: the §2.6 "observed N, violated M" measurement per LIVE rule in the
|
|
115
|
+
* active scope. This is the SHIPPABLE half of R2: the terminal-outcome half (project a COMMITTED
|
|
116
|
+
* violation, ie "the action the deny named actually happened") is BLOCKED BY DESIGN because the supported
|
|
117
|
+
* PreToolUse payload carries no tool_use_id and heuristic post correlation is forbidden (§9.10, §2.6).
|
|
118
|
+
* The measurement that licenses promoting a rule out of DRY_RUN needs no correlation: it is a pure
|
|
119
|
+
* projection of the records MLA already owns at evaluation time (tool_attempt + rule_evaluation_record),
|
|
120
|
+
* so this command reads them with one local query and never calls the backend or crosses scope. A thin IO
|
|
121
|
+
* shell over summarizeRuleActivity; the runtime-scope resolver and the store path are injectable.
|
|
122
|
+
*/
|
|
123
|
+
async function runRulesActivity(argv, deps = {}) {
|
|
124
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
125
|
+
const json = argv.includes("--json");
|
|
126
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
127
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
128
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
129
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
130
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
131
|
+
const store = open(dbPath);
|
|
132
|
+
try {
|
|
133
|
+
const rules = (0, rule_activity_1.summarizeRuleActivity)(store, runtimeScopeId);
|
|
134
|
+
out(json ? JSON.stringify({ runtimeScopeId, rules }) : formatActivityText(runtimeScopeId, rules));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Render the per-rule measurement as a compact, stable text block (one record per LIVE rule). */
|
|
142
|
+
function formatActivityText(runtimeScopeId, rules) {
|
|
143
|
+
const lines = [`runtime scope: ${runtimeScopeId}`];
|
|
144
|
+
if (rules.length === 0) {
|
|
145
|
+
lines.push("no LIVE rules attested in this scope");
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
lines.push(`${rules.length} LIVE rule(s)`, "");
|
|
149
|
+
for (const r of rules) {
|
|
150
|
+
lines.push(`${r.ruleId} (${r.versionId})`);
|
|
151
|
+
lines.push(` observed ${r.observed}, compliant ${r.compliant}, ` +
|
|
152
|
+
`violation ${r.violation}, denied ${r.deniedEmitted}, ` +
|
|
153
|
+
`enforcement-unavailable ${r.enforcementUnavailable}`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
return lines.join("\n").trimEnd();
|
|
157
|
+
}
|
|
158
|
+
/** Read the audited operator from the authenticated session; only a user-token is a human attestor. */
|
|
159
|
+
function defaultResolveOperator() {
|
|
160
|
+
const cfg = (0, config_1.readConfig)();
|
|
161
|
+
if (cfg.auth.mode !== "user-token")
|
|
162
|
+
return null;
|
|
163
|
+
return { attestedBy: cfg.auth.user.id, displayName: cfg.auth.user.displayName || cfg.auth.user.id };
|
|
164
|
+
}
|
|
165
|
+
/** Synchronously read one line of confirmation from stdin (the interactive default). */
|
|
166
|
+
function defaultConfirm(prompt) {
|
|
167
|
+
process.stderr.write(`${prompt} [y/N] `);
|
|
168
|
+
const buf = Buffer.alloc(256);
|
|
169
|
+
try {
|
|
170
|
+
const n = fs.readSync(0, buf, 0, buf.length, null);
|
|
171
|
+
const answer = buf.toString("utf8", 0, n).trim().toLowerCase();
|
|
172
|
+
return answer === "y" || answer === "yes";
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Render the normalized payload P IN FULL so the human confirms the matcher + ceiling, not the prose
|
|
179
|
+
* line alone (proposal lines 2102-2107). */
|
|
180
|
+
function formatAttestPayload(payload, canonicalPayloadHash, ruleId) {
|
|
181
|
+
const a = payload.applicability;
|
|
182
|
+
const matcher = a.mode === "action"
|
|
183
|
+
? `${a.tools.join(", ")} on ${a.matcher.field}${a.matcher.glob ? ` matching ${a.matcher.glob}` : ""}`
|
|
184
|
+
: "(ambient)";
|
|
185
|
+
return [
|
|
186
|
+
`rule: ${ruleId}`,
|
|
187
|
+
`scope: ${payload.runtimeScopeId}`,
|
|
188
|
+
`hash: ${canonicalPayloadHash}`,
|
|
189
|
+
`text: ${payload.text}`,
|
|
190
|
+
`applies to: ${matcher}`,
|
|
191
|
+
`effect: ${payload.effect}`,
|
|
192
|
+
`strength: ${payload.strength}`,
|
|
193
|
+
`delivery: ${payload.deliveryChannels.join(", ")}`,
|
|
194
|
+
`ceiling: ${payload.enforcementCeiling}`,
|
|
195
|
+
`on failure: ${payload.infrastructureFailurePolicy}`,
|
|
196
|
+
`evaluator: ${payload.compliance.evaluatorContractVersion} / ` +
|
|
197
|
+
`${payload.compliance.matcherSchemaVersion} / ${payload.compliance.pathCanonicalizerVersion}`,
|
|
198
|
+
`forbids: ${payload.compliance.config.forbiddenRootRelativePath}`,
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
function parseAttestIdentity(argv) {
|
|
202
|
+
const newRuleIdx = argv.indexOf("--new-rule");
|
|
203
|
+
const ruleIdx = argv.indexOf("--rule");
|
|
204
|
+
const newRuleGiven = newRuleIdx >= 0;
|
|
205
|
+
const ruleGiven = ruleIdx >= 0;
|
|
206
|
+
if (newRuleGiven && ruleGiven)
|
|
207
|
+
return { kind: "usage-error" };
|
|
208
|
+
if (!newRuleGiven && !ruleGiven)
|
|
209
|
+
return { kind: "default" };
|
|
210
|
+
if (newRuleGiven) {
|
|
211
|
+
const id = argv[newRuleIdx + 1];
|
|
212
|
+
if (!id || id.startsWith("--"))
|
|
213
|
+
return { kind: "usage-error" };
|
|
214
|
+
return { kind: "explicit", identity: { mode: "NEW_RULE", ruleId: id } };
|
|
215
|
+
}
|
|
216
|
+
const id = argv[ruleIdx + 1];
|
|
217
|
+
if (!id || id.startsWith("--"))
|
|
218
|
+
return { kind: "usage-error" };
|
|
219
|
+
return { kind: "explicit", identity: { mode: "SUCCESSOR", ruleId: id } };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `mla rules attest --from-observed <observedRuleHash>`: mint the LIVE notes-location version from an
|
|
223
|
+
* R0 observed snapshot (proposal worked attest flow lines 2037-2069). It resolves the EXACT observed
|
|
224
|
+
* snapshot in the active scope (A.3), runs the §2.4 admission gate + conversion (the pure core),
|
|
225
|
+
* resolves the accountable operator from the authenticated session, short-circuits an idempotent
|
|
226
|
+
* re-attest, displays the full normalized payload, selects the attestation method from the terminal /
|
|
227
|
+
* agent flags, and mints (superseding any prior LIVE version) in the A.4 repo's single transaction. It
|
|
228
|
+
* mints the attested DENY ceiling regardless of the runtime deny-admission gates (§10.2): attestation
|
|
229
|
+
* and effective enforcement are separate (lines 2117-2124). A miss at any step mints nothing.
|
|
230
|
+
*/
|
|
231
|
+
async function runRulesAttest(argv, deps = {}) {
|
|
232
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
233
|
+
const err = deps.err ?? ((line) => console.error(line));
|
|
234
|
+
// `--from-code-rule <name>` is a distinct attest source from `--from-observed <hash>`: it mints a LIVE
|
|
235
|
+
// version of a PRODUCT-SHIPPED code rule (e.g. CE0 consult-evidence) rather than an R0 observed snapshot.
|
|
236
|
+
// It is dispatched FIRST and owns the whole invocation; the two sources never combine.
|
|
237
|
+
if (argv.includes("--from-code-rule")) {
|
|
238
|
+
return attestCodeRule(argv, deps, out, err);
|
|
239
|
+
}
|
|
240
|
+
const flagIdx = argv.indexOf("--from-observed");
|
|
241
|
+
const observedRuleHash = flagIdx >= 0 ? argv[flagIdx + 1] : undefined;
|
|
242
|
+
if (!observedRuleHash || observedRuleHash.startsWith("--")) {
|
|
243
|
+
err(ATTEST_USAGE);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
const agentOnUserRequest = argv.includes("--agent-on-user-request");
|
|
247
|
+
const yes = argv.includes("--yes");
|
|
248
|
+
const noPublish = argv.includes("--no-publish");
|
|
249
|
+
const identityChoice = parseAttestIdentity(argv);
|
|
250
|
+
if (identityChoice.kind === "usage-error") {
|
|
251
|
+
err(ATTEST_USAGE);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
const ruleId = identityChoice.kind === "explicit" ? identityChoice.identity.ruleId : attest_notes_location_1.NOTES_LOCATION_RULE_ID;
|
|
255
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
256
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
257
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
258
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
259
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
260
|
+
const store = open(dbPath);
|
|
261
|
+
let stateChanged = false;
|
|
262
|
+
try {
|
|
263
|
+
const resolution = (0, interception_store_1.resolveObservedSnapshotInScope)(store, runtimeScopeId, observedRuleHash);
|
|
264
|
+
if (resolution.kind === "NOT_FOUND") {
|
|
265
|
+
err(`no observed rule with hash ${observedRuleHash} in runtime scope ${runtimeScopeId}: ` +
|
|
266
|
+
`not found, nothing to attest`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
if (resolution.kind === "COLLISION") {
|
|
270
|
+
err(`observed hash ${observedRuleHash} is a collision in scope ${runtimeScopeId}: ` +
|
|
271
|
+
`${resolution.distinctSnapshotCount} distinct snapshots share it; refusing to attest`);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
const conversion = identityChoice.kind === "explicit"
|
|
275
|
+
? (0, attest_notes_location_1.convertForbiddenRootSnapshot)(resolution.observedRuleSnapshot, runtimeScopeId)
|
|
276
|
+
: (0, attest_notes_location_1.convertNotesLocationSnapshot)(resolution.observedRuleSnapshot, runtimeScopeId);
|
|
277
|
+
if (!conversion.admitted) {
|
|
278
|
+
err(`snapshot is not a supported forbidden-root rule (${conversion.reason}): ${conversion.detail}`);
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
const payload = conversion.payload;
|
|
282
|
+
const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
|
|
283
|
+
const operator = resolveOperator();
|
|
284
|
+
if (!operator) {
|
|
285
|
+
err("refusing to attest: not logged in as a human operator (run `mla login`); " +
|
|
286
|
+
"attestation requires an authenticated MLA operator");
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
const canonicalPayloadHash = (0, rule_version_hash_1.ruleVersionHash)(payload);
|
|
290
|
+
// The idempotent re-attest short-circuit is valid ONLY when this attest could legitimately match an
|
|
291
|
+
// existing LIVE version (the notes default or an explicit SUCCESSOR). A NEW_RULE must never no-op on a
|
|
292
|
+
// hash match: an id that is already taken is a COLLISION the mint rejects (P0.55), never a silent reuse.
|
|
293
|
+
const isNewRule = identityChoice.kind === "explicit" && identityChoice.identity.mode === "NEW_RULE";
|
|
294
|
+
const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
|
|
295
|
+
if (!isNewRule && current && current.canonicalPayloadHash === canonicalPayloadHash) {
|
|
296
|
+
out(`already attested: LIVE version ${current.versionId} already carries ` +
|
|
297
|
+
`${canonicalPayloadHash}; no-op`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
out("note: enforcementCeiling is DENY. The live PreToolUse deny is armed: when this version is LIVE and " +
|
|
301
|
+
"the deny-admission gates pass (check `mla doctor`), a VIOLATION is denied on the wire; otherwise it " +
|
|
302
|
+
"degrades to observe-only. Attestation mints the DENY-ceiling authority; effective enforcement is " +
|
|
303
|
+
"the separate runtime decision (§10.2).");
|
|
304
|
+
out(formatAttestPayload(payload, canonicalPayloadHash, ruleId));
|
|
305
|
+
let attestationMethod;
|
|
306
|
+
if (agentOnUserRequest && yes) {
|
|
307
|
+
attestationMethod = "AGENT_ON_USER_REQUEST";
|
|
308
|
+
}
|
|
309
|
+
else if (deps.isInteractive ? deps.isInteractive() : Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
310
|
+
const confirm = deps.confirm ?? defaultConfirm;
|
|
311
|
+
const ok = await confirm(`Attest this notes-location DENY rule for scope ${runtimeScopeId}?`);
|
|
312
|
+
if (!ok) {
|
|
313
|
+
err("attestation not confirmed; nothing minted");
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
attestationMethod = "HUMAN_DIRECT";
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
err("refusing to attest non-interactively without confirmation; pass " +
|
|
320
|
+
"--agent-on-user-request --yes to attest on the operator's explicit instruction");
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
const newVersionId = deps.newVersionId ?? (() => `ver:${(0, ulid_1.ulid)()}`);
|
|
324
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
325
|
+
let outcome;
|
|
326
|
+
try {
|
|
327
|
+
const mintBase = {
|
|
328
|
+
payload,
|
|
329
|
+
observedRuleHash,
|
|
330
|
+
attestedBy: operator.attestedBy,
|
|
331
|
+
attestationMethod,
|
|
332
|
+
versionId: newVersionId(),
|
|
333
|
+
attestedAt: now(),
|
|
334
|
+
};
|
|
335
|
+
outcome =
|
|
336
|
+
identityChoice.kind === "explicit"
|
|
337
|
+
? (0, attest_rule_version_1.mintAttestedRuleVersion)(store, { ...mintBase, identity: identityChoice.identity })
|
|
338
|
+
: (0, attest_notes_location_1.mintAttestedNotesLocationVersion)(store, mintBase);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
// P0.55 identity faults are operator errors, not crashes: a NEW_RULE collision or a SUCCESSOR with
|
|
342
|
+
// no prior LIVE version mints nothing and exits 1 with the writer's exact, actionable message.
|
|
343
|
+
if (e instanceof attest_rule_version_1.RuleIdentityCollisionError || e instanceof local_rule_version_repo_1.NoLiveVersionToSupersedeError) {
|
|
344
|
+
err(e.message);
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
if (outcome.outcome === "SUPERSEDED") {
|
|
350
|
+
out(`SUPERSEDED ${outcome.supersededVersionId} -> MINTED version ${outcome.version.versionId} ` +
|
|
351
|
+
`(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
|
|
352
|
+
stateChanged = true;
|
|
353
|
+
}
|
|
354
|
+
else if (outcome.outcome === "MINTED") {
|
|
355
|
+
out(`MINTED version ${outcome.version.versionId} ` +
|
|
356
|
+
`(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
|
|
357
|
+
stateChanged = true;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
out(`already attested: ${outcome.version.versionId}; no-op`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
365
|
+
}
|
|
366
|
+
// A successful mint changed local truth: best-effort project it to control so the console Rules surface
|
|
367
|
+
// stays in sync without a manual `mla rules publish`. The sync NEVER fails the attest (it already
|
|
368
|
+
// committed locally); a missing workspace / logged-out CLI / unreachable backend is reported, not fatal.
|
|
369
|
+
if (stateChanged) {
|
|
370
|
+
await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
|
|
371
|
+
}
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
/** Render the code-rule's frozen payload IN FULL so the human confirms what identity they are arming. It
|
|
375
|
+
* is RECORD_ONLY by construction (a CE0 forcing function, not a forbidden-root deny), so there is no
|
|
376
|
+
* matcher/effect/ceiling to negotiate: arming changes NO runtime behavior, it only gives the rule's
|
|
377
|
+
* obligations a durable, attested version identity to bind to. */
|
|
378
|
+
function formatCodeRuleAttest(codeRule, runtimeScopeId) {
|
|
379
|
+
return [
|
|
380
|
+
`rule: ${codeRule.ruleId}`,
|
|
381
|
+
`scope: ${runtimeScopeId}`,
|
|
382
|
+
`hash: ${codeRule.canonicalPayloadHash}`,
|
|
383
|
+
`ceiling: RECORD_ONLY (no deny; arming changes no runtime behavior)`,
|
|
384
|
+
`payload: ${codeRule.serializedPayload}`,
|
|
385
|
+
].join("\n");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* `mla rules attest --from-code-rule <name>`: mint the LIVE LocalRuleVersion of a product-shipped code
|
|
389
|
+
* rule (the GAP 3 slice-2 arm that gives the CE0 consult-evidence obligation a durable attested identity to
|
|
390
|
+
* bind to). The logical id is PINNED by the registry (P0.55 is satisfied because no logical-identity choice
|
|
391
|
+
* is being MADE here -- the registry already pinned it), so `--new-rule` / `--rule` and the `--from-observed`
|
|
392
|
+
* source are all rejected as usage errors. NEW_RULE vs SUCCESSOR is auto-derived from store state (a LIVE
|
|
393
|
+
* version present -> SUCCESSOR, else NEW_RULE); a re-attest whose frozen hash matches the LIVE version is an
|
|
394
|
+
* idempotent no-op. The rule is RECORD_ONLY, so arming it is provably inert under the R4 three-class
|
|
395
|
+
* partition (INV-CONFLICT / P0.13): it cannot silently disarm an out-of-family PROHIBIT deny. It reuses the
|
|
396
|
+
* operator resolution, confirmation, and attestation-method selection of the observed arm; a miss at any
|
|
397
|
+
* step mints nothing.
|
|
398
|
+
*/
|
|
399
|
+
async function attestCodeRule(argv, deps, out, err) {
|
|
400
|
+
const flagIdx = argv.indexOf("--from-code-rule");
|
|
401
|
+
const name = flagIdx >= 0 ? argv[flagIdx + 1] : undefined;
|
|
402
|
+
if (!name || name.startsWith("--")) {
|
|
403
|
+
err(CODE_RULE_ATTEST_USAGE);
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
406
|
+
// A code rule's logical id is pinned by the registry; an operator may not choose it, nor mix in the
|
|
407
|
+
// observed source. These are usage errors (exit 2), distinct from a clean miss that mints nothing.
|
|
408
|
+
if (argv.includes("--from-observed") || argv.includes("--new-rule") || argv.includes("--rule")) {
|
|
409
|
+
err(CODE_RULE_ATTEST_USAGE);
|
|
410
|
+
return 2;
|
|
411
|
+
}
|
|
412
|
+
const agentOnUserRequest = argv.includes("--agent-on-user-request");
|
|
413
|
+
const yes = argv.includes("--yes");
|
|
414
|
+
const noPublish = argv.includes("--no-publish");
|
|
415
|
+
const codeRule = (0, code_rule_registry_1.getCodeRule)(name);
|
|
416
|
+
if (!codeRule) {
|
|
417
|
+
err(`no code rule named '${name}' ships in this build; nothing to attest`);
|
|
418
|
+
return 1;
|
|
419
|
+
}
|
|
420
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
421
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
422
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
423
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
424
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
425
|
+
const store = open(dbPath);
|
|
426
|
+
let stateChanged = false;
|
|
427
|
+
try {
|
|
428
|
+
const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
|
|
429
|
+
const operator = resolveOperator();
|
|
430
|
+
if (!operator) {
|
|
431
|
+
err("refusing to attest: not logged in as a human operator (run `mla login`); " +
|
|
432
|
+
"attestation requires an authenticated MLA operator");
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
// The idempotent short-circuit before we display anything or prompt: a re-attest of the same frozen
|
|
436
|
+
// bytes is a clean no-op (the writer would also catch it, but short-circuiting keeps the UX quiet).
|
|
437
|
+
const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, codeRule.ruleId);
|
|
438
|
+
if (current && current.canonicalPayloadHash === codeRule.canonicalPayloadHash) {
|
|
439
|
+
out(`already attested: LIVE version ${current.versionId} already carries ` +
|
|
440
|
+
`${codeRule.canonicalPayloadHash}; no-op`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
out("note: this rule is RECORD_ONLY. Arming it changes NO runtime behavior; it only gives the rule's " +
|
|
444
|
+
"obligations a durable, attested version identity to bind to (the CE0 measurement is identical " +
|
|
445
|
+
"armed or unarmed).");
|
|
446
|
+
out(formatCodeRuleAttest(codeRule, runtimeScopeId));
|
|
447
|
+
let attestationMethod;
|
|
448
|
+
if (agentOnUserRequest && yes) {
|
|
449
|
+
attestationMethod = "AGENT_ON_USER_REQUEST";
|
|
450
|
+
}
|
|
451
|
+
else if (deps.isInteractive ? deps.isInteractive() : Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
452
|
+
const confirm = deps.confirm ?? defaultConfirm;
|
|
453
|
+
const ok = await confirm(`Attest this RECORD_ONLY code rule ${codeRule.ruleId} for scope ${runtimeScopeId}?`);
|
|
454
|
+
if (!ok) {
|
|
455
|
+
err("attestation not confirmed; nothing minted");
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
attestationMethod = "HUMAN_DIRECT";
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
err("refusing to attest non-interactively without confirmation; pass " +
|
|
462
|
+
"--agent-on-user-request --yes to attest on the operator's explicit instruction");
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
// NEW_RULE on a fresh id, SUCCESSOR once a LIVE version exists (a seed bump rotated the frozen hash).
|
|
466
|
+
// The registry pins the id, so this is auto-derived, never an operator choice. A re-arm after a revoke
|
|
467
|
+
// (history present, nothing LIVE) surfaces the writer's RuleIdentityCollisionError honestly.
|
|
468
|
+
const mode = current ? "SUCCESSOR" : "NEW_RULE";
|
|
469
|
+
const newVersionId = deps.newVersionId ?? (() => `ver:${(0, ulid_1.ulid)()}`);
|
|
470
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
471
|
+
let outcome;
|
|
472
|
+
try {
|
|
473
|
+
outcome = (0, attest_code_rule_version_1.mintAttestedCodeRuleVersion)(store, {
|
|
474
|
+
mode,
|
|
475
|
+
codeRule,
|
|
476
|
+
runtimeScopeId,
|
|
477
|
+
attestedBy: operator.attestedBy,
|
|
478
|
+
attestationMethod,
|
|
479
|
+
versionId: newVersionId(),
|
|
480
|
+
attestedAt: now(),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
if (e instanceof attest_rule_version_1.RuleIdentityCollisionError || e instanceof local_rule_version_repo_1.NoLiveVersionToSupersedeError) {
|
|
485
|
+
err(e.message);
|
|
486
|
+
return 1;
|
|
487
|
+
}
|
|
488
|
+
throw e;
|
|
489
|
+
}
|
|
490
|
+
if (outcome.outcome === "SUPERSEDED") {
|
|
491
|
+
out(`SUPERSEDED ${outcome.supersededVersionId} -> MINTED version ${outcome.version.versionId} ` +
|
|
492
|
+
`(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
|
|
493
|
+
stateChanged = true;
|
|
494
|
+
}
|
|
495
|
+
else if (outcome.outcome === "MINTED") {
|
|
496
|
+
out(`MINTED version ${outcome.version.versionId} ` +
|
|
497
|
+
`(${outcome.version.canonicalPayloadHash}) LIVE in scope ${runtimeScopeId}`);
|
|
498
|
+
stateChanged = true;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
out(`already attested: ${outcome.version.versionId}; no-op`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
506
|
+
}
|
|
507
|
+
// A successful arm changed local truth: best-effort project it to control (see runRulesAttest). Sync is
|
|
508
|
+
// never fatal to the attest, which already committed; `--no-publish` opts out for local-only operators.
|
|
509
|
+
if (stateChanged) {
|
|
510
|
+
await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* `mla rules revoke [--rule <ruleId>] [--yes]`: the kill switch (the answer to "what's the harm" of
|
|
516
|
+
* wiring the deny live). It flips the current LIVE version of a (scope, rule) to REVOKED in the A.4
|
|
517
|
+
* repo's single transaction. After this the (scope, rule) has NO LIVE version, so the enforce seam
|
|
518
|
+
* finds NO_LIVE_VERSION and fails open: enforcement stops cleanly without deleting any history. The
|
|
519
|
+
* rule defaults to the notes-location pilot; --rule names another logical rule in this scope (the
|
|
520
|
+
* revoke path is rule-agnostic, it needs only a ruleId, never a payload). Disarming governance must be
|
|
521
|
+
* deliberate, so it confirms exactly like attest: an explicit --yes, or an interactive prompt. Pulling
|
|
522
|
+
* an already-pulled switch (nothing LIVE) is an idempotent success, never an error. Best-effort names
|
|
523
|
+
* the operator who pulled it; it never blocks the disarm on being logged in.
|
|
524
|
+
*/
|
|
525
|
+
async function runRulesRevoke(argv, deps = {}) {
|
|
526
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
527
|
+
const err = deps.err ?? ((line) => console.error(line));
|
|
528
|
+
const ruleFlagIdx = argv.indexOf("--rule");
|
|
529
|
+
const ruleId = ruleFlagIdx >= 0 ? argv[ruleFlagIdx + 1] : attest_notes_location_1.NOTES_LOCATION_RULE_ID;
|
|
530
|
+
if (!ruleId || ruleId.startsWith("--")) {
|
|
531
|
+
err(REVOKE_USAGE);
|
|
532
|
+
return 2;
|
|
533
|
+
}
|
|
534
|
+
const yes = argv.includes("--yes");
|
|
535
|
+
const noPublish = argv.includes("--no-publish");
|
|
536
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
537
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
538
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
539
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
540
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
541
|
+
const store = open(dbPath);
|
|
542
|
+
let stateChanged = false;
|
|
543
|
+
try {
|
|
544
|
+
const current = (0, local_rule_version_repo_1.getLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
|
|
545
|
+
if (!current) {
|
|
546
|
+
out(`nothing LIVE to revoke for rule ${ruleId} in scope ${runtimeScopeId}; already disarmed`);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
out(`about to disarm rule ${ruleId} in scope ${runtimeScopeId}: ` +
|
|
550
|
+
`LIVE version ${current.versionId} (${current.canonicalPayloadHash})`);
|
|
551
|
+
if (!yes) {
|
|
552
|
+
const interactive = deps.isInteractive
|
|
553
|
+
? deps.isInteractive()
|
|
554
|
+
: Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
555
|
+
if (!interactive) {
|
|
556
|
+
err("refusing to disarm non-interactively without confirmation; pass --yes to revoke");
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
const confirm = deps.confirm ?? defaultConfirm;
|
|
560
|
+
const ok = await confirm(`Revoke (disarm) rule ${ruleId} for scope ${runtimeScopeId}? It will then fail open.`);
|
|
561
|
+
if (!ok) {
|
|
562
|
+
err("revoke not confirmed; the rule stays LIVE");
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const revoked = (0, local_rule_version_repo_1.revokeLiveLocalRuleVersion)(store, runtimeScopeId, ruleId);
|
|
567
|
+
const resolveOperator = deps.resolveOperator ?? defaultResolveOperator;
|
|
568
|
+
const operator = resolveOperator();
|
|
569
|
+
const by = operator ? ` by ${operator.displayName ?? operator.attestedBy}` : "";
|
|
570
|
+
out(`REVOKED version ${revoked.versionId} (${revoked.canonicalPayloadHash})${by}; ` +
|
|
571
|
+
`enforcement disarmed for rule ${ruleId} in scope ${runtimeScopeId}; the rule now fails open`);
|
|
572
|
+
stateChanged = true;
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
576
|
+
}
|
|
577
|
+
// A revoke drops the rule from the scope's LIVE set: best-effort re-project so control reconciles the
|
|
578
|
+
// now-absent rule to STALE and it leaves the console Active tab. Never fatal to the local disarm.
|
|
579
|
+
if (stateChanged) {
|
|
580
|
+
await autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err);
|
|
581
|
+
}
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
/** The console renders evidenceJson.statement verbatim as the rule headline; pull the human-readable rule
|
|
585
|
+
* text out of the opaque payload, falling back to the logical id for code rules with no `.text` field. */
|
|
586
|
+
function ruleHeadline(record) {
|
|
587
|
+
try {
|
|
588
|
+
const parsed = JSON.parse(record.rulePayload);
|
|
589
|
+
if (typeof parsed.text === "string" && parsed.text.trim())
|
|
590
|
+
return parsed.text.trim();
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// opaque / non-JSON payload (should not happen for a stored version); fall through to the id.
|
|
594
|
+
}
|
|
595
|
+
return record.ruleId;
|
|
596
|
+
}
|
|
597
|
+
/** The default network seam: POST the batch to control with the session bearer the rest of the CLI uses. */
|
|
598
|
+
function defaultPublish(cfg, body) {
|
|
599
|
+
return (0, http_1.post)(cfg, "/internal/v1/relationship-candidates/publish-rules", body, 15000);
|
|
600
|
+
}
|
|
601
|
+
/** Map one LIVE LocalRuleVersion to its control publish item (headline pulled from the opaque payload). */
|
|
602
|
+
function buildPublishItem(r) {
|
|
603
|
+
return {
|
|
604
|
+
ruleId: r.ruleId,
|
|
605
|
+
versionId: r.versionId,
|
|
606
|
+
text: ruleHeadline(r),
|
|
607
|
+
payloadHash: r.canonicalPayloadHash,
|
|
608
|
+
lifecycleStatus: "LIVE",
|
|
609
|
+
attestedBy: r.attestedBy ?? null,
|
|
610
|
+
attestedAt: r.attestedAt ?? null,
|
|
611
|
+
attestationMethod: r.attestationMethod ?? null,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Project the LIVE attested rules in `runtimeScopeId` to control (the exact batch `mla rules publish`
|
|
616
|
+
* sends). It is the single projection path shared by the explicit publish command and the auto-publish
|
|
617
|
+
* hooks on attest/revoke. It NEVER throws: an unbound workspace / logged-out CLI is reported as `skipped`
|
|
618
|
+
* (loadConfig threw before any network call), an unreachable backend as `failed`; only a real POST that
|
|
619
|
+
* returns is `synced`. The store is opened read-only and always closed; an empty LIVE set still posts so a
|
|
620
|
+
* revoked-to-nothing scope reconciles away on the backend.
|
|
621
|
+
*/
|
|
622
|
+
async function publishLiveRulesForScope(runtimeScopeId, deps) {
|
|
623
|
+
let cfg;
|
|
624
|
+
try {
|
|
625
|
+
cfg = deps.loadConfig ? deps.loadConfig() : (0, config_1.loadWorkspaceConfig)();
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
return { kind: "skipped", reason: e.message };
|
|
629
|
+
}
|
|
630
|
+
const dbPath = deps.storePath ?? (0, evidence_1.defaultCe0StorePath)();
|
|
631
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
632
|
+
const open = deps.openStore ?? ce0_store_1.openCe0Store;
|
|
633
|
+
const store = open(dbPath);
|
|
634
|
+
let live;
|
|
635
|
+
try {
|
|
636
|
+
live = (0, local_rule_version_repo_1.listLiveLocalRuleVersions)(store, runtimeScopeId);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
(0, ce0_store_1.closeCe0Store)(store);
|
|
640
|
+
}
|
|
641
|
+
const rules = live.map(buildPublishItem);
|
|
642
|
+
const body = { workspaceId: cfg.workspaceId, runtimeScopeId, rules };
|
|
643
|
+
const publish = deps.publish ?? defaultPublish;
|
|
644
|
+
try {
|
|
645
|
+
const result = await publish(cfg, body);
|
|
646
|
+
return { kind: "synced", sent: rules.length, workspaceId: cfg.workspaceId, result };
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
return { kind: "failed", reason: e.message };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* After a state-changing attest/revoke, best-effort sync the scope's LIVE rules into control so the console
|
|
654
|
+
* Rules surface stays current without a manual `mla rules publish`. This is the auto-publish hook: the local
|
|
655
|
+
* mutation has ALREADY committed, so a sync miss is reported (skip/warn) but never changes the command's
|
|
656
|
+
* success. `--no-publish` opts out entirely (offline / local-only operators, or a CI run with no workspace).
|
|
657
|
+
*/
|
|
658
|
+
async function autoPublishAfterMutation(runtimeScopeId, noPublish, deps, out, err) {
|
|
659
|
+
if (noPublish) {
|
|
660
|
+
out("note: --no-publish set; skipped the console sync. The change is attested locally; run " +
|
|
661
|
+
"`mla rules publish` to surface it on the console Rules page.");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const outcome = await publishLiveRulesForScope(runtimeScopeId, deps);
|
|
665
|
+
if (outcome.kind === "synced") {
|
|
666
|
+
out(`synced to console: workspace ${outcome.workspaceId} now reflects ${outcome.result.published} ` +
|
|
667
|
+
`published, ${outcome.result.retired} retired (from ${outcome.sent} LIVE rule(s) in this scope).`);
|
|
668
|
+
}
|
|
669
|
+
else if (outcome.kind === "skipped") {
|
|
670
|
+
out(`note: skipped console sync (${outcome.reason}). The change is attested locally; run ` +
|
|
671
|
+
"`mla rules publish` once a workspace is bound to surface it.");
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
err(`warning: console sync failed (${outcome.reason}). The change IS attested locally; run ` +
|
|
675
|
+
"`mla rules publish` to retry the sync.");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** Render the publish outcome as a compact, stable text block. */
|
|
679
|
+
function formatPublishText(runtimeScopeId, workspaceId, sent, result) {
|
|
680
|
+
const lines = [
|
|
681
|
+
`runtime scope: ${runtimeScopeId}`,
|
|
682
|
+
`workspace: ${workspaceId}`,
|
|
683
|
+
`sent ${sent} LIVE rule(s); published ${result.published}, retired ${result.retired}`,
|
|
684
|
+
];
|
|
685
|
+
if (result.items.length > 0) {
|
|
686
|
+
lines.push("");
|
|
687
|
+
for (const it of result.items) {
|
|
688
|
+
lines.push(` ${it.action.padEnd(9)} ${it.ruleId} (${it.candidateId})`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return lines.join("\n");
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* `mla rules publish [--json]`: project the LIVE attested rule versions in the ACTIVE runtime scope into
|
|
695
|
+
* control so they surface on the console Rules page. It is the bridge half of the local-first rules engine:
|
|
696
|
+
* `attest` / `revoke` only ever mutate the local CE0 store, and this command is the one place that pushes
|
|
697
|
+
* that local truth to the backend. It reads EVERY LIVE LocalRuleVersion in the scope, maps each to a publish
|
|
698
|
+
* item (the human-readable headline pulled from the opaque payload), and POSTs the whole set to control,
|
|
699
|
+
* which upserts each as an ACCEPTED workspace-scoped rule-kind candidate (idempotent by workspace + ruleId)
|
|
700
|
+
* and reconciles-by-omission: any rule it published from THIS scope before that is no longer LIVE is driven
|
|
701
|
+
* to STALE so a revoked rule disappears from the Active tab. An empty LIVE set is NOT a no-op: it still posts
|
|
702
|
+
* (with the scope) so the last-revoked rule reconciles away. Read-only on the local store; one network call.
|
|
703
|
+
*/
|
|
704
|
+
async function runRulesPublish(argv, deps = {}) {
|
|
705
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
706
|
+
const err = deps.err ?? ((line) => console.error(line));
|
|
707
|
+
const json = argv.includes("--json");
|
|
708
|
+
const resolveScope = deps.resolveRuntimeScopeId ?? runtime_scope_1.resolveActiveRuntimeScopeId;
|
|
709
|
+
const runtimeScopeId = resolveScope(deps.cwd);
|
|
710
|
+
// The explicit command shares the ONE projection path with the auto-publish hooks, but maps the outcome
|
|
711
|
+
// to its own hard exit codes: an unbound workspace is a usage error (2), a failed POST is a failure (1).
|
|
712
|
+
const outcome = await publishLiveRulesForScope(runtimeScopeId, deps);
|
|
713
|
+
if (outcome.kind === "skipped") {
|
|
714
|
+
err(outcome.reason);
|
|
715
|
+
return 2;
|
|
716
|
+
}
|
|
717
|
+
if (outcome.kind === "failed") {
|
|
718
|
+
err(`failed to publish rules to control: ${outcome.reason}`);
|
|
719
|
+
return 1;
|
|
720
|
+
}
|
|
721
|
+
if (json) {
|
|
722
|
+
out(JSON.stringify({ runtimeScopeId, workspaceId: outcome.workspaceId, ...outcome.result }));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
out(formatPublishText(runtimeScopeId, outcome.workspaceId, outcome.sent, outcome.result));
|
|
726
|
+
}
|
|
727
|
+
return 0;
|
|
728
|
+
}
|