Release 1.1.4: human-only shell rule fix (#88), mention hint (#86) (#89)

- 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:
BillyOutlast
2026-10-08 12:55:59 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8e8a14c6e6
commit 7091f2c313
9 changed files with 176 additions and 27 deletions
+43
View File
@@ -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
+7 -5
View File
@@ -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`,
+9 -5
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+67 -12
View File
@@ -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." }
+45
View File
@@ -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")
+2 -2
View File
@@ -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",