arkaos 2.63.0 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.63.0
1
+ 2.65.0
@@ -19,12 +19,25 @@ SESSION_ID=""
19
19
  TRANSCRIPT_PATH=""
20
20
  STOP_HOOK_ACTIVE=""
21
21
  CWD=""
22
+ EFFORT_LEVEL=""
22
23
  if command -v jq &>/dev/null; then
23
24
  SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
24
25
  TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null)
25
26
  STOP_HOOK_ACTIVE=$(echo "$input" | jq -r '.stop_hook_active // ""' 2>/dev/null)
26
27
  CWD=$(echo "$input" | jq -r '.cwd // ""' 2>/dev/null)
28
+ # PR46 v2.65.0 — Claude Code W19 ships effort.level in hook stdin and
29
+ # $CLAUDE_EFFORT env var. Soft-block checks (kb-cite, meta-tag) only
30
+ # run at high|xhigh; hard enforcement runs regardless.
31
+ EFFORT_LEVEL=$(echo "$input" | jq -r '.effort.level // ""' 2>/dev/null)
27
32
  fi
33
+ # Fallback to env var if stdin didn't carry it
34
+ [ -z "$EFFORT_LEVEL" ] && EFFORT_LEVEL="${CLAUDE_EFFORT:-}"
35
+
36
+ # Telemetry-only signal. Soft-block checks (kb_cite, meta_tag, sycophancy)
37
+ # always run here because they're cheap and feed /arka compliance.
38
+ # What is effort-gated is the NUDGE SURFACING in user-prompt-submit.sh
39
+ # (whether the next turn sees a [arka:suggest] line). Record the level
40
+ # on the telemetry row so we can later analyze suppression rates.
28
41
 
29
42
  # Prevent infinite loops when Stop hook was triggered by its own decision.
30
43
  if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
@@ -60,6 +73,7 @@ SESSION_ID_VAL="$SESSION_ID" \
60
73
  TRANSCRIPT_PATH_VAL="$TRANSCRIPT_PATH" \
61
74
  CWD_VAL="$CWD" \
62
75
  ARKAOS_ROOT_VAL="$ARKAOS_ROOT" \
76
+ EFFORT_LEVEL_VAL="$EFFORT_LEVEL" \
63
77
  python3 - <<'PY' 2>/dev/null
64
78
  import json
65
79
  import os
@@ -223,6 +237,9 @@ entry = {
223
237
  "kb_cite_topic_score": cite_topic_score,
224
238
  "meta_tag_check_passed": meta_passed,
225
239
  "meta_tag_check_reason": meta_reason,
240
+ # PR46 v2.65.0 — Claude Code effort level captured for later analysis
241
+ # of nudge-suppression rates. Unset / unknown values land as "".
242
+ "effort_level": os.environ.get("EFFORT_LEVEL_VAL", ""),
226
243
  "mode": "warn",
227
244
  }
228
245
 
@@ -84,10 +84,25 @@ mkdir -p "$CACHE_DIR" 2>/dev/null
84
84
  # ─── Extract user input from hook JSON ───────────────────────────────────
85
85
  user_input=""
86
86
  SESSION_ID=""
87
+ EFFORT_LEVEL=""
87
88
  if command -v jq &>/dev/null; then
88
89
  user_input=$(echo "$input" | jq -r '.userInput // .message // ""' 2>/dev/null)
89
90
  SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
91
+ # PR46 v2.65.0 — Claude Code W19 ships effort.level in hook stdin.
92
+ # Soft-block nudges (KB-first + meta-tag) are gated by effort: only
93
+ # surfaced at high|xhigh; low/medium skip the nudge to avoid forcing
94
+ # the model to comply with full contracts during cheap exploratory
95
+ # turns. Hard enforcement (PreToolUse flow_enforcer) runs regardless.
96
+ EFFORT_LEVEL=$(echo "$input" | jq -r '.effort.level // ""' 2>/dev/null)
90
97
  fi
98
+ [ -z "$EFFORT_LEVEL" ] && EFFORT_LEVEL="${CLAUDE_EFFORT:-}"
99
+
100
+ # Decide whether soft-block nudges surface to the next turn.
101
+ _ARKA_SURFACE_NUDGES="true"
102
+ case "${EFFORT_LEVEL:-high}" in
103
+ low|medium) _ARKA_SURFACE_NUDGES="false" ;;
104
+ *) _ARKA_SURFACE_NUDGES="true" ;;
105
+ esac
91
106
 
92
107
  # ─── Flow marker cache invalidation (v2 — new turn, reset ALLOW cache) ──
93
108
  # Cheap, non-blocking, runs before Synapse so a stuck Python later cannot
@@ -376,7 +391,7 @@ fi
376
391
  # the suggestion to the model in this turn's additionalContext. One-shot:
