avorelo 0.4.0-rc.1 → 0.4.0-rc.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avorelo",
3
- "version": "0.4.0-rc.1",
3
+ "version": "0.4.0-rc.5",
4
4
  "private": false,
5
5
  "avoreloDistributionState": "ready_to_publish",
6
6
  "description": "Avorelo local-first activation, safety, and optimization runtime.",
@@ -33,6 +33,8 @@ const READINESS_CHECKS = Object.freeze([
33
33
  { id: "brain_pack", label: "Brain pack", required: true },
34
34
  { id: "session_context", label: "Session context ready", required: true },
35
35
  { id: "operating_value", label: "Value tracking baseline", required: true },
36
+ { id: "handler_sanity", label: "Handler sanity (structural)", required: true },
37
+ { id: "runtime_sanity", label: "Live runtime verified", required: false },
36
38
  { id: "dashboard_preview", label: "Dashboard ready", required: false },
37
39
  { id: "proof_summary", label: "Proof summary", required: false },
38
40
  { id: "security_baseline", label: "Security checks ready", required: false },
@@ -232,6 +234,28 @@ function checkReadiness(cwd, context) {
232
234
  // security_baseline (non-blocking — intake may not have run yet)
233
235
  checks.security_baseline = safeExists(cwd, ".claude/cco/security");
234
236
 
237
+ // handler_sanity (RC5) — STRUCTURAL behavioral check, required. Runs a benign and a
238
+ // mutating turn through the real handlers and requires the benign turn is NOT
239
+ // self-blocked. This is NOT live-host proof — it cannot show Claude Code loaded the
240
+ // hooks. It only guarantees Avorelo's own handlers won't self-trap a conversation.
241
+ try {
242
+ const { buildHandlerSanityProof } = require("../lifecycle-hooks");
243
+ const proof = buildHandlerSanityProof({});
244
+ checks.handler_sanity = proof.status === "pass";
245
+ } catch {
246
+ checks.handler_sanity = false;
247
+ }
248
+
249
+ // runtime_sanity (RC5) — LIVE-HOST state, NON-blocking. Passed only after a real
250
+ // CLAUDE_CODE_LIVE benign turn; pending at activation time (the customer has not taken
251
+ // a turn yet). Surfaced honestly; never satisfied by simulation/handler/test.
252
+ try {
253
+ const { readRuntimeSanity } = require("../lifecycle-hooks");
254
+ checks.runtime_sanity = readRuntimeSanity(cwd).state === "passed";
255
+ } catch {
256
+ checks.runtime_sanity = false;
257
+ }
258
+
235
259
  // model_routing (from context, non-blocking)
236
260
  checks.model_routing = Boolean(context && context.detectedModelRouting && context.detectedModelRouting.status);
237
261
 
@@ -35,7 +35,7 @@ function defaultActivationState(cwd) {
35
35
  manualFixesRequired: [],
36
36
  nextAction: {
37
37
  label: "Check setup",
38
- command: "npx avorelo@latest activate",
38
+ command: require("../runtime-provenance").versionAwareActivateCommand("activate"),
39
39
  reason: "Activation has not run yet.",
40
40
  blocking: false,
41
41
  },
@@ -39,7 +39,7 @@ function buildContextPack(cwd, context) {
39
39
  commands: {
40
40
  status: "avorelo status --text",
41
41
  audit: "avorelo audit",
42
- activate: "npx avorelo@latest activate",
42
+ activate: require("../runtime-provenance").versionAwareActivateCommand("activate"),
43
43
  },
44
44
  hosts: (context.detectedHosts || [])
45
45
  .filter((host) => host.detected)
@@ -145,7 +145,7 @@ function runRepairEngine(cwd, context, options = {}) {
145
145
  manualFixesRequired.push({
146
146
  id: "run_activation_with_repair",
147
147
  label: "Run activation with repair enabled",
148
- command: "npx avorelo@latest activate --repair",
148
+ command: require("../runtime-provenance").versionAwareActivateCommand("activate --repair"),
149
149
  reason: "Avorelo can recreate missing local state automatically when repair is enabled.",
150
150
  blocking: false,
151
151
  });
@@ -302,7 +302,7 @@ function verifyRunEntry(cwd, options = {}) {
302
302
  checks,
303
303
  missingRequired,
304
304
  advisoryMissing: advisory,
305
- repairCommand: "npx avorelo@latest activate",
305
+ repairCommand: require("../runtime-provenance").versionAwareActivateCommand("activate"),
306
306
  };
307
307
  }
308
308
 
@@ -193,6 +193,9 @@ function getMatchedRules(input = {}, classification, profile) {
193
193
  const unknownTool = reasons.has("UNKNOWN_MCP_TOOL");
194
194
  const writeCapableTool = reasons.has("WRITE_CAPABLE_TOOL");
195
195
  const destructiveCommand = reasons.has("DESTRUCTIVE_COMMAND") || reasons.has("FORCE_PUSH_OR_HISTORY_REWRITE");
196
+ const secretFileRead = reasons.has("SECRET_FILE_READ");
197
+ const broadPermissionChange = reasons.has("BROAD_PERMISSION_CHANGE");
198
+ const writeOutsideProject = reasons.has("WRITE_OUTSIDE_PROJECT");
196
199
  const externalExport = reasons.has("EXTERNAL_REPO_EXPORT");
197
200
  const externalNetwork = reasons.has("EXTERNAL_NETWORK_REQUEST");
198
201
  const localhostVisualQa = actionType === "visual_qa_run" && reasons.has("VISUAL_QA_LOCAL_TARGET");
@@ -309,6 +312,30 @@ function getMatchedRules(input = {}, classification, profile) {
309
312
  });
310
313
  }
311
314
 
315
+ if (secretFileRead) {
316
+ rules.push({
317
+ id: "secret_file_read_block",
318
+ outcome: "block",
319
+ summary: "Reading secret-bearing files (.env, keys, credentials) is blocked.",
320
+ });
321
+ }
322
+
323
+ if (writeOutsideProject) {
324
+ rules.push({
325
+ id: "write_outside_project_block",
326
+ outcome: "block",
327
+ summary: "Writes outside the project root are blocked.",
328
+ });
329
+ }
330
+
331
+ if (broadPermissionChange) {
332
+ rules.push({
333
+ id: "broad_permission_change_guard",
334
+ outcome: "approval_required",
335
+ summary: "Broad or recursive permission changes require explicit approval.",
336
+ });
337
+ }
338
+
312
339
  if (externalExport) {
313
340
  rules.push({
314
341
  id: "external_export_guard",
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ // Shared detection for secret-bearing file targets, secret-file reads,
4
+ // and broad permission changes. Used by both the live lifecycle PreToolUse
5
+ // hook and the guard/agent-enforcement CLI path so the two surfaces cannot drift.
6
+
7
+ function normalize(value) {
8
+ return String(value || "").replace(/\\/g, "/").trim();
9
+ }
10
+
11
+ // Secret-bearing file patterns (env files, package/registry auth, ssh/tls keys, secret bundles).
12
+ const SECRET_FILE_PATTERNS = Object.freeze([
13
+ /(^|\/)\.env(\.[A-Za-z0-9._-]+)?$/i,
14
+ /(^|\/)\.npmrc$/i,
15
+ /(^|\/)\.pypirc$/i,
16
+ /(^|\/)\.netrc$/i,
17
+ /(^|\/)id_rsa(\.pub)?$/i,
18
+ /(^|\/)id_ed25519(\.pub)?$/i,
19
+ /(^|\/)id_dsa(\.pub)?$/i,
20
+ /(^|\/)id_ecdsa(\.pub)?$/i,
21
+ /\.pem$/i,
22
+ /\.key$/i,
23
+ /\.p12$/i,
24
+ /\.pfx$/i,
25
+ /(^|\/)secrets?(\.[A-Za-z0-9._-]+)?$/i,
26
+ /(^|\/)credentials?(\.[A-Za-z0-9._-]+)?$/i,
27
+ /(^|\/)\.ssh(\/|$)/i,
28
+ /(^|\/)\.aws(\/|$)/i,
29
+ ]);
30
+
31
+ function isSecretFilePath(target) {
32
+ const value = normalize(target);
33
+ if (!value) return false;
34
+ return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(value));
35
+ }
36
+
37
+ // Shell verbs that read/print file contents (POSIX + PowerShell/Windows).
38
+ const READ_VERB = /\b(cat|type|more|less|head|tail|nl|strings|xxd|od|hexdump|base64|grep|egrep|fgrep|rg|ag|findstr|sort|uniq|awk|sed|Get-Content|gc|Select-String|sls)\b/i;
39
+
40
+ // A token inside a command that looks like a secret file path.
41
+ const SECRET_TOKEN = /(?:^|[\s"'=(])(?:[^\s"'|;&()]*\/)?(?:\.env(?:\.[A-Za-z0-9._-]+)?|\.npmrc|\.pypirc|\.netrc|id_rsa|id_ed25519|id_dsa|id_ecdsa|[^\s"'|;&()]*\.pem|[^\s"'|;&()]*\.key|[^\s"'|;&()]*\.p12|[^\s"'|;&()]*\.pfx|secrets?(?:\.[A-Za-z0-9._-]+)?|credentials?(?:\.[A-Za-z0-9._-]+)?)(?=$|[\s"'|;&)])/i;
42
+
43
+ function detectSecretFileRead(command) {
44
+ const text = String(command || "");
45
+ if (!text.trim()) return null;
46
+ if (!READ_VERB.test(text)) return null;
47
+ if (!SECRET_TOKEN.test(text)) return null;
48
+ return {
49
+ code: "SECRET_FILE_READ",
50
+ message: "Reading secret-bearing files (.env, keys, credentials) is blocked.",
51
+ };
52
+ }
53
+
54
+ // Broad / recursive permission changes: chmod 777, recursive chmod, or broad Windows ACL grants.
55
+ function detectBroadPermissionChange(command) {
56
+ const text = String(command || "");
57
+ if (!text.trim()) return null;
58
+
59
+ const chmod = /\bchmod\b/i.test(text);
60
+ const recursive = /\b(-R|--recursive)\b/i.test(text) || /\bchmod\b[^\n]*\s-\w*R/i.test(text);
61
+ const worldWritable = /\b(777|666|a\+rwx|ugo\+rwx|o\+w|a\+w)\b/i.test(text);
62
+ const broadTarget = /\bchmod\b[^\n]*\s(\.|\/|\*|~|\$HOME)(\s|$)/i.test(text);
63
+
64
+ if (chmod && worldWritable && (recursive || broadTarget)) {
65
+ return {
66
+ code: "BROAD_PERMISSION_CHANGE",
67
+ severity: "high",
68
+ message: "Broad recursive permission change (chmod 777 across the repo) is blocked.",
69
+ };
70
+ }
71
+ if (chmod && worldWritable) {
72
+ return {
73
+ code: "BROAD_PERMISSION_CHANGE",
74
+ severity: "medium",
75
+ message: "World-writable chmod is unsafe. Prefer least privilege.",
76
+ };
77
+ }
78
+ // Windows broad ACL grant to Everyone / full control across a tree.
79
+ if (/\bicacls\b/i.test(text) && /\/grant\b/i.test(text) && /(Everyone|Users):\(?[^)]*F/i.test(text) && /\/t\b/i.test(text)) {
80
+ return {
81
+ code: "BROAD_PERMISSION_CHANGE",
82
+ severity: "high",
83
+ message: "Broad recursive Windows ACL grant is blocked.",
84
+ };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ module.exports = {
90
+ SECRET_FILE_PATTERNS,
91
+ isSecretFilePath,
92
+ detectSecretFileRead,
93
+ detectBroadPermissionChange,
94
+ };
@@ -80,42 +80,68 @@ function readLatestDogfoodSignals(cwd) {
80
80
 
81
81
  // ── Project builder ───────────────────────────────────────────────────────────
82
82
 
83
+ function extractClaudeMdBullets(content, headingRegex) {
84
+ const lines = String(content || "").split("\n");
85
+ const out = [];
86
+ let capturing = false;
87
+ for (const line of lines) {
88
+ if (/^#{1,6}\s/.test(line)) {
89
+ if (capturing) break; // a new heading ends the captured section
90
+ capturing = headingRegex.test(line);
91
+ continue;
92
+ }
93
+ if (capturing) {
94
+ const m = line.match(/^\s*[-*]\s+(.*)$/);
95
+ if (m && m[1].trim()) out.push(m[1].trim().slice(0, 120));
96
+ }
97
+ }
98
+ return out.slice(0, 8);
99
+ }
100
+
101
+ // Derive project facts from real evidence (CLAUDE.md, package.json, work-control).
102
+ // Never inject Avorelo's own roadmap/architecture into an unrelated project.
83
103
  function buildProject(cwd, pkg) {
84
104
  const name = pkg?.name || path.basename(cwd);
85
105
  const productName = pkg?.productName || pkg?.name || name;
86
106
 
87
- // Try to read CLAUDE.md for product direction hints
88
- let productDirection = "Local AI coding optimization platform.";
89
- let architecturePrinciples = [
90
- "local-first, no mandatory cloud",
91
- "deterministic, no LLM calls in core logic",
92
- "compact receipts, not huge JSON",
93
- "capability-router pattern: one router, no duplicates",
94
- "evidence-backed decisions",
95
- ];
96
- let nonGoals = [
97
- "AI Workspace Registry (deferred)",
98
- "Browser Proof (deferred)",
99
- "Workflow Intelligence Radar (deferred)",
100
- "Full model gateway (deferred)",
101
- "Pricing / website (deferred)",
102
- ];
107
+ let productDirection = "";
108
+ let architecturePrinciples = [];
109
+ let nonGoals = [];
110
+ let evidenceBasis = "none";
103
111
 
112
+ let claudeMd = "";
104
113
  try {
105
114
  const claudeMdPath = path.join(cwd, "CLAUDE.md");
106
- if (fs.existsSync(claudeMdPath)) {
107
- const content = fs.readFileSync(claudeMdPath, "utf8");
108
- // Extract first non-empty line after # heading as direction hint
109
- const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#")).slice(0, 3);
110
- if (lines.length) productDirection = lines[0].slice(0, 200);
111
- }
115
+ if (fs.existsSync(claudeMdPath)) claudeMd = fs.readFileSync(claudeMdPath, "utf8");
112
116
  } catch {}
113
117
 
114
- // Detect current roadmap stage from work-control or package version
118
+ if (claudeMd) {
119
+ evidenceBasis = "claude_md";
120
+ const lines = claudeMd
121
+ .split("\n")
122
+ .map((l) => l.trim())
123
+ .filter((l) => l && !l.startsWith("#") && !l.startsWith("<!--") && !l.startsWith("-->") && !l.startsWith("<"))
124
+ .slice(0, 5);
125
+ if (lines.length) productDirection = lines[0].slice(0, 200);
126
+ architecturePrinciples = extractClaudeMdBullets(claudeMd, /(architecture|convention|principle)/i);
127
+ nonGoals = extractClaudeMdBullets(claudeMd, /(non[- ]?goal|boundaries|out of scope|do not)/i);
128
+ }
129
+
130
+ if (!productDirection) {
131
+ if (pkg?.description) {
132
+ productDirection = String(pkg.description).slice(0, 200);
133
+ if (evidenceBasis === "none") evidenceBasis = "package_json";
134
+ } else {
135
+ productDirection = "No project-specific direction recorded yet.";
136
+ }
137
+ }
138
+
115
139
  const wcReceipt = readWorkControlForBrainPack(cwd);
116
140
  const currentRoadmapStage = wcReceipt?.task?.text
117
141
  ? String(wcReceipt.task.text).slice(0, 100)
118
- : "Context Intelligence Completion v1";
142
+ : pkg?.version
143
+ ? `v${pkg.version}`
144
+ : "No project-specific stage recorded yet.";
119
145
 
120
146
  return {
121
147
  name: redactIfSecret(name),
@@ -125,6 +151,7 @@ function buildProject(cwd, pkg) {
125
151
  productDirection,
126
152
  nonGoals,
127
153
  architecturePrinciples,
154
+ evidenceBasis,
128
155
  };
129
156
  }
130
157
 
@@ -711,7 +711,7 @@ function buildStatusDashboard(cwd, options = {}) {
711
711
  const scopeRepair = (() => { try { return buildScopeRepairSurface(cwd); } catch { return null; } })();
712
712
  const nextRunContinuity = (() => { try { return buildNextRunContinuitySurface(cwd, { buildIfMissing: false }); } catch { return null; } })();
713
713
  const dataPermissions = (() => { try { return buildDataPermissionsGuardSurface(cwd, { buildIfMissing: false }); } catch { return null; } })();
714
- const deploymentEnvironment = (() => { try { return buildDeploymentEnvironmentGuardSurface(cwd, { buildIfMissing: false }); } catch { return null; } })();
714
+ const deploymentEnvironment = (() => { try { return buildDeploymentEnvironmentGuardSurface(cwd, { buildIfMissing: true }); } catch { return null; } })();
715
715
  const supplyChainDependency = (() => { try { return buildSupplyChainDependencyGuardSurface(cwd, { buildIfMissing: false }); } catch { return null; } })();
716
716
  const observabilityRecovery = (() => { try { return buildObservabilityRecoveryGuardSurface(cwd, { buildIfMissing: false }); } catch { return null; } })();
717
717
  const costAbuse = (() => { try { return buildCostAbuseGuardSurface(cwd, { buildIfMissing: false }); } catch { return null; } })();