Skip to content

Commit 2144e42

Browse files
authored
Merge branch 'main' into hardening/egress-defaults
2 parents e7365f2 + 2958008 commit 2144e42

27 files changed

Lines changed: 804 additions & 29 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@moonshot-ai/agent-core-v2': patch
3+
'@moonshot-ai/agent-core': patch
4+
---
5+
6+
Extend the URL-fetch private-address blocklist to cover NAT64-embedded IPv4
7+
ranges (RFC 6052 `64:ff9b::/96` and the RFC 8215 local-use prefix), and give the
8+
capability downloader optional SHA-256 verification that discards an artifact
9+
whose digest does not match. The macOS computer-use bundle keeps its quarantine
10+
attribute so Gatekeeper still evaluates it.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@moonshot-ai/agent-core-v2': minor
3+
---
4+
5+
Make the permission layer enforce what it is configured to enforce:
6+
7+
- Seed each agent with the user's `[permission]` rules from config. The rules Op
8+
is not persisted, so the model always started empty and configured
9+
allow/deny/ask rules never reached the policy chain.
10+
- Order the sensitive-file and git-control asks ahead of the blanket auto-mode
11+
approval, so auto mode no longer waives them. An explicit prior approval or a
12+
user `allow` rule still wins, so nothing already approved re-prompts.
13+
- Fail closed when no approval broker is bound: a call that reached the "ask"
14+
path is now blocked instead of being treated as approved.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@moonshot-ai/agent-core-v2': minor
3+
'@moonshot-ai/agent-core': minor
4+
---
5+
6+
Widen sensitive-file coverage for the file tools. Credential directories
7+
(`.ssh`, `.gnupg`, `.aws`, `.azure`, `.kube`, `.config/gcloud`, the CLI's own
8+
`credentials` dir) are now treated as sensitive whatever the file inside is
9+
called, since private keys and cloud credential files routinely carry local
10+
names a basename list cannot predict. Adds the common credential files
11+
(`.git-credentials`, `.netrc`, `.npmrc`, `.pypirc`, `.docker/config.json`,
12+
`kubeconfig`, the CLI's own `config.toml`). Public keys, `known_hosts`,
13+
`.ssh/config` and `.aws/config` stay readable.
14+
15+
Grep and Glob now apply the sensitive-file check that Read, Write and Edit
16+
already applied, so searching cannot return the contents of files those tools
17+
refuse to open.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@moonshot-ai/agent-core-v2': minor
3+
---
4+
5+
Re-check file-tool paths against their symlink-resolved target before reading,
6+
writing or editing. Path canonicalization is lexical, so a symlink sitting
7+
inside the workspace reads as inside it while the OS follows the link at open
8+
time. Read, Write and Edit now resolve the longest existing prefix of the path
9+
and require that a path which looked in-workspace is still in-workspace once
10+
symlinks are resolved, and that the resolved target is not a sensitive file even
11+
when the link itself is innocuously named. Paths the caller already gave as
12+
outside the workspace are unaffected — those remain the approval layer's call.

‎packages/agent-core-v2/src/agent/permissionPolicy/permissionPolicyService.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,24 @@ export class AgentPermissionPolicyService
4242
@IInstantiationService private readonly instantiation: IInstantiationService,
4343
) {
4444
super();
45+
// Order matters: the first policy to return a result wins.
46+
//
47+
// `AutoModeApprove` sits after the two content-sensitive asks (secrets on
48+
// disk, and the .git control directory) rather than ahead of them, so
49+
// enabling auto mode speeds up ordinary work without also silently
50+
// waiving the checks that exist for the highest-consequence paths.
51+
// Everything else keeps its previous relative order: an explicit prior
52+
// approval (`SessionApprovalHistory`) or a user `allow` rule still wins,
53+
// so this does not re-prompt for something already approved.
4554
this.policies = [
4655
this.instantiation.createInstance(AutoModeAskUserQuestionDenyPermissionPolicyService),
4756
this.instantiation.createInstance(UserConfiguredDenyPermissionPolicyService),
48-
this.instantiation.createInstance(AutoModeApprovePermissionPolicyService),
4957
this.instantiation.createInstance(SessionApprovalHistoryPermissionPolicyService),
5058
this.instantiation.createInstance(UserConfiguredAskPermissionPolicyService),
5159
this.instantiation.createInstance(UserConfiguredAllowPermissionPolicyService),
5260
this.instantiation.createInstance(SensitiveFileAccessAskPermissionPolicyService),
5361
this.instantiation.createInstance(GitControlPathAccessAskPermissionPolicyService),
62+
this.instantiation.createInstance(AutoModeApprovePermissionPolicyService),
5463
this.instantiation.createInstance(YoloModeApprovePermissionPolicyService),
5564
this.instantiation.createInstance(DefaultToolApprovePermissionPolicyService),
5665
this.instantiation.createInstance(GitCwdWriteApprovePermissionPolicyService),

‎packages/agent-core-v2/src/agent/toolApproval/toolApprovalService.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,14 @@ export class AgentToolApprovalService extends Service implements IAgentToolAppro
131131
let response: ApprovalResponse;
132132
const approvalService = this.tryApprovalService();
133133
if (approvalService === undefined) {
134-
response = { decision: 'approved' };
134+
// Fail closed. Reaching here means a policy decided this call needs
135+
// confirmation, but no broker is bound to ask (an embedding host that
136+
// never wired one up). Treating "nobody to ask" as consent would let
137+
// every gated tool call through unreviewed.
138+
response = {
139+
decision: 'rejected',
140+
feedback: 'No approval broker is available to confirm this tool call.',
141+
};
135142
} else {
136143
this.eventBus.publish({ type: 'permission.approval.requested', ...approvalContext });
137144
try {

‎packages/agent-core-v2/src/agent/tools/edit/editTool.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121

2222
import {
2323
extendWorkspaceWithSkillRoots,
24+
assertRealPathAccess,
2425
resolvePathAccessPath,
2526
type WorkspaceConfig,
2627
} from '#/tool/path-access';
2728
import { toInputJsonSchema } from '#/tool/input-schema';
2829
import { literalRulePattern, matchesPathRuleSubject } from '#/tool/rule-match';
2930
import { IFileEditService } from '#/app/edit/fileEdit';
3031
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
32+
import { IHostFileSystem } from '#/os/interface/hostFileSystem';
3133
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
3234
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
3335
import {
@@ -48,6 +50,7 @@ export class EditTool implements IEditTool {
4850

4951
constructor(
5052
@IFileEditService private readonly editor: IFileEditService,
53+
@IHostFileSystem private readonly fs: IHostFileSystem,
5154
@IHostEnvironment private readonly env: IHostEnvironment,
5255
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
5356
@ISessionSkillCatalog private readonly skillCatalog?: ISessionSkillCatalog,
@@ -92,6 +95,11 @@ export class EditTool implements IEditTool {
9295
}
9396

9497
private async execution(args: EditInput, safePath: string): Promise<ExecutableToolResult> {
98+
// The path was canonicalized lexically; re-check it against what the
99+
// symlinks actually resolve to before editing through them.
100+
await assertRealPathAccess(safePath, args.path, this.workspaceConfig, this.fs, {
101+
pathClass: this.env.pathClass,
102+
});
95103
if (args.old_string === args.new_string) {
96104
return {
97105
isError: true,

‎packages/agent-core-v2/src/agent/tools/os/glob/globTool.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export class GlobTool implements IGlobTool {
152152
env: this.env,
153153
workspace: this.workspaceConfig,
154154
operation: 'search',
155-
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: false },
155+
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: true },
156156
});
157157
}
158158
const searchRoots = [path ?? this.workspaceConfig.workspaceDir];

‎packages/agent-core-v2/src/agent/tools/os/grep/grepTool.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class GrepTool implements IGrepTool {
127127
env: this.env,
128128
workspace: this.workspace,
129129
operation: 'search',
130-
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: false },
130+
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: true },
131131
});
132132
}
133133
const searchPaths = [path ?? this.workspace.workspaceDir];

‎packages/agent-core-v2/src/agent/tools/os/read/readTool.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import { registerAgentToolService } from '#/agent/toolRegistry/toolContribution';
3636
import {
3737
extendWorkspaceWithSkillRoots,
38+
assertRealPathAccess,
3839
resolvePathAccessPath,
3940
type WorkspaceConfig,
4041
} from '#/tool/path-access';
@@ -271,6 +272,11 @@ export class ReadTool implements IReadTool {
271272
}
272273

273274
private async execution(args: ReadInput, safePath: string): Promise<ExecutableToolResult> {
275+
// The path was canonicalized lexically; re-check it against what the
276+
// symlinks actually resolve to before reading through them.
277+
await assertRealPathAccess(safePath, args.path, this.workspaceConfig, this.fs, {
278+
pathClass: this.env.pathClass,
279+
});
274280
try {
275281
let stat: Awaited<ReturnType<IHostFileSystem['stat']>>;
276282
try {

0 commit comments

Comments
 (0)