377
392
  # the file is deleted after read so the nudge does not repeat across turns.
378
393
  _KB_CITE_NUDGE=""
379
- if [ -n "$SESSION_ID" ]; then
394
+ if [ -n "$SESSION_ID" ] && [ "$_ARKA_SURFACE_NUDGES" = "true" ]; then
380
395
  _CITE_FILE="/tmp/arkaos-cite/${SESSION_ID}.json"
381
396
  if [ -f "$_CITE_FILE" ]; then
382
397
  if command -v jq &>/dev/null; then
@@ -397,7 +412,7 @@ fi
397
412
  # Mirror of the KB citation nudge but for the [arka:meta] one-liner
398
413
  # contract. One-shot; deleted after read.
399
414
  _META_TAG_NUDGE=""
400
- if [ -n "$SESSION_ID" ]; then
415
+ if [ -n "$SESSION_ID" ] && [ "$_ARKA_SURFACE_NUDGES" = "true" ]; then
401
416
  _META_FILE="/tmp/arkaos-meta/${SESSION_ID}.json"
402
417
  if [ -f "$_META_FILE" ]; then
403
418
  if command -v jq &>/dev/null; then
@@ -0,0 +1,163 @@
1
+ // autoMode.hard_deny defaults for Claude Code (PR45 v2.64.0).
2
+ //
3
+ // Claude Code v2.1.131+ shipped `autoMode.hard_deny` — actions that block
4
+ // unconditionally in auto mode regardless of allow rules. Without this,
5
+ // auto mode is structurally unsafe (allowlist alone cannot express
6
+ // "never run this even if a broader allow matches"). PR45 ships a
7
+ // curated default deny list and merges it into ~/.claude/settings.json
8
+ // without clobbering operator-authored entries.
9
+ //
10
+ // Operator extensions live in ~/.arkaos/hard-deny.json — installer
11
+ // reads it on every run and merges into the Claude settings file.
12
+ //
13
+ // Behaviour:
14
+ // - No-op when runtime is not Claude Code
15
+ // - Idempotent: merges by string equality; duplicates are dropped
16
+ // - Atomic write via .tmp + rename
17
+ // - Preserves all other settings.json content untouched
18
+ // - Never raises — failures logged but don't break the installer
19
+
20
+ import {
21
+ existsSync, readFileSync, writeFileSync, renameSync, copyFileSync, mkdirSync,
22
+ } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join, dirname } from "node:path";
25
+
26
+ // Curated default deny list. Each entry follows Claude Code's
27
+ // permission-rule syntax: ToolName(pattern). The patterns are
28
+ // load-bearing — pick conservatively, since auto mode without these
29
+ // rules silently allows them when a broader allow matches.
30
+ export const DEFAULT_HARD_DENY_RULES = [
31
+ // Destructive git operations
32
+ "Bash(git push --force*)",
33
+ "Bash(git push -f*)",
34
+ "Bash(git reset --hard*)",
35
+ "Bash(git clean -fd*)",
36
+ "Bash(git branch -D*)",
37
+
38
+ // Filesystem destruction
39
+ "Bash(rm -rf /*)",
40
+ "Bash(rm -rf ~/*)",
41
+ "Bash(rm -rf ~)",
42
+ "Bash(rm -rf .*)",
43
+
44
+ // Secrets and credential paths
45
+ "Read(~/.ssh/*)",
46
+ "Read(~/.ssh/**)",
47
+ "Read(~/.aws/credentials)",
48
+ "Read(~/.aws/config)",
49
+ "Read(~/.gnupg/*)",
50
+ "Read(~/.gnupg/**)",
51
+ "Read(~/.npmrc)",
52
+ "Read(~/.docker/config.json)",
53
+ "Read(~/.config/gh/*)",
54
+ "Read(/etc/shadow)",
55
+ "Read(/etc/sudoers)",
56
+
57
+ // Writes to credential / config dirs
58
+ "Write(~/.ssh/*)",
59
+ "Write(~/.aws/credentials)",
60
+ "Write(~/.gnupg/*)",
61
+ "Write(~/.npmrc)",
62
+
63
+ // Process / privilege escalation
64
+ "Bash(sudo *)",
65
+ "Bash(su -*)",
66
+ "Bash(chmod 777*)",
67
+ "Bash(curl * | sh*)",
68
+ "Bash(curl * | bash*)",
69
+ "Bash(wget * | sh*)",
70
+ "Bash(wget * | bash*)",
71
+ ];
72
+
73
+
74
+ // Operator extension file. Lines added here are merged into the
75
+ // Claude settings on every install/update.
76
+ const _USER_EXTENSION_FILE = "hard-deny.json";
77
+
78
+
79
+ export function seedAutoModeHardDeny({
80
+ runtime = "claude-code",
81
+ home = homedir(),
82
+ defaults = DEFAULT_HARD_DENY_RULES,
83
+ } = {}) {
84
+ if (runtime !== "claude-code") {
85
+ return { skipped: "runtime-not-claude-code", action: null, count: 0 };
86
+ }
87
+ const settingsPath = join(home, ".claude", "settings.json");
88
+ if (!existsSync(settingsPath)) {
89
+ return { skipped: "claude-settings-not-found", action: null, count: 0 };
90
+ }
91
+ const userExtensions = readUserExtensions(home);
92
+ const merged = mergeUnique(defaults, userExtensions);
93
+ return writeMergedSettings(settingsPath, merged);
94
+ }
95
+
96
+
97
+ function readUserExtensions(home) {
98
+ const path = join(home, ".arkaos", _USER_EXTENSION_FILE);
99
+ if (!existsSync(path)) return [];
100
+ try {
101
+ const data = JSON.parse(readFileSync(path, "utf-8"));
102
+ const rules = Array.isArray(data.hard_deny) ? data.hard_deny : [];
103
+ return rules.filter((r) => typeof r === "string" && r.length > 0);
104
+ } catch {
105
+ return [];
106
+ }
107
+ }
108
+
109
+
110
+ function mergeUnique(a, b) {
111
+ const seen = new Set();
112
+ const merged = [];
113
+ for (const rule of [...a, ...b]) {
114
+ if (!seen.has(rule)) {
115
+ seen.add(rule);
116
+ merged.push(rule);
117
+ }
118
+ }
119
+ return merged;
120
+ }
121
+
122
+
123
+ function writeMergedSettings(settingsPath, mergedRules) {
124
+ let settings;
125
+ try {
126
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
127
+ } catch {
128
+ return { skipped: "settings-not-parseable", action: null, count: 0 };
129
+ }
130
+ if (typeof settings !== "object" || settings === null) {
131
+ return { skipped: "settings-not-object", action: null, count: 0 };
132
+ }
133
+ settings.autoMode = settings.autoMode && typeof settings.autoMode === "object"
134
+ ? settings.autoMode
135
+ : {};
136
+ const existing = Array.isArray(settings.autoMode.hard_deny)
137
+ ? settings.autoMode.hard_deny
138
+ : [];
139
+ // Preserve any operator-authored rules already in settings.json,
140
+ // then merge our defaults on top. Operator wins on duplicates
141
+ // because they appear first in mergeUnique.
142
+ const finalRules = mergeUnique(existing, mergedRules);
143
+ if (sameRules(existing, finalRules)) {
144
+ return { skipped: null, action: "noop", count: finalRules.length };
145
+ }
146
+ settings.autoMode.hard_deny = finalRules;
147
+ // Atomic write
148
+ const tmp = `${settingsPath}.tmp-${process.pid}`;
149
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
150
+ renameSync(tmp, settingsPath);
151
+ return {
152
+ skipped: null,
153
+ action: existing.length === 0 ? "created" : "merged",
154
+ count: finalRules.length,
155
+ };
156
+ }
157
+
158
+
159
+ function sameRules(a, b) {
160
+ if (a.length !== b.length) return false;
161
+ const setA = new Set(a);
162
+ return b.every((r) => setA.has(r));
163
+ }
@@ -311,6 +311,25 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
311
311
  console.log(` Warning: could not scaffold user-data (${err.message})`);
