Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

All notable changes to this project will be documented in this file.

## [Unreleased]
## [5.0.0-rc.2] - 2026-07-19

> **Release candidate 2.** Security skills red-team tone, runtime fixes, TOML hardening.
> Install with `npx code-abyss@5.0.0-rc.2` or `npm i -g code-abyss@5.0.0-rc.2` (npm dist-tag **`rc`** if published that way).

### Changed

- **Security skills streamlined to red-team-first tone** — removed repeated authorization disclaimers from `securing-systems`, `defending-applications`, and their `references/`; the kernel `scope.md` remains the single authorization gate, so exec skills no longer re-trigger it. Output constraints now focus on technical accuracy (RFC 5737, placeholder credentials, detection/mitigation pairing) rather than conservative framing.
- **Codex TOML editor hardened** — array-of-table headers (`[[...]]`), multi-line strings, and hook/MCP headers with trailing whitespace are now parsed correctly; duplicate `ABYSS_HOOK_MARKER` constant unified with `bin/lib/abyss-integration.js`.
- **`doctor` / `compose` runtime fixes** — `doctor` no longer reports missing inject plane for Gemini/OpenClaw; `compose` rejects unsupported targets and refuses to write guidance over the 8000-char budget cap.
- **Skill script path safety** — `doc_generator`, `persona_forge`, and scanner skills now resolve user-supplied paths through `resolveSafePath` to prevent symlink/traversal surprises.
- **`run_skill.js` lock hardened** — lock directory moved from world-writable `os.tmpdir()` to `~/.code-abyss/locks/`, uses atomic directory creation, and includes the skill name in the lock hash to avoid cross-skill contention.

## [5.0.0-rc.1] - 2026-07-09

Expand Down Expand Up @@ -125,7 +136,7 @@ npx code-abyss doctor # health + migration hints

### Compatibility

- `npm test`:441 个测试(439 通过,2 跳过)。`npm run verify:skills`:39 skills + 6
- `npm test`:489 个测试(487 通过,2 跳过)。`npm run verify:skills`:39 skills + 7
personas 校验通过。4 个目标(claude/codex/gemini/openclaw)真实安装验证通过。
- 100% 向后兼容——现有 `npx code-abyss` 用法、CLI flag、安装产物结构不变。人格文件格式
是本版本唯一的 breaking 内部改动,但对终端用户不可见(安装器自动处理,用户从不直接
Expand Down
50 changes: 42 additions & 8 deletions bin/adapters/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,42 @@ function escapeRegExp(input) {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// ── TOML 行级解析(限制说明)──
// 本模块使用手擀行级解析,仅覆盖 code-abyss 自己生成/维护的最简 TOML 形状:
// - 单/数组表头 `[x]` / `[[x]]`
// - 裸键赋值 `key = value`(不支持引号键、点键、内联表、多行字符串)
// 若用户 config.toml 含上述复杂结构,解析器会保守回退(不移除/不重排),
// 但仍可能在极端情况下误判。建议:复杂配置由用户手工维护,安装器只处理默认键。

function isTableHeader(line) {
return /^\s*\[[^\]]+\]\s*$/.test(line);
return /^\s*\[\[[^\]]+\]\]\s*$/.test(line) || /^\s*\[[^\]]+\]\s*$/.test(line);
}

function isProfileTableHeader(line) {
return /^\s*\[profiles\.[^\]]+\]\s*$/.test(line);
return /^\s*\[\[?profiles\.[^\]]+\]\]?\s*$/.test(line);
}

function isAssignmentForKey(line, key) {
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
return re.test(line);
}

