agent-permission 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -4,8 +4,20 @@ Give AI agents a permission gate before they touch external URLs.
4
4
 
5
5
  ## Quickstart
6
6
 
7
+ For one personal Codex setup that works across repositories:
8
+
9
+ ```bash
10
+ npx -y agent-permission@latest install --adapter codex
11
+ node "$HOME/.config/agent-permission/agent-permission.mjs" config set api-key <YOUR_API_KEY>
12
+ node "$HOME/.config/agent-permission/agent-permission.mjs" config doctor
13
+ ```
14
+
15
+ `install --adapter codex` defaults to user scope, writes personal Codex instructions under your Agent Permission config directory, and installs a reusable runner there. The install output prints the exact runner command for your machine.
16
+
17
+ For a repository-owned instruction block:
18
+
7
19
  ```bash
8
- npx agent-permission install --adapter auto
20
+ npx -y agent-permission@latest install --adapter auto --scope project
9
21
  ```
10
22
 
11
23
  This adds an Agent Permission block to the local agent instruction file, such as `AGENTS.md`, `CLAUDE.md`, or `agent-permission.chatgpt.md`, depending on the selected adapter. It also writes a tiny project-local runner at `.agent-permission/agent-permission.mjs` so repeated checks do not need npm registry access.
@@ -13,14 +25,20 @@ This adds an Agent Permission block to the local agent instruction file, such as
13
25
  To install instructions and configure live checks in one step:
14
26
 
15
27
  ```bash
16
- npx agent-permission install --adapter auto --api-key <YOUR_API_KEY>
28
+ npx -y agent-permission@latest install --adapter codex --api-key <YOUR_API_KEY>
29
+ ```
30
+
31
+ To force a project-local Codex install instead:
32
+
33
+ ```bash
34
+ npx -y agent-permission@latest install --adapter codex --scope project
17
35
  ```
18
36
 
19
37
  Add an API key when you want live checks from the command line, CI, or agent runs:
20
38
 
21
39
  ```bash
22
- node .agent-permission/agent-permission.mjs config set api-key <YOUR_API_KEY>
23
- node .agent-permission/agent-permission.mjs check https://example.com/blog-post --action summarize --exit-code
40
+ node "$HOME/.config/agent-permission/agent-permission.mjs" config set api-key <YOUR_API_KEY>
41
+ node "$HOME/.config/agent-permission/agent-permission.mjs" check https://example.com/blog-post --action summarize --exit-code
24
42
  ```
25
43
 
26
44
  ## What the Agent Runs
@@ -28,22 +46,24 @@ node .agent-permission/agent-permission.mjs check https://example.com/blog-post
28
46
  After install, project instructions tell compatible agents to run:
29
47
 
30
48
  ```bash
31
- node .agent-permission/agent-permission.mjs check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
49
+ node <installed-runner-path> check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
32
50
  ```
33
51
 
34
- If the local runner is missing, the installed instructions tell the agent to use npm as a fallback:
52
+ If the runner is missing, the installed instructions tell the agent to stop and ask you to repair the installation once:
35
53
 
36
54
  ```bash
37
- npx -y agent-permission@latest check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
55
+ npx -y agent-permission@latest install --adapter codex --scope user
38
56
  ```
39
57
 
58
+ If the API key is missing, the generated instructions tell the agent to stop and ask you to run the exact `config set api-key` command for that installed runner. The generated preflight block does not tell agents to run `npx ... check` during a URL request, so repeated checks do not need registry access or unpinned-code approval.
59
+
40
60
  ## Commands
41
61
 