312
312
  }
313
313
 
314
+ // PR45 v2.64.0 — seed autoMode.hard_deny defaults into
315
+ // ~/.claude/settings.json so auto mode blocks destructive actions
316
+ // (git push --force, ~/.ssh reads, rm -rf, sudo, etc.) regardless
317
+ // of allow rules. Preserves operator-authored entries; merges
318
+ // operator extensions from ~/.arkaos/hard-deny.json.
319
+ try {
320
+ const { seedAutoModeHardDeny } = await import("./hard-deny.js");
321
+ const denyResult = seedAutoModeHardDeny({ runtime });
322
+ if (!denyResult.skipped) {
323
+ if (denyResult.action === "created") {
324
+ console.log(` autoMode.hard_deny created (${denyResult.count} rules).`);
325
+ } else if (denyResult.action === "merged") {
326
+ console.log(` autoMode.hard_deny merged (${denyResult.count} rules, operator entries preserved).`);
327
+ }
328
+ }
329
+ } catch (err) {
330
+ console.log(` Warning: could not seed autoMode.hard_deny (${err.message})`);
331
+ }
332
+
314
333
  // PR43 v2.62.0 — auto-install default Claude Code plugins. Only runs
315
334
  // when runtime is Claude Code AND the `claude` CLI is available.
316
335
  // Idempotent: skips plugins already in installed_plugins.json.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.63.0",
3
+ "version": "2.65.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.63.0"
3
+ version = "2.65.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}