From 71ea958613f90003e741012cd400535e5d738e07 Mon Sep 17 00:00:00 2001 From: Mr-Lucky Date: Mon, 18 May 2026 11:56:29 +0800 Subject: [PATCH] fix: add policy pull command --- CHANGELOG.md | 5 +++ src/cli.ts | 47 ++++++++++++++++++++++++++ src/tests/cli-policy.test.ts | 65 ++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/tests/cli-policy.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5e069..89b35cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [Unreleased] + +### Fixed +- Added the missing `agentguard policy pull` command used by AgentGuard Cloud policy refresh instructions. + ## [1.1.5] - 2026-05-18 ### Added diff --git a/src/cli.ts b/src/cli.ts index ca4822d..65db418 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -107,6 +107,53 @@ async function main() { console.log(`Audit log: ${config.auditPath}`); }); + const policy = program + .command('policy') + .description('Manage local runtime policy cache'); + + policy + .command('pull') + .description('Pull the latest effective runtime policy from AgentGuard Cloud into the local cache') + .option('--json', 'Print JSON output') + .action(async (options) => { + const config = ensureConfig(); + const client = new AgentGuardCloudClient(config); + if (!client.connected) { + const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key ` first.'; + if (options.json) { + console.log(JSON.stringify({ success: false, error: message }, null, 2)); + } else { + console.error(message); + } + process.exitCode = 1; + return; + } + + try { + const pulledPolicy = await client.fetchEffectivePolicy(); + saveCachedPolicy(config.policyCachePath, pulledPolicy); + if (options.json) { + console.log(JSON.stringify({ + success: true, + policyVersion: pulledPolicy.policyVersion, + updatedAt: pulledPolicy.updatedAt, + cachePath: config.policyCachePath, + }, null, 2)); + } else { + console.log(`Pulled policy ${pulledPolicy.policyVersion}.`); + console.log(`Policy cache: ${config.policyCachePath}`); + } + } catch (err) { + const message = `Policy pull failed: ${(err as Error).message}`; + if (options.json) { + console.log(JSON.stringify({ success: false, error: message }, null, 2)); + } else { + console.error(message); + } + process.exitCode = 1; + } + }); + program .command('doctor') .description('Check local AgentGuard setup') diff --git a/src/tests/cli-policy.test.ts b/src/tests/cli-policy.test.ts new file mode 100644 index 0000000..a32f6b8 --- /dev/null +++ b/src/tests/cli-policy.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { promisify } from 'node:util'; +import { getDefaultEffectiveRuntimePolicy } from '../runtime/policy.js'; + +const execFileAsync = promisify(execFile); + +describe('policy CLI', () => { + it('pulls the effective Cloud policy into the local cache', async () => { + const home = mkdtempSync(join(tmpdir(), 'agentguard-policy-cli-')); + const policy = getDefaultEffectiveRuntimePolicy(); + policy.policyVersion = 'runtime-cli-test'; + policy.blockedCommandPatterns = ['cli-policy-danger']; + policy.updatedAt = '2026-05-18T00:00:00.000Z'; + + const server = createServer((req, res) => { + if (req.url === '/api/v1/policies/effective' && req.headers['x-api-key'] === 'ag_live_test_key_123456') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: policy })); + return; + } + res.writeHead(404, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: { message: 'not found' } })); + }); + + await new Promise((resolvePromise) => server.listen(0, '127.0.0.1', resolvePromise)); + try { + const address = server.address(); + assert.ok(address && typeof address === 'object'); + const cloudUrl = `http://127.0.0.1:${address.port}`; + const cachePath = join(home, 'policy-cache.json'); + writeFileSync(join(home, 'config.json'), JSON.stringify({ + version: 1, + level: 'balanced', + cloudUrl, + apiKey: 'ag_live_test_key_123456', + policyCachePath: cachePath, + auditPath: join(home, 'audit.jsonl'), + eventSpoolPath: join(home, 'events-spool.jsonl'), + })); + + const cliPath = resolve('dist/cli.js'); + const { stdout } = await execFileAsync(process.execPath, [cliPath, 'policy', 'pull', '--json'], { + env: { ...process.env, AGENTGUARD_HOME: home }, + }); + + const result = JSON.parse(stdout) as { success: boolean; policyVersion: string; cachePath: string }; + assert.equal(result.success, true); + assert.equal(result.policyVersion, 'runtime-cli-test'); + assert.equal(result.cachePath, cachePath); + const cached = JSON.parse(readFileSync(cachePath, 'utf8')) as typeof policy; + assert.equal(cached.policyVersion, 'runtime-cli-test'); + assert.deepEqual(cached.blockedCommandPatterns, ['cli-policy-danger']); + } finally { + await new Promise((resolvePromise, reject) => { + server.close((err) => err ? reject(err) : resolvePromise()); + }); + } + }); +});