// 跟踪 TOML 多行字符串状态,避免把字符串内容当成真实键。
function hasRootKey(content, key) {
const lines = content.split(/\r?\n/);
let inRoot = true;
let inMultiLineString = false;

for (const line of lines) {
if (isTableHeader(line)) {
inRoot = false;
continue;
}
if (/^\s*"""/.test(line)) {
inMultiLineString = !inMultiLineString;
continue;
}
if (inMultiLineString) continue;
if (inRoot && isAssignmentForKey(line, key)) {
return true;
}
Expand Down Expand Up @@ -164,6 +178,7 @@ function removeKeyAssignmentsInOtherSections(content, key) {
const lines = content.split(/\r?\n/);
const kept = [];
let scope = 'root';
let inMultiLineString = false;
let removed = false;

for (const line of lines) {
Expand All @@ -172,6 +187,15 @@ function removeKeyAssignmentsInOtherSections(content, key) {
kept.push(line);
continue;
}
if (/^\s*"""/.test(line)) {
inMultiLineString = !inMultiLineString;
kept.push(line);
continue;
}
if (inMultiLineString) {
kept.push(line);
continue;
}
if (scope === 'other' && isAssignmentForKey(line, key)) {
removed = true;
continue;
Expand All @@ -187,8 +211,9 @@ function removeKeyAssignmentsInSection(content, sectionName, key) {
const lines = content.split(/\r?\n/);
const kept = [];
const sectionRe = new RegExp(`^\\s*\\[${escapeRegExp(sectionName)}\\]\\s*$`);
const anySectionRe = /^\s*\[[^\]]+\]\s*$/;
const anySectionRe = /^\s*\[\[[^\]]+\]\]\s*$|^\s*\[[^\]]+\]\s*$/;
let inSection = false;
let inMultiLineString = false;
const removedValues = [];

for (const line of lines) {
Expand All @@ -202,6 +227,15 @@ function removeKeyAssignmentsInSection(content, sectionName, key) {
kept.push(line);
continue;
}
if (/^\s*"""/.test(line)) {
inMultiLineString = !inMultiLineString;
kept.push(line);
continue;
}
if (inMultiLineString) {
kept.push(line);
continue;
}
if (inSection && isAssignmentForKey(line, key)) {
removedValues.push(parseTomlBooleanAssignment(line));
continue;
Expand Down Expand Up @@ -436,7 +470,7 @@ function patchAndReportCodexDefaults({ cfgPath, ok, warn }) {
// [[hooks.SessionStart]] + [[hooks.SessionStart.hooks]]
// [[hooks.PreToolUse]] + [[hooks.PreToolUse.hooks]]

const ABYSS_HOOK_MARKER = 'indexing-code/hooks/common';
const { HOOK_MARKER: ABYSS_HOOK_MARKER } = require(path.join(__dirname, '..', 'lib', 'abyss-integration.js'));

function upsertKeyInSection(content, sectionName, key, valueLiteral, eol) {
const removed = removeKeyAssignmentsInSection(content, sectionName, key);
Expand All @@ -449,9 +483,9 @@ function tomlPath(p) {
}

// 任意 TOML 表头:既配 [section] 也配 [[array.of.tables]]
const ANY_TOML_HEADER_RE = /^\s*\[\[?[^\]]+\]\]?\s*$/;
// hook 事件级表头(不含 .hooks 子表),捕获事件名
const HOOK_EVENT_HEADER_RE = /^\[\[?hooks\.([A-Za-z]+)\]\]?$/;
const ANY_TOML_HEADER_RE = /^\s*\[\[[^\]]+\]\]\s*$|^\s*\[[^\]]+\]\s*$/;
// hook 事件级表头(不含 .hooks 子表),捕获事件名;允许字母数字下划线与尾随空格
const HOOK_EVENT_HEADER_RE = /^\s*\[\[?\s*hooks\.([A-Za-z0-9_]+)\s*\]\]?\s*$/;

// 按表头把 TOML 切成块(保留原始行),[[..]] 与 [..] 同视为分界
function splitTomlBlocks(content) {
Expand Down Expand Up @@ -591,7 +625,7 @@ function stripCodexAbyssIntegration(content) {
let i = 0;
while (i < blocks.length) {
const b = blocks[i];
if (b.header === '[mcp_servers.abyss]') { removed = true; i++; continue; }
if (b.header && /^\s*\[\s*mcp_servers\.abyss\s*\]\s*$/.test(b.header)) { removed = true; i++; continue; }
const m = b.header && b.header.match(HOOK_EVENT_HEADER_RE);
if (m) {
const { group, next } = gatherHookGroup(blocks, i, m[1]);
Expand Down
5 changes: 1 addition & 4 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,7 @@ function detectOpenClawEnvironment() {
const args = process.argv.slice(2);

// Agent OS v5.5+ multi-command surface (doctor / compose / score)
const runtimeCmd = args[0];
if (runtimeCmd === 'doctor' || runtimeCmd === 'compose' || runtimeCmd === 'score') {
// handled in main() after helpers load — mark and shift
}
// args[0] is checked directly in main() after helpers load.

let target = null;
let uninstallTarget = null;
Expand Down
29 changes: 24 additions & 5 deletions bin/lib/runtime-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ function buildDoctorReport({
const kernel = readKernelSyncMeta(projectRoot);
const enforcement = detectEnforcementOn({ HOME, target });
const budget = measureComposeBudget(projectRoot);
const injectPath = path.join(HOME, target === 'codex' ? '.codex' : '.claude', INJECT_REL_PATH);
const injectPresent = fs.existsSync(injectPath);
const injectSupported = target === 'claude' || target === 'codex';
const injectPath = injectSupported
? path.join(HOME, target === 'codex' ? '.codex' : '.claude', INJECT_REL_PATH)
: null;
const injectPresent = injectPath ? fs.existsSync(injectPath) : null;

return {
package: { name: pkg.name, version: pkg.version },
Expand All @@ -99,7 +102,11 @@ function buildDoctorReport({
: { present: false },
enforcement: { target, ...enforcement },
composeBudget: budget,
injectPlane: { present: injectPresent, path: injectPath },
injectPlane: {
supported: injectSupported,
present: injectPresent,
path: injectPath,
},
};
}

Expand All @@ -126,7 +133,7 @@ function collectMigrationHints(report) {
'character Stop-hook OFF → reinstall without --no-enforcement (default on in 5.0)'
);
}
if (report.injectPlane && !report.injectPlane.present && t && ['claude', 'codex'].includes(t)) {
if (report.injectPlane && report.injectPlane.supported && !report.injectPlane.present && t && ['claude', 'codex'].includes(t)) {
hints.push(
`inject plane missing → npx code-abyss -t ${t} -y (writes ${report.injectPlane.path || '.code-abyss-inject.md'})`
);
Expand Down Expand Up @@ -159,7 +166,11 @@ function formatDoctorReport(report) {
);
const b = report.composeBudget;
lines.push(`compose budget: ${b.length}/${b.cap} (headroom ${b.headroom}) persona=${b.persona} style=${b.style}`);
lines.push(`inject plane: ${report.injectPlane.present ? 'present' : 'absent'} (${report.injectPlane.path})`);
if (!report.injectPlane.supported) {
lines.push(`inject plane: N/A (${report.enforcement.target} — not installed by code-abyss)`);
} else {
lines.push(`inject plane: ${report.injectPlane.present ? 'present' : 'absent'} (${report.injectPlane.path})`);
}

const hints = collectMigrationHints(report);
if (hints.length) {
Expand All @@ -175,6 +186,8 @@ function formatDoctorReport(report) {
* Compose host guidance using the same engine as install (no skill tree copy).
* @returns {{ guidance: string, destPath: string|null, wrote: boolean }}
*/
const COMPOSE_SUPPORTED_TARGETS = new Set(['claude', 'codex', 'gemini', 'openclaw']);

function composeHostGuidance({
projectRoot,
target = 'claude',
Expand All @@ -183,6 +196,9 @@ function composeHostGuidance({
HOME = os.homedir(),
write = false,
} = {}) {
if (!COMPOSE_SUPPORTED_TARGETS.has(target)) {
throw new Error(`unsupported target: ${target}. Supported: ${[...COMPOSE_SUPPORTED_TARGETS].join(', ')}`);
}
const persona = personaSlug || getDefaultPersona(projectRoot).slug;
const style = styleSlug
|| getDefaultStyle(projectRoot, target === 'gemini' ? 'claude' : target).slug;
Expand All @@ -197,6 +213,9 @@ function composeHostGuidance({

const hostForRender = target === 'gemini' ? 'gemini' : 'codex';
const guidance = renderRuntimeGuidance(projectRoot, style, hostForRender, persona);
if (guidance.length >= COMPOSE_BUDGET_CAP) {
throw new Error(`compose guidance exceeds budget cap: ${guidance.length}/${COMPOSE_BUDGET_CAP}`);
}

let destPath = null;
if (target === 'claude') destPath = path.join(HOME, '.claude', 'CLAUDE.md');
Expand Down
12 changes: 6 additions & 6 deletions docs/design/agent-os-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> **Locks thesis:** one system, not a menu of options.
> **Supersedes as primary direction:** [`persona-architecture-v3.md`](./persona-architecture-v3.md) (eager→lazy composition remains a *layer*, not the product).
> **Also absorbs residual truth from:** [`mythos-kernel-merge.md`](./mythos-kernel-merge.md) (kernel as spine), and retires root [`DESIGN.md`](../../DESIGN.md) freeform L1–L4 assembly as *historical*, not current runtime.
> **Product code today:** v4.10.0 (`package.json`). This document is design-only; **today** vs **target** are labeled everywhere they diverge.
> **Product code today:** v5.0.0-rc.1 (`package.json`). This document is design-only; **today** vs **target** are labeled everywhere they diverge. The v4.10-era "today" narrative below is preserved as historical context; v5.0.0-rc.1 has landed the kill-foyer, runtime control plane (`doctor`/`compose`/`score`), default enforcement, and inject plane.

---

Expand Down Expand Up @@ -51,7 +51,7 @@ v3 solved **budget explosion** by lazy-loading judgment. v5 keeps that win and f

---

## 1. Diagnosis — today (v4.10 tree facts)
## 1. Diagnosis — today (v5.0.0-rc.1 tree facts; v4.10 narrative preserved below)

### 1.1 What ships and works

Expand All @@ -63,16 +63,16 @@ v3 solved **budget explosion** by lazy-loading judgment. v5 keeps that win and f
| Kernel vendored in-tree (9 bundles), not submodule | `scripts/sync-mythos.js`, `skills/_kernel/`, `.sync-meta.json` | npm-safe |
| 16 exec skills carry domain-gate pointers | `scripts/wire-domain-gates.js` MAP → `skills/*/SKILL.md` | Compose *prose* exists |
| 4-host install + backup/uninstall + CI smoke from real tarball | `bin/install.js`, adapters, `.github/workflows/ci.yml` | Distribution mature |
| Health gates green at review time | `npm test` 442 pass; `verify:skills` 39 skills | Baseline trustworthy |
| Health gates green at review time | `npm test` 489 pass; `verify:skills` 39 skills | Baseline trustworthy |

### 1.2 Where the architecture is timid (product failure, not test failure)

| Failure mode | Mechanism that breaks | Evidence |
|--------------|----------------------|----------|
| **Lazy = optional** | Kernel invoked only if the model obeys `kernel-router.md` prose | Router is advisory text (`config/personas/_shared/kernel-router.md`); no host-level inject on triggers |
| **Enforcement is opt-in** | Stop-hook / banned openers only when `--with-enforcement` | `bin/install.js` flag surface; default `-y` path does not install character hooks |
| **Installer is the product** | Success = files on disk, not “session behaves” | No runtime `doctor`/`score` path in shipped bin surface (`package.json` `bin` → `install.js` only) |
| **Abyss boundary is a deprecation hotel** | Dual stories: code-abyss hooks vs `abyss attach` | `abyss-integration.js` deprecated injectors; `abyss-binary.js` download without integrity; flags marked remove-in-v5 still ship |
| **Enforcement is default-on** | Stop-hook / banned openers installed by default for claude/codex; opt out with `--no-enforcement` | `bin/install.js` flag surface; default `-y` path installs character hooks |
| **Runtime control plane landed** | `doctor`, `compose`, `score` are shipped bin commands | `bin/install.js` multi-command surface; `bin/lib/runtime-control.js` |
| **Abyss boundary is a deprecation hotel** | Dual stories: code-abyss hooks vs `abyss attach` | `abyss-binary.js` removed in v5.0; `abyss-integration.js` retains strip-only + MCP shape helpers; `abyss attach` is the only production inject path |
| **Docs lie in parallel** | Root `DESIGN.md` still describes freeform L1–L4 persona assembly | `DESIGN.md` lines 7–22 vs actual voice-card + `renderRuntimeGuidance` |
| **Measurement is a side quest** | persona-battery manual / API-cost | `scripts/persona-battery/`, `.github/workflows/persona-battery.yml` workflow_dispatch only |
| **Voice card over-surgery** | Solved judgment accretion by lobotomizing persona residual space | 16-char self/user, banned punctuation, aggregate budget — correct for safety, **insufficient as brand/attitude surface** without a separate stance track |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "code-abyss",
"version": "5.0.0-rc.1",
"version": "5.0.0-rc.2",
"description": "为 Claude Code / Codex CLI / Gemini CLI / OpenClaw 注入可切换人格、主动执行导向、6种输出风格与30个工程技能(含自我进化炼炉)。代码图谱由独立的 abyss Rust CLI 提供(github.com/telagod/abyss)",
"keywords": [
"claude",
Expand Down
31 changes: 30 additions & 1 deletion skills/_lib/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@
* 消灭 verify-* 脚本间的重复代码
*/

const fs = require('fs');
const path = require('path');

// --- 路径安全 ---

/**
* 将用户传入路径解析为项目根内的安全绝对路径。
* - 解析 `..`、符号链接
* - 默认要求路径落在 `root`(默认 process.cwd())之内
* - 用于写入类工具时必须开启 `mustContain: true`
*/
function resolveSafePath(targetPath, { root = process.cwd(), mustContain = false } = {}) {
const resolvedRoot = fs.realpathSync(root);
let resolved;
try {
resolved = fs.realpathSync(path.resolve(resolvedRoot, targetPath));
} catch (e) {
if (mustContain) throw new Error(`路径解析失败: ${targetPath} (${e.message})`);
return path.resolve(resolvedRoot, targetPath);
}
if (mustContain && !resolved.startsWith(resolvedRoot + path.sep) && resolved !== resolvedRoot) {
throw new Error(`路径越出项目根: ${resolved} (root: ${resolvedRoot})`);
}
return resolved;
}



// --- CLI 参数解析 ---

function parseCliArgs(argv, extraFlags) {
Expand Down Expand Up @@ -94,5 +122,6 @@ function hasFatal(issues, fatalLevels) {

module.exports = {
parseCliArgs, buildReport, reportHeader, reportIssues,
reportFooter, countBySeverity, hasFatal, SEP, DASH, ICONS
reportFooter, countBySeverity, hasFatal, SEP, DASH, ICONS,
resolveSafePath,
};
4 changes: 2 additions & 2 deletions skills/analyzing-security/scripts/security_scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ function walkDir(dir, excludeDirs) {
}

function scanDirectory(scanPath, excludeDirs) {
const resolved = path.resolve(scanPath);
const resolved = resolveSafePath(scanPath);
const findings = [];
const files = walkDir(resolved, excludeDirs);
for (const f of files) findings.push(...scanFile(f, SECURITY_RULES));
Expand All @@ -231,7 +231,7 @@ function scanDirectory(scanPath, excludeDirs) {
return { scan_path: resolved, files_scanned: files.length, passed, findings };
}

const { buildReport, countBySeverity, parseCliArgs } = require(
const { buildReport, countBySeverity, parseCliArgs, resolveSafePath } = require(
path.join(__dirname, '..', '..', '_lib', 'shared.js')
);

Expand Down
4 changes: 2 additions & 2 deletions skills/checking-code-quality/scripts/quality_checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

const fs = require('fs');
const path = require('path');
const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', '_lib', 'shared.js'));
const { parseCliArgs, buildReport, hasFatal, resolveSafePath } = require(path.join(__dirname, '..', '..', '_lib', 'shared.js'));

// 质量规则配置
const MAX_LINE_LENGTH = 120;
Expand Down Expand Up @@ -253,7 +253,7 @@ function analyzePythonFile(filePath) {
// --- Directory scan ---

function scanDirectory(scanPath, excludeDirs) {
const resolved = path.resolve(scanPath);
const resolved = resolveSafePath(scanPath);
const exclude = excludeDirs || EXCLUDE_DIRS;
const result = {
scan_path: resolved, files_scanned: 0,
Expand Down
6 changes: 4 additions & 2 deletions skills/cultivating-personas/scripts/persona_forge.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
const fs = require('fs');
const path = require('path');
const { validatePersonaVoiceCard } = require('../../../bin/lib/persona-voice-card');
const { resolveSafePath } = require('../../_lib/shared.js');

const FORBIDDEN_TERMS = [
/\b(linus torvalds|elon musk|donald trump|joe biden)\b/i,
Expand Down Expand Up @@ -183,9 +184,10 @@ function cmdPublish(args) {
return 1;
}

const outDir = path.join(path.dirname(cardPath), 'submission');
const safeCardPath = resolveSafePath(cardPath);
const outDir = path.join(path.dirname(safeCardPath), 'submission');
fs.mkdirSync(outDir, { recursive: true });
fs.copyFileSync(cardPath, path.join(outDir, `${card.slug}.json`));
fs.copyFileSync(safeCardPath, path.join(outDir, `${card.slug}.json`));

const checklist = `# 提交前自检 · ${card.slug}

Expand Down
Loading