harness-everything 0.11.3 → 0.11.5

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.
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.11.3",
13
+ "version": "0.11.5",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.11.3",
4
+ "version": "0.11.5",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.11.5](https://github.com/dyphn1/Harness-everything/compare/v0.11.4...v0.11.5) (2026-09-18)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **workflow:** lex shell separators safely ([df1a92c](https://github.com/dyphn1/Harness-everything/commit/df1a92ca6129ea96390e463ebcaef769604742e2))
10
+
11
+ ## [0.11.4](https://github.com/dyphn1/Harness-everything/compare/v0.11.3...v0.11.4) (2026-09-18)
12
+
13
+ ### Bug Fixes
14
+
15
+ * **workflow:** Stop classifyShell misclassifying safe read-only chains as mutations ([96d687a](https://github.com/dyphn1/Harness-everything/commit/96d687ab4437ab19e3a743d1b9bf5113cfe52525)), closes [82/#149](https://github.com/82/Harness-everything/issues/149) [#155](https://github.com/dyphn1/Harness-everything/issues/155) [#82](https://github.com/dyphn1/Harness-everything/issues/82) [#133](https://github.com/dyphn1/Harness-everything/issues/133) [#149](https://github.com/dyphn1/Harness-everything/issues/149) [#153](https://github.com/dyphn1/Harness-everything/issues/153)
16
+
5
17
  ## [0.11.3](https://github.com/dyphn1/Harness-everything/compare/v0.11.2...v0.11.3) (2026-09-18)
6
18
 
7
19
  ### Bug Fixes
@@ -106,6 +106,52 @@ try {
106
106
  const readOnly = runGate('Bash', repo, { command: 'git status --short' });
107
107
  check(readOnly.status === 0, 'read-only discovery remains allowed before isolation');
108
108
 
109
+ // #82-family follow-up: chaining/redirects that are hidden behind the old
110
+ // blanket metacharacter reject must stay blocked, but a compound command
111
+ // whose every segment is independently trusted read-only must not burn
112
+ // iteration budget or the mutation-timestamp just because it uses `;`,
113
+ // `&&`, `|`, or a non-writing `2>&1` redirect.
114
+ for (const command of [
115
+ 'git status --short; git log --oneline -1',
116
+ 'git status --short && git log --oneline -1',
117
+ 'git log --oneline -1 | head -1',
118
+ 'echo hello',
119
+ 'echo hello; git status --short',
120
+ 'git status --short 2>&1',
121
+ 'gh issue view 1 --repo octocat/hello-world --json number,title 2>&1',
122
+ 'gh pr list --repo octocat/hello-world --json number | head -1',
123
+ 'gh repo view octocat/hello-world --json name',
124
+ 'gh auth status',
125
+ 'cat README.md | grep fixture',
126
+ 'grep -n "fixture\\|missing" README.md | head -20',
127
+ "grep -n 'fixture|missing' README.md | head -20",
128
+ 'echo "fixture|missing" | grep fixture',
129
+ 'git log --grep="fix|feat" | head -5',
130
+ "grep 'fixture;missing' README.md | head -1",
131
+ 'grep fixture\\|missing README.md | head -1',
132
+ 'echo "2>&1"',
133
+ 'echo "safe; rm -rf src"',
134
+ ]) {
135
+ check(runGate('Bash', repo, { command }).status === 0, `trusted read-only chain remains allowed: ${command}`);
136
+ }
137
+
138
+ for (const command of [
139
+ 'git status --short; rm -rf src',
140
+ 'git status --short | rm -rf src',
141
+ 'echo hi & rm -rf src',
142
+ 'git status --short > tracked.txt',
143
+ 'git status --short 2>tracked.txt',
144
+ 'git worktree add "harness-chain-probe" -b harness-chain-probe; rm -rf src',
145
+ 'gh issue comment 1 --body hi',
146
+ 'gh issue close 1',
147
+ 'git status --short || rm -rf src',
148
+ 'grep "unterminated README.md | head -1',
149
+ "grep 'unterminated README.md | head -1",
150
+ 'echo safe | rm -rf src',
151
+ ]) {
152
+ check(runGate('Bash', repo, { command }).status === 2, `chained/redirected command is not read-only: ${command}`);
153
+ }
154
+
109
155
  const setup = runGate('Bash', repo, { command: `git worktree add "${linked}" -b harness-isolation-test` });
110
156
  check(setup.status === 0, 'git worktree creation is allowed as the isolation transition');
111
157
 
@@ -158,6 +158,31 @@ try {
158
158
  check(route('Fix this checkout bug with a regression test').status === 0 && read(file).strategy === 'iterative-single', 'fresh bounded fix returns to Tier 2 iterative lifecycle');
159
159
  const iterationLimit = read(file).workflowPlan.limits.maxIterations;
160
160
  check(Number.isInteger(iterationLimit) && iterationLimit > 0, 'iterative route exposes a numeric iteration budget');
161
+
162
+ // #155/#157 regression lock: read-only shell composition, including quoted
163
+ // and escaped separator characters inside arguments, must never look like an
164
+ // iterative mutation or advance the mutation clock.
165
+ const beforeReadOnly = read(file);
166
+ const readOnlyIterations = beforeReadOnly.budget?.counters?.iterations || 0;
167
+ const readOnlyMutationAt = beforeReadOnly.lastMutationAt || 0;
168
+ const quotedReadOnly = [
169
+ 'git status --short; git log --oneline -1',
170
+ 'gh issue view 1 --repo octocat/hello-world --json number,title 2>&1',
171
+ 'grep -n "fixture\\|missing" README.md | head -20',
172
+ "grep -n 'fixture|missing' README.md | head -20",
173
+ 'echo "fixture|missing" | grep fixture',
174
+ "grep 'fixture;missing' README.md | head -1",
175
+ ];
176
+ for (let i = 0; i < iterationLimit + 2; i++) {
177
+ const command = quotedReadOnly[i % quotedReadOnly.length];
178
+ check(gate('Bash', { command }, repo).status === 0, 'quoted/escaped read-only shell command stays outside iteration budget: ' + command);
179
+ }
180
+ const afterReadOnly = read(file);
181
+ check((afterReadOnly.budget?.counters?.iterations || 0) === readOnlyIterations,
182
+ 'read-only shell composition does not consume iterative budget');
183
+ check((afterReadOnly.lastMutationAt || 0) === readOnlyMutationAt,
184
+ 'read-only shell composition does not advance lastMutationAt');
185
+
161
186
  for (let i = 0; i < iterationLimit; i++) {
162
187
  check(gate('Write', { file_path: path.join(repo, `small-${i}.js`) }, repo).status === 0, `iterative mutation ${i + 1}/${iterationLimit} stays within budget`);
163
188
  }
@@ -43,16 +43,115 @@ function linkedWorktree(cwd, boundRoot) {
43
43
  return worktrees.some(field => field.startsWith('worktree ') && key(field.slice(9)) === key(top)) ? top : null;
44
44
  }
45
45
 
46
- // A conservative read subset, not a general shell sandbox.
46
+ // A conservative read subset, not a general shell sandbox. Top-level chaining
47
+ // (`;`, `&&`, `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect
48
+ // are permitted only when every resulting segment independently matches the
49
+ // trusted read-only set below. The scanner is deliberately lexical rather than
50
+ // a raw regex split so quoted/escaped separator characters remain arguments.
51
+ // Unsupported shell syntax still fails closed.
52
+ const DANGEROUS_FLAG_RE = /\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i;
53
+ const READ_ONLY_PREFIX_RE = /^(?:git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)|git\s+branch\s+--show-current$|git\s+worktree\s+list(?:\s|$)|gh\s+(?:issue|pr)\s+(?:view|list)(?:\s|$)|gh\s+repo\s+view(?:\s|$)|gh\s+auth\s+status(?:\s|$)|(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|grep|echo|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$))/i;
54
+
55
+ function isReadOnlySegment(segment) {
56
+ return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
57
+ }
58
+
59
+ function splitShellSegments(text) {
60
+ const segments = [];
61
+ let current = '';
62
+ let quote = null;
63
+
64
+ const push = () => {
65
+ const segment = current.trim();
66
+ if (!segment) return false;
67
+ segments.push(segment);
68
+ current = '';
69
+ return true;
70
+ };
71
+
72
+ for (let i = 0; i < text.length; i++) {
73
+ const ch = text[i];
74
+
75
+ if (quote === "'") {
76
+ current += ch;
77
+ if (ch === "'") quote = null;
78
+ continue;
79
+ }
80
+
81
+ if (quote === '"') {
82
+ if (ch === '$' || ch === '`') return null;
83
+ current += ch;
84
+ if (ch === '"') {
85
+ quote = null;
86
+ } else if (ch === '\\') {
87
+ if (i + 1 >= text.length) return null;
88
+ current += text[++i];
89
+ }
90
+ continue;
91
+ }
92
+
93
+ if (ch === "'" || ch === '"') {
94
+ quote = ch;
95
+ current += ch;
96
+ continue;
97
+ }
98
+
99
+ if (ch === '\\') {
100
+ if (i + 1 >= text.length) return null;
101
+ current += ch + text[++i];
102
+ continue;
103
+ }
104
+
105
+ if (ch === '$' || ch === '`' || ch === '(' || ch === ')' || ch === '{' || ch === '}' ||
106
+ ch === '\r' || ch === '\n' || ch === '\x00') return null;
107
+
108
+ // Only treat fd duplication as syntax at a token boundary. Quoted text such
109
+ // as "2>&1" was handled above and remains literal argument content.
110
+ if ((ch === '1' || ch === '2') && text[i + 1] === '>' && text[i + 2] === '&' &&
111
+ (text[i + 3] === '1' || text[i + 3] === '2') &&
112
+ (i === 0 || /\s/.test(text[i - 1])) &&
113
+ (i + 4 === text.length || /[\s;|&]/.test(text[i + 4]))) {
114
+ current += ' ';
115
+ i += 3;
116
+ continue;
117
+ }
118
+
119
+ if (ch === '>' || ch === '<') return null;
120
+
121
+ if (ch === ';') {
122
+ if (!push()) return null;
123
+ continue;
124
+ }
125
+
126
+ if (ch === '|') {
127
+ if (!push()) return null;
128
+ if (text[i + 1] === '|') i++;
129
+ continue;
130
+ }
131
+
132
+ if (ch === '&') {
133
+ if (text[i + 1] !== '&' || !push()) return null;
134
+ i++;
135
+ continue;
136
+ }
137
+
138
+ current += ch;
139
+ }
140
+
141
+ if (quote || !push()) return null;
142
+ return segments;
143
+ }
144
+
47
145
  function classifyShell(command) {
48
146
  const text = String(command || '').trim();
49
- if (!text || /[;&|`\r\n<>$(){}\x00]/.test(text)) return 'mutation-or-unknown';
50
- if (/^git\s+worktree\s+add\s+/i.test(text) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(text)) return 'worktree-setup';
51
- if (/\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i.test(text)) return 'mutation-or-unknown';
52
- if (/^git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)/i.test(text) ||
53
- /^git\s+branch\s+--show-current$/i.test(text) || /^git\s+worktree\s+list(?:\s|$)/i.test(text) ||
54
- /^(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$)/i.test(text)) return 'read-only';
55
- return 'mutation-or-unknown';
147
+ if (!text) return 'mutation-or-unknown';
148
+ const segments = splitShellSegments(text);
149
+ if (!segments) return 'mutation-or-unknown';
150
+ if (segments.length === 1) {
151
+ if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
152
+ return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
153
+ }
154
+ return segments.every(isReadOnlySegment) ? 'read-only' : 'mutation-or-unknown';
56
155
  }
57
156
 
58
157
  function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.11.3",
4
+ "version": "0.11.5",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -43,16 +43,115 @@ function linkedWorktree(cwd, boundRoot) {
43
43
  return worktrees.some(field => field.startsWith('worktree ') && key(field.slice(9)) === key(top)) ? top : null;
44
44
  }
45
45
 
46
- // A conservative read subset, not a general shell sandbox.
46
+ // A conservative read subset, not a general shell sandbox. Top-level chaining
47
+ // (`;`, `&&`, `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect
48
+ // are permitted only when every resulting segment independently matches the
49
+ // trusted read-only set below. The scanner is deliberately lexical rather than
50
+ // a raw regex split so quoted/escaped separator characters remain arguments.
51
+ // Unsupported shell syntax still fails closed.
52
+ const DANGEROUS_FLAG_RE = /\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i;
53
+ const READ_ONLY_PREFIX_RE = /^(?:git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)|git\s+branch\s+--show-current$|git\s+worktree\s+list(?:\s|$)|gh\s+(?:issue|pr)\s+(?:view|list)(?:\s|$)|gh\s+repo\s+view(?:\s|$)|gh\s+auth\s+status(?:\s|$)|(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|grep|echo|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$))/i;
54
+
55
+ function isReadOnlySegment(segment) {
56
+ return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
57
+ }
58
+
59
+ function splitShellSegments(text) {
60
+ const segments = [];
61
+ let current = '';
62
+ let quote = null;
63
+
64
+ const push = () => {
65
+ const segment = current.trim();
66
+ if (!segment) return false;
67
+ segments.push(segment);
68
+ current = '';
69
+ return true;
70
+ };
71
+
72
+ for (let i = 0; i < text.length; i++) {
73
+ const ch = text[i];
74
+
75
+ if (quote === "'") {
76
+ current += ch;
77
+ if (ch === "'") quote = null;
78
+ continue;
79
+ }
80
+
81
+ if (quote === '"') {
82
+ if (ch === '$' || ch === '`') return null;
83
+ current += ch;
84
+ if (ch === '"') {
85
+ quote = null;
86
+ } else if (ch === '\\') {
87
+ if (i + 1 >= text.length) return null;
88
+ current += text[++i];
89
+ }
90
+ continue;
91
+ }
92
+
93
+ if (ch === "'" || ch === '"') {
94
+ quote = ch;
95
+ current += ch;
96
+ continue;
97
+ }
98
+
99
+ if (ch === '\\') {
100
+ if (i + 1 >= text.length) return null;
101
+ current += ch + text[++i];
102
+ continue;
103
+ }
104
+
105
+ if (ch === '$' || ch === '`' || ch === '(' || ch === ')' || ch === '{' || ch === '}' ||
106
+ ch === '\r' || ch === '\n' || ch === '\x00') return null;
107
+
108
+ // Only treat fd duplication as syntax at a token boundary. Quoted text such
109
+ // as "2>&1" was handled above and remains literal argument content.
110
+ if ((ch === '1' || ch === '2') && text[i + 1] === '>' && text[i + 2] === '&' &&
111
+ (text[i + 3] === '1' || text[i + 3] === '2') &&
112
+ (i === 0 || /\s/.test(text[i - 1])) &&
113
+ (i + 4 === text.length || /[\s;|&]/.test(text[i + 4]))) {
114
+ current += ' ';
115
+ i += 3;
116
+ continue;
117
+ }
118
+
119
+ if (ch === '>' || ch === '<') return null;
120
+
121
+ if (ch === ';') {
122
+ if (!push()) return null;
123
+ continue;
124
+ }
125
+
126
+ if (ch === '|') {
127
+ if (!push()) return null;
128
+ if (text[i + 1] === '|') i++;
129
+ continue;
130
+ }
131
+
132
+ if (ch === '&') {
133
+ if (text[i + 1] !== '&' || !push()) return null;
134
+ i++;
135
+ continue;
136
+ }
137
+
138
+ current += ch;
139
+ }
140
+
141
+ if (quote || !push()) return null;
142
+ return segments;
143
+ }
144
+
47
145
  function classifyShell(command) {
48
146
  const text = String(command || '').trim();
49
- if (!text || /[;&|`\r\n<>$(){}\x00]/.test(text)) return 'mutation-or-unknown';
50
- if (/^git\s+worktree\s+add\s+/i.test(text) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(text)) return 'worktree-setup';
51
- if (/\s(?:--(?:output|ext-diff|textconv|exec|pre|pre-glob|pager|open|batch|filters)|-[xoO])(?:\b|=)|\s-(?:exec|execdir|delete|fprint|fprintf)\b/i.test(text)) return 'mutation-or-unknown';
52
- if (/^git\s+(?:status|rev-parse|diff|log|show|ls-files|check-ignore)(?:\s|$)/i.test(text) ||
53
- /^git\s+branch\s+--show-current$/i.test(text) || /^git\s+worktree\s+list(?:\s|$)/i.test(text) ||
54
- /^(?:pwd|ls|dir|cat|type|head|tail|wc|stat|rg|Get-Location|Get-ChildItem|Get-Content|Select-String|Test-Path)(?:\s|$)/i.test(text)) return 'read-only';
55
- return 'mutation-or-unknown';
147
+ if (!text) return 'mutation-or-unknown';
148
+ const segments = splitShellSegments(text);
149
+ if (!segments) return 'mutation-or-unknown';
150
+ if (segments.length === 1) {
151
+ if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
152
+ return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
153
+ }
154
+ return segments.every(isReadOnlySegment) ? 'read-only' : 'mutation-or-unknown';
56
155
  }
57
156
 
58
157
  function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.11.3",
4
+ "version": "0.11.5",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",