feat: default to full-access mode with hook-based sensitive guards
This commit is contained in:
@@ -626,6 +626,12 @@ Control sensitive operations with pattern matching:
|
|||||||
hooks:
|
hooks:
|
||||||
confirm: # Requires user approval via Telegram
|
confirm: # Requires user approval via Telegram
|
||||||
- shell.*
|
- shell.*
|
||||||
|
- process.start
|
||||||
|
- process.kill
|
||||||
|
- browser.*
|
||||||
|
- message.send
|
||||||
|
- cron.create
|
||||||
|
- cron.delete
|
||||||
- file.write
|
- file.write
|
||||||
- file.patch
|
- file.patch
|
||||||
log: # Logs but doesn't block
|
log: # Logs but doesn't block
|
||||||
@@ -640,7 +646,7 @@ For unrestricted deployments, pair hooks with agent-level sensitive gating:
|
|||||||
```yaml
|
```yaml
|
||||||
agents:
|
agents:
|
||||||
# deny_without_elevation | confirm_without_elevation
|
# deny_without_elevation | confirm_without_elevation
|
||||||
sensitive_mode: deny_without_elevation
|
sensitive_mode: confirm_without_elevation
|
||||||
immutable_denylist:
|
immutable_denylist:
|
||||||
- tool: shell.exec
|
- tool: shell.exec
|
||||||
args_pattern: "git push origin main"
|
args_pattern: "git push origin main"
|
||||||
|
|||||||
@@ -246,6 +246,12 @@ models:
|
|||||||
hooks:
|
hooks:
|
||||||
confirm:
|
confirm:
|
||||||
- shell.*
|
- shell.*
|
||||||
|
- process.start
|
||||||
|
- process.kill
|
||||||
|
- browser.*
|
||||||
|
- message.send
|
||||||
|
- cron.create
|
||||||
|
- cron.delete
|
||||||
- file.write
|
- file.write
|
||||||
- file.patch
|
- file.patch
|
||||||
log:
|
log:
|
||||||
@@ -260,6 +266,11 @@ hooks:
|
|||||||
# Those permissions are enforced at runtime when requests are routed into a skill context.
|
# Those permissions are enforced at runtime when requests are routed into a skill context.
|
||||||
# - See: docs/security/SAFE_PERSONAL_AGENT.md
|
# - See: docs/security/SAFE_PERSONAL_AGENT.md
|
||||||
|
|
||||||
|
agents:
|
||||||
|
# In full-access mode, sensitive operations are gated by HookEngine confirmation
|
||||||
|
# (instead of requiring temporary /elevate windows).
|
||||||
|
sensitive_mode: confirm_without_elevation
|
||||||
|
|
||||||
# ── Prompt Assembly ───────────────────────────────────────────────────
|
# ── Prompt Assembly ───────────────────────────────────────────────────
|
||||||
# Tune how much context Flynn loads into the system prompt.
|
# Tune how much context Flynn loads into the system prompt.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -5390,6 +5390,24 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/cli/companion.test.ts src/cli/index.test.ts + pnpm typecheck passing"
|
"test_status": "pnpm test:run src/cli/companion.test.ts src/cli/index.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"full-access-hooks-guard-defaults": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-18",
|
||||||
|
"updated": "2026-02-18",
|
||||||
|
"summary": "Aligned default behavior to full-access-with-hooks: setup now defaults to `tools.profile: full`, sensitive host operations default to `agents.sensitive_mode: confirm_without_elevation`, and default hook confirmations were expanded to cover high-impact actions (shell/process/browser/message send/cron mutations/file writes). This removes mandatory `/elevate` for standard operation while keeping explicit confirmation gates.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/config/schema.ts",
|
||||||
|
"src/config/schema.test.ts",
|
||||||
|
"src/cli/setup/config.ts",
|
||||||
|
"src/cli/setup/config.test.ts",
|
||||||
|
"src/cli/setup/security.ts",
|
||||||
|
"src/cli/setup/sections.test.ts",
|
||||||
|
"config/default.yaml",
|
||||||
|
"README.md",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/config/schema.test.ts src/cli/setup/config.test.ts src/cli/setup/sections.test.ts + pnpm typecheck passing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ describe('ConfigBuilder', () => {
|
|||||||
expect(obj.models.default.provider).toBe('anthropic');
|
expect(obj.models.default.provider).toBe('anthropic');
|
||||||
expect(obj.models.default.api_key).toBe('sk-ant-test');
|
expect(obj.models.default.api_key).toBe('sk-ant-test');
|
||||||
expect(obj.server.port).toBe(3777);
|
expect(obj.server.port).toBe(3777);
|
||||||
|
expect(obj.tools?.profile).toBe('full');
|
||||||
|
expect(obj.agents?.sensitive_mode).toBe('confirm_without_elevation');
|
||||||
|
expect((obj.hooks?.confirm as string[]) ?? []).toEqual(
|
||||||
|
expect.arrayContaining(['shell.*', 'browser.*', 'message.send']),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds telegram channel', () => {
|
it('adds telegram channel', () => {
|
||||||
|
|||||||
+22
-1
@@ -33,6 +33,9 @@ export interface SetupConfig {
|
|||||||
sandbox?: { enabled?: boolean };
|
sandbox?: { enabled?: boolean };
|
||||||
pairing?: { enabled?: boolean };
|
pairing?: { enabled?: boolean };
|
||||||
tools?: { profile?: string };
|
tools?: { profile?: string };
|
||||||
|
agents?: {
|
||||||
|
sensitive_mode?: 'deny_without_elevation' | 'confirm_without_elevation';
|
||||||
|
};
|
||||||
agent_configs?: Record<string, {
|
agent_configs?: Record<string, {
|
||||||
model_tier?: 'fast' | 'default' | 'complex' | 'local';
|
model_tier?: 'fast' | 'default' | 'complex' | 'local';
|
||||||
tool_profile?: string;
|
tool_profile?: string;
|
||||||
@@ -85,10 +88,22 @@ export class ConfigBuilder {
|
|||||||
models: {},
|
models: {},
|
||||||
server: { port: 18800, localhost: true },
|
server: { port: 18800, localhost: true },
|
||||||
hooks: {
|
hooks: {
|
||||||
confirm: ['shell.*', 'file.write', 'file.patch'],
|
confirm: [
|
||||||
|
'shell.*',
|
||||||
|
'process.start',
|
||||||
|
'process.kill',
|
||||||
|
'browser.*',
|
||||||
|
'message.send',
|
||||||
|
'cron.create',
|
||||||
|
'cron.delete',
|
||||||
|
'file.write',
|
||||||
|
'file.patch',
|
||||||
|
],
|
||||||
log: ['web.*', 'file.read'],
|
log: ['web.*', 'file.read'],
|
||||||
silent: ['notify'],
|
silent: ['notify'],
|
||||||
},
|
},
|
||||||
|
tools: { profile: 'full' },
|
||||||
|
agents: { sensitive_mode: 'confirm_without_elevation' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +186,12 @@ export class ConfigBuilder {
|
|||||||
this.config.tools = { profile };
|
this.config.tools = { profile };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSensitiveMode(mode: 'deny_without_elevation' | 'confirm_without_elevation'): void {
|
||||||
|
const agents = (this.config.agents ?? {}) as Record<string, unknown>;
|
||||||
|
agents.sensitive_mode = mode;
|
||||||
|
this.config.agents = agents as SetupConfig['agents'];
|
||||||
|
}
|
||||||
|
|
||||||
setResearchAgentEnabled(options: ResearchAgentOptions): void {
|
setResearchAgentEnabled(options: ResearchAgentOptions): void {
|
||||||
const agentConfigs = (this.config.agent_configs ?? {}) as Record<string, Record<string, unknown>>;
|
const agentConfigs = (this.config.agent_configs ?? {}) as Record<string, Record<string, unknown>>;
|
||||||
const existing = (agentConfigs.research ?? {}) as Record<string, unknown>;
|
const existing = (agentConfigs.research ?? {}) as Record<string, unknown>;
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ describe('setupSecurity', () => {
|
|||||||
const config = builder.build();
|
const config = builder.build();
|
||||||
expect(config.sandbox!.enabled).toBe(true);
|
expect(config.sandbox!.enabled).toBe(true);
|
||||||
expect(config.pairing!.enabled).toBe(true);
|
expect(config.pairing!.enabled).toBe(true);
|
||||||
|
expect(config.tools?.profile).toBe('full');
|
||||||
|
expect(config.agents?.sensitive_mode).toBe('confirm_without_elevation');
|
||||||
expect(config.agent_configs?.research?.model_tier).toBe('complex');
|
expect(config.agent_configs?.research?.model_tier).toBe('complex');
|
||||||
expect(config.agent_configs?.research?.tool_profile).toBe('messaging');
|
expect(config.agent_configs?.research?.tool_profile).toBe('messaging');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { Prompter } from './prompts.js';
|
|||||||
import type { ConfigBuilder } from './config.js';
|
import type { ConfigBuilder } from './config.js';
|
||||||
|
|
||||||
const TOOL_PROFILES = [
|
const TOOL_PROFILES = [
|
||||||
{ label: 'messaging (recommended)', value: 'messaging' },
|
{ label: 'full (recommended)', value: 'full' },
|
||||||
{ label: 'minimal (status only)', value: 'minimal' },
|
|
||||||
{ label: 'coding (fs + runtime)', value: 'coding' },
|
{ label: 'coding (fs + runtime)', value: 'coding' },
|
||||||
{ label: 'full (unrestricted)', value: 'full' },
|
{ label: 'messaging (read + services, no shell/writes)', value: 'messaging' },
|
||||||
|
{ label: 'minimal (status only)', value: 'minimal' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RESEARCH_AGENT_TIERS = [
|
const RESEARCH_AGENT_TIERS = [
|
||||||
@@ -35,12 +35,13 @@ export async function setupSecurity(p: Prompter, builder: ConfigBuilder): Promis
|
|||||||
|
|
||||||
p.println();
|
p.println();
|
||||||
p.println(' Tool profiles control which tools the agent can use:');
|
p.println(' Tool profiles control which tools the agent can use:');
|
||||||
p.println(' full — all tools available (file, shell, web, memory, messaging)');
|
p.println(' full — all tools available (file, shell, web, memory, messaging) (default)');
|
||||||
p.println(' coding — file system + shell + sessions + memory');
|
p.println(' coding — file system + shell + sessions + memory');
|
||||||
p.println(' messaging — read-only + web/memory + connected services (no file writes/shell)');
|
p.println(' messaging — read-only + web/memory + connected services (no file writes/shell)');
|
||||||
p.println(' minimal — status checks only (read-only, safest)');
|
p.println(' minimal — status checks only (read-only, safest)');
|
||||||
const profile = await p.choose('Tool policy profile:', TOOL_PROFILES);
|
const profile = await p.choose('Tool policy profile:', TOOL_PROFILES);
|
||||||
builder.setToolProfile(profile);
|
builder.setToolProfile(profile);
|
||||||
|
builder.setSensitiveMode('confirm_without_elevation');
|
||||||
|
|
||||||
p.println();
|
p.println();
|
||||||
p.println(' Research agent adds a dedicated specialist for deep web research.');
|
p.println(' Research agent adds a dedicated specialist for deep web research.');
|
||||||
|
|||||||
@@ -1544,7 +1544,7 @@ describe('configSchema — agents truthfulness/autonomy', () => {
|
|||||||
const result = configSchema.parse(minimalConfig);
|
const result = configSchema.parse(minimalConfig);
|
||||||
expect(result.agents.truthfulness_mode).toBe('standard');
|
expect(result.agents.truthfulness_mode).toBe('standard');
|
||||||
expect(result.agents.autonomy_level).toBe('standard');
|
expect(result.agents.autonomy_level).toBe('standard');
|
||||||
expect(result.agents.sensitive_mode).toBe('deny_without_elevation');
|
expect(result.agents.sensitive_mode).toBe('confirm_without_elevation');
|
||||||
expect(result.agents.immutable_denylist).toEqual(
|
expect(result.agents.immutable_denylist).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ tool: 'shell.exec', args_pattern: 'git push origin main' }),
|
expect.objectContaining({ tool: 'shell.exec', args_pattern: 'git push origin main' }),
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ const agentsSchema = z.object({
|
|||||||
/** Autonomy level for tool execution: conservative | standard | autonomous. */
|
/** Autonomy level for tool execution: conservative | standard | autonomous. */
|
||||||
autonomy_level: autonomyLevelSchema.default('standard'),
|
autonomy_level: autonomyLevelSchema.default('standard'),
|
||||||
/** Sensitive host-action behavior for high-impact tools. */
|
/** Sensitive host-action behavior for high-impact tools. */
|
||||||
sensitive_mode: sensitiveModeSchema.default('deny_without_elevation'),
|
sensitive_mode: sensitiveModeSchema.default('confirm_without_elevation'),
|
||||||
/** Immutable denylist enforced even during elevated mode. */
|
/** Immutable denylist enforced even during elevated mode. */
|
||||||
immutable_denylist: z.array(immutableDenyRuleSchema).default([
|
immutable_denylist: z.array(immutableDenyRuleSchema).default([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user