42
62
  ```bash
43
63
  agent-permission check <url> --action <crawl|summarize|extract|train|transact|post>
44
64
  agent-permission batch --file checks.json
45
65
  agent-permission verify-receipt --file receipt.json
46
- agent-permission install --adapter auto|codex|claude|chatgpt|openclaw [--api-key <key>]
66
+ agent-permission install --adapter auto|codex|claude|chatgpt|openclaw [--scope project|user] [--api-key <key>]
47
67
  agent-permission config set api-key <key>
48
68
  agent-permission config doctor
49
69
  ```
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAKA,OAAO,EAAyD,KAAK,EAAuD,OAAO,EAAE,MAAM,YAAY,CAAC;AAExJ,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BzF;AAmID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CA2BtD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAKA,OAAO,EAAyD,KAAK,EAAuD,OAAO,EAAE,MAAM,YAAY,CAAC;AAExJ,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BzF;AA6ID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CA2BtD"}
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ export async function runCli(argv, runtime, io) {
8
8
  const invoked = argv[1] || 'agent-permission';
9
9
  const args = argv.slice(2);
10
10
  if (isSkillsInvocation(invoked, args)) {
11
- return runCli([argv[0], argv[1], 'install', '--adapter', 'auto'], runtime, io);
11
+ return runCli([argv[0], argv[1], 'install', '--adapter', 'codex', '--scope', 'user'], runtime, io);
12
12
  }
13
13
  const command = args[0];
14
14
  const rest = args.slice(1);
@@ -42,7 +42,14 @@ async function runCheck(args, runtime, io) {
42
42
  if (!url || !action) {
43
43
  throw new Error('Usage: agent-permission check <url> --action <crawl|summarize|extract|train|transact|post>');
44
44
  }
45
+ const localDecision = localUrlDecision(url, action);
46
+ if (localDecision) {
47
+ const format = outputFormat(parsed);
48
+ io.stdout(format === 'json' ? JSON.stringify(localDecision, null, 2) : formatCheckHuman(localDecision));
49
+ return parsed.flags['exit-code'] ? decisionExitCode(localDecision.decision) : 0;
50
+ }
45
51
  const config = await resolveConfig(runtime);
52
+ ensureApiKeyConfigured(config.apiKey, invokedRunnerCommand(runtime));
46
53
  const params = {
47
54
  url,
48
55
  action,
@@ -66,6 +73,7 @@ async function runBatch(args, runtime, io) {
66
73
  throw new Error('Usage: agent-permission batch --file checks.json');
67
74
  }
68
75
  const config = await resolveConfig(runtime);
76
+ ensureApiKeyConfigured(config.apiKey, invokedRunnerCommand(runtime));
69
77
  const batch = JSON.parse(await readFile(file, 'utf8'));
70
78
  const result = await batchCheckPermissions(batch, {
71
79
  apiUrl: stringFlag(parsed, 'api-url') || config.apiUrl,
@@ -98,11 +106,12 @@ async function runVerify(args, runtime, io) {
98
106
  async function runInstall(args, runtime, io) {
99
107
  const parsed = parseFlags(args);
100
108
  const adapter = parseAdapter(stringFlag(parsed, 'adapter') || parsed.positionals[0] || 'auto');
101
- const scope = parseScope(stringFlag(parsed, 'scope') || 'project');
109
+ const scope = parseScope(stringFlag(parsed, 'scope') || defaultInstallScope(adapter));
102
110
  const result = await installAgentInstructions(adapter, scope, runtime);
103
111
  const messages = [
104
112
  `Installed Agent Permission instructions for ${result.adapter} at ${result.path}`,
105
113
  `Installed local Agent Permission runner at ${result.runnerPath}`,
114
+ `Agent preflight command: ${result.checkCommand}`,
106
115
  ];
107
116
  const apiKey = stringFlag(parsed, 'api-key');
108
117
  if (apiKey) {
@@ -113,7 +122,7 @@ async function runInstall(args, runtime, io) {
113
122
  else {
114
123
  const config = await resolveConfig(runtime);
115
124
  if (!config.apiKey) {
116
- messages.push(`API key is not configured. Live checks will fail until you run \`${result.runnerCommand} config set api-key <YOUR_API_KEY>\` or set AGENT_PERMISSION_API_KEY.`, `Verify setup with \`${result.runnerCommand} config doctor\`.`);
125
+ messages.push(`API key is not configured. Live checks will fail until you run \`${result.apiKeyCommand}\` or set AGENT_PERMISSION_API_KEY.`, `Verify setup with \`${result.doctorCommand}\`.`);
117
126
  }
118
127
  }
119
128
  io.stdout(messages.join('\n'));
@@ -197,6 +206,89 @@ function parseScope(value) {
197
206
  return value;
198
207
  throw new Error('Scope must be project or user.');
199
208
  }
209
+ function ensureApiKeyConfigured(apiKey, runnerCommand) {
210
+ if (!apiKey) {
211
+ throw new Error(`Missing API key. Set AGENT_PERMISSION_API_KEY or run \`${runnerCommand} config set api-key <key>\`.`);
212
+ }
213
+ }
214
+ function invokedRunnerCommand(runtime) {
215
+ return runtime.env.AGENT_PERMISSION_RUNNER_COMMAND || 'agent-permission';
216
+ }
217
+ function defaultInstallScope(adapter) {
218
+ return adapter === 'codex' ? 'user' : 'project';
219
+ }
220
+ function localUrlDecision(url, action) {
221
+ const localKind = localUrlKind(url);
222
+ if (!localKind)
223
+ return undefined;
224
+ const issuedAt = new Date().toISOString();
225
+ return {
226
+ id: 'local_url_check',
227
+ decision: 'allow',
228
+ confidence: 'high',
229
+ risk: 'low',
230
+ reasons: [
231
+ `${localKind} URL was not sent to the Agent Permission API; local development and in-app browser URLs are allowed without external preflight.`,
232
+ ],
233
+ sources: [
234
+ {
235
+ type: 'local_url',
236
+ url,
237
+ fetched_at: issuedAt,
238
+ evidence: localKind,
239
+ },
240
+ ],
241
+ receipt: {
242
+ receipt_id: 'local_only_no_external_receipt',
243
+ check_id: 'local_url_check',
244
+ url_hash: 'local_only',
245
+ action,
246
+ decision: 'allow',
247
+ source_hashes: [],
248
+ issued_at: issuedAt,
249
+ account_id: 'local',
250
+ signature_alg: 'none',
251
+ signature: 'not_signed',
252
+ },
253
+ };
254
+ }
255
+ function localUrlKind(value) {
256
+ let parsed;
257
+ try {
258
+ parsed = new URL(value);
259
+ }
260
+ catch {
261
+ return undefined;
262
+ }
263
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
264
+ return undefined;
265
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
266
+ if (hostname === 'localhost' || hostname.endsWith('.localhost'))
267
+ return 'localhost';
268
+ if (hostname === '::1')
269
+ return 'loopback';
270
+ if (hostname.startsWith('127.'))
271
+ return 'loopback';
272
+ if (hostname === '0.0.0.0')
273
+ return 'local bind address';
274
+ if (hostname.endsWith('.local'))
275
+ return 'local network';
276
+ const parts = hostname.split('.').map((part) => Number(part));
277
+ if (parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)) {
278
+ const [first, second] = parts;
279
+ if (first === 10)
280
+ return 'private network';
281
+ if (first === 172 && second >= 16 && second <= 31)
282
+ return 'private network';
283
+ if (first === 192 && second === 168)
284
+ return 'private network';
285
+ if (first === 169 && second === 254)
286
+ return 'link-local';
287
+ }
288
+ if (hostname.startsWith('fc') || hostname.startsWith('fd') || hostname.startsWith('fe80:'))
289
+ return 'private network';
290
+ return undefined;
291
+ }
200
292
  function isSkillsInvocation(invoked, args) {
201
293
  const name = invoked.split('/').pop() || invoked;
202
294
  return name === 'skills' && args[0] === 'add' && args[1] === 'agent-permission';
package/dist/install.d.ts CHANGED
@@ -4,8 +4,16 @@ export interface InstallResult {
4
4
  path: string;
5
5
  runnerPath: string;
6
6
  runnerCommand: string;
7
+ checkCommand: string;
8
+ apiKeyCommand: string;
9
+ doctorCommand: string;
10
+ repairCommand: string;
7
11
  }
8
12
  export declare function installAgentInstructions(adapter: Adapter, scope: InstallScope, runtime: Runtime): Promise<InstallResult>;
9
13
  export declare function upsertBlock(existing: string, blockBody: string): string;
10
- export declare function buildInstructionBlock(adapter: Exclude<Adapter, 'auto'>, runnerCommand?: string): string;
14
+ export declare function buildInstructionBlock(adapter: Exclude<Adapter, 'auto'>, commands?: {
15
+ checkCommand?: string;
16
+ apiKeyCommand?: string;
17
+ repairCommand?: string;
18
+ }): string;
11
19
  //# sourceMappingURL=install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAmB,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAM7E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB9H;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAU9D;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa,SAAgC,UA2BrH"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAmB,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAM7E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAiC9H;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAU9D;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EACjC,QAAQ,GAAE;IACR,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,UAqCP"}
package/dist/install.js CHANGED
@@ -9,16 +9,33 @@ export async function installAgentInstructions(adapter, scope, runtime) {
9
9
  const path = getInstructionPath(resolved, scope, runtime);
10
10
  const runner = getRunnerPath(scope, runtime);
11
11
  const runnerCommand = getRunnerCommand(scope, runner);
12
+ const repairCommand = getRepairCommand(resolved, scope);
13
+ const checkCommand = `${runnerCommand} check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code`;
14
+ const apiKeyCommand = `${runnerCommand} config set api-key <YOUR_API_KEY>`;
15
+ const doctorCommand = `${runnerCommand} config doctor`;
12
16
  const existing = await readFile(path, 'utf8').catch((err) => {
13
17
  if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT')
14
18
  return '';
15
19
  throw err;
16
20
  });
17
- const next = upsertBlock(existing, buildInstructionBlock(resolved, runnerCommand));
21
+ const next = upsertBlock(existing, buildInstructionBlock(resolved, {
22
+ checkCommand,
23
+ apiKeyCommand,
24
+ repairCommand,
25
+ }));
18
26
  await mkdir(dirname(path), { recursive: true });
19
- await writeLocalRunner(runner);
27
+ await writeLocalRunner(runner, runnerCommand);
20
28
  await writeFile(path, next);
21
- return { adapter: resolved, path, runnerPath: runner, runnerCommand };
29
+ return {
30
+ adapter: resolved,
31
+ path,
32
+ runnerPath: runner,
33
+ runnerCommand,
34
+ checkCommand,
35
+ apiKeyCommand,
36
+ doctorCommand,
37
+ repairCommand,
38
+ };
22
39
  }
23
40
  export function upsertBlock(existing, blockBody) {
24
41
  const block = `${BLOCK_START}\n${blockBody.trim()}\n${BLOCK_END}`;
@@ -29,13 +46,16 @@ export function upsertBlock(existing, blockBody) {
29
46
  const prefix = existing.trimEnd();
30
47
  return prefix ? `${prefix}\n\n${block}\n` : `${block}\n`;
31
48
  }
32
- export function buildInstructionBlock(adapter, runnerCommand = `node ${PROJECT_RUNNER_PATH}`) {
49
+ export function buildInstructionBlock(adapter, commands = {}) {
33
50
  const agentName = {
34
51
  codex: 'Codex',
35
52
  claude: 'Claude',
36
53
  chatgpt: 'ChatGPT',
37
54
  openclaw: 'OpenClaw',
38
55
  }[adapter];
56
+ const checkCommand = commands.checkCommand || `node ${PROJECT_RUNNER_PATH} check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code`;
57
+ const apiKeyCommand = commands.apiKeyCommand || `node ${PROJECT_RUNNER_PATH} config set api-key <YOUR_API_KEY>`;
58
+ const repairCommand = commands.repairCommand || `npx -y agent-permission@latest install --adapter ${adapter} --scope project`;
39
59
  return `# Agent Permission
40
60
 
41
61
  These instructions equip ${agentName} with Agent Permission preflight checks.
@@ -43,13 +63,19 @@ These instructions equip ${agentName} with Agent Permission preflight checks.
43
63
  Before crawling, summarizing, extracting, training on, transacting with, or posting to a URL, run:
44
64
 
45
65
  \`\`\`bash
46
- ${runnerCommand} check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
66
+ ${checkCommand}
67
+ \`\`\`
68
+
69
+ Do not use npm while handling an individual URL request. If the runner command is missing or cannot start, stop and ask the user to repair the installation once with:
70
+
71
+ \`\`\`bash
72
+ ${repairCommand}
47
73
  \`\`\`
48
74
 
49
- If the local runner is missing, run the same check through npm:
75
+ If the runner reports that the API key is missing, stop and ask the user to configure it with:
50
76
 
51
77
  \`\`\`bash
52
- npx -y agent-permission@latest check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
78
+ ${apiKeyCommand}
53
79
  \`\`\`
54
80
 
55
81
  Use \`crawl\` for fetching or indexing pages, \`summarize\` for compacting page content, \`extract\` for structured data pulls, \`train\` for model training or dataset creation, \`transact\` for purchase/account-changing actions, and \`post\` for creating or modifying remote content.
@@ -97,9 +123,12 @@ function getRunnerCommand(scope, runnerPath) {
97
123
  }
98
124
  return `node ${PROJECT_RUNNER_PATH}`;
99
125
  }
100
- async function writeLocalRunner(path) {
126
+ function getRepairCommand(adapter, scope) {
127
+ return `npx -y agent-permission@latest install --adapter ${adapter} --scope ${scope}`;
128
+ }
129
+ async function writeLocalRunner(path, runnerCommand) {
101
130
  await mkdir(dirname(path), { recursive: true });
102
- await writeFile(path, buildLocalRunner(), { mode: 0o644 });
131
+ await writeFile(path, buildLocalRunner(runnerCommand), { mode: 0o644 });
103
132
  }
104
133
  function shellQuote(value) {
105
134
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -123,13 +152,14 @@ function isOpenClawInstructions(content) {
123
152
  function escapeRegExp(value) {
124
153
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
125
154
  }
126
- function buildLocalRunner() {
155
+ function buildLocalRunner(runnerCommand = `node ${PROJECT_RUNNER_PATH}`) {
127
156
  return `#!/usr/bin/env node
128
157
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
129
158
  import { dirname, join } from 'node:path';
130
159
  import { homedir } from 'node:os';
131
160
 
132
161
  const DEFAULT_API_URL = '${DEFAULT_API_URL}';
162
+ const RUNNER_COMMAND = ${JSON.stringify(runnerCommand)};
133
163
  const ACTIONS = ['crawl', 'summarize', 'extract', 'train', 'transact', 'post'];
134
164
 
135
165
  const code = await main(process.argv.slice(2)).catch((err) => {
@@ -163,6 +193,12 @@ async function runCheck(args) {
163
193
  throw new Error('Usage: agent-permission check <url> --action <crawl|summarize|extract|train|transact|post>');
164
194
  }
165
195
 
196
+ const localDecision = localUrlDecision(url, action);
197
+ if (localDecision) {
198
+ console.log(parsed.flags.json ? JSON.stringify(localDecision, null, 2) : formatCheckHuman(localDecision));
199
+ return parsed.flags['exit-code'] ? decisionExitCode(localDecision.decision) : 0;
200
+ }
201
+
166
202
  const config = await resolveConfig();
167
203
  const result = await checkPermission({
168
204
  url,
@@ -211,7 +247,7 @@ async function runConfig(args) {
211
247
 
212
248
  async function checkPermission(params, options) {
213
249
  if (!options.apiKey) {
214
- throw new Error('Missing API key. Set AGENT_PERMISSION_API_KEY or run \`node .agent-permission/agent-permission.mjs config set api-key <key>\`.');
250
+ throw new Error(\`Missing API key. Set AGENT_PERMISSION_API_KEY or run \\\`\${RUNNER_COMMAND} config set api-key <key>\\\`.\`);
215
251
  }
216
252
 
217
253
  const apiUrl = trimSlash(options.apiUrl);
@@ -322,6 +358,73 @@ function decisionExitCode(decision) {
322
358
  return 1;
323
359
  }
324
360
 
361
+ function localUrlDecision(url, action) {
362
+ const localKind = localUrlKind(url);
363
+ if (!localKind) return undefined;
364
+
365
+ const issuedAt = new Date().toISOString();
366
+ return {
367
+ id: 'local_url_check',
368
+ decision: 'allow',
369
+ confidence: 'high',
370
+ risk: 'low',
371
+ reasons: [
372
+ \`\${localKind} URL was not sent to the Agent Permission API; local development and in-app browser URLs are allowed without external preflight.\`,
373
+ ],
374
+ sources: [
375
+ {
376
+ type: 'local_url',
377
+ url,
378
+ fetched_at: issuedAt,
379
+ evidence: localKind,
380
+ },
381
+ ],
382
+ receipt: {
383
+ receipt_id: 'local_only_no_external_receipt',
384
+ check_id: 'local_url_check',
385
+ url_hash: 'local_only',
386
+ action,
387
+ decision: 'allow',
388
+ source_hashes: [],
389
+ issued_at: issuedAt,
390
+ account_id: 'local',
391
+ signature_alg: 'none',
392
+ signature: 'not_signed',
393
+ },
394
+ };
395
+ }
396
+
397
+ function localUrlKind(value) {
398
+ let parsed;
399
+ try {
400
+ parsed = new URL(value);
401
+ } catch {
402
+ return undefined;
403
+ }
404
+
405
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined;
406
+
407
+ const hostname = parsed.hostname.toLowerCase().replace(/^\\[|\\]$/g, '');
408
+ if (hostname === 'localhost' || hostname.endsWith('.localhost')) return 'localhost';
409
+ if (hostname === '::1') return 'loopback';
410
+ if (hostname.startsWith('127.')) return 'loopback';
411
+ if (hostname === '0.0.0.0') return 'local bind address';
412
+ if (hostname.endsWith('.local')) return 'local network';
413
+
414
+ const parts = hostname.split('.').map((part) => Number(part));
415
+ if (parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255)) {
416
+ const [first, second] = parts;
417
+ if (first === 10) return 'private network';
418
+ if (first === 172 && second >= 16 && second <= 31) return 'private network';
419
+ if (first === 192 && second === 168) return 'private network';
420
+ if (first === 169 && second === 254) return 'link-local';
421
+ }
422
+
423
+ if (hostname.startsWith('fc') || hostname.startsWith('fd') || hostname.startsWith('fe80:')) return 'private network';
424
+
425
+ return undefined;
426
+ }
427
+
325
428
  function getConfigPath() {
326
429
  if (process.env.AGENT_PERMISSION_CONFIG) return process.env.AGENT_PERMISSION_CONFIG;
327
430
 
@@ -380,9 +483,9 @@ function helpText() {
380
483
  return \`Agent Permission local runner
381
484
 
382
485
  Usage:
383
- node .agent-permission/agent-permission.mjs check <url> --action <action> [--json] [--exit-code]
384
- node .agent-permission/agent-permission.mjs config set api-key <key>
385
- node .agent-permission/agent-permission.mjs config doctor
486
+ \${RUNNER_COMMAND} check <url> --action <action> [--json] [--exit-code]
487
+ \${RUNNER_COMMAND} config set api-key <key>
488
+ \${RUNNER_COMMAND} config doctor
386
489
 
387
490
  Actions:
388
491
  crawl, summarize, extract, train, transact, post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-permission",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Public CLI for Agent Permission checks and agent workspace installation.",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",