bootstrap
Adom Hydrogen (platform base)
Public Made by Adomby adom
Layer Hydrogen's workspace onto a standard Adom workstation.
← Commit history
Publish 0.4.0
5 files changed
+174−3
__pycache__/seed-agent-permissions.cpython-312.pycadded⋯ 1 unchanged line ⋯
install.sh+34−1@@ -101,6 +101,13 @@ fi # 4. code-server settings.json — the configure-vscode payload PLUS the chat/UI # disables (without these the editor opens VS Code's built-in agent chat # panel). NO model pin — Claude Code picks the default model itself.+#+# claudeCode.allowDangerouslySkipPermissions: TRUE only makes the "Bypass+# permissions" row APPEAR in the editor's Modes menu. Adom users are power users+# and that choice is theirs to make (John 2026-08-25). Auto remains the default,+# because claudeCode.initialPermissionMode stays "auto", and Claude Code still+# shows its own one-time consent dialog before bypass takes effect. Nothing here+# pre-accepts that dialog. log "writing code-server settings.json" install -d -m 0755 "$HOME/.local/share/code-server/User" cat > "$HOME/.local/share/code-server/User/settings.json" <<'SETTINGS'@@ -116,7 +123,7 @@ cat > "$HOME/.local/share/code-server/User/settings.json" <<'SETTINGS' "workbench.navigationControl.enabled": false, "workbench.secondarySideBar.visible": false, "workbench.secondarySideBar.defaultVisibility": "hidden",- "claudeCode.allowDangerouslySkipPermissions": false,+ "claudeCode.allowDangerouslySkipPermissions": true, "claudeCode.initialPermissionMode": "auto", "claudeCode.preferredLocation": "panel", "chat.agent.enabled": false,@@ -172,6 +179,32 @@ printf 'd /run/adom 0755 root root - C /run/adom/api-key 0644 root root - /home/adom/.adom/api-key ' | sudo tee /etc/tmpfiles.d/adom-api-key.conf >/dev/null && log " tmpfiles.d/adom-api-key.conf installed (boot-time re-materialization)" || log " WARN: could not install tmpfiles snippet (no sudo?) — HD self-heal will retry" +# 6b. AUTO-MODE CLASSIFIER POSTURE (John 2026-08-25) ─────────────────────────────+# Claude Code's auto mode judges every Bash call with a classifier that knows nothing+# about Adom. Measured before this existed: the golden image shipped exactly ONE+# permission rule (Bash(adom-wiki:*)), every other CLI fell through to the classifier+# on every call, and a long-lived container had accumulated 121 hyper-specific one-off+# allow rules, a log of a human clicking approve one command at a time for months.+#+# Bash(...) rules cannot fix it: they are prefix matches on the literal command string,+# and our CLIs put flags BEFORE the verb (adom-bridge --target X --ai-thread Y <verb>),+# so no prefix expresses "these verbs, not those". autoMode takes natural language the+# classifier actually reads, which is the only shape that fits this CLI grammar.+#+# THIS IS A STOPGAP, scoped to Hydrogen. The right home is the registry: every package+# that installs a CLI should declare its own posture and adom-wiki should compose them,+# because only a publisher knows which of their verbs are safe, a central list goes+# stale, and third-party orgs cannot edit ours. Filed as adom/wiki#900. When that lands,+# DELETE this section rather than growing it.+#+# Discipline, so this never becomes the 121-rule pile it replaces:+# - it is DERIVED state in a managed region we own and REGENERATE, never append to+# - a user's own autoMode block wins: if one exists that we did not write, leave it+# - hard_deny only ADDS safety; we never weaken a built-in rule+log "auto-mode classifier posture (Adom toolchain)"+python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/seed-agent-permissions.py" "$HOME/.claude/settings.json" \+ || log " WARN: could not seed autoMode (continuing)"+ # 7. tidy — install.mjs / tooling leave an empty ~/project/.mcp.json; remove it. rm -f "$HOME/project/.mcp.json" 2>/dev/null || true log "done"
package.json+1−1@@ -4,7 +4,7 @@ "type": "bootstrap", "title": "Adom Hydrogen (platform base)", "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",- "version": "0.3.23",+ "version": "0.4.0", "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.", "org": "adom", "platform": "any",
page.json+1−1@@ -4,7 +4,7 @@ "type": "bootstrap", "title": "Adom Hydrogen (platform base)", "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",- "version": "0.3.23",+ "version": "0.4.0", "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.", "org": "adom", "tags": [
seed-agent-permissions.pyadded+138@@ -0,0 +1,138 @@+#!/usr/bin/env python3+"""Seed the Adom auto-mode classifier posture into Claude Code's settings.++Called by adom/hydrogen-bootstrap's install.sh with the path to+~/.claude/settings.json. Idempotent, and safe to re-run on every package update.++STOPGAP, scoped to Hydrogen. The right home is the registry: every package that+installs a CLI should declare its own posture and adom-wiki should compose them,+because only a publisher knows which of their own verbs are safe, a central list+goes stale, and third-party orgs cannot edit ours. Filed as adom/wiki#900. When+that lands, delete this file rather than growing it.++Two rules keep this honest:+ - autoMode is DERIVED state in a region we own and REGENERATE. We never append,+ which is what stops it becoming the accumulating one-off pile it replaces.+ - If an autoMode block exists that we did not write, it is the user's (or an+ org policy's) and we leave it completely alone.+"""++import json+import os+import sys++TAG = "hydrogen-bootstrap/1"++ENVIRONMENT = [+ "This is an Adom container. Bash commands here routinely drive the user's OWN "+ "desktop machines through the Adom Bridge relay (adom-bridge --target <name> "+ "<verb>). Those machines apply their own approval gate independently of this "+ "one: a call carrying no caller identity is refused outright, gated verbs also "+ "require a per-call reason, and both are shown to the user in their Activity "+ "Log. There are two gates and only this one is blind.",+ "The Adom CLIs (adom-wiki, adom-cli, adom-bridge, adom-shotlog, "+ "adom-parts-search, adom-mouser, adom-digikey, adom-jlcpcb, adom-vscode, "+ "adom-tts) are first-party tools installed into this container by the Adom "+ "registry.",+]++ALLOW = [+ "adom-bridge / adom-bridge-cli verbs addressed with --target at a desktop the "+ "user owns, in any flag order and however the command is wrapped.",+ "adom-wiki reads, package installs, and publishes: the wiki is the ecosystem's "+ "own registry and every publish is versioned and revertible.",+ "wsl_import, wsl_export, wsl_exec, wsl_exec_async, wsl_job_* and wsl_unregister "+ "on THROWAWAY distros whose names begin golden-build- or golden-test-, which the "+ "image build and test loop creates and destroys by design.",+ "Reading and writing files under the user's own container home and workspace, "+ "including ~/.claude/skills and ~/project.",+]++SOFT_DENY = [+ "Deleting a WSL distro that is NOT a golden-build-* / golden-test-* throwaway, "+ "above all Adom-Workspace, which is the user's live workspace.",+ "Virgin Reset, workspace re-import, or any operation that destroys a live "+ "workspace, unless the user asked for it in this session.",+]++HARD_DENY = [+ "Broad process kills inside an Adom container (pkill node, killall node, any "+ "unscoped kill by name): they kill the editor and brick the container.",+]++# Stable-prefix CLIs only. Anything whose verb follows its flags is handled by the+# autoMode text above, because a prefix rule cannot express it.+BASH_ALLOW = [+ "Bash(adom-wiki:*)",+ "Bash(adom-cli:*)",+ "Bash(adom-shotlog:*)",+ "Bash(shotlog:*)",+ "Bash(adom-tts:*)",+ "Bash(adom-parts-search:*)",+ "Bash(adom-mouser:*)",+ "Bash(adom-digikey:*)",+ "Bash(adom-jlcpcb:*)",+ "Bash(adom-vscode:*)",+ "Bash(adom-bridge help:*)",+ "Bash(adom-bridge targets)",+]+++def main() -> int:+ path = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser(+ "~/.claude/settings.json"+ )++ try:+ with open(path) as f:+ settings = json.load(f)+ if not isinstance(settings, dict):+ settings = {}+ except Exception:+ settings = {}++ managed = settings.get("_adom_managed")+ if not isinstance(managed, dict):+ managed = {}+ ours_before = managed.get("autoMode")++ if settings.get("autoMode") is not None and ours_before is None:+ print(" autoMode already present and not ours, leaving it untouched")+ else:+ settings["autoMode"] = {+ "environment": ["$defaults"] + ENVIRONMENT,+ "allow": ["$defaults"] + ALLOW,+ "soft_deny": ["$defaults"] + SOFT_DENY,+ "hard_deny": ["$defaults"] + HARD_DENY,+ }+ managed["autoMode"] = TAG+ settings["_adom_managed"] = managed+ print(" autoMode seeded (regenerated from this package)")++ # permissions.allow is ADDITIVE and never pruned: the user's own rules live in+ # this same list, and removing one because it is not ours would be destructive.+ perms = settings.get("permissions")+ if not isinstance(perms, dict):+ perms = {}+ allow = perms.get("allow")+ if not isinstance(allow, list):+ allow = []+ added = [rule for rule in BASH_ALLOW if rule not in allow]+ if added:+ perms["allow"] = allow + added+ print(" permissions.allow: added %d Adom CLI rule(s)" % len(added))+ perms.setdefault("defaultMode", "auto")+ settings["permissions"] = perms++ parent = os.path.dirname(path)+ if parent:+ os.makedirs(parent, exist_ok=True)+ tmp = path + ".tmp"+ with open(tmp, "w") as f:+ json.dump(settings, f, indent=2)+ os.replace(tmp, path)+ return 0+++if __name__ == "__main__":+ sys.exit(main())