Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/windows-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ on:
- 'packages/runtime/src/sandbox/windows-sandbox.ts'
- 'packages/runtime/src/shell-detect.ts'
- 'packages/runtime/src/shell-exec.ts'
- 'packages/runtime/src/tool-runtime.ts'
- 'packages/storage/src/__tests__/managed-dependency-environment.test.ts'
- 'packages/storage/src/__tests__/root-authority.test.ts'
- 'packages/storage/src/artifact-store.ts'
Expand Down
5 changes: 3 additions & 2 deletions docs/windows-test-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ Locations intentionally omit line numbers so unrelated edits do not invalidate t
|---|---:|
| windows-backend-gap | 27 |
| portable-candidate | 18 |
| platform-contract | 31 |
| platform-contract | 32 |

Total Windows-excluded declarations: **76**
Total Windows-excluded declarations: **77**

## Inventory

Expand Down Expand Up @@ -66,6 +66,7 @@ Total Windows-excluded declarations: **76**
| platform-contract | `packages/runtime/src/__tests__/filesystem-worker-process-runner.test.ts` filesystem worker rejects boundedly when a detached descendant retains stdout | `process.platform === 'win32' ? 'POSIX detached process-group semantics required' : false` |
| platform-contract | `packages/runtime/src/__tests__/filesystem-worker-smoke.test.ts` macOS filesystem worker smoke | `process.platform !== 'darwin'` |
| portable-candidate | `packages/runtime/src/__tests__/node-pty-write-lifecycle.test.ts` does not carry queued Unix PTY writes past native exit | `process.platform === 'win32' ? 'Unix PTY file-descriptor lifecycle only' : false` |
| platform-contract | `packages/runtime/src/__tests__/pre-dispatch-refusal-ledger.test.ts` persistent path denies canonicalize case-insensitive macOS paths | `process.platform !== 'darwin'` |
| portable-candidate | `packages/runtime/src/__tests__/shell-exec.test.ts` writes a legacy WSL Bash command through stdin | `process.platform === 'win32' ? 'uses /bin/sh as a portable stdin probe' : false` |
| platform-contract | `packages/runtime/src/__tests__/shell-exec.test.ts` bounds output drain after the root exits while a detached descendant retains stdout | `process.platform === 'win32' ? 'POSIX detached process-group semantics required' : false` |
| platform-contract | `packages/runtime/src/__tests__/shell-run-manager.test.ts` latches timeout when the root exits during POSIX process discovery | `process.platform === 'win32' ? 'POSIX process discovery only' : false` |
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ Maka asks before privileged tool operations by default. `maka run --yolo` grants
and network access and should only be used in an environment you are prepared to let the task
modify.

## Persistent permission rules

The Runtime Host can persist explicit deny rules for commands and filesystem paths. They are checked
before tool dispatch, including when a turn uses `--yolo`:

```sh
maka permissions deny-command 'git commit *'
maka permissions deny-command 'git push *'
maka permissions deny-path /mnt --scope subtree
maka permissions deny-path /etc/wsl.conf --scope exact
maka permissions list
```

Use `remove-command` or `remove-path` with the same value and scope to remove a rule. Paths must be
absolute. Command patterns use fragment-level glob matching (`*` and `?`), not regular expressions;
character classes are not supported, and Unicode command text is allowed. Bash and PTY input are
split conservatively at newlines and `;|&`; this is not a full shell parser, so expansion and command
substitution are outside the rule's guarantee. Filesystem rules are canonicalized using the Runtime
Host's actual filesystem semantics, including macOS case sensitivity and symlinks. A native
provider-side `apply_patch` is hidden whenever path denies are active, and an existing backend
refreshes that routing before the next provider request. Arbitrary MCP tool arguments are not
treated as filesystem paths; MCP servers must enforce their own path permissions. Unmatched
operations continue to use the Session permission mode and sandbox. To manage a different local or
remote Runtime Host, pass `--root <path>` and, where applicable, `--host <profile-id>`.

## Upgrade

While using prereleases, keep the `next` tag explicit:
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ maka run --help
Maka 默认会在执行高权限工具操作前询问。`maka run --yolo` 会授予该任务完整的文件和网络
权限,只应在你允许任务修改的环境中使用。

