@sanctix/client 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/sanctix.js CHANGED
@@ -5,6 +5,7 @@ import { uninstall } from '../cli/uninstall.js';
5
5
  import { status } from '../cli/status.js';
6
6
  import { start } from '../cli/start.js';
7
7
  import { stop } from '../cli/stop.js';
8
+ import { launch } from '../cli/launch.js';
8
9
 
9
10
  program
10
11
  .name('sanctix')
@@ -39,6 +40,13 @@ program
39
40
  .option('-y, --yes', 'Skip confirmation for high-risk tasks')
40
41
  .action(start);
41
42
 
43
+ program
44
+ .command('launch <task> <runtime>')
45
+ .description('Start a governed session, launch a runtime and stop when it exits')
46
+ .option('--risk <level>', 'Risk level: low, medium, high')
47
+ .option('-y, --yes', 'Skip confirmation for high-risk tasks')
48
+ .action(launch);
49
+
42
50
  program
43
51
  .command('stop')
44
52
  .description('Complete the active governed session')
package/cli/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import url from 'url';
4
+ import { randomUUID } from 'crypto';
4
5
  import inquirer from 'inquirer';
5
6
  import chalk from 'chalk';
6
7
  import {
@@ -20,12 +21,12 @@ const HOSTED_AUDIT_URL = 'https://awf.ruvoni.com';
20
21
  const SANCTIX_HOOK_COMMANDS = {
21
22
  PreToolUse: [
22
23
  { matcher: 'Bash', command: 'node .claude/hooks/pre-tool-use/check-bash.cjs' },
23
- { matcher: '.*', command: 'node .claude/hooks/pre-tool-use/check-file-edit.cjs' },
24
+ { matcher: 'Edit|Write', command: 'node .claude/hooks/pre-tool-use/check-file-edit.cjs' },
24
25
  { matcher: 'Agent', command: 'node .claude/hooks/pre-tool-use/check-agent-spawn.cjs' },
25
26
  ],
26
27
  PostToolUse: [
27
28
  { matcher: 'Agent', command: 'node .claude/hooks/post-tool-use/check-agent-spawn-result.cjs' },
28
- { matcher: '.*', command: 'node .claude/hooks/post-tool-use/check-file-edit-result.cjs' },
29
+ { matcher: 'Edit|Write', command: 'node .claude/hooks/post-tool-use/check-file-edit-result.cjs' },
29
30
  ],
30
31
  SubagentStart: [
31
32
  { matcher: null, command: 'node .claude/hooks/sub-agent-start/check-subagent-start.cjs' },
@@ -123,7 +124,17 @@ export async function init(options) {
123
124
  await fs.ensureDir(path.join(cwd, '.sanctix'));
124
125
  await fs.writeJSON(path.join(cwd, '.sanctix/sanctix.config.json'), manifest, { spaces: 2 });
125
126
 
126
- // Step 7: Print confirmation.
127
+ // Step 7: Discover agents from Claude Code permissions and assign stable UUIDs.
128
+ if (runtime === 'claude_code') {
129
+ const agents = await discoverAgents(cwd);
130
+ const names = Object.keys(agents);
131
+ if (names.length > 0) {
132
+ const parts = names.map((n) => `${n} (${agents[n]})`);
133
+ console.log('Discovered agents: ' + parts.join(', '));
134
+ }
135
+ }
136
+
137
+ // Step 8: Print confirmation.
127
138
  console.log('');
128
139
  console.log(chalk.green('[ok] Sanctix is now governing your ' + runtime + ' sessions.'));
129
140
  console.log('');
@@ -137,6 +148,39 @@ export async function init(options) {
137
148
  console.log(' Docs: https://sanctix.ai');
138
149
  }
139
150
 
151
+ async function discoverAgents(cwd) {
152
+ const settingsPath = path.join(cwd, '.claude/settings.json');
153
+ if (!(await fs.pathExists(settingsPath))) return {};
154
+
155
+ let settings;
156
+ try {
157
+ settings = await fs.readJSON(settingsPath);
158
+ } catch {
159
+ return {};
160
+ }
161
+
162
+ const allowList = (settings.permissions && settings.permissions.allow) || [];
163
+ const agentNames = allowList
164
+ .filter((p) => typeof p === 'string' && p.startsWith('Agent(') && p.endsWith(')'))
165
+ .map((p) => p.slice(6, -1).trim())
166
+ .filter((name) => name.length > 0);
167
+
168
+ if (agentNames.length === 0) return {};
169
+
170
+ const agentsPath = path.join(cwd, '.sanctix/agents.json');
171
+ const existing = (await fs.pathExists(agentsPath)) ? await fs.readJSON(agentsPath) : {};
172
+
173
+ const result = {};
174
+ for (const name of agentNames) {
175
+ result[name] = existing[name] || randomUUID();
176
+ }
177
+
178
+ await fs.ensureDir(path.join(cwd, '.sanctix'));
179
+ await fs.writeJSON(agentsPath, result, { spaces: 2 });
180
+
181
+ return result;
182
+ }
183
+
140
184
  function managedFilesFor(runtime) {
141
185
  if (runtime === 'claude_code') return CLAUDE_MANAGED_FILES;
142
186
  if (runtime === 'cursor') return ['.cursor/rules/sanctix-governance.mdc'];
package/cli/launch.js ADDED
@@ -0,0 +1,133 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import chalk from 'chalk';
5
+ import { start } from './start.js';
6
+ import { stop } from './stop.js';
7
+
8
+ const RUNTIMES = {
9
+ claude_code: {
10
+ command: 'claude',
11
+ args: ['--dangerously-skip-permissions'],
12
+ label: 'Claude Code',
13
+ },
14
+ cursor: {
15
+ command: 'cursor',
16
+ args: ['.'],
17
+ label: 'Cursor',
18
+ },
19
+ codex: {
20
+ command: 'codex',
21
+ args: [],
22
+ label: 'Codex',
23
+ },
24
+ };
25
+
26
+ export async function launch(taskDescription, runtime, options = {}) {
27
+ const selectedRuntime = String(runtime || '').toLowerCase();
28
+ const spec = RUNTIMES[selectedRuntime];
29
+
30
+ if (!spec) {
31
+ console.error(chalk.red('Unknown runtime: ' + runtime));
32
+ console.error('Runtime must be one of: claude_code, cursor, codex');
33
+ process.exit(1);
34
+ }
35
+
36
+ let runtimeResult = { code: 1, signal: null };
37
+ let started = false;
38
+
39
+ try {
40
+ const session = await start(taskDescription, {
41
+ ...options,
42
+ runtime: selectedRuntime,
43
+ launch: true,
44
+ noExit: true,
45
+ });
46
+ started = true;
47
+
48
+ const sessionEnv = await readSessionEnv(process.cwd());
49
+ const childEnv = {
50
+ ...process.env,
51
+ ...sessionEnv,
52
+ AWF_CORRELATION_ID: session.correlationId,
53
+ AWF_SESSION_ID: session.sessionId,
54
+ AWF_USER_ID: session.userId,
55
+ SANCTIX_AUDIT_URL: session.auditUrl,
56
+ SANCTIX_RUNTIME: selectedRuntime,
57
+ };
58
+
59
+ if (session.apiKey) {
60
+ childEnv.SANCTIX_API_KEY = session.apiKey;
61
+ }
62
+
63
+ console.log('');
64
+ console.log(chalk.bold('Launching ' + spec.label + ' with Sanctix session ' + session.correlationId));
65
+ runtimeResult = await runRuntime(spec, childEnv);
66
+ } finally {
67
+ if (started) {
68
+ await stop({ yes: true, noExit: true });
69
+ }
70
+ }
71
+
72
+ exitWithRuntimeResult(runtimeResult);
73
+ }
74
+
75
+ function runRuntime(spec, env) {
76
+ return new Promise((resolve) => {
77
+ const child = spawn(spec.command, spec.args, {
78
+ stdio: 'inherit',
79
+ env,
80
+ });
81
+
82
+ child.on('error', (err) => {
83
+ console.error(chalk.red('Failed to launch ' + spec.command + ': ' + err.message));
84
+ resolve({ code: 127, signal: null });
85
+ });
86
+
87
+ child.on('exit', (code, signal) => {
88
+ resolve({ code: code == null ? 1 : code, signal });
89
+ });
90
+ });
91
+ }
92
+
93
+ async function readSessionEnv(cwd) {
94
+ const sessionEnvPath = path.join(cwd, '.sanctix/session.env');
95
+ if (!(await fs.pathExists(sessionEnvPath))) return {};
96
+
97
+ const text = await fs.readFile(sessionEnvPath, 'utf8');
98
+ const env = {};
99
+ for (const line of text.split('\n')) {
100
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
101
+ if (m) env[m[1]] = m[2];
102
+ }
103
+ return env;
104
+ }
105
+
106
+ function exitWithRuntimeResult(result) {
107
+ if (result.signal) {
108
+ const signalNumber = signalExitCode(result.signal);
109
+ process.exit(signalNumber || 1);
110
+ }
111
+ process.exit(result.code == null ? 1 : result.code);
112
+ }
113
+
114
+ function signalExitCode(signal) {
115
+ const signals = {
116
+ SIGHUP: 1,
117
+ SIGINT: 2,
118
+ SIGQUIT: 3,
119
+ SIGILL: 4,
120
+ SIGTRAP: 5,
121
+ SIGABRT: 6,
122
+ SIGBUS: 7,
123
+ SIGFPE: 8,
124
+ SIGKILL: 9,
125
+ SIGUSR1: 10,
126
+ SIGSEGV: 11,
127
+ SIGUSR2: 12,
128
+ SIGPIPE: 13,
129
+ SIGALRM: 14,
130
+ SIGTERM: 15,
131
+ };
132
+ return signals[signal] ? 128 + signals[signal] : null;
133
+ }
package/cli/start.js CHANGED
@@ -29,6 +29,7 @@ export async function start(taskDescription, options = {}) {
29
29
  const sessionId = crypto.randomUUID();
30
30
  const startedAt = new Date().toISOString();
31
31
  const userId = process.env.USER || process.env.USERNAME || 'user';
32
+ const runtimeProvider = options.runtime || manifest.runtime || 'unknown';
32
33
 
33
34
  // Step 3: Infer risk if not provided.
34
35
  let risk;
@@ -52,7 +53,7 @@ export async function start(taskDescription, options = {}) {
52
53
  timestamp_utc: startedAt,
53
54
  correlation_id: correlationId,
54
55
  user_id: userId,
55
- runtime_provider: manifest.runtime,
56
+ runtime_provider: runtimeProvider,
56
57
  event_data: {
57
58
  session_id: sessionId,
58
59
  task_description: taskDescription,
@@ -100,6 +101,7 @@ export async function start(taskDescription, options = {}) {
100
101
  'SANCTIX_SESSION_STARTED=' + startedAt,
101
102
  'SANCTIX_TASK=' + truncatedTask,
102
103
  'SANCTIX_RISK=' + risk,
104
+ 'SANCTIX_RUNTIME=' + runtimeProvider,
103
105
  '',
104
106
  ];
105
107
  await fs.ensureDir(path.join(cwd, '.sanctix'));
@@ -112,6 +114,21 @@ export async function start(taskDescription, options = {}) {
112
114
  console.log('Task: ' + taskDescription);
113
115
  console.log('Risk: ' + risk);
114
116
  console.log('Started: ' + startedAt);
117
+ if (options.launch) {
118
+ console.log('Runtime: ' + runtimeProvider);
119
+ console.log(chalk.bold('================================'));
120
+ return {
121
+ correlationId,
122
+ sessionId,
123
+ userId,
124
+ startedAt,
125
+ task: taskDescription,
126
+ risk,
127
+ auditUrl: manifest.auditUrl,
128
+ apiKey,
129
+ runtime: runtimeProvider,
130
+ };
131
+ }
115
132
  console.log('');
116
133
  console.log('Activate this session by running ONE of the following');
117
134
  console.log('before launching your runtime:');
@@ -137,6 +154,19 @@ export async function start(taskDescription, options = {}) {
137
154
  console.log(chalk.bold('================================'));
138
155
 
139
156
  // Step 8: Exit 0.
157
+ if (options.noExit) {
158
+ return {
159
+ correlationId,
160
+ sessionId,
161
+ userId,
162
+ startedAt,
163
+ task: taskDescription,
164
+ risk,
165
+ auditUrl: manifest.auditUrl,
166
+ apiKey,
167
+ runtime: runtimeProvider,
168
+ };
169
+ }
140
170
  process.exit(0);
141
171
  }
142
172
 
package/cli/stop.js CHANGED
@@ -10,6 +10,7 @@ export async function stop(_options = {}) {
10
10
  const sessionEnvPath = path.join(cwd, '.sanctix/session.env');
11
11
  if (!(await fs.pathExists(sessionEnvPath))) {
12
12
  console.log('No active Sanctix session found.');
13
+ if (_options.noExit) return;
13
14
  process.exit(0);
14
15
  }
15
16
 
@@ -26,6 +27,7 @@ export async function stop(_options = {}) {
26
27
  const manifestPath = path.join(cwd, '.sanctix/sanctix.config.json');
27
28
  if (!(await fs.pathExists(manifestPath))) {
28
29
  console.error(chalk.red('Sanctix manifest missing. Cannot complete session cleanly.'));
30
+ if (_options.noExit) return;
29
31
  process.exit(1);
30
32
  }
31
33
  const manifest = await fs.readJSON(manifestPath);
@@ -79,6 +81,7 @@ export async function stop(_options = {}) {
79
81
  const toolBreakdown = s.tool_breakdown || {};
80
82
  const toolCalls = Object.values(toolBreakdown).reduce((a, b) => a + (Number(b) || 0), 0);
81
83
  const filesChanged = Array.isArray(s.files_changed) ? s.files_changed : [];
84
+ const commands = Array.isArray(s.commands) ? s.commands : [];
82
85
 
83
86
  console.log('Task: ' + task);
84
87
  console.log('Risk: ' + risk);
@@ -89,6 +92,11 @@ export async function stop(_options = {}) {
89
92
  console.log(' Files changed: ' + filesChanged.length);
90
93
  console.log(' Commands run: ' + (s.commands_run || 0));
91
94
  console.log(' Agent spawns: ' + (s.agent_spawns || 0));
95
+ if (commands.length > 0) {
96
+ console.log('');
97
+ console.log('Commands run:');
98
+ for (const c of commands) console.log(' ' + c);
99
+ }
92
100
  if (filesChanged.length > 0) {
93
101
  console.log('');
94
102
  console.log('Files touched:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanctix/client",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Sanctix governed edge client. Installs governance hooks for Claude Code, Codex and Cursor. Connects to the Sanctix control plane.",
5
5
  "type": "module",
6
6
  "bin": {