feat: default to full-access mode with hook-based sensitive guards

This commit is contained in:
William Valentin
2026-02-18 11:14:35 -08:00
parent fc2090b599
commit a76c5ae346
9 changed files with 72 additions and 8 deletions
+7 -1
View File
@@ -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"
+11
View File
@@ -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.
# #
+18
View File
@@ -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": {
+5
View File
@@ -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
View File
@@ -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>;
+2
View File
@@ -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');
}); });
+5 -4
View File
@@ -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.');
+1 -1
View File
@@ -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' }),
+1 -1
View File
@@ -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([
{ {