@north-light/crouter 0.3.302 → 0.3.303
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/dist/core/__tests__/bash-guard.test.d.ts +1 -0
- package/dist/core/__tests__/bash-guard.test.js +190 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +51 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +57 -0
- package/dist/core/bash-guard.d.ts +6 -0
- package/dist/core/bash-guard.js +393 -0
- package/dist/core/runtime/broker/fault-retry.d.ts +4 -0
- package/dist/core/runtime/broker/fault-retry.js +66 -6
- package/dist/core/shell-segments.d.ts +42 -0
- package/dist/core/shell-segments.js +169 -0
- package/dist/core/substrate/surface-match.d.ts +0 -9
- package/dist/core/substrate/surface-match.js +4 -160
- package/dist/pi-extensions/canvas-bash-valve.d.ts +0 -3
- package/dist/pi-extensions/canvas-bash-valve.js +2 -34
- package/package.json +1 -1
- package/runtime.lock.json +5 -5
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// shell-segments.ts — best-effort shell text reading, shared by everything in
|
|
2
|
+
// crtr that has to reason about a command string it did not execute: the doc
|
|
3
|
+
// substrate's `command`/`pre-command` surface matching and the bash valve's
|
|
4
|
+
// refusals. Pure text in, structure out; no execution, no expansion.
|
|
5
|
+
//
|
|
6
|
+
// This is deliberately NOT a shell parser. Subshells, backticks, process
|
|
7
|
+
// substitution, and path-prefixed binaries stay unhandled. Every caller must
|
|
8
|
+
// treat a miss as "this text did not obviously do X", never as "this text
|
|
9
|
+
// cannot do X".
|
|
10
|
+
/** Split a command into shell segments on unquoted `;`, newline, `&&`, `||`,
|
|
11
|
+
* `|`, and a single `&`. Subshell parentheses and backticks are left inside
|
|
12
|
+
* their segment. Heredoc bodies are a separate pre-read concern (see
|
|
13
|
+
* `stripHeredocs`). */
|
|
14
|
+
export function splitCommandSegments(command) {
|
|
15
|
+
const segments = [];
|
|
16
|
+
let current = '';
|
|
17
|
+
let quote = null;
|
|
18
|
+
let escaped = false;
|
|
19
|
+
const finish = (background = false) => {
|
|
20
|
+
segments.push({ text: current, background });
|
|
21
|
+
current = '';
|
|
22
|
+
};
|
|
23
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
24
|
+
const ch = command[i];
|
|
25
|
+
if (escaped) {
|
|
26
|
+
current += ch;
|
|
27
|
+
escaped = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '\\' && quote !== "'") {
|
|
31
|
+
current += ch;
|
|
32
|
+
escaped = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (quote !== null) {
|
|
36
|
+
current += ch;
|
|
37
|
+
if (ch === quote)
|
|
38
|
+
quote = null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === "'" || ch === '"') {
|
|
42
|
+
current += ch;
|
|
43
|
+
quote = ch;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '\n' || ch === ';') {
|
|
47
|
+
finish();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if ((ch === '&' || ch === '|') && command[i + 1] === ch) {
|
|
51
|
+
finish();
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '|') {
|
|
56
|
+
finish();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '&') {
|
|
60
|
+
finish(true);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
current += ch;
|
|
64
|
+
}
|
|
65
|
+
finish();
|
|
66
|
+
return segments;
|
|
67
|
+
}
|
|
68
|
+
/** Tokenize one segment into shell words, marking quoted spans opaque. */
|
|
69
|
+
export function tokenizeCommandSegment(segment) {
|
|
70
|
+
const tokens = [];
|
|
71
|
+
let text = '';
|
|
72
|
+
let opaque = false;
|
|
73
|
+
let started = false;
|
|
74
|
+
let quote = null;
|
|
75
|
+
let escaped = false;
|
|
76
|
+
const finish = () => {
|
|
77
|
+
if (started)
|
|
78
|
+
tokens.push({ text, opaque });
|
|
79
|
+
text = '';
|
|
80
|
+
opaque = false;
|
|
81
|
+
started = false;
|
|
82
|
+
};
|
|
83
|
+
for (let i = 0; i < segment.length; i += 1) {
|
|
84
|
+
const ch = segment[i];
|
|
85
|
+
if (escaped) {
|
|
86
|
+
text += ch;
|
|
87
|
+
started = true;
|
|
88
|
+
escaped = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === '\\' && quote !== "'") {
|
|
92
|
+
escaped = true;
|
|
93
|
+
started = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (quote !== null) {
|
|
97
|
+
started = true;
|
|
98
|
+
if (ch === quote) {
|
|
99
|
+
quote = null;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
text += ch;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === "'" || ch === '"') {
|
|
107
|
+
quote = ch;
|
|
108
|
+
opaque = true;
|
|
109
|
+
started = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (/\s/.test(ch)) {
|
|
113
|
+
finish();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
text += ch;
|
|
117
|
+
started = true;
|
|
118
|
+
}
|
|
119
|
+
if (escaped)
|
|
120
|
+
text += '\\';
|
|
121
|
+
finish();
|
|
122
|
+
return tokens;
|
|
123
|
+
}
|
|
124
|
+
/** Every non-empty command segment, paired with its raw text. */
|
|
125
|
+
export function commandSegments(command) {
|
|
126
|
+
return splitCommandSegments(command)
|
|
127
|
+
.map(({ text, background }) => {
|
|
128
|
+
const tokens = tokenizeCommandSegment(text);
|
|
129
|
+
let first = 0;
|
|
130
|
+
while (first < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[first].text))
|
|
131
|
+
first += 1;
|
|
132
|
+
return { text: text.trim(), assignments: tokens.slice(0, first).map((t) => t.text), tokens: tokens.slice(first), background };
|
|
133
|
+
})
|
|
134
|
+
.filter((segment) => segment.assignments.length > 0 || segment.tokens.length > 0);
|
|
135
|
+
}
|
|
136
|
+
const HEREDOC_OPEN = /<<(-?)\s*(["']?)([A-Za-z_][A-Za-z0-9_]*)\2/g;
|
|
137
|
+
/** Remove heredoc BODIES (the stdin data between `<<DELIM` and its closing
|
|
138
|
+
* `DELIM` line) from a command before it is read. The opening line is kept —
|
|
139
|
+
* `crtr push final <<'EOF'` is still a real `push final` invocation — but the
|
|
140
|
+
* body lines, which are data fed on stdin and never executed as commands, are
|
|
141
|
+
* dropped. Without this, a doc-writing command whose body merely MENTIONS a
|
|
142
|
+
* matched invocation would have its whole call held or refused. */
|
|
143
|
+
export function stripHeredocs(command) {
|
|
144
|
+
const lines = command.split('\n');
|
|
145
|
+
const out = [];
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < lines.length) {
|
|
148
|
+
const line = lines[i];
|
|
149
|
+
out.push(line);
|
|
150
|
+
i += 1;
|
|
151
|
+
// Every heredoc opened on this line, left to right; their bodies stack and
|
|
152
|
+
// are consumed in that order.
|
|
153
|
+
const delims = [];
|
|
154
|
+
HEREDOC_OPEN.lastIndex = 0;
|
|
155
|
+
let m;
|
|
156
|
+
while ((m = HEREDOC_OPEN.exec(line)) !== null)
|
|
157
|
+
delims.push({ name: m[3], dash: m[1] === '-' });
|
|
158
|
+
for (const d of delims) {
|
|
159
|
+
while (i < lines.length) {
|
|
160
|
+
// `<<-` strips leading TABS from the body and the closing delimiter.
|
|
161
|
+
const probe = d.dash ? lines[i].replace(/^\t+/, '') : lines[i];
|
|
162
|
+
i += 1;
|
|
163
|
+
if (probe === d.name)
|
|
164
|
+
break; // closing delimiter line — drop it, stop
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out.join('\n');
|
|
169
|
+
}
|
|
@@ -20,15 +20,6 @@ export declare function matchesReadEntry(entry: SurfaceEntry, doc: Pick<Substrat
|
|
|
20
20
|
export declare function matchesMemoryReadEntry(entry: SurfaceEntry, routingAnchor: string, subject: NodeConfigSubject | null, name: string): boolean;
|
|
21
21
|
/** Does a `command` entry fit the executed command string? */
|
|
22
22
|
export declare function matchesCommandEntry(entry: SurfaceEntry, subject: NodeConfigSubject | null, command: string): boolean;
|
|
23
|
-
/** Remove heredoc BODIES (the stdin data between `<<DELIM` and its closing
|
|
24
|
-
* `DELIM` line) from a command before it is matched. The opening line is kept
|
|
25
|
-
* — `crtr push final <<'EOF'` is still a real `push final` invocation — but the
|
|
26
|
-
* body lines, which are data fed on stdin and never executed as commands, are
|
|
27
|
-
* dropped. Without this a doc-writing command whose body merely MENTIONS a
|
|
28
|
-
* matched invocation would have its whole call held. Post-execution `command`
|
|
29
|
-
* delivery does not need this (a spurious late injection costs a paragraph);
|
|
30
|
-
* a `pre-command` block costs the agent a turn, so it does. */
|
|
31
|
-
export declare function stripHeredocs(command: string): string;
|
|
32
23
|
/** Does a `pre-command` entry fit a command that is ABOUT to run? The same
|
|
33
24
|
* glob machinery `command` uses — one matcher, one semantics — applied to the
|
|
34
25
|
* command with its heredoc bodies stripped. Subshell parentheses, backticks,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// container (its owning repo dir for `read`, its canonical routing anchor for
|
|
11
11
|
// `memory-read`).
|
|
12
12
|
import { basename, isAbsolute, matchesGlob, relative, sep } from 'node:path';
|
|
13
|
+
import { commandSegments, stripHeredocs } from '../shell-segments.js';
|
|
13
14
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
14
15
|
import { realpathOrSelf } from '../fs-utils.js';
|
|
15
16
|
import { evalCondition } from '../predicate.js';
|
|
@@ -102,129 +103,6 @@ export function matchesMemoryReadEntry(entry, routingAnchor, subject, name) {
|
|
|
102
103
|
return false;
|
|
103
104
|
return entry.match.some((g) => memoryReadGlobMatches(g, routingAnchor, name));
|
|
104
105
|
}
|
|
105
|
-
/** Split a command into shell segments on unquoted `;`, newline, `&&`, `||`,
|
|
106
|
-
* and `|`. A single `&`, subshell parentheses, and backticks are left inside
|
|
107
|
-
* their segment — this is a best-effort guardrail, not a shell parser.
|
|
108
|
-
* Heredoc bodies are a separate pre-match concern (see `stripHeredocs`). */
|
|
109
|
-
function splitCommandSegments(command) {
|
|
110
|
-
const segments = [];
|
|
111
|
-
let current = '';
|
|
112
|
-
let quote = null;
|
|
113
|
-
let escaped = false;
|
|
114
|
-
const finish = () => {
|
|
115
|
-
segments.push(current);
|
|
116
|
-
current = '';
|
|
117
|
-
};
|
|
118
|
-
for (let i = 0; i < command.length; i += 1) {
|
|
119
|
-
const ch = command[i];
|
|
120
|
-
if (escaped) {
|
|
121
|
-
current += ch;
|
|
122
|
-
escaped = false;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (ch === '\\' && quote !== "'") {
|
|
126
|
-
current += ch;
|
|
127
|
-
escaped = true;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (quote !== null) {
|
|
131
|
-
current += ch;
|
|
132
|
-
if (ch === quote)
|
|
133
|
-
quote = null;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (ch === "'" || ch === '"') {
|
|
137
|
-
current += ch;
|
|
138
|
-
quote = ch;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (ch === '\n' || ch === ';') {
|
|
142
|
-
finish();
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if ((ch === '&' || ch === '|') && command[i + 1] === ch) {
|
|
146
|
-
finish();
|
|
147
|
-
i += 1;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (ch === '|') {
|
|
151
|
-
finish();
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
current += ch;
|
|
155
|
-
}
|
|
156
|
-
finish();
|
|
157
|
-
return segments;
|
|
158
|
-
}
|
|
159
|
-
/** Tokenize one segment into shell words, marking quoted spans opaque. */
|
|
160
|
-
function tokenizeCommandSegment(segment) {
|
|
161
|
-
const tokens = [];
|
|
162
|
-
let text = '';
|
|
163
|
-
let opaque = false;
|
|
164
|
-
let started = false;
|
|
165
|
-
let quote = null;
|
|
166
|
-
let escaped = false;
|
|
167
|
-
const finish = () => {
|
|
168
|
-
if (started)
|
|
169
|
-
tokens.push({ text, opaque });
|
|
170
|
-
text = '';
|
|
171
|
-
opaque = false;
|
|
172
|
-
started = false;
|
|
173
|
-
};
|
|
174
|
-
for (let i = 0; i < segment.length; i += 1) {
|
|
175
|
-
const ch = segment[i];
|
|
176
|
-
if (escaped) {
|
|
177
|
-
text += ch;
|
|
178
|
-
started = true;
|
|
179
|
-
escaped = false;
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (ch === '\\' && quote !== "'") {
|
|
183
|
-
escaped = true;
|
|
184
|
-
started = true;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (quote !== null) {
|
|
188
|
-
started = true;
|
|
189
|
-
if (ch === quote) {
|
|
190
|
-
quote = null;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
text += ch;
|
|
194
|
-
}
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (ch === "'" || ch === '"') {
|
|
198
|
-
quote = ch;
|
|
199
|
-
opaque = true;
|
|
200
|
-
started = true;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (/\s/.test(ch)) {
|
|
204
|
-
finish();
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
text += ch;
|
|
208
|
-
started = true;
|
|
209
|
-
}
|
|
210
|
-
if (escaped)
|
|
211
|
-
text += '\\';
|
|
212
|
-
finish();
|
|
213
|
-
return tokens;
|
|
214
|
-
}
|
|
215
|
-
/** Every non-empty command segment as its env-stripped token list: leading
|
|
216
|
-
* `VAR=value` assignments are dropped so `tokens[0]` is the executable. */
|
|
217
|
-
function commandSegments(command) {
|
|
218
|
-
return splitCommandSegments(command)
|
|
219
|
-
.map(tokenizeCommandSegment)
|
|
220
|
-
.map((tokens) => {
|
|
221
|
-
let first = 0;
|
|
222
|
-
while (first < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[first].text))
|
|
223
|
-
first += 1;
|
|
224
|
-
return tokens.slice(first);
|
|
225
|
-
})
|
|
226
|
-
.filter((tokens) => tokens.length > 0);
|
|
227
|
-
}
|
|
228
106
|
function commandGlobPattern(glob) {
|
|
229
107
|
const words = glob.trim().split(/\s+/);
|
|
230
108
|
if (words.length === 1 && words[0] === '')
|
|
@@ -275,7 +153,9 @@ function commandGlobMatches(glob, command) {
|
|
|
275
153
|
const pattern = commandGlobPattern(glob);
|
|
276
154
|
if (pattern === null)
|
|
277
155
|
return false;
|
|
278
|
-
return commandSegments(command)
|
|
156
|
+
return commandSegments(command)
|
|
157
|
+
.filter(({ tokens }) => tokens.length > 0)
|
|
158
|
+
.some(({ tokens }) => segmentMatches(pattern, tokens));
|
|
279
159
|
}
|
|
280
160
|
/** Does a `command` entry fit the executed command string? */
|
|
281
161
|
export function matchesCommandEntry(entry, subject, command) {
|
|
@@ -283,42 +163,6 @@ export function matchesCommandEntry(entry, subject, command) {
|
|
|
283
163
|
return false;
|
|
284
164
|
return entry.match.some((g) => commandGlobMatches(g, command));
|
|
285
165
|
}
|
|
286
|
-
const HEREDOC_OPEN = /<<(-?)\s*(["']?)([A-Za-z_][A-Za-z0-9_]*)\2/g;
|
|
287
|
-
/** Remove heredoc BODIES (the stdin data between `<<DELIM` and its closing
|
|
288
|
-
* `DELIM` line) from a command before it is matched. The opening line is kept
|
|
289
|
-
* — `crtr push final <<'EOF'` is still a real `push final` invocation — but the
|
|
290
|
-
* body lines, which are data fed on stdin and never executed as commands, are
|
|
291
|
-
* dropped. Without this a doc-writing command whose body merely MENTIONS a
|
|
292
|
-
* matched invocation would have its whole call held. Post-execution `command`
|
|
293
|
-
* delivery does not need this (a spurious late injection costs a paragraph);
|
|
294
|
-
* a `pre-command` block costs the agent a turn, so it does. */
|
|
295
|
-
export function stripHeredocs(command) {
|
|
296
|
-
const lines = command.split('\n');
|
|
297
|
-
const out = [];
|
|
298
|
-
let i = 0;
|
|
299
|
-
while (i < lines.length) {
|
|
300
|
-
const line = lines[i];
|
|
301
|
-
out.push(line);
|
|
302
|
-
i += 1;
|
|
303
|
-
// Every heredoc opened on this line, left to right; their bodies stack and
|
|
304
|
-
// are consumed in that order.
|
|
305
|
-
const delims = [];
|
|
306
|
-
HEREDOC_OPEN.lastIndex = 0;
|
|
307
|
-
let m;
|
|
308
|
-
while ((m = HEREDOC_OPEN.exec(line)) !== null)
|
|
309
|
-
delims.push({ name: m[3], dash: m[1] === '-' });
|
|
310
|
-
for (const d of delims) {
|
|
311
|
-
while (i < lines.length) {
|
|
312
|
-
// `<<-` strips leading TABS from the body and the closing delimiter.
|
|
313
|
-
const probe = d.dash ? lines[i].replace(/^\t+/, '') : lines[i];
|
|
314
|
-
i += 1;
|
|
315
|
-
if (probe === d.name)
|
|
316
|
-
break; // closing delimiter line — drop it, stop
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return out.join('\n');
|
|
321
|
-
}
|
|
322
166
|
/** Does a `pre-command` entry fit a command that is ABOUT to run? The same
|
|
323
167
|
* glob machinery `command` uses — one matcher, one semantics — applied to the
|
|
324
168
|
* command with its heredoc bodies stripped. Subshell parentheses, backticks,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { createBashToolDefinition, type BashOperations } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
|
-
/** Seconds a leading `sleep ...` will block for, or null when the command does
|
|
4
|
-
* not open with a sleep (or its duration isn't statically knowable). */
|
|
5
|
-
export declare function leadingSleepSeconds(command: string): number | null;
|
|
6
3
|
/** The valve's BashOperations backend. Each tool execution closes over its own
|
|
7
4
|
* `takePurpose` and its own preview-result path, so concurrent calls cannot
|
|
8
5
|
* exchange labels or result mirrors. The purpose is consumed even when the
|
|
@@ -31,6 +31,7 @@ import { homedir } from 'node:os';
|
|
|
31
31
|
import { dirname } from 'node:path';
|
|
32
32
|
import { performance } from 'node:perf_hooks';
|
|
33
33
|
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose, readBashNiceness, writeJobDeadlineMs } from '../core/bash-jobs.js';
|
|
34
|
+
import { bashCommandRefusal } from '../core/bash-guard.js';
|
|
34
35
|
import { readConfig } from '../core/config.js';
|
|
35
36
|
import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-result-path.js';
|
|
36
37
|
import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
@@ -164,39 +165,6 @@ function readExitCode(path) {
|
|
|
164
165
|
return null;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
// Long-sleep guard. A command that opens with `sleep <long>` is a node burning
|
|
168
|
-
// a live window to wait on a clock — the runtime already has waiting primitives
|
|
169
|
-
// that cost nothing (`crtr cron` for a scheduled wake, `crtr node wait deadline`
|
|
170
|
-
// for an inbox-vs-deadline race). Anything over 10s is refused before it runs.
|
|
171
|
-
const MAX_INLINE_SLEEP_SECONDS = 10;
|
|
172
|
-
const SLEEP_UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
173
|
-
/** Seconds a leading `sleep ...` will block for, or null when the command does
|
|
174
|
-
* not open with a sleep (or its duration isn't statically knowable). */
|
|
175
|
-
export function leadingSleepSeconds(command) {
|
|
176
|
-
const match = /^\s*sleep(\s+[^;&|\n]+)/.exec(command);
|
|
177
|
-
if (!match)
|
|
178
|
-
return null;
|
|
179
|
-
// `sleep 1 2m 3` sums its operands.
|
|
180
|
-
let total = 0;
|
|
181
|
-
for (const operand of match[1].trim().split(/\s+/)) {
|
|
182
|
-
const parsed = /^([0-9]*\.?[0-9]+)([smhd]?)$/.exec(operand);
|
|
183
|
-
if (!parsed)
|
|
184
|
-
return null; // variable/expansion/flag — not statically knowable
|
|
185
|
-
total += Number(parsed[1]) * SLEEP_UNIT_SECONDS[parsed[2] || 's'];
|
|
186
|
-
}
|
|
187
|
-
return total;
|
|
188
|
-
}
|
|
189
|
-
function longSleepRefusal(command) {
|
|
190
|
-
const seconds = leadingSleepSeconds(command);
|
|
191
|
-
if (seconds === null || seconds <= MAX_INLINE_SLEEP_SECONDS)
|
|
192
|
-
return null;
|
|
193
|
-
return (`Refused: this command starts by sleeping ${seconds}s, over the ${MAX_INLINE_SLEEP_SECONDS}s inline limit.\n` +
|
|
194
|
-
`Holding a live window on a clock is never how you wait here. Instead:\n` +
|
|
195
|
-
` - schedule the wake and end your turn: crtr cron -h (one-shot at a time, or recurring)\n` +
|
|
196
|
-
` - race an inbox message against a deadline: crtr node wait deadline -h\n` +
|
|
197
|
-
` - waiting on a child, a reply from the user, or any spine event: just stop — the runtime wakes you.\n` +
|
|
198
|
-
`If you truly need a short pause inline, sleep ${MAX_INLINE_SLEEP_SECONDS}s or less.`);
|
|
199
|
-
}
|
|
200
168
|
/** A node can legitimately remove its own merged worktree. Never recreate that
|
|
201
169
|
* path: it would look like a valid checkout but no longer be one. Run later
|
|
202
170
|
* commands from a durable directory instead, and make the changed semantics
|
|
@@ -228,7 +196,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
228
196
|
reject(new Error('aborted'));
|
|
229
197
|
return;
|
|
230
198
|
}
|
|
231
|
-
const refusal =
|
|
199
|
+
const refusal = bashCommandRefusal(command, cwd);
|
|
232
200
|
if (refusal !== null) {
|
|
233
201
|
reject(new Error(refusal));
|
|
234
202
|
return;
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.303",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.303",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"workspaces": [
|
|
@@ -5206,17 +5206,17 @@
|
|
|
5206
5206
|
},
|
|
5207
5207
|
"packages/crouter-api": {
|
|
5208
5208
|
"name": "@north-light/crouter-api",
|
|
5209
|
-
"version": "0.3.
|
|
5209
|
+
"version": "0.3.303",
|
|
5210
5210
|
"license": "UNLICENSED"
|
|
5211
5211
|
},
|
|
5212
5212
|
"packages/crouter-env-docker": {
|
|
5213
5213
|
"name": "@north-light/crouter-env-docker",
|
|
5214
|
-
"version": "0.3.
|
|
5214
|
+
"version": "0.3.303",
|
|
5215
5215
|
"license": "UNLICENSED"
|
|
5216
5216
|
},
|
|
5217
5217
|
"packages/crouter-sdk": {
|
|
5218
5218
|
"name": "@north-light/crouter-sdk",
|
|
5219
|
-
"version": "0.3.
|
|
5219
|
+
"version": "0.3.303",
|
|
5220
5220
|
"license": "UNLICENSED",
|
|
5221
5221
|
"dependencies": {
|
|
5222
5222
|
"@north-light/crouter-api": "^0.3.295"
|