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 +1 -1
- package/scripts/lib/activation/activation-self-healing.js +24 -0
- package/scripts/lib/activation/activation-state.js +1 -1
- package/scripts/lib/activation/repair-engine.js +2 -2
- package/scripts/lib/activation/run-entry.js +1 -1
- package/scripts/lib/agent-enforcement.js +27 -0
- package/scripts/lib/agent-security/sensitive-targets.js +94 -0
- package/scripts/lib/brain-pack.js +51 -24
- package/scripts/lib/dashboard.js +1 -1
- package/scripts/lib/hook-apply.js +187 -50
- package/scripts/lib/host-provenance.js +114 -0
- package/scripts/lib/lifecycle-hooks.js +487 -24
- package/scripts/lib/orchestration/index.js +27 -1
- package/scripts/lib/outcome-evidence-stack.js +24 -1
- package/scripts/lib/public-cli.js +44 -8
- package/scripts/lib/runtime-provenance.js +43 -0
- package/scripts/lib/security-risk-classifier.js +59 -2
- package/scripts/lib/security-scan.js +10 -3
- package/scripts/lib/session-context-optimizer.js +45 -3
package/package.json
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
88
|
-
let
|
|
89
|
-
let
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
package/scripts/lib/dashboard.js
CHANGED
|
@@ -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:
|
|
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; } })();
|