Skip to content
Merged
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
16 changes: 10 additions & 6 deletions config/copilot.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,17 @@
| Auto-approve Permission Requests
|--------------------------------------------------------------------------
|
| When true, all permission requests (except for shell and write)
| are automatically approved when using the Copilot facade (CopilotManager).
| Set to false to require explicit permission handling like the official SDK.
| Controls how permission requests are handled when using the Copilot facade
| (Copilot::run(), Copilot::start(), etc.).
|
| Note: This only applies when using Copilot::run(), Copilot::start(), etc.
| Direct Client usage always requires an explicit onPermissionRequest handler.
| Accepted values:
| "deny-all" - Deny all requests automatically (default, safest)
| "approve-safety" - Approve all except shell and write
| "approve-all" - Approve all requests automatically
| false - No handler injected; onPermissionRequest is required
|
| Note: Direct Client usage always requires an explicit onPermissionRequest handler.
|
*/
'permission_approve' => env('COPILOT_PERMISSION_APPROVE', true),
'permission_approve' => env('COPILOT_PERMISSION_APPROVE', 'deny-all'),
];
34 changes: 15 additions & 19 deletions docs/jp/permission-request.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
# Permission Request

## Auto-approve (デフォルト)
## デフォルト動作(deny-all)

`config/copilot.php`で`permission_approve`が`true`(デフォルト)の場合、`Copilot::run()`や`Copilot::start()`を使う時は自動的にすべてのPermission Requestが許可されます。(ただし危険性の高い`shell`, `write`は除きます)
`config/copilot.php`で`permission_approve`が`"deny-all"`(デフォルト)の場合、`Copilot::run()`や`Copilot::start()`を使う時は自動的にすべてのPermission Requestが**拒否**されます。

公式SDKはすべて拒否がデフォルトですがLaravel版では `Copilot::run()`, `Copilot::start()` で使う時の利便性を優先して許可にしています。

> [!CAUTION]
> ユーザーからのプロンプト入力を許可する使い方の場合は、自動許可は危険なので必ずfalseにしてください。
> readでLaravelプロジェクトのコードを読めるだけでも危険です。
テキスト生成が主な目的の場合、パーミッションは不要なので安全なデフォルトです。

```php
// config/copilot.php
'permission_approve' => env('COPILOT_PERMISSION_APPROVE', true),
'permission_approve' => env('COPILOT_PERMISSION_APPROVE', 'deny-all'),
```

この設定が有効な場合、`onPermissionRequest`を明示的に指定しなくても`PermissionHandler::approveSafety()`が自動的に使われます。

```php
use Revolution\Copilot\Facades\Copilot;

// onPermissionRequestを指定しなくても自動的に全許可
$response = Copilot::run(prompt: 'Hello');
```
## 設定可能な値

公式SDKと同様にデフォルトで拒否したい場合は`false`に設定します。
| 値 | 動作 |
|---|---|
| `"deny-all"` | すべてを自動拒否(**デフォルト**) |
| `"approve-safety"` | `shell`, `write` のみ拒否、他は自動許可 |
| `"approve-all"` | すべてを自動許可 |
| `false` | ハンドラなし → `onPermissionRequest` の指定が必須(公式SDK同様) |

```php
// .env
COPILOT_PERMISSION_APPROVE=false,
COPILOT_PERMISSION_APPROVE="approve-safety"
```

この場合は`onPermissionRequest`の指定が必須になります。
> [!CAUTION]
> ユーザーからのプロンプト入力を許可する使い方の場合は、`"approve-safety"`や`"approve-all"`は危険なので必ず`false`または`"deny-all"`にしてください。
> readでLaravelプロジェクトのコードを読めるだけでも危険です。

## PermissionHandler::approveAll()

