mirror of
https://github.com/Heretek-AI/IUMBTEMS.git
synced 2026-10-11 21:09:53 +00:00
- Security (#88): the human-only shell rule finds the es verb past flags and `--`, as the CLI parser does; `es --cwd . <verb>` no longer slips past it. - #86: refusals for text that only names a guarded verb point at passing it by file (gh --body-file, git commit -F); the quote-blind deny stays. - @babel/core 7.29.6 in packages/opencode (dev only; replaces #87). - Release 1.1.4: versions, CHANGELOG, README status. Closes the 1.1.x proof-of-work phase; the user waived the dogfood and the local real-model run. Fixes #88. Fixes #86. Closes #35. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e8a14c6e6
commit
7091f2c313
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.4 — 2026-10-08
|
||||
|
||||
A security fix for the human-only shell rule, plus the E2E feedback fixes
|
||||
(#85). This closes the 1.1.x proof-of-work phase (#35); the user waived the
|
||||
dogfood and the local real-model run.
|
||||
|
||||
**Security.**
|
||||
- **#88: a flag before the verb slipped past the human-only shell rule.**
|
||||
The rule read only the word right after the es binary, but the CLI takes
|
||||
`--flag`, `--flag value`, `--flag=value` and `--` anywhere. So
|
||||
`es --cwd . approve spec`, `es -- approve spec`,
|
||||
`es --run <id> factory resume` and `es factory --cwd . resume` were
|
||||
allowed for every agent, factory seats included.
|
||||
- Approvals and keys stayed guarded by the masked key and the
|
||||
terminal-only passphrase. Spend-bearing `audit` and `scout` runs and
|
||||
`factory resume` were not.
|
||||
- The rule now finds the verb the way the CLI parser does. A bare `--flag`
|
||||
counts both as a boolean and as taking a value.
|
||||
- Upgrade from any earlier version.
|
||||
|
||||
**Fixes.**
|
||||
- **#86: refusals for text that only names a guarded verb.** The rule stays
|
||||
quote-blind, so a here-doc, a commit message or a `--body` argument that
|
||||
mentions a human-only verb is still refused. The refusal now says how to
|
||||
pass such text by file (`gh … --body-file`, `git commit -F`).
|
||||
- **The grill skill's first turn asks and never records** (#85). The seat
|
||||
had been writing the frontier before asking anything.
|
||||
|
||||
**Evals and CI (#85).**
|
||||
- The `programmer` case stages a BUILD run with a phase worktree, so it can
|
||||
pass (#66).
|
||||
- Step caps were re-budgeted from observed need, and the `ES_EVAL_MAX_STEPS`
|
||||
ceiling went from 16 to 24 (#67).
|
||||
- `e2e-feedback` fails only when a historically stable case fails
|
||||
(`scripts/e2e-gate.ts`, #68). `audit-fires` is marked model-limited on the
|
||||
free CI model.
|
||||
- Grading reads completed tool outputs, except the raw fetch tools, whose
|
||||
verbatim quoting is the skill under test.
|
||||
- Kept workspaces mirror `.factory/` to `keep/factory/`, because the
|
||||
artifact upload drops dot-dirs.
|
||||
|
||||
**Dependencies.** `@babel/core` 7.29.6, dev only (GHSA-4x5r-pxfx-6jf8).
|
||||
|
||||
## 1.1.3 — 2026-10-08
|
||||
|
||||
Live-run visibility and seat discipline, from the 1.1.2 dogfood (#63): a
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
> phases under dual QA, and open a PR a human merges. Mechanical gates and a
|
||||
> human-only approval channel keep autonomous runs honest.
|
||||
|
||||
> **Status: 1.1.3.** Live-run visibility and seat discipline from the 1.1.2
|
||||
> dogfood (#63). Every status view leads with whether the run is working,
|
||||
> waiting, stuck or done: `es status`, `es watch`, `es runs`, the factory
|
||||
> dashboard and a TUI footer indicator. Factory seats run in the foreground,
|
||||
> with one writer per research file. 1.1.2 added `audit.phase`, pipeline A and
|
||||
> **Status: 1.1.4.** A security fix: a flag before the verb
|
||||
> (`es --cwd . <verb>`) no longer slips past the human-only shell rule
|
||||
> (#88). 1.1.3 added live-run visibility and seat discipline. Every status
|
||||
> view leads with whether the run is working, waiting, stuck or done:
|
||||
> `es status`, `es watch`, `es runs`, the factory dashboard and a TUI footer
|
||||
> indicator. Factory seats run in the foreground, with one writer per
|
||||
> research file. 1.1.2 added `audit.phase`, pipeline A and
|
||||
> signed run state; 1.1.1 was the security release (bubblewrap for every agent
|
||||
> shell, the passphrase-sealed Ed25519 human key, terminal-only approvals).
|
||||
> Install from npm (`@heretek-ai/epistemic-swarm`, `@heretek-ai/es-core`,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@heretek-ai/es-cli",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"bin": {
|
||||
"es-factory": "./dist/bin.js",
|
||||
"iumbtems": "./dist/bin.js",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@heretek-ai/es-core",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "9.0.3",
|
||||
"vscode-languageserver-protocol": "3.18.4",
|
||||
@@ -35,13 +35,13 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@heretek-ai/epistemic-swarm",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"dependencies": {
|
||||
"@heretek-ai/es-core": "workspace:*",
|
||||
"@opencode/plugin": "2.0.24",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/core": "7.29.6",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@heretek-ai/es-testkit": "workspace:*",
|
||||
"@opencode/sdk": "2.0.24",
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/[email protected]8.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
"@babel/core": ["@babel/[email protected]9.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.29.2", "@babel/parser": "^7.29.3", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA=="],
|
||||
|
||||
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
@@ -260,6 +260,8 @@
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||
@@ -1136,6 +1138,8 @@
|
||||
|
||||
"@opentui/core/web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||
|
||||
"@opentui/solid/@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@heretek-ai/es-cli",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"description": "Epistemic Swarm CLI: factory control, human-only checkpoints, gates, headless runs and a coarse MCP server.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/** The `es` CLI version. Single source: kept equal to the package version by test/cli.test.ts. */
|
||||
export const VERSION = "1.1.3"
|
||||
export const VERSION = "1.1.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@heretek-ai/es-core",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"description": "Epistemic Swarm core: factory state machine, mechanical gates, audit log, trust, research, brainstorm, harvest, LSP and hook/agent compilers.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -157,18 +157,77 @@ function isPlainHelp(command: string): boolean {
|
||||
return help > 0 && (end === -1 || help < end)
|
||||
}
|
||||
|
||||
/** Whether the command invokes a human-only es verb, however it is quoted or wrapped. */
|
||||
/** A word position, and whether a `--` before it made every word positional. */
|
||||
interface Cursor {
|
||||
readonly at: number
|
||||
readonly literal: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the CLI parser's next positional could be, scanning from `from` (an
|
||||
* index past the end means there is none). The parser takes flags anywhere
|
||||
* (#88): `--` makes the rest positional, and `--flag value` consumes the
|
||||
* value unless the flag is boolean. The boolean list is the CLI's, so a bare
|
||||
* `--flag` counts both ways; `--flag=value` is two words here (`words` splits
|
||||
* at `=`) and reads the same.
|
||||
*/
|
||||
function nextPositionals(tokens: readonly string[], from: Cursor): Cursor[] {
|
||||
const found: Cursor[] = []
|
||||
const seen = new Set<string>()
|
||||
const stack = [from]
|
||||
while (stack.length > 0) {
|
||||
const cursor = stack.pop()!
|
||||
const key = `${cursor.at}:${cursor.literal}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
const token = tokens[cursor.at]
|
||||
if (token === undefined || cursor.literal || !token.startsWith("--")) found.push(cursor)
|
||||
else if (token === "--") stack.push({ at: cursor.at + 1, literal: true })
|
||||
else stack.push({ at: cursor.at + 1, literal: false }, { at: cursor.at + 2, literal: false })
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/** Whether the es binary at `tokens[binary]` can run a human-only verb. */
|
||||
function humanVerbAfter(tokens: readonly string[], binary: number): boolean {
|
||||
return nextPositionals(tokens, { at: binary + 1, literal: false }).some((verb) => {
|
||||
const rule = HUMAN_VERBS.find(([name]) => name === tokens[verb.at])
|
||||
if (rule === undefined) return false
|
||||
const [, sub] = rule
|
||||
if (sub === undefined) return true
|
||||
return nextPositionals(tokens, { at: verb.at + 1, literal: verb.literal }).some((next) => sub(tokens[next.at]))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the command invokes a human-only es verb, however it is quoted or
|
||||
* wrapped. Deliberately quote-blind, so text that only names a verb (a
|
||||
* here-doc, a `-m` or `--body` argument) reads as a call too (#86). That false
|
||||
* positive is the price of catching `sh -c`, `xargs` and interpreter wrappers;
|
||||
* the refusal tells the agent to pass such text by file instead.
|
||||
*/
|
||||
export function invokesHumanOnly(command: string): boolean {
|
||||
if (isPlainHelp(command)) return false
|
||||
const tokens = words(unquoteShell(command))
|
||||
return tokens.some((token, index) => {
|
||||
if (!ES_BINARY.test(token)) return false
|
||||
const verb = tokens[index + 1]
|
||||
const rule = HUMAN_VERBS.find(([name]) => name === verb)
|
||||
return rule !== undefined && (rule[1] === undefined || rule[1](tokens[index + 2]))
|
||||
})
|
||||
return tokens.some((token, index) => ES_BINARY.test(token) && humanVerbAfter(tokens, index))
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a human-only match is a plain call: no here-doc, and an es binary
|
||||
* heading its segment. Anything else may be text that only names the verb.
|
||||
*/
|
||||
const calledAtHead = (command: string): boolean =>
|
||||
!command.includes("<<") &&
|
||||
segments(unquoteShell(command)).some((segment) => {
|
||||
const tokens = words(segment)
|
||||
return ES_BINARY.test(tokens[0] ?? "") && humanVerbAfter(tokens, 0)
|
||||
})
|
||||
|
||||
const HUMAN_ONLY =
|
||||
"That command is human-only (approvals, trust, waivers, resume, rebaseline, keys, config set, retractions, exports, audit and scout runs); agents cannot run it."
|
||||
const MENTION_HINT =
|
||||
" If it only names the verb in text (a here-doc, a commit message, a `--body` argument), the rule still reads a call: write the text to a file with the edit tool and pass the file (`gh … --body-file`, `git commit -F`)."
|
||||
|
||||
const CONTROL_MENTION =
|
||||
/\.factory\/(gates\.json|config\.json|frontier\.json|waivers|approvals|runtime|STOP|git-hooks|claims\b|audits\b|research\/(sources\b|(coverage|dossier|brief\.pcrb)\.json)|(brainstorm|harvest|design|scout)\/[^\s'"]*\.json)|\.git\/(config|hooks)|\.opencode\/(hooks\.json|plugins|opencode\.jsonc?)|\.claude\/settings|opencode\.jsonc?\b/i
|
||||
/** Any mention of the factory dir (a `cd .factory` reaches control files without naming them). */
|
||||
@@ -249,11 +308,7 @@ export function evaluateShell(
|
||||
const spec = agentSpec(agentId)
|
||||
const plain = unquoteShell(command)
|
||||
if (invokesHumanOnly(command))
|
||||
return {
|
||||
effect: "deny",
|
||||
reason:
|
||||
"That command is human-only (approvals, trust, waivers, resume, rebaseline, keys, config set, retractions, exports, audit and scout runs); agents cannot run it.",
|
||||
}
|
||||
return { effect: "deny", reason: calledAtHead(command) ? HUMAN_ONLY : HUMAN_ONLY + MENTION_HINT }
|
||||
const privateDir = canonicalPath(context.stateDir ?? stateDir())
|
||||
if (plain.includes(privateDir) || /epistemic-swarm\/(key|trust|engine)|state\/epistemic-swarm/i.test(plain))
|
||||
return { effect: "deny", reason: "Agents may not access Epistemic Swarm's private state dir." }
|
||||
|
||||
@@ -232,6 +232,51 @@ describe("shell policy", () => {
|
||||
expect([command, shell("build", command).effect]).toEqual([command, "deny"])
|
||||
})
|
||||
|
||||
test("a flag or `--` before the verb still reads as that verb, as the CLI parser does (#88)", () => {
|
||||
for (const command of [
|
||||
"es --cwd . approve spec",
|
||||
"es --cwd=. approve spec",
|
||||
"es -- approve spec",
|
||||
"es --json approve spec",
|
||||
"es --cwd . reseal --sign",
|
||||
"es --cwd . --json key seal",
|
||||
"es --run run-1 factory resume",
|
||||
"es factory --cwd . resume",
|
||||
"es gates --cwd=. install-git",
|
||||
"es --cwd . audit src --max-usd 5",
|
||||
"es --cwd . config set models.deep x/y",
|
||||
"npx @heretek-ai/es-cli --cwd . approve spec",
|
||||
])
|
||||
for (const agent of ["build", "es-programmer"])
|
||||
expect([agent, command, shell(agent, command).effect]).toEqual([agent, command, "deny"])
|
||||
// Reads keep working with flags around them.
|
||||
for (const command of [
|
||||
"es --cwd . status",
|
||||
"es --run run-1 status",
|
||||
"es --cwd . config show",
|
||||
"es --cwd . audit verify",
|
||||
"es --cwd . factory status",
|
||||
])
|
||||
expect([command, shell("build", command).effect]).toEqual([command, "allow"])
|
||||
})
|
||||
|
||||
test("a verb only mentioned in text is still denied, and the refusal points at passing the text by file (#86)", () => {
|
||||
const hint = /--body-file/
|
||||
const reason = (decision: ReturnType<typeof shell>) => (decision.effect === "deny" ? decision.reason : "")
|
||||
for (const command of [
|
||||
"gh issue comment 1 --body \"$(cat <<'EOF'\nrun es approve spec in a terminal\nEOF\n)\"",
|
||||
"gh issue comment 1 --body-file - <<'EOF'\nes approve spec\nEOF",
|
||||
'git commit -m "docs: es reseal --sign re-signs the state"',
|
||||
]) {
|
||||
const decision = shell("build", command)
|
||||
expect([command, decision.effect]).toEqual([command, "deny"])
|
||||
expect([command, reason(decision)]).toEqual([command, expect.stringMatching(hint)])
|
||||
}
|
||||
// A plain call gets the plain refusal.
|
||||
for (const command of ["es approve spec", "cd x && es --cwd . reseal --sign"])
|
||||
expect([command, reason(shell("build", command))]).toEqual([command, expect.not.stringMatching(hint)])
|
||||
})
|
||||
|
||||
test("seats may not reach the evidence cache from the shell; others may read but not mutate it", () => {
|
||||
expect(shell("es-research-alpha", "cat .factory/research/sources/index.json").effect).toBe("deny")
|
||||
expect(shell("build", "cat .factory/research/sources/index.json").effect).toBe("allow")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@heretek-ai/epistemic-swarm",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"description": "Epistemic Swarm: an AI build factory for OpenCode v2 \u2014 grill, research, spec, build, QA and release, gated mechanically.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@opencode/plugin": "2.0.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/core": "7.29.6",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@heretek-ai/es-testkit": "workspace:*",
|
||||
"@opencode/sdk": "2.0.24",
|
||||
|
||||
Reference in New Issue
Block a user