agent-permission 0.1.6 → 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 +21 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +95 -3
- package/dist/install.d.ts +9 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +102 -4
- package/package.json +1 -1
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 -y agent-permission@latest 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,20 +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 -y agent-permission@latest install --adapter
|
|
28
|
+
npx -y agent-permission@latest install --adapter codex --api-key <YOUR_API_KEY>
|
|
17
29
|
```
|
|
18
30
|
|
|
19
|
-
To
|
|
31
|
+
To force a project-local Codex install instead:
|
|
20
32
|
|
|
21
33
|
```bash
|
|
22
|
-
npx -y agent-permission@latest install --adapter codex --scope
|
|
34
|
+
npx -y agent-permission@latest install --adapter codex --scope project
|
|
23
35
|
```
|
|
24
36
|
|
|
25
37
|
Add an API key when you want live checks from the command line, CI, or agent runs:
|
|
26
38
|
|
|
27
39
|
```bash
|
|
28
|
-
node
|
|
29
|
-
node
|
|
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
|
|
30
42
|
```
|
|
31
43
|
|
|
32
44
|
## What the Agent Runs
|
|
@@ -34,16 +46,16 @@ node .agent-permission/agent-permission.mjs check https://example.com/blog-post
|
|
|
34
46
|
After install, project instructions tell compatible agents to run:
|
|
35
47
|
|
|
36
48
|
```bash
|
|
37
|
-
node
|
|
49
|
+
node <installed-runner-path> check <url> --action <crawl|summarize|extract|train|transact|post> --exit-code
|
|
38
50
|
```
|
|
39
51
|
|
|
40
52
|
If the runner is missing, the installed instructions tell the agent to stop and ask you to repair the installation once:
|
|
41
53
|
|
|
42
54
|
```bash
|
|
43
|
-
npx -y agent-permission@latest install --adapter
|
|
55
|
+
npx -y agent-permission@latest install --adapter codex --scope user
|
|
44
56
|
```
|
|
45
57
|
|
|
46
|
-
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.
|
|
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.
|
|
47
59
|
|
|
48
60
|
## Commands
|
|
49
61
|
|
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;
|
|
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', '
|
|
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') ||
|
|
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.
|
|
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'>,
|
|
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
|
package/dist/install.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -10,16 +10,32 @@ export async function installAgentInstructions(adapter, scope, runtime) {
|
|
|
10
10
|
const runner = getRunnerPath(scope, runtime);
|
|
11
11
|
const runnerCommand = getRunnerCommand(scope, runner);
|
|
12
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`;
|
|
13
16
|
const existing = await readFile(path, 'utf8').catch((err) => {
|
|
14
17
|
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT')
|
|
15
18
|
return '';
|
|
16
19
|
throw err;
|
|
17
20
|
});
|
|
18
|
-
const next = upsertBlock(existing, buildInstructionBlock(resolved,
|
|
21
|
+
const next = upsertBlock(existing, buildInstructionBlock(resolved, {
|
|
22
|
+
checkCommand,
|
|
23
|
+
apiKeyCommand,
|
|
24
|
+
repairCommand,
|
|
25
|
+
}));
|
|
19
26
|
await mkdir(dirname(path), { recursive: true });
|
|
20
27
|
await writeLocalRunner(runner, runnerCommand);
|
|
21
28
|
await writeFile(path, next);
|
|
22
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
adapter: resolved,
|
|
31
|
+
path,
|
|
32
|
+
runnerPath: runner,
|
|
33
|
+
runnerCommand,
|
|
34
|
+
checkCommand,
|
|
35
|
+
apiKeyCommand,
|
|
36
|
+
doctorCommand,
|
|
37
|
+
repairCommand,
|
|
38
|
+
};
|
|
23
39
|
}
|
|
24
40
|
export function upsertBlock(existing, blockBody) {
|
|
25
41
|
const block = `${BLOCK_START}\n${blockBody.trim()}\n${BLOCK_END}`;
|
|
@@ -30,13 +46,16 @@ export function upsertBlock(existing, blockBody) {
|
|
|
30
46
|
const prefix = existing.trimEnd();
|
|
31
47
|
return prefix ? `${prefix}\n\n${block}\n` : `${block}\n`;
|
|
32
48
|
}
|
|
33
|
-
export function buildInstructionBlock(adapter,
|
|
49
|
+
export function buildInstructionBlock(adapter, commands = {}) {
|
|
34
50
|
const agentName = {
|
|
35
51
|
codex: 'Codex',
|
|
36
52
|
claude: 'Claude',
|
|
37
53
|
chatgpt: 'ChatGPT',
|
|
38
54
|
openclaw: 'OpenClaw',
|
|
39
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`;
|
|
40
59
|
return `# Agent Permission
|
|
41
60
|
|
|
42
61
|
These instructions equip ${agentName} with Agent Permission preflight checks.
|
|
@@ -44,7 +63,7 @@ These instructions equip ${agentName} with Agent Permission preflight checks.
|
|
|
44
63
|
Before crawling, summarizing, extracting, training on, transacting with, or posting to a URL, run:
|
|
45
64
|
|
|
46
65
|
\`\`\`bash
|
|
47
|
-
${
|
|
66
|
+
${checkCommand}
|
|
48
67
|
\`\`\`
|
|
49
68
|
|
|
50
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:
|
|
@@ -53,6 +72,12 @@ Do not use npm while handling an individual URL request. If the runner command i
|
|
|
53
72
|
${repairCommand}
|
|
54
73
|
\`\`\`
|
|
55
74
|
|
|
75
|
+
If the runner reports that the API key is missing, stop and ask the user to configure it with:
|
|
76
|
+
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
${apiKeyCommand}
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
56
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.
|
|
57
82
|
|
|
58
83
|
Treat \`allow\` as permitted, \`deny\` as blocked, and \`escalate\` as requiring human approval before continuing. Preserve the returned receipt ID in task notes when a permission decision affects user-visible work.`;
|
|
@@ -168,6 +193,12 @@ async function runCheck(args) {
|
|
|
168
193
|
throw new Error('Usage: agent-permission check <url> --action <crawl|summarize|extract|train|transact|post>');
|
|
169
194
|
}
|
|
170
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
|
+
|
|
171
202
|
const config = await resolveConfig();
|
|
172
203
|
const result = await checkPermission({
|
|
173
204
|
url,
|
|
@@ -327,6 +358,73 @@ function decisionExitCode(decision) {
|
|
|
327
358
|
return 1;
|
|
328
359
|
}
|
|
329
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
|
+
|
|
330
428
|
function getConfigPath() {
|
|
331
429
|
if (process.env.AGENT_PERMISSION_CONFIG) return process.env.AGENT_PERMISSION_CONFIG;
|
|
332
430
|
|