harness-everything 0.11.4 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/ci/mechanism-2af-worktree-isolation.test.js +12 -0
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +25 -0
- package/hooks/scripts/lib/workflow-isolation.js +96 -15
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/workflow-isolation.js +96 -15
- package/plugins/harness-everything/plugin.json +1 -1
|
@@ -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.
|
|
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.
|
|
4
|
+
"version": "0.11.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
+
|
|
5
11
|
## [0.11.4](https://github.com/dyphn1/Harness-everything/compare/v0.11.3...v0.11.4) (2026-09-18)
|
|
6
12
|
|
|
7
13
|
### Bug Fixes
|
|
@@ -123,6 +123,14 @@ try {
|
|
|
123
123
|
'gh repo view octocat/hello-world --json name',
|
|
124
124
|
'gh auth status',
|
|
125
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"',
|
|
126
134
|
]) {
|
|
127
135
|
check(runGate('Bash', repo, { command }).status === 0, `trusted read-only chain remains allowed: ${command}`);
|
|
128
136
|
}
|
|
@@ -136,6 +144,10 @@ try {
|
|
|
136
144
|
'git worktree add "harness-chain-probe" -b harness-chain-probe; rm -rf src',
|
|
137
145
|
'gh issue comment 1 --body hi',
|
|
138
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',
|
|
139
151
|
]) {
|
|
140
152
|
check(runGate('Bash', repo, { command }).status === 2, `chained/redirected command is not read-only: ${command}`);
|
|
141
153
|
}
|
|
@@ -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,13 +43,12 @@ 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.
|
|
47
|
-
// `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect
|
|
48
|
-
// when every resulting segment independently matches the
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
const SAFE_FD_REDIRECT_RE = /[12]>&[12]/g;
|
|
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.
|
|
53
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;
|
|
54
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;
|
|
55
54
|
|
|
@@ -57,20 +56,102 @@ function isReadOnlySegment(segment) {
|
|
|
57
56
|
return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
|
|
58
57
|
}
|
|
59
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
|
+
|
|
60
145
|
function classifyShell(command) {
|
|
61
146
|
const text = String(command || '').trim();
|
|
62
|
-
if (!text
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const withoutSafeRedirects = text.replace(SAFE_FD_REDIRECT_RE, ' ');
|
|
66
|
-
if (/&/.test(withoutSafeRedirects.replace(/&&/g, ''))) return 'mutation-or-unknown';
|
|
67
|
-
if (/[<>]/.test(withoutSafeRedirects)) return 'mutation-or-unknown';
|
|
68
|
-
const segments = text.split(SHELL_SEPARATOR_RE).map(segment => segment.replace(SAFE_FD_REDIRECT_RE, '').trim());
|
|
147
|
+
if (!text) return 'mutation-or-unknown';
|
|
148
|
+
const segments = splitShellSegments(text);
|
|
149
|
+
if (!segments) return 'mutation-or-unknown';
|
|
69
150
|
if (segments.length === 1) {
|
|
70
151
|
if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
|
|
71
152
|
return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
|
|
72
153
|
}
|
|
73
|
-
return segments.every(
|
|
154
|
+
return segments.every(isReadOnlySegment) ? 'read-only' : 'mutation-or-unknown';
|
|
74
155
|
}
|
|
75
156
|
|
|
76
157
|
function inputOf(payload) { return payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {}; }
|
package/package.json
CHANGED
|
@@ -43,13 +43,12 @@ 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.
|
|
47
|
-
// `||`, `|`) and the `2>&1`/`1>&2` fd-duplication redirect
|
|
48
|
-
// when every resulting segment independently matches the
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
const SAFE_FD_REDIRECT_RE = /[12]>&[12]/g;
|
|
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.
|
|
53
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;
|
|
54
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;
|
|
55
54
|
|
|
@@ -57,20 +56,102 @@ function isReadOnlySegment(segment) {
|
|
|
57
56
|
return READ_ONLY_PREFIX_RE.test(segment) && !DANGEROUS_FLAG_RE.test(segment);
|
|
58
57
|
}
|
|
59
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
|
+
|
|
60
145
|
function classifyShell(command) {
|
|
61
146
|
const text = String(command || '').trim();
|
|
62
|
-
if (!text
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const withoutSafeRedirects = text.replace(SAFE_FD_REDIRECT_RE, ' ');
|
|
66
|
-
if (/&/.test(withoutSafeRedirects.replace(/&&/g, ''))) return 'mutation-or-unknown';
|
|
67
|
-
if (/[<>]/.test(withoutSafeRedirects)) return 'mutation-or-unknown';
|
|
68
|
-
const segments = text.split(SHELL_SEPARATOR_RE).map(segment => segment.replace(SAFE_FD_REDIRECT_RE, '').trim());
|
|
147
|
+
if (!text) return 'mutation-or-unknown';
|
|
148
|
+
const segments = splitShellSegments(text);
|
|
149
|
+
if (!segments) return 'mutation-or-unknown';
|
|
69
150
|
if (segments.length === 1) {
|
|
70
151
|
if (/^git\s+worktree\s+add\s+/i.test(segments[0]) && !/\s--(?:force|checkout|detach)(?:\s|=|$)/i.test(segments[0])) return 'worktree-setup';
|
|
71
152
|
return isReadOnlySegment(segments[0]) ? 'read-only' : 'mutation-or-unknown';
|
|
72
153
|
}
|
|
73
|
-
return segments.every(
|
|
154
|
+
return segments.every(isReadOnlySegment) ? 'read-only' : 'mutation-or-unknown';
|
|
74
155
|
}
|
|
75
156
|
|
|
76
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.
|
|
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",
|