## 持久化权限拒绝规则

Runtime Host 支持为命令和文件路径持久化显式拒绝规则。规则会在工具真正执行前检查,
即使 Turn 使用了 `--yolo` 也仍然生效:

```sh
maka permissions deny-command 'git commit *'
maka permissions deny-command 'git push *'
maka permissions deny-path /mnt --scope subtree
maka permissions deny-path /etc/wsl.conf --scope exact
maka permissions list
```

删除规则时,使用相同值和 scope 的 `remove-command` 或 `remove-path`。路径必须是绝对路径;
命令模式使用分片级 glob 匹配(`*` 和 `?`),不是正则表达式;不支持字符类,但支持 Unicode
命令文本。Bash 和 PTY 输入会在换行及 `;|&` 处分片;这不是完整的 shell parser,因此展开和
命令替换不在规则保证范围内。文件系统规则会使用 Runtime Host 实际文件系统的语义进行
canonicalize,包括 macOS 的大小写敏感性和符号链接。存在路径拒绝规则时,provider-side 的
原生 `apply_patch` 会隐藏;已有 backend 会在下一次 provider 请求前刷新该路由。MCP 任意工具
参数不会被当作文件系统路径提取,MCP server 需要自行执行路径权限控制。没有匹配规则的操作
继续遵循当前 Session 的 permission mode 和 sandbox。要管理其他本地或远程 Runtime Host,
可传入 `--root <path>`,并在需要时传入 `--host <profile-id>`。

## 升级

