Skip to content

Commit 3b09689

Browse files
G9PedroClawdious
andauthored
feat: add continuous safety rails for autonomous ops (#34)
Add kernel safety rails with persisted rate limiting, circuit breaker, and kill switch state in .workgraph/safety.yaml plus ledger-backed safety events. Expose safety status/pause/resume/reset/log commands in the CLI and cover core safety behaviors with dedicated tests. Made-with: Cursor Co-authored-by: Clawdious <clawdious@agentmail.to>
1 parent f9a8383 commit 3b09689

File tree

5 files changed

+1110
-0
lines changed

5 files changed

+1110
-0
lines changed

packages/cli/src/cli.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { registerAutonomyCommands } from './cli/commands/autonomy.js';
77
import { registerConversationCommands } from './cli/commands/conversation.js';
88
import { registerDispatchCommands } from './cli/commands/dispatch.js';
99
import { registerMcpCommands } from './cli/commands/mcp.js';
10+
import { registerSafetyCommands } from './cli/commands/safety.js';
1011
import { registerTriggerCommands } from './cli/commands/trigger.js';
1112
import {
1213
addWorkspaceOption,
@@ -2214,6 +2215,12 @@ registerTriggerCommands(program, DEFAULT_ACTOR);
22142215

22152216
registerConversationCommands(program, DEFAULT_ACTOR);
22162217

2218+
// ============================================================================
2219+
// safety
2220+
// ============================================================================
2221+
2222+
registerSafetyCommands(program, DEFAULT_ACTOR);
2223+
22172224
// ============================================================================
22182225
// onboarding
22192226
// ============================================================================
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Command } from 'commander';
2+
import * as workgraph from '@versatly/workgraph-kernel';
3+
import {
4+
addWorkspaceOption,
5+
resolveWorkspacePath,
6+
runCommand,
7+
} from '../core.js';
8+
9+
export function registerSafetyCommands(program: Command, defaultActor: string): void {
10+
const safetyCmd = program
11+
.command('safety')
12+
.description('Continuous operations safety rails (rate limit, circuit breaker, kill switch)');
13+
14+
addWorkspaceOption(
15+
safetyCmd
16+
.command('status')
17+
.description('Show current safety configuration and runtime state')
18+
.option('--json', 'Emit structured JSON output'),
19+
).action((opts) =>
20+
runCommand(
21+
opts,
22+
() => {
23+
const workspacePath = resolveWorkspacePath(opts);
24+
return workgraph.safety.getSafetyStatus(workspacePath);
25+
},
26+
(result) => [
27+
`Blocked: ${result.blocked ? 'yes' : 'no'}`,
28+
...(result.reasons.length > 0 ? result.reasons.map((reason) => `Reason: ${reason}`) : []),
29+
`Kill switch: ${result.config.killSwitch.engaged ? 'engaged' : 'released'}`,
30+
`Rate limit: enabled=${result.config.rateLimit.enabled} window=${result.config.rateLimit.windowSeconds}s max=${result.config.rateLimit.maxOperations} used=${result.config.runtime.rateLimitOperations}`,
31+
`Circuit breaker: enabled=${result.config.circuitBreaker.enabled} state=${result.config.runtime.circuitState} failures=${result.config.runtime.consecutiveFailures}`,
32+
`Updated at: ${result.config.updatedAt}`,
33+
],
34+
),
35+
);
36+
37+
addWorkspaceOption(
38+
safetyCmd
39+
.command('pause')
40+
.description('Engage kill switch to pause autonomous operations')
41+
.option('-a, --actor <name>', 'Actor', defaultActor)
42+
.option('--reason <text>', 'Optional pause reason')
43+
.option('--json', 'Emit structured JSON output'),
44+
).action((opts) =>
45+
runCommand(
46+
opts,
47+
() => {
48+
const workspacePath = resolveWorkspacePath(opts);
49+
return {
50+
config: workgraph.safety.pauseSafetyOperations(workspacePath, opts.actor, opts.reason),
51+
};
52+
},
53+
(result) => [
54+
'Safety kill switch engaged.',
55+
`Reason: ${String(result.config.killSwitch.reason ?? 'none')}`,
56+
`Updated at: ${result.config.updatedAt}`,
57+
],
58+
),
59+
);
60+
61+
addWorkspaceOption(
62+
safetyCmd
63+
.command('resume')
64+
.description('Release kill switch and resume autonomous operations')
65+
.option('-a, --actor <name>', 'Actor', defaultActor)
66+
.option('--json', 'Emit structured JSON output'),
67+
).action((opts) =>
68+
runCommand(
69+
opts,
70+
() => {
71+
const workspacePath = resolveWorkspacePath(opts);
72+
return {
73+
config: workgraph.safety.resumeSafetyOperations(workspacePath, opts.actor),
74+
};
75+
},
76+
(result) => [
77+
'Safety kill switch released.',
78+
`Updated at: ${result.config.updatedAt}`,
79+
],
80+
),
81+
);
82+
83+
addWorkspaceOption(
84+
safetyCmd
85+
.command('reset')
86+
.description('Reset safety runtime counters and circuit state')
87+
.option('-a, --actor <name>', 'Actor', defaultActor)
88+
.option('--full', 'Also clear kill switch state')
89+
.option('--json', 'Emit structured JSON output'),
90+
).action((opts) =>
91+
runCommand(
92+
opts,
93+
() => {
94+
const workspacePath = resolveWorkspacePath(opts);
95+
return {
96+
config: workgraph.safety.resetSafetyRails(workspacePath, {
97+
actor: opts.actor,
98+
clearKillSwitch: !!opts.full,
99+
}),
100+
};
101+
},
102+
(result) => [
103+
'Safety runtime reset complete.',
104+
`Circuit state: ${result.config.runtime.circuitState}`,
105+
`Rate limit used: ${result.config.runtime.rateLimitOperations}`,
106+
`Kill switch: ${result.config.killSwitch.engaged ? 'engaged' : 'released'}`,
107+
],
108+
),
109+
);
110+
111+
addWorkspaceOption(
112+
safetyCmd
113+
.command('log')
114+
.description('Show recent safety events from ledger')
115+
.option('--count <n>', 'Number of entries', '20')
116+
.option('--json', 'Emit structured JSON output'),
117+
).action((opts) =>
118+
runCommand(
119+
opts,
120+
() => {
121+
const workspacePath = resolveWorkspacePath(opts);
122+
const parsedCount = Number.parseInt(String(opts.count), 10);
123+
const count = Number.isFinite(parsedCount) ? Math.max(0, parsedCount) : 20;
124+
return {
125+
entries: workgraph.safety.listSafetyEvents(workspacePath, { count }),
126+
count,
127+
};
128+
},
129+
(result) => {
130+
if (result.entries.length === 0) return ['No safety events found.'];
131+
return result.entries.map((entry) => {
132+
const eventName = readEventName(entry);
133+
return `${entry.ts} ${eventName} actor=${entry.actor}`;
134+
});
135+
},
136+
),
137+
);
138+
}
139+
140+
function readEventName(entry: workgraph.LedgerEntry): string {
141+
const data = entry.data as Record<string, unknown> | undefined;
142+
const event = data?.event;
143+
return typeof event === 'string' && event.trim().length > 0 ? event : 'safety.unknown';
144+
}

packages/kernel/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export * as triggerEngine from './trigger-engine.js';
3939
export * as trigger from './trigger.js';
4040
export * as autonomy from './autonomy.js';
4141
export * as autonomyDaemon from './autonomy-daemon.js';
42+
export * as safety from './safety.js';
4243
export * as commandCenter from './command-center.js';
4344
export * as board from './board.js';
4445
export * as agent from './agent.js';

0 commit comments

Comments
 (0)