arkaos 2.63.0 → 2.64.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.64.0
@@ -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.64.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.64.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"}