使用预发布版本时,请继续明确指定 `next`:
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('Maka CLI args', () => {
assert.match(help.text, /^ maka run /m);
assert.match(help.text, /^ maka activate /m);
assert.match(help.text, /^ maka eval /m);
assert.match(help.text, /^ maka permissions list /m);
assert.match(help.text, /^ maka update --target /m);
assert.match(
help.text,
Expand Down
130 changes: 130 additions & 0 deletions packages/cli/src/__tests__/permissions-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createDefaultRuntimePolicy, type RuntimePolicySnapshot } from '@maka/core/runtime-policy';
import type { RuntimeHostCliConnectionContext } from '../runtime-host-cli-context.js';
import { parsePermissionsCommand } from '../permissions-command-parser.js';
import { runPermissionsCli, updatePermissionRules } from '../permissions-command.js';

test('parses permission list and mutation commands', () => {
assert.deepEqual(
parsePermissionsCommand([
'deny-command',
'git push *',
'--root',
'/srv/maka',
'--host',
'office',
]),
{
kind: 'permissions',
action: { kind: 'deny-command', pattern: 'git push *' },
rootPath: '/srv/maka',
hostProfileId: 'office',
},
);
assert.deepEqual(parsePermissionsCommand(['deny-path', '/mnt/**', '--scope', 'subtree']), {
kind: 'permissions',
action: { kind: 'deny-path', path: '/mnt', scope: 'subtree' },
});
assert.deepEqual(parsePermissionsCommand(['list', '--scope', 'exact']), {
kind: 'error',
message: 'permissions list does not accept a path or --scope',
exitCode: 2,
});
assert.deepEqual(parsePermissionsCommand(['remove-path', 'relative', '--scope', 'exact']), {
kind: 'error',
message: 'permissions remove-path requires an absolute path',
exitCode: 2,
});
});

test('updates permission rules canonically and removes normalized paths', () => {
const current = updatePermissionRules(
{ denyCommands: [], denyPaths: [] },
{ kind: 'deny-path', path: '/mnt/', scope: 'subtree' },
);
assert.deepEqual(current, {
denyCommands: [],
denyPaths: [{ path: '/mnt', scope: 'subtree' }],
});
assert.deepEqual(
updatePermissionRules(current, { kind: 'remove-path', path: '/mnt/', scope: 'subtree' }),
{ denyCommands: [], denyPaths: [] },
);
});

test('queries and CAS-mutates the Host-owned permission rules', async () => {
const requests: { operation: string; input: unknown }[] = [];
const initial: RuntimePolicySnapshot = {
revision: 7,
policy: createDefaultRuntimePolicy(),
};
const context = {
connection: {
request: async (operation: string, input: unknown) => {
requests.push({ operation, input });
if (operation === 'runtime.policy.query') return initial;
return { kind: 'committed', revision: 8 };
},
},
close: async () => {},
} as unknown as RuntimeHostCliConnectionContext;
let output = '';
const exitCode = await runPermissionsCli(
{
kind: 'permissions',
action: { kind: 'deny-command', pattern: 'git commit *' },
},
{ defaultRootPath: '/state', clientDataRoot: '/client' },
{
connect: async (input) => {
assert.deepEqual(input, {
rootPath: '/state',
clientDataRoot: '/client',
});
return context;
},
write: (value) => {
output += value;
},
},
);
assert.equal(exitCode, 0);
assert.deepEqual(requests, [
{ operation: 'runtime.policy.query', input: {} },
{
operation: 'runtime.policy.mutate',
input: {
expectedRevision: 7,
operation: {
kind: 'set_permission_rules',
value: { denyCommands: ['git commit *'], denyPaths: [] },
},
},
},
]);
assert.deepEqual(JSON.parse(output), {
revision: 8,
denyCommands: ['git commit *'],
denyPaths: [],
});
});
18 changes: 18 additions & 0 deletions packages/cli/src/cli-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import {
parseRuntimeHostInstalledUpdateCommand,
type RuntimeHostCliCommand,
} from './runtime-host-cli.js';
import {
parsePermissionsCommand,
type PermissionCliCommand,
} from './permissions-command-parser.js';
import { resolveCliUiLocale } from './cli-ui-locale.js';

export type MakaCliCommand =
Expand All @@ -44,6 +48,7 @@ export type MakaCliCommand =
| { kind: 'activate'; args: string[] }
| { kind: 'eval'; args: string[] }
| { kind: 'acp' }
| PermissionCliCommand
| RuntimeHostCliCommand
| { kind: 'help'; text: string }
| { kind: 'version'; text: string }
Expand Down Expand Up @@ -84,6 +89,7 @@ export function parseMakaCliArgs(
if (first === 'run' || first === '-p') return { kind: 'run', args: argv.slice(1) };
if (first === 'activate') return { kind: 'activate', args: argv.slice(1) };
if (first === 'eval') return { kind: 'eval', args: argv.slice(1) };
if (first === 'permissions') return parsePermissionsCommand(argv.slice(1));
if (first === 'update') return parseRuntimeHostInstalledUpdateCommand(argv.slice(1));
if (first === 'runtime-host') return parseRuntimeHostCommand(argv.slice(1));
return {
Expand Down Expand Up @@ -138,6 +144,11 @@ function helpText(cliCommand: string): string {
` ${cliCommand} activate ... Run one Cloud Session activation and emit JSONL`,
` ${cliCommand} -p ... Alias for ${cliCommand} run`,
` ${cliCommand} eval ... Run one declarative multi-arm experiment`,
` ${cliCommand} permissions list [--root <path>] [--host <profile-id>]`,
` ${cliCommand} permissions deny-command <glob> [--root <path>] [--host <profile-id>]`,
` ${cliCommand} permissions deny-path <path> --scope <exact|subtree> [--root <path>] [--host <profile-id>]`,
` ${cliCommand} permissions remove-command <glob> [--root <path>] [--host <profile-id>]`,
` ${cliCommand} permissions remove-path <path> --scope <exact|subtree> [--root <path>] [--host <profile-id>]`,
` ${cliCommand} update --target <latest|next|version> Update this npm-global CLI and its local Runtime Host`,
` ${cliCommand} runtime-host serve [options] Run a Runtime Host service`,
` ${cliCommand} runtime-host activate --framed --root-id <id>`,
Expand Down Expand Up @@ -299,6 +310,13 @@ export async function runMakaCli(
const { runMakaEvalCli } = await import('@maka/eval');
return runMakaEvalCli(command.args);
}
case 'permissions': {
const { runPermissionsCli } = await import('./permissions-command.js');
return runPermissionsCli(command, {
defaultRootPath: dataRoots.workspaceRoot,
clientDataRoot: dataRoots.clientDataRoot,
});
}
case 'acp': {
const { runMakaAcpStdioServer } = await import('./acp/stdio-server.js');
return runMakaAcpStdioServer({
Expand Down
Loading
Loading