Expand Down
26 changes: 20 additions & 6 deletions src/CopilotManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,29 @@ public function createSession(SessionConfig|array $config = []): CopilotSession
/**
* Ensure the config has an onPermissionRequest handler.
*
* If no handler is provided and the `permission_approve` config is true,
* automatically injects PermissionHandler::approveAll().
* Injects a handler based on the `permission_approve` config value:
* - "deny-all" → PermissionHandler::denyAll()
* - "approve-safety" → PermissionHandler::approveSafety()
* - "approve-all" → PermissionHandler::approveAll()
* - false → no handler injected (onPermissionRequest required)
*/
protected function ensurePermissionHandler(array $config): array
{
if (! isset($config['onPermissionRequest'])) {
if ($this->config['permission_approve'] ?? config('copilot.permission_approve', true)) {
$config['onPermissionRequest'] = PermissionHandler::approveSafety();
}
if (isset($config['onPermissionRequest'])) {
return $config;
}

$setting = $this->config['permission_approve'] ?? config('copilot.permission_approve', 'deny-all');

$handler = match ($setting) {
'deny-all' => PermissionHandler::denyAll(),
'approve-safety' => PermissionHandler::approveSafety(),
'approve-all', true => PermissionHandler::approveAll(),
default => null,
};

if ($handler !== null) {
$config['onPermissionRequest'] = $handler;
}

return $config;
Expand Down
68 changes: 68 additions & 0 deletions tests/Feature/CopilotManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,74 @@
Copilot::clearResolvedInstances();
});

describe('ensurePermissionHandler', function () {
/**
* @param array|string|bool $setting
*/
$callEnsure = function ($setting, array $config = []) {
$manager = new CopilotManager(['permission_approve' => $setting]);
$reflection = new ReflectionClass($manager);
$method = $reflection->getMethod('ensurePermissionHandler');
$method->setAccessible(true);

return $method->invoke($manager, $config);
};

it('injects denyAll handler for deny-all setting', function () use ($callEnsure) {
$result = $callEnsure('deny-all');

expect($result)->toHaveKey('onPermissionRequest')
->and($result['onPermissionRequest'])->toBeInstanceOf(Closure::class);

// Verify it actually denies
$response = ($result['onPermissionRequest'])(['kind' => 'read'], ['sessionId' => 'x']);
expect($response['kind'])->not->toBe('approved');
});

it('injects approveSafety handler for approve-safety setting', function () use ($callEnsure) {
$result = $callEnsure('approve-safety');

expect($result)->toHaveKey('onPermissionRequest');

$response = ($result['onPermissionRequest'])(['kind' => 'read'], ['sessionId' => 'x']);
expect($response['kind'])->toBe('approved');

$response = ($result['onPermissionRequest'])(['kind' => 'shell'], ['sessionId' => 'x']);
expect($response['kind'])->not->toBe('approved');
});

it('injects approveAll handler for approve-all setting', function () use ($callEnsure) {
$result = $callEnsure('approve-all');

expect($result)->toHaveKey('onPermissionRequest');

$response = ($result['onPermissionRequest'])(['kind' => 'shell'], ['sessionId' => 'x']);
expect($response['kind'])->toBe('approved');
});

it('injects approveAll handler for legacy true setting', function () use ($callEnsure) {
$result = $callEnsure(true);

expect($result)->toHaveKey('onPermissionRequest');

$response = ($result['onPermissionRequest'])(['kind' => 'shell'], ['sessionId' => 'x']);
expect($response['kind'])->toBe('approved');
});

it('does not inject handler when setting is false', function () use ($callEnsure) {
$result = $callEnsure(false);

expect($result)->not->toHaveKey('onPermissionRequest');
});

it('does not overwrite existing onPermissionRequest', function () use ($callEnsure) {
$custom = fn () => ['kind' => 'custom'];
$result = $callEnsure('deny-all', ['onPermissionRequest' => $custom]);

expect($result['onPermissionRequest'])->toBe($custom);
});
});

describe('CopilotManager', function () {
it('can be instantiated with config', function () {
$manager = new CopilotManager([
Expand Down