@idevelopers/agentlock 0.1.0
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/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +11 -0
- package/.mcp.json +11 -0
- package/README.md +114 -0
- package/agentlock.policy.example.json +84 -0
- package/codex-plugin/plugin.json +24 -0
- package/commands/check.md +6 -0
- package/commands/guard.md +6 -0
- package/commands/scan.md +6 -0
- package/dist/audit/log.js +32 -0
- package/dist/config/policy.js +47 -0
- package/dist/config/schema.js +51 -0
- package/dist/enforcement/classify.js +51 -0
- package/dist/enforcement/evaluator.js +126 -0
- package/dist/enforcement/generate_policy.js +73 -0
- package/dist/enforcement/match.js +28 -0
- package/dist/enforcement/runner.js +66 -0
- package/dist/enforcement/secrets.js +38 -0
- package/dist/index.js +17 -0
- package/dist/license/constants.js +2 -0
- package/dist/license/gate.js +6 -0
- package/dist/license/validator.js +187 -0
- package/dist/scan/environment.js +100 -0
- package/dist/server.js +14 -0
- package/dist/tools/activate_license.js +24 -0
- package/dist/tools/audit_log.js +22 -0
- package/dist/tools/check_action.js +44 -0
- package/dist/tools/generate_policy.js +19 -0
- package/dist/tools/response.js +10 -0
- package/dist/tools/run_guarded_command.js +68 -0
- package/dist/tools/scan_environment.js +16 -0
- package/dist/util/paths.js +35 -0
- package/dist/util/redact.js +28 -0
- package/package.json +49 -0
- package/skills/agentlock-workflow/SKILL.md +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "agentlock-marketplace",
|
|
4
|
+
"description": "Local marketplace for AgentLock, a permission firewall for AI coding agents and MCP tools.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "iDevelopers"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "agentlock",
|
|
11
|
+
"description": "Local permission firewall for AI coding agents and MCP tools",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "iDevelopers"
|
|
14
|
+
},
|
|
15
|
+
"category": "development",
|
|
16
|
+
"source": "./",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"homepage": "https://www.npmjs.com/package/@idevelopers/agentlock"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentlock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local permission firewall for AI coding agents and MCP tools",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "iDevelopers",
|
|
7
|
+
"url": "https://github.com/manish-1988"
|
|
8
|
+
},
|
|
9
|
+
"repository": "https://www.npmjs.com/package/@idevelopers/agentlock",
|
|
10
|
+
"license": "MIT"
|
|
11
|
+
}
|
package/.mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# AgentLock
|
|
2
|
+
|
|
3
|
+
Stop AI coding agents from touching secrets, running unsafe commands, or overusing MCP tools without explicit policy.
|
|
4
|
+
|
|
5
|
+
AgentLock is a local MCP server plus Claude Code and Codex plugin packaging. It gives each agent task a permission capsule for files, shell commands, secret reads, network egress, MCP tools, and actions that require approval.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install the MCP server directly from npm in Claude Code:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
claude mcp add agentlock -- npx -y @idevelopers/agentlock
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install the Claude Code plugin from the public installer marketplace:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
/plugin marketplace add https://github.com/manish-1988/agentlock-installer
|
|
19
|
+
/plugin install agentlock@agentlock-marketplace
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For local source development from this private repository:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
node dist/index.js
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Polar handles paid licenses; npm distributes the runtime package. The public installer repository contains only plugin metadata and points Claude/Codex at `npx -y @idevelopers/agentlock`.
|
|
31
|
+
|
|
32
|
+
## Free, Pro, Team
|
|
33
|
+
|
|
34
|
+
Free is the local AgentLock scanner and starter policy generator. The source repository remains private; the public installer repository only contains plugin metadata that points to the public npm runtime.
|
|
35
|
+
|
|
36
|
+
| Capability | Free | Pro | Team |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| Local AI-agent/MCP risk scan | Yes | Yes | Yes |
|
|
39
|
+
| Starter policy generation | Yes | Yes | Yes |
|
|
40
|
+
| Action checks against policy | No | Yes | Yes |
|
|
41
|
+
| Guarded command execution | No | Yes | Yes |
|
|
42
|
+
| Redacted local audit log | No | Yes | Yes |
|
|
43
|
+
| Shared policies, signed bundles, seats | No | No | Yes |
|
|
44
|
+
|
|
45
|
+
Pro is intended for `$19/dev/month` or local INR equivalent. Team is intended for `$99/team/month` with seats.
|
|
46
|
+
|
|
47
|
+
The AgentLock live checkout URL is added after the Polar product is created. Until then, set `AGENTLOCK_CHECKOUT_URL` locally during sandbox or live payment validation.
|
|
48
|
+
|
|
49
|
+
## Policy
|
|
50
|
+
|
|
51
|
+
Create `agentlock.policy.json` in your repo. Start from `agentlock.policy.example.json` or call `agentlock_generate_policy`.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"version": 1,
|
|
56
|
+
"defaultCapsule": "default",
|
|
57
|
+
"capsules": [
|
|
58
|
+
{
|
|
59
|
+
"id": "default",
|
|
60
|
+
"allowedFiles": ["**"],
|
|
61
|
+
"deniedFiles": [".env", ".env.*", "~/.aws/**", "~/.ssh/**"],
|
|
62
|
+
"allowedCommands": ["npm test", "npm run", "git status", "git diff"],
|
|
63
|
+
"deniedCommands": ["rm", "sudo", "npm publish", "git push"],
|
|
64
|
+
"approvalRequired": ["secret-read", "package-publish", "git-push"]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For smoke tests or nonstandard filenames, point AgentLock at a specific policy:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
export AGENTLOCK_POLICY=agentlock.policy.example.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
- `agentlock_scan_environment`: free local risk scan for secret-adjacent files and agent/MCP config.
|
|
79
|
+
- `agentlock_generate_policy`: free starter permission capsule generator.
|
|
80
|
+
- `agentlock_check_action`: Pro policy decision for file, command, network, and MCP actions.
|
|
81
|
+
- `agentlock_run_guarded_command`: Pro command runner using `child_process.spawn` with `shell:false`.
|
|
82
|
+
- `agentlock_audit_log`: Pro redacted local audit log.
|
|
83
|
+
- `agentlock_activate_license`: store and verify a Polar license key.
|
|
84
|
+
|
|
85
|
+
## License Activation
|
|
86
|
+
|
|
87
|
+
AgentLock uses Polar license keys and caches successful validation in `~/.agentlock/license.json` with a 72-hour offline grace window for Pro and Team licenses.
|
|
88
|
+
|
|
89
|
+
For sandbox checks, set both values:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
export POLAR_ENVIRONMENT=sandbox
|
|
93
|
+
export POLAR_ORGANIZATION_ID="your_sandbox_org_id"
|
|
94
|
+
export AGENTLOCK_CHECKOUT_URL="your_sandbox_checkout_url"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then call `agentlock_activate_license` with the Polar license key. By default the activation label is `agentlock-local`; override it with `AGENTLOCK_ACTIVATION_LABEL` if you want a more specific device label in Polar.
|
|
98
|
+
|
|
99
|
+
## Security
|
|
100
|
+
|
|
101
|
+
AgentLock runs locally and does not upload files or secrets to an AgentLock server. The scanner reports file presence and risky surfaces, not secret values. Reports, command output tails, and audit entries are redacted.
|
|
102
|
+
|
|
103
|
+
Guarded commands run with `shell:false` and a sanitized environment that strips secret-like variables. Treat `agentlock.policy.json` like a CI or firewall file: review it before trusting an agent task.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npm install
|
|
109
|
+
npm run build
|
|
110
|
+
npx vitest run
|
|
111
|
+
npm pack --dry-run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The package binary is `agentlock`, mapped to `dist/index.js`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaultCapsule": "default",
|
|
4
|
+
"audit": {
|
|
5
|
+
"enabled": true
|
|
6
|
+
},
|
|
7
|
+
"capsules": [
|
|
8
|
+
{
|
|
9
|
+
"id": "default",
|
|
10
|
+
"allowedFiles": [
|
|
11
|
+
"**"
|
|
12
|
+
],
|
|
13
|
+
"deniedFiles": [
|
|
14
|
+
".env",
|
|
15
|
+
".env.*",
|
|
16
|
+
"**/.env",
|
|
17
|
+
"**/.env.*",
|
|
18
|
+
"**/.npmrc",
|
|
19
|
+
"~/.npmrc",
|
|
20
|
+
"~/.aws/**",
|
|
21
|
+
"~/.ssh/**",
|
|
22
|
+
"~/.config/gcloud/**",
|
|
23
|
+
"~/.docker/config.json",
|
|
24
|
+
"~/.kube/config",
|
|
25
|
+
"**/*secret*",
|
|
26
|
+
"**/*credential*",
|
|
27
|
+
"**/*token*",
|
|
28
|
+
"**/Cookies"
|
|
29
|
+
],
|
|
30
|
+
"allowedCommands": [
|
|
31
|
+
"node",
|
|
32
|
+
"npm test",
|
|
33
|
+
"npm run",
|
|
34
|
+
"npm install",
|
|
35
|
+
"npx vitest",
|
|
36
|
+
"pnpm test",
|
|
37
|
+
"pnpm run",
|
|
38
|
+
"yarn test",
|
|
39
|
+
"git status",
|
|
40
|
+
"git diff",
|
|
41
|
+
"git log",
|
|
42
|
+
"git show",
|
|
43
|
+
"rg",
|
|
44
|
+
"ls",
|
|
45
|
+
"pwd",
|
|
46
|
+
"cat package.json"
|
|
47
|
+
],
|
|
48
|
+
"deniedCommands": [
|
|
49
|
+
"rm",
|
|
50
|
+
"sudo",
|
|
51
|
+
"chmod",
|
|
52
|
+
"chown",
|
|
53
|
+
"dd",
|
|
54
|
+
"mkfs",
|
|
55
|
+
"kill",
|
|
56
|
+
"pkill",
|
|
57
|
+
"npm publish",
|
|
58
|
+
"pnpm publish",
|
|
59
|
+
"yarn npm publish",
|
|
60
|
+
"git push",
|
|
61
|
+
"wrangler deploy",
|
|
62
|
+
"vercel deploy",
|
|
63
|
+
"firebase deploy",
|
|
64
|
+
"gcloud app deploy",
|
|
65
|
+
"terraform apply",
|
|
66
|
+
"terraform destroy"
|
|
67
|
+
],
|
|
68
|
+
"allowedDomains": [],
|
|
69
|
+
"deniedDomains": [],
|
|
70
|
+
"allowedMcpServers": [],
|
|
71
|
+
"allowedMcpTools": [],
|
|
72
|
+
"approvalRequired": [
|
|
73
|
+
"destructive-command",
|
|
74
|
+
"secret-read",
|
|
75
|
+
"network-egress",
|
|
76
|
+
"package-publish",
|
|
77
|
+
"git-push",
|
|
78
|
+
"cloud-deploy",
|
|
79
|
+
"database-command",
|
|
80
|
+
"mcp-tool"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentlock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local permission firewall for AI coding agents and MCP tools",
|
|
5
|
+
"author": "iDevelopers",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"agentlock": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": [
|
|
11
|
+
"-y",
|
|
12
|
+
"@idevelopers/agentlock"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"skills": [
|
|
17
|
+
"skills/agentlock-workflow/SKILL.md"
|
|
18
|
+
],
|
|
19
|
+
"commands": [
|
|
20
|
+
"commands/scan.md",
|
|
21
|
+
"commands/check.md",
|
|
22
|
+
"commands/guard.md"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Check an action against AgentLock policy
|
|
3
|
+
argument-hint: "[action-json]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `agentlock_check_action` with the provided action JSON. If the decision is `block`, stop. If the decision is `needs_approval`, ask for explicit user approval before retrying with `approved: true`.
|
package/commands/scan.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scan the current repo for AI-agent secret and MCP risk surfaces
|
|
3
|
+
argument-hint: ""
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run `agentlock_scan_environment` for the current repository with home checks enabled. Summarize the risk categories and recommend creating `agentlock.policy.json` from `agentlock_generate_policy`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { auditLogPath } from "../util/paths.js";
|
|
4
|
+
import { redactValue } from "../util/redact.js";
|
|
5
|
+
export async function appendAuditEntry(entry, logPath = auditLogPath()) {
|
|
6
|
+
await mkdir(path.dirname(logPath), { recursive: true });
|
|
7
|
+
const redacted = redactValue(entry);
|
|
8
|
+
await writeFile(logPath, `${JSON.stringify(redacted)}\n`, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
flag: "a",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function readAuditEntries(limit = 50, logPath = auditLogPath()) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(logPath, "utf8");
|
|
16
|
+
return raw
|
|
17
|
+
.trim()
|
|
18
|
+
.split("\n")
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.slice(-limit)
|
|
21
|
+
.map((line) => JSON.parse(line));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isNodeError(error) {
|
|
31
|
+
return error instanceof Error && "code" in error;
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ZodError } from "zod";
|
|
4
|
+
import { policySchema } from "./schema.js";
|
|
5
|
+
import { generatePolicy } from "../enforcement/generate_policy.js";
|
|
6
|
+
export async function loadPolicy(cwd = process.cwd()) {
|
|
7
|
+
const policyPath = process.env.AGENTLOCK_POLICY
|
|
8
|
+
? path.resolve(cwd, process.env.AGENTLOCK_POLICY)
|
|
9
|
+
: path.join(cwd, "agentlock.policy.json");
|
|
10
|
+
try {
|
|
11
|
+
const raw = await readFile(policyPath, "utf8");
|
|
12
|
+
return {
|
|
13
|
+
policy: parsePolicy(raw, policyPath),
|
|
14
|
+
source: "file",
|
|
15
|
+
path: policyPath,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
20
|
+
return {
|
|
21
|
+
policy: generatePolicy({ mode: "balanced" }),
|
|
22
|
+
source: "default",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function parsePolicy(raw, policyPath = "agentlock.policy.json") {
|
|
29
|
+
try {
|
|
30
|
+
return policySchema.parse(JSON.parse(raw));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof SyntaxError) {
|
|
34
|
+
throw new Error(`Invalid JSON in ${policyPath}: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof ZodError) {
|
|
37
|
+
const details = error.issues
|
|
38
|
+
.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
|
|
39
|
+
.join("; ");
|
|
40
|
+
throw new Error(`Invalid AgentLock policy at ${policyPath}: ${details}`);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isNodeError(error) {
|
|
46
|
+
return error instanceof Error && "code" in error;
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const actionTypeSchema = z.enum([
|
|
3
|
+
"file_read",
|
|
4
|
+
"file_write",
|
|
5
|
+
"command",
|
|
6
|
+
"network",
|
|
7
|
+
"mcp_tool",
|
|
8
|
+
]);
|
|
9
|
+
export const approvalCategorySchema = z.enum([
|
|
10
|
+
"destructive-command",
|
|
11
|
+
"secret-read",
|
|
12
|
+
"network-egress",
|
|
13
|
+
"package-publish",
|
|
14
|
+
"git-push",
|
|
15
|
+
"cloud-deploy",
|
|
16
|
+
"database-command",
|
|
17
|
+
"mcp-tool",
|
|
18
|
+
]);
|
|
19
|
+
export const permissionCapsuleSchema = z.object({
|
|
20
|
+
id: z.string().min(1),
|
|
21
|
+
allowedFiles: z.array(z.string()).default(["**"]),
|
|
22
|
+
deniedFiles: z.array(z.string()).default([]),
|
|
23
|
+
allowedCommands: z.array(z.string()).default([]),
|
|
24
|
+
deniedCommands: z.array(z.string()).default([]),
|
|
25
|
+
allowedDomains: z.array(z.string()).default([]),
|
|
26
|
+
deniedDomains: z.array(z.string()).default([]),
|
|
27
|
+
allowedMcpServers: z.array(z.string()).default([]),
|
|
28
|
+
allowedMcpTools: z.array(z.string()).default([]),
|
|
29
|
+
approvalRequired: z.array(approvalCategorySchema).default([]),
|
|
30
|
+
});
|
|
31
|
+
export const policySchema = z.object({
|
|
32
|
+
version: z.literal(1),
|
|
33
|
+
defaultCapsule: z.string().min(1).default("default"),
|
|
34
|
+
audit: z
|
|
35
|
+
.object({
|
|
36
|
+
enabled: z.boolean().default(true),
|
|
37
|
+
})
|
|
38
|
+
.default({ enabled: true }),
|
|
39
|
+
capsules: z.array(permissionCapsuleSchema).min(1),
|
|
40
|
+
});
|
|
41
|
+
export const actionSchema = z.object({
|
|
42
|
+
type: actionTypeSchema,
|
|
43
|
+
capsuleId: z.string().min(1).optional(),
|
|
44
|
+
path: z.string().min(1).optional(),
|
|
45
|
+
command: z.string().min(1).optional(),
|
|
46
|
+
args: z.array(z.string()).default([]),
|
|
47
|
+
domain: z.string().min(1).optional(),
|
|
48
|
+
mcpServer: z.string().min(1).optional(),
|
|
49
|
+
mcpTool: z.string().min(1).optional(),
|
|
50
|
+
approved: z.boolean().default(false),
|
|
51
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isLikelySecretPath } from "./secrets.js";
|
|
2
|
+
export function classifyCommand(command, args) {
|
|
3
|
+
const line = commandLine(command, args);
|
|
4
|
+
const categories = new Set();
|
|
5
|
+
if (/(^|\s)(rm|sudo|chmod|chown|dd|mkfs|kill|pkill)(\s|$)/.test(line)) {
|
|
6
|
+
categories.add("destructive-command");
|
|
7
|
+
}
|
|
8
|
+
if (/(^|\s)(npm|pnpm)\s+publish(\s|$)/.test(line) || /(^|\s)yarn\s+npm\s+publish(\s|$)/.test(line)) {
|
|
9
|
+
categories.add("package-publish");
|
|
10
|
+
}
|
|
11
|
+
if (/(^|\s)git\s+push(\s|$)/.test(line)) {
|
|
12
|
+
categories.add("git-push");
|
|
13
|
+
}
|
|
14
|
+
if (/(^|\s)(wrangler|vercel|firebase|serverless)\s+deploy(\s|$)/.test(line) ||
|
|
15
|
+
/(^|\s)gcloud\s+(app\s+deploy|run\s+deploy)(\s|$)/.test(line) ||
|
|
16
|
+
/(^|\s)terraform\s+(apply|destroy)(\s|$)/.test(line)) {
|
|
17
|
+
categories.add("cloud-deploy");
|
|
18
|
+
}
|
|
19
|
+
if (/(^|\s)(psql|mysql|mongosh|redis-cli)(\s|$)/.test(line) ||
|
|
20
|
+
/(^|\s)prisma\s+migrate\s+(deploy|reset)(\s|$)/.test(line)) {
|
|
21
|
+
categories.add("database-command");
|
|
22
|
+
}
|
|
23
|
+
if (/(^|\s)(curl|wget)(\s|$)/.test(line) || extractDomains(args).length > 0) {
|
|
24
|
+
categories.add("network-egress");
|
|
25
|
+
}
|
|
26
|
+
if (/(^|\s)(cat|head|tail|less|more|grep|rg)(\s|$)/.test(line) && args.some(isLikelySecretPath)) {
|
|
27
|
+
categories.add("secret-read");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
categories: [...categories],
|
|
31
|
+
domains: extractDomains(args),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function commandLine(command, args) {
|
|
35
|
+
return [command, ...args].join(" ").trim();
|
|
36
|
+
}
|
|
37
|
+
export function extractDomains(values) {
|
|
38
|
+
const domains = new Set();
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(value);
|
|
42
|
+
domains.add(url.hostname.toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
if (/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
|
|
46
|
+
domains.add(value.toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...domains];
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { normalizeForPolicy } from "../util/paths.js";
|
|
2
|
+
import { classifyCommand, commandLine } from "./classify.js";
|
|
3
|
+
import { matchesAny } from "./match.js";
|
|
4
|
+
import { isLikelySecretPath } from "./secrets.js";
|
|
5
|
+
export function evaluateAction(policy, action, cwd = process.cwd()) {
|
|
6
|
+
const capsule = selectCapsule(policy, action.capsuleId);
|
|
7
|
+
const reasons = [];
|
|
8
|
+
const categories = new Set();
|
|
9
|
+
if (action.type === "command") {
|
|
10
|
+
evaluateCommand(capsule, action.command ?? "", action.args, action.approved, reasons, categories);
|
|
11
|
+
}
|
|
12
|
+
if (action.type === "file_read" || action.type === "file_write") {
|
|
13
|
+
evaluateFile(capsule, action, cwd, reasons, categories);
|
|
14
|
+
}
|
|
15
|
+
if (action.type === "network") {
|
|
16
|
+
evaluateDomains(capsule, action.domain ? [action.domain] : [], reasons, categories);
|
|
17
|
+
categories.add("network-egress");
|
|
18
|
+
}
|
|
19
|
+
if (action.type === "mcp_tool") {
|
|
20
|
+
evaluateMcp(capsule, action, reasons, categories);
|
|
21
|
+
categories.add("mcp-tool");
|
|
22
|
+
}
|
|
23
|
+
if (reasons.some((reason) => reason.startsWith("blocked:"))) {
|
|
24
|
+
return {
|
|
25
|
+
decision: "block",
|
|
26
|
+
capsuleId: capsule.id,
|
|
27
|
+
reasons,
|
|
28
|
+
categories: [...categories],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const approvalReasons = [...categories]
|
|
32
|
+
.filter((category) => capsule.approvalRequired.includes(category))
|
|
33
|
+
.map((category) => `approval required: ${category}`);
|
|
34
|
+
if (approvalReasons.length > 0 && !action.approved) {
|
|
35
|
+
return {
|
|
36
|
+
decision: "needs_approval",
|
|
37
|
+
capsuleId: capsule.id,
|
|
38
|
+
reasons: [...reasons, ...approvalReasons],
|
|
39
|
+
categories: [...categories],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
decision: "allow",
|
|
44
|
+
capsuleId: capsule.id,
|
|
45
|
+
reasons: reasons.length > 0 ? reasons : ["allowed by capsule policy"],
|
|
46
|
+
categories: [...categories],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function selectCapsule(policy, capsuleId) {
|
|
50
|
+
const selectedId = capsuleId ?? policy.defaultCapsule;
|
|
51
|
+
const capsule = policy.capsules.find((candidate) => candidate.id === selectedId) ??
|
|
52
|
+
policy.capsules.find((candidate) => candidate.id === policy.defaultCapsule);
|
|
53
|
+
if (!capsule) {
|
|
54
|
+
throw new Error(`AgentLock policy does not contain capsule ${selectedId}`);
|
|
55
|
+
}
|
|
56
|
+
return capsule;
|
|
57
|
+
}
|
|
58
|
+
function evaluateCommand(capsule, command, args, approved, reasons, categories) {
|
|
59
|
+
if (!command) {
|
|
60
|
+
reasons.push("blocked: command is required");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const line = commandLine(command, args);
|
|
64
|
+
const classification = classifyCommand(command, args);
|
|
65
|
+
let approvedDeniedCommand = false;
|
|
66
|
+
for (const category of classification.categories) {
|
|
67
|
+
categories.add(category);
|
|
68
|
+
}
|
|
69
|
+
evaluateDomains(capsule, classification.domains, reasons, categories);
|
|
70
|
+
if (matchesAny(line, capsule.deniedCommands) || matchesAny(command, capsule.deniedCommands)) {
|
|
71
|
+
const canApprove = [...categories].some((category) => capsule.approvalRequired.includes(category));
|
|
72
|
+
reasons.push(canApprove && approved ? `approved denied-pattern command: ${line}` : `blocked: denied command ${line}`);
|
|
73
|
+
if (!approved) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
approvedDeniedCommand = canApprove;
|
|
77
|
+
}
|
|
78
|
+
if (!approvedDeniedCommand && capsule.allowedCommands.length > 0 && !matchesAny(line, capsule.allowedCommands) && !matchesAny(command, capsule.allowedCommands)) {
|
|
79
|
+
reasons.push(`blocked: command is not in allowedCommands: ${line}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function evaluateFile(capsule, action, cwd, reasons, categories) {
|
|
83
|
+
if (!action.path) {
|
|
84
|
+
reasons.push("blocked: path is required");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const policyPath = normalizeForPolicy(action.path, cwd);
|
|
88
|
+
if (isLikelySecretPath(policyPath) && action.type === "file_read") {
|
|
89
|
+
categories.add("secret-read");
|
|
90
|
+
}
|
|
91
|
+
if (matchesAny(policyPath, capsule.deniedFiles)) {
|
|
92
|
+
const canApprove = categories.has("secret-read") && capsule.approvalRequired.includes("secret-read");
|
|
93
|
+
if (canApprove) {
|
|
94
|
+
reasons.push(action.approved ? `approved denied-pattern file: ${policyPath}` : `approval required: denied-pattern file ${policyPath}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
reasons.push(`blocked: denied file ${policyPath}`);
|
|
98
|
+
if (!action.approved) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (capsule.allowedFiles.length > 0 && !matchesAny(policyPath, capsule.allowedFiles)) {
|
|
103
|
+
reasons.push(`blocked: file is not in allowedFiles: ${policyPath}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function evaluateDomains(capsule, domains, reasons, categories) {
|
|
107
|
+
const extracted = domains.length > 0 ? domains : [];
|
|
108
|
+
for (const domain of extracted) {
|
|
109
|
+
categories.add("network-egress");
|
|
110
|
+
if (matchesAny(domain, capsule.deniedDomains)) {
|
|
111
|
+
reasons.push(`blocked: denied domain ${domain}`);
|
|
112
|
+
}
|
|
113
|
+
if (capsule.allowedDomains.length > 0 && !matchesAny(domain, capsule.allowedDomains)) {
|
|
114
|
+
reasons.push(`blocked: domain is not in allowedDomains: ${domain}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function evaluateMcp(capsule, action, reasons, _categories) {
|
|
119
|
+
if (capsule.allowedMcpServers.length > 0 && (!action.mcpServer || !matchesAny(action.mcpServer, capsule.allowedMcpServers))) {
|
|
120
|
+
reasons.push(`blocked: MCP server is not allowed: ${action.mcpServer ?? "<missing>"}`);
|
|
121
|
+
}
|
|
122
|
+
const toolName = action.mcpTool ? `${action.mcpServer ?? "*"}.${action.mcpTool}` : undefined;
|
|
123
|
+
if (capsule.allowedMcpTools.length > 0 && (!toolName || !matchesAny(toolName, capsule.allowedMcpTools) && !matchesAny(action.mcpTool ?? "", capsule.allowedMcpTools))) {
|
|
124
|
+
reasons.push(`blocked: MCP tool is not allowed: ${toolName ?? "<missing>"}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defaultSecretPathPatterns } from "./secrets.js";
|
|
2
|
+
const approvalRequired = [
|
|
3
|
+
"destructive-command",
|
|
4
|
+
"secret-read",
|
|
5
|
+
"network-egress",
|
|
6
|
+
"package-publish",
|
|
7
|
+
"git-push",
|
|
8
|
+
"cloud-deploy",
|
|
9
|
+
"database-command",
|
|
10
|
+
"mcp-tool",
|
|
11
|
+
];
|
|
12
|
+
export function generatePolicy(options) {
|
|
13
|
+
const allowedCommands = options.mode === "strict"
|
|
14
|
+
? ["npm test", "npm run test", "npm run lint", "npm run typecheck"]
|
|
15
|
+
: [
|
|
16
|
+
"node",
|
|
17
|
+
"npm test",
|
|
18
|
+
"npm run",
|
|
19
|
+
"npm install",
|
|
20
|
+
"npx vitest",
|
|
21
|
+
"pnpm test",
|
|
22
|
+
"pnpm run",
|
|
23
|
+
"yarn test",
|
|
24
|
+
"git status",
|
|
25
|
+
"git diff",
|
|
26
|
+
"git log",
|
|
27
|
+
"git show",
|
|
28
|
+
"rg",
|
|
29
|
+
"ls",
|
|
30
|
+
"pwd",
|
|
31
|
+
"cat package.json",
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
version: 1,
|
|
35
|
+
defaultCapsule: "default",
|
|
36
|
+
audit: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
},
|
|
39
|
+
capsules: [
|
|
40
|
+
{
|
|
41
|
+
id: "default",
|
|
42
|
+
allowedFiles: options.mode === "strict" ? ["src/**", "test/**", "package.json"] : ["**"],
|
|
43
|
+
deniedFiles: defaultSecretPathPatterns(),
|
|
44
|
+
allowedCommands: options.mode === "relaxed" ? [] : allowedCommands,
|
|
45
|
+
deniedCommands: [
|
|
46
|
+
"rm",
|
|
47
|
+
"sudo",
|
|
48
|
+
"chmod",
|
|
49
|
+
"chown",
|
|
50
|
+
"dd",
|
|
51
|
+
"mkfs",
|
|
52
|
+
"kill",
|
|
53
|
+
"pkill",
|
|
54
|
+
"npm publish",
|
|
55
|
+
"pnpm publish",
|
|
56
|
+
"yarn npm publish",
|
|
57
|
+
"git push",
|
|
58
|
+
"wrangler deploy",
|
|
59
|
+
"vercel deploy",
|
|
60
|
+
"firebase deploy",
|
|
61
|
+
"gcloud app deploy",
|
|
62
|
+
"terraform apply",
|
|
63
|
+
"terraform destroy",
|
|
64
|
+
],
|
|
65
|
+
allowedDomains: [],
|
|
66
|
+
deniedDomains: [],
|
|
67
|
+
allowedMcpServers: [],
|
|
68
|
+
allowedMcpTools: [],
|
|
69
|
+
approvalRequired,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|