Skip to content

Commit c2135ca

Browse files
committed
fix: engine and gateway hardening
- Parse Bash heredocs when scanning commands so quoted heredoc bodies no longer trigger extra approval prompts; raise the parse budget to 500ms. - Let the Bash tool run with a cwd outside the workspace roots; the terminal service keeps its guard. - Keep the gateway process alive on uncaughtException (log instead of exit). - Report the journaled event watermark as last_seq on session details so clients resume without replaying history. - Drop null fields when flattening telemetry events; track the session index mirror give-up event once per failure streak. - Allow injecting a pino destination stream for tests.
1 parent 91b1311 commit c2135ca

26 files changed

Lines changed: 581 additions & 42 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Allow the Bash tool to run with a working directory outside the workspace roots.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Handle heredocs when scanning Bash commands so quoted heredoc content no longer forces extra approval prompts.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Keep the gateway server running after an unexpected error instead of exiting the process.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Fetch the current event position with session details so clients resume without replaying past events.

packages/agent-core-v2/src/agent/agentsMdReminder/agentsMdReminderService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { extractBashTargetDirs } from './bashTargets';
4141

4242
const AGENTS_MD_BASENAMES: ReadonlySet<string> = new Set<string>(AGENTS_MD_PLAIN_NAMES);
4343

44-
const BASH_PARSE_OPTIONS = { timeoutMs: 20, maxNodes: 10_000 } as const;
44+
const BASH_PARSE_OPTIONS = { timeoutMs: 500, maxNodes: 10_000 } as const;
4545

4646
export const agentsMdReminderKnownKey = defineState<Set<string>>(
4747
'agentsMdReminder.known',

packages/agent-core-v2/src/agent/permissionPolicy/policies/dangerous-command-ask.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
PermissionPolicyResult,
1313
} from '#/agent/permissionPolicy/types';
1414

15-
const PARSE_OPTIONS = { timeoutMs: 20, maxNodes: 10_000 } as const;
15+
const PARSE_OPTIONS = { timeoutMs: 500, maxNodes: 10_000 } as const;
1616

1717
const MAX_NESTED_SHELL_DEPTH = 4;
1818

packages/agent-core-v2/src/app/sessionIndex/sessionIndexMirrorService.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro
3737
private readonly timer = this._register(new IntervalTimer({ unref: true }));
3838
private flushing: Promise<void> | undefined;
3939
private consecutiveFailures = 0;
40+
private giveUpTracked = false;
4041
private disposed = false;
4142
private overflowLogged = false;
4243

@@ -169,6 +170,7 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro
169170
if (this.pendingMap.get(id) === summary) this.pendingMap.delete(id);
170171
}
171172
this.consecutiveFailures = 0;
173+
this.giveUpTracked = false;
172174
} catch (error) {
173175
this.consecutiveFailures += 1;
174176
this.log.warn('failed to flush session index mirror chunk', {
@@ -180,10 +182,13 @@ export class SessionIndexMirror extends Disposable implements ISessionIndexMirro
180182
this.log.warn('session index mirror giving up until the next record; reconciliation will heal', {
181183
pending: this.pendingMap.size,
182184
});
183-
this.telemetry.track2('session_index_mirror_give_up', {
184-
pending_count: this.pendingMap.size,
185-
consecutive_failures: this.consecutiveFailures,
186-
});
185+
if (!this.giveUpTracked) {
186+
this.giveUpTracked = true;
187+
this.telemetry.track2('session_index_mirror_give_up', {
188+
pending_count: this.pendingMap.size,
189+
consecutive_failures: this.consecutiveFailures,
190+
});
191+
}
187192
}
188193
}
189194
}

packages/agent-core-v2/src/app/telemetry/cloudTransport.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ export function flattenEvent(event: EnrichedCloudEvent): Record<string, CloudPri
263263
flattenNested(out, 'context', value);
264264
} else {
265265
assertPrimitive(key, value);
266-
out[key] = value;
266+
if (value !== null) {
267+
out[key] = value;
268+
}
267269
}
268270
}
269271
return out;
@@ -285,7 +287,9 @@ function flattenNested(target: Record<string, CloudPrimitive>, prefix: string, v
285287
if (value === null || typeof value !== 'object' || Array.isArray(value)) return;
286288
for (const [key, nestedValue] of Object.entries(value)) {
287289
assertPrimitive(`${prefix}.${key}`, nestedValue);
288-
target[`${prefix}_${key}`] = nestedValue;
290+
if (nestedValue !== null) {
291+
target[`${prefix}_${key}`] = nestedValue;
292+
}
289293
}
290294
}
291295

packages/agent-core-v2/src/runtime/runtimeWorkspaceView.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ export class RuntimeWorkspaceView {
3030
resolve(path: string, cwd = this.workDir): string {
3131
const env = this.runtime.environment;
3232
const bridged = env.pathClass === 'win32' ? getShellPathBridge(env).fromShellPath(path) : path;
33-
const resolved = this.runtime.path.isAbsolute(bridged)
33+
return this.runtime.path.isAbsolute(bridged)
3434
? this.runtime.path.resolve(bridged)
3535
: this.runtime.path.resolve(cwd, bridged);
36-
this.assertAllowed(resolved);
37-
return resolved;
3836
}
3937

40-
assertAllowed(path: string): void {
38+
assertAllowed(path: string): string {
4139
const resolved = this.runtime.path.resolve(path);
42-
if (this.roots.some((root) => contains(this.runtime, root, resolved))) return;
40+
if (this.roots.some((root) => contains(this.runtime, root, resolved))) return resolved;
4341
throw new Error2(
4442
ErrorCodes.FS_PATH_ESCAPES,
4543
`path ${path} is outside runtime workspace ${this.binding.runtimeId}`,

packages/agent-core-v2/src/session/terminal/terminalService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class SessionTerminalService extends Disposable implements ISessionTermin
7979
['terminal'],
8080
);
8181
const view = new RuntimeWorkspaceView(lease.runtime, this.workspace);
82-
const cwd = input.cwd === undefined ? view.workDir : view.resolve(input.cwd);
82+
const cwd = input.cwd === undefined ? view.workDir : view.assertAllowed(view.resolve(input.cwd));
8383
const shell = input.shell ?? lease.runtime.environment.shellPath;
8484
let process: TerminalProcess;
8585
try {

0 commit comments

Comments
 (0)