From 3a312654bbc9e9540fd18c9267eb330a0d3d20e0 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Tue, 19 May 2026 16:41:20 +0800 Subject: [PATCH] Add cloud disconnect command --- skills/agentguard/SKILL.md | 1 + src/cli.ts | 11 +++++++++++ src/config.ts | 13 ++++++++++++- src/index.ts | 1 + src/tests/runtime-cloud.test.ts | 34 +++++++++++++++++++++++++++++++-- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/skills/agentguard/SKILL.md b/skills/agentguard/SKILL.md index 274746a..3b36bfd 100644 --- a/skills/agentguard/SKILL.md +++ b/skills/agentguard/SKILL.md @@ -80,6 +80,7 @@ Supported CLI commands and options: |---|---|---| | `agentguard init` | `--level `, `--agent `, `--cloud `, `--force` | Creates local config and optionally installs agent templates | | `agentguard connect` | `--key `, `--api-key `, `--url `, `--cloud ` | Prefer `AGENTGUARD_API_KEY` over passing secrets in flags | +| `agentguard disconnect` | none | Removes local Cloud API key, connection timestamp, pending event spool, and cached Cloud policy; keeps Cloud URL, audit log, and installed hooks/templates | | `agentguard status` | none | Shows local config, Cloud URL/API key status, policy cache, audit path | | `agentguard policy pull` | `--json` | Pulls Cloud effective runtime policy into the local cache | | `agentguard doctor` | none | Checks local setup and Cloud reachability when connected | diff --git a/src/cli.ts b/src/cli.ts index 65db418..107cbf4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import { Command } from 'commander'; import { AgentGuardCloudClient } from './cloud/client.js'; import { connectCloud, + disconnectCloud, ensureConfig, getAgentGuardPaths, loadConfig, @@ -93,6 +94,16 @@ async function main() { } }); + program + .command('disconnect') + .description('Disconnect local AgentGuard from AgentGuard Cloud') + .action(() => { + const config = disconnectCloud(); + console.log('Disconnected from AgentGuard Cloud.'); + console.log('Removed local Cloud API key, connection timestamp, pending event spool, and cached Cloud policy.'); + console.log(`Local protection remains active using the built-in policy. Audit log: ${config.auditPath}`); + }); + program .command('status') .description('Show local and Cloud connection status') diff --git a/src/config.ts b/src/config.ts index 6f7cfb4..0889949 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; @@ -104,6 +104,17 @@ export function connectCloud(options: { apiKey: string; cloudUrl?: string }): Ag return next; } +export function disconnectCloud(): AgentGuardConfig { + const current = ensureConfig(); + const next: AgentGuardConfig = { ...current }; + delete next.apiKey; + delete next.connectedAt; + rmSync(current.eventSpoolPath, { force: true }); + rmSync(current.policyCachePath, { force: true }); + saveConfig(next); + return next; +} + export function maskApiKey(apiKey?: string): string { if (!apiKey) return 'not configured'; if (apiKey.length <= 12) return `${apiKey.slice(0, 4)}…`; diff --git a/src/index.ts b/src/index.ts index 65d0e65..52a2ea9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,7 @@ export { loadConfig as loadAgentGuardConfig, saveConfig as saveAgentGuardConfig, connectCloud, + disconnectCloud, getAgentGuardPaths, type AgentGuardConfig, } from './config.js'; diff --git a/src/tests/runtime-cloud.test.ts b/src/tests/runtime-cloud.test.ts index c7f7957..c941c65 100644 --- a/src/tests/runtime-cloud.test.ts +++ b/src/tests/runtime-cloud.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { evaluateLocalAction } from '../runtime/evaluator.js'; @@ -8,7 +8,7 @@ import { getDefaultEffectiveRuntimePolicy } from '../runtime/policy.js'; import { redactText } from '../runtime/redaction.js'; import { flushEventSpool, spoolEvent } from '../runtime/audit.js'; import { protectAction } from '../runtime/protect.js'; -import { connectCloud, getAgentGuardPaths } from '../config.js'; +import { connectCloud, disconnectCloud, getAgentGuardPaths } from '../config.js'; import { AgentGuardCloudClient } from '../cloud/client.js'; import type { AgentGuardConfig } from '../config.js'; import type { RuntimeAuditEvent } from '../runtime/types.js'; @@ -60,6 +60,36 @@ describe('Runtime Cloud bridge', () => { } }); + it('disconnects Cloud without deleting the local audit log', () => { + const previousHome = process.env.AGENTGUARD_HOME; + process.env.AGENTGUARD_HOME = mkdtempSync(join(tmpdir(), 'agentguard-disconnect-')); + try { + const config = connectCloud({ + apiKey: 'ag_live_test_key_123456', + cloudUrl: 'https://agentguard.example', + }); + writeFileSync(config.eventSpoolPath, `${JSON.stringify(sampleEvent())}\n`); + writeFileSync(config.policyCachePath, JSON.stringify(getDefaultEffectiveRuntimePolicy())); + writeFileSync(config.auditPath, `${JSON.stringify(sampleEvent())}\n`); + + const disconnected = disconnectCloud(); + const saved = JSON.parse(readFileSync(getAgentGuardPaths().configPath, 'utf8')) as AgentGuardConfig; + + assert.equal(disconnected.apiKey, undefined); + assert.equal(disconnected.connectedAt, undefined); + assert.equal(disconnected.cloudUrl, 'https://agentguard.example'); + assert.equal(saved.apiKey, undefined); + assert.equal(saved.connectedAt, undefined); + assert.equal(saved.cloudUrl, 'https://agentguard.example'); + assert.equal(existsSync(config.eventSpoolPath), false); + assert.equal(existsSync(config.policyCachePath), false); + assert.equal(existsSync(config.auditPath), true); + } finally { + if (previousHome === undefined) delete process.env.AGENTGUARD_HOME; + else process.env.AGENTGUARD_HOME = previousHome; + } + }); + it('evaluates local action with cached Cloud policy shape', async () => { const policy = getDefaultEffectiveRuntimePolicy(); policy.policyVersion = 'runtime-test';