ai-install-guard 0.2.0 → 0.3.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +36 -18
- package/hooks/gate/gate.ts +116 -0
- package/hooks/gate/recheck.ts +33 -0
- package/hooks/gate/requests.ts +728 -0
- package/hooks/gate/resolve.ts +210 -0
- package/hooks/guard/decisions.ts +32 -0
- package/hooks/guard/pane.tsx +146 -22
- package/hooks/guard/passes.ts +34 -0
- package/hooks/guard/run.ts +158 -0
- package/hooks/guard/view.ts +6 -1
- package/hooks/register.tsx +49 -66
- package/hooks/scan/checks/mod.ts +9 -0
- package/hooks/scan/remote.ts +24 -3
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "ai-install-guard",
|
|
12
12
|
"source": "./",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.3.0",
|
|
14
14
|
"description": "Scans plugins, mods, skills and MCP servers without running them, shows a PASS / WARN / FAIL verdict, and installs nothing until you approve.",
|
|
15
15
|
"category": "security",
|
|
16
16
|
"keywords": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-install-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A pre-install security gate for Claude Code: scans plugins, mods, skills and MCP servers without running them, and installs nothing until you approve.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Boaz Ziniman",
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to ai-install-guard. Ordered newest to oldest.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v0.3.0 · 2026-10-23
|
|
8
|
+
|
|
9
|
+
**`/plugin install`** - The guard now holds installs. `/plugin install <name>@<marketplace>` and `/plugin marketplace add <source>` are looked up (the repository, folder and commit Claude Code would install from), scanned at that commit and answered in the command's place, so nothing installs until you press Install in the pane. Install records your approval for that commit and tree hash, looks the plugin up once more, and runs your command again; a commit that moved since the scan is not installed. A version you already approved installs without asking again unless a new scan judges it worse, a rejected one stays refused, and what the guard cannot pin (an npm package, a plugin no marketplace lists, a name two marketplaces share) is Could not verify, with Install anyway behind a second confirmation that lets that one command through once. The `/plugin` menu itself is not gated yet, and says so when it opens
|
|
10
|
+
|
|
11
|
+
**`claude plugin`** - When Claude runs `claude plugin install`, `claude plugin update` or `claude plugin marketplace add` itself, including inside `sh -c`, `eval`, a here-document or a longer command line, the command is refused with a reason telling Claude the install waits for your decision, and the scan opens in the pane. Once you approve, the same command goes through. Approving it approves that whole command, so the install must stand on its own: one that also runs something else, or that is only known as it runs (a variable, a script piped into a shell, inline code for another language), is held as Could not verify; so is a shell or `eval` told to run a variable, whatever it holds. A marketplace Claude adds must be named by its full path or its https address, since the guard cannot see the shell's current folder. Every other command runs untouched, including ones that only print an install's words
|
|
12
|
+
|
|
13
|
+
**`/guard scan`** - The same scan and pane now serve `/guard scan` and the gates; a version approved through `/guard scan` counts when you install that same commit of that same folder
|
|
14
|
+
|
|
15
|
+
**`checks`** - A mod's `tool.call` or `tool.check` hook narrowed to one tool is described as that tool's ("sees every Bash call"), still as high risk, instead of as every tool call
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
7
19
|
## v0.2.0 · 2026-10-22
|
|
8
20
|
|
|
9
21
|
**`outsideLookups`** - Harder to fool: each package runner's own options are read (`-p` is a package to npx but a Python version to uvx), so `--with`, `--with-requirements` and file arguments no longer hide the main package; Python extras such as `httpx[cli]` are looked up; an advisory OSV answers with an empty or malformed record stays unresolved and a package with no usable answer is reported as not looked up; up to 300 advisories are read for malware instead of 30; every marketplace name is compared; and one scan asks for the official list and each advisory once, however many plugins it lists
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
> **Status: early development.**
|
|
21
|
+
> **Status: early development.** `/plugin install` and `/plugin marketplace add`, and the same installs when Claude runs `claude plugin` itself, are held until you approve the exact version the guard scanned. `/guard scan` checks a folder or a repository before you install it anywhere. The other gates are being built in stages; see [Where it steps in](#where-it-steps-in) and [Roadmap](#roadmap).
|
|
22
22
|
|
|
23
23
|
Everything you add to Claude Code runs with your permissions. A plugin can ship shell hooks that run every session, an MCP server can download and launch a package, a mod can read files or auto-approve tools, and a skill can carry hidden instructions the model will follow. The install prompt asks you to trust the author, and an update can change the files after you looked.
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ AI Install Guard is a Claude Code mod that stops every new plugin, mod, skill, M
|
|
|
26
26
|
|
|
27
27
|
## Who it is for
|
|
28
28
|
|
|
29
|
-
You, before you add something to Claude Code. It runs inside your own session, needs no account, server or agent, and is open source, so you can read every check it makes.
|
|
29
|
+
You, before you add something to Claude Code. It runs inside your own session, needs no account, server or agent, and is open source, so you can read every check it makes. It reaches the network only to fetch the candidate from where it lives and to read that repository's public details; any outside lookup stays optional and off by default.
|
|
30
30
|
|
|
31
31
|
Company security platforms such as AIR, Pluto and Neo watch a whole fleet of agents and enforce policy across it. The guard does not replace them: it is an open, local check for one developer's installs, and it works the same whether or not your company runs one.
|
|
32
32
|
|
|
@@ -41,13 +41,25 @@ Approvals are remembered by commit and content hash, so the same version is neve
|
|
|
41
41
|
|
|
42
42
|
## Where it steps in
|
|
43
43
|
|
|
44
|
-
| Layer | Catches | What happens |
|
|
45
|
-
|
|
46
|
-
| Slash command | `/plugin install
|
|
47
|
-
| Claude's own
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
44
|
+
| Layer | Catches | What happens | Today |
|
|
45
|
+
|-------|---------|--------------|-------|
|
|
46
|
+
| Slash command | `/plugin install <name>@<marketplace>`, `/plugin marketplace add <source>` | Held until you press Install | Yes |
|
|
47
|
+
| Claude's own commands | `claude plugin install`, `claude plugin update`, `claude plugin marketplace add`, also inside `sh -c`, `eval` or a here-document | Refused, with a note telling Claude the install waits for your decision | Yes |
|
|
48
|
+
| Claude's own commands | `claude mcp add`, copies into skills folders, edits to `.mcp.json` or settings, `npx -y`, `curl \| sh` | Refused until scanned | Next stage |
|
|
49
|
+
| Mod loading | Any mod about to load, however it got there, including plugins picked from the `/plugin` menu | Refused if its files do not match what you approved | Later |
|
|
50
|
+
| First use of a skill | Skills and commands, including ones copied in by hand | Blocked until scanned | Later |
|
|
51
|
+
| Session start | Anything installed while the guard was not running | Scanned, with Keep or Uninstall | Later |
|
|
52
|
+
|
|
53
|
+
### Installing with the guard on
|
|
54
|
+
|
|
55
|
+
Run `/plugin install notes@acme` as usual. The guard looks up where Claude Code would install it from (the repository, folder and commit its marketplace names), scans that commit, and answers in the command's place, so nothing is installed yet. The pane shows the verdict and the command it is holding.
|
|
56
|
+
|
|
57
|
+
- **Install** (`i`) records your approval for that exact commit and tree hash, looks the plugin up once more, and runs your `/plugin` command again. If the commit moved since the scan, nothing is installed and the next `/plugin install` scans the new one.
|
|
58
|
+
- **Reject** (`r`) records the rejection. The same version is refused from then on; `/guard forget notes@acme` judges it again.
|
|
59
|
+
- A version you already approved (the same commit of the same folder, here or with `/guard scan`) is scanned again and installs without asking, unless it now judges worse than when you approved it (an outside lookup turned on since may know it as malware). A version you rejected is refused at once. A new commit is always scanned.
|
|
60
|
+
- When the verdict is **Do not install**, the button reads **Install anyway** and asks a second time. **Could not verify** (the guard could not tell what would be installed: a package from npm, a plugin no marketplace lists, a name two marketplaces share) works the same way, and lets that one command through once.
|
|
61
|
+
|
|
62
|
+
When Claude runs `claude plugin install` itself, the guard refuses the command and tells Claude why: the install waits for your decision, and Claude should not retry it or find another way. Approve it in the pane, then tell Claude to run it again; the same command now goes through. Because approving it approves that whole command, the install must stand on its own (a `tail` after it is fine): a command that also runs something else, changes folder or writes a file, or that is only known as it runs (`$CMD plugin install …`, `bash -c "$CMD"`, a script piped into a shell, code for another language that mentions `claude`), is held as Could not verify. A marketplace Claude adds must be named by its full path or as `https://github.com/<owner>/<repo>`, since the guard cannot see the shell's current folder.
|
|
51
63
|
|
|
52
64
|
## What it checks
|
|
53
65
|
|
|
@@ -78,7 +90,7 @@ A remote scan resolves the ref to one commit, fetches only that commit (no hooks
|
|
|
78
90
|
|
|
79
91
|
Each row opens to say what it found in plain words first: which programs the candidate starts and what each is for, which secrets its code reads, and what is worth a closer look. The code lines that back it up sit behind **Show the code**.
|
|
80
92
|
|
|
81
|
-
In the pane, **Approve** (`i`) and **Reject** (`r`) record your decision for that exact content (its tree hash)
|
|
93
|
+
In the pane, **Approve** (`i`) and **Reject** (`r`) record your decision for that exact content (its tree hash); for an install the guard is holding, the button reads **Install**. When the verdict is Do not install or Could not verify, the button reads **Approve anyway** (or **Install anyway**) and asks a second time. **Details** (`d`) opens every row, and **Copy report** (`c`) copies the full report as text. Where no pane can show (a headless `claude -p` run), `/guard scan` prints the report instead.
|
|
82
94
|
|
|
83
95
|
## Use it in Claude chat
|
|
84
96
|
|
|
@@ -121,12 +133,14 @@ A security tool should be held to its own rules, so here is everything this mod
|
|
|
121
133
|
|
|
122
134
|
| Capability | Why |
|
|
123
135
|
|------------|-----|
|
|
124
|
-
| `process.run` | Runs `git` to resolve a ref and fetch one commit into the guard's own temporary folder (made with `mktemp` and removed with `rm` afterwards), `claude plugin validate` on a candidate (it reads source and runs none of it), `readlink` to see where a symbolic link points without following it, and `printenv HOME` to expand `~`. It never runs anything from a candidate. |
|
|
125
|
-
| `fs.read`, `fs.list`, `fs.stat` | Reads the candidate's files to scan them. It writes no files itself; only `git` writes, into the guard's temporary folder. |
|
|
136
|
+
| `process.run` | Runs `git` to resolve a ref and fetch one commit into the guard's own temporary folder (made with `mktemp` and removed with `rm` afterwards), `claude plugin validate` on a candidate (it reads source and runs none of it), `claude plugin list --available --json` and `claude plugin marketplace list --json` to find where a plugin you install comes from, `readlink` to see where a symbolic link points without following it, and `printenv HOME` to expand `~`. It never runs anything from a candidate. |
|
|
137
|
+
| `fs.read`, `fs.list`, `fs.stat` | Reads the candidate's files to scan them, and a marketplace's catalog in Claude Code's own copy of it. It writes no files itself; only `git` writes, into the guard's temporary folder. |
|
|
138
|
+
| `command.run` (hook) | Intercepts `/plugin`, so `/plugin install` and `/plugin marketplace add` are scanned and held until you approve them. Every other `/plugin` command goes on untouched. |
|
|
139
|
+
| `command.run` (call) | When you press Install, runs your own `/plugin` command again, after checking the version is still the one you approved. |
|
|
140
|
+
| `tool.call` (hook) | Reads the text of each shell command Claude is about to run, to catch `claude plugin install` and its kin, and refuses those until you approve. Every other command and every other tool goes on untouched; it never rewrites or answers one. |
|
|
126
141
|
| `command.register` | Adds the `/guard` command. |
|
|
127
|
-
| `store`, `state` | Remembers your decisions and the last report,
|
|
142
|
+
| `store`, `state` | Remembers your decisions and the last report, holds what the pane shows, and keeps a one-time pass for an install you allowed anyway. |
|
|
128
143
|
| `ui` | The verdict pane, its status line and toasts, and copying the report. |
|
|
129
|
-
|
|
130
144
|
| `http.fetch` | For a repository on GitHub, reads its public metadata from GitHub's API: age, owner, stars, contributors, and whether the scanned commit is signed and on the default branch. GitHub is where the code came from, so this tells it nothing new. With **outside lookups** on, also sends package names to [OSV](https://osv.dev) and reads Anthropic's [official plugin list](https://github.com/anthropics/claude-plugins-official). |
|
|
131
145
|
|
|
132
146
|
A later stage adds, optionally, `model.complete` (a review that can only add warnings).
|
|
@@ -151,6 +165,9 @@ A lookup that fails or hits a rate limit is noted in the report and never fails
|
|
|
151
165
|
- **Not a proof of safety.** Static analysis raises the bar and makes every capability visible; a determined attacker can still evade it.
|
|
152
166
|
- **Install it first.** It judges only mods that load after it. For a company, the strongest setup is deploying it through managed settings so it loads first.
|
|
153
167
|
- **No per-plugin block for classic shell hooks.** Claude Code runs them outside mods, so the guard scans before install and uninstalls on reject.
|
|
168
|
+
- **The `/plugin` menu is not gated yet.** A plugin picked from the menu installs without a scan; the guard says so when the menu opens. The mod loading gate in a later stage closes this. Until then, install with `/plugin install <name>@<marketplace>`.
|
|
169
|
+
- **Claude's commands are read, not run.** The guard finds an install in a shell command however it is quoted or wrapped (`env`, `sudo`, `sh -c`, `eval`, a here-document), and holds a command it cannot read (one built while it runs, a script piped into a shell, inline code for another language) as Could not verify. A script file that installs something is not read; the mod loading gate is the backstop for that.
|
|
170
|
+
- **Between Install and the install itself** Claude Code fetches the plugin again. The guard checks the commit just before, and a commit is fixed content, but a marketplace that names a branch rather than a commit can move in that moment. The scan warns about such marketplaces.
|
|
154
171
|
|
|
155
172
|
## Roadmap
|
|
156
173
|
|
|
@@ -158,10 +175,11 @@ A lookup that fails or hits a rate limit is noted in the report and never fails
|
|
|
158
175
|
2. [x] Checks for structure, mod capabilities, classic hooks, skills, code patterns and committed secrets
|
|
159
176
|
3. [x] The verdict pane with Install and Reject
|
|
160
177
|
4. [x] Remote fetch pinned to a commit, repository provenance, MCP server checks, and optional lookups against OSV and the official plugin list
|
|
161
|
-
5. [
|
|
162
|
-
6. [ ]
|
|
163
|
-
7. [ ]
|
|
164
|
-
8. [ ]
|
|
178
|
+
5. [x] Gates on `/plugin install`, `/plugin marketplace add` and Claude's own `claude plugin` installs, with approvals pinned to a commit
|
|
179
|
+
6. [ ] Gates on Claude adding MCP servers and writing into skill, plugin and settings folders
|
|
180
|
+
7. [ ] Module load gate with hash match, skill gate, session-start reconciliation
|
|
181
|
+
8. [ ] Update diffs and the optional model review
|
|
182
|
+
9. [ ] Full documentation
|
|
165
183
|
|
|
166
184
|
## Contributing
|
|
167
185
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The install gate: an install asked for by `/plugin` or by a shell command
|
|
2
|
+
// Claude runs is let through only when the person already approved that exact
|
|
3
|
+
// version. Anything else is scanned and held in the pane, where Install
|
|
4
|
+
// carries it out.
|
|
5
|
+
|
|
6
|
+
import type { EngineInterface } from 'claude-code'
|
|
7
|
+
|
|
8
|
+
import type { GuardRequest } from '../../types'
|
|
9
|
+
import { pinnedDecision } from '../guard/decisions'
|
|
10
|
+
import { takePass } from '../guard/passes'
|
|
11
|
+
import { showScan, showUnverified, type Shown } from '../guard/run'
|
|
12
|
+
import { sourceRoot } from '../guard/view'
|
|
13
|
+
import { modHost } from '../mod-host'
|
|
14
|
+
import { resolvePin } from '../scan/remote'
|
|
15
|
+
import { safeLine } from '../scan/safe-text'
|
|
16
|
+
import type { InstallRequest, Reading } from './requests'
|
|
17
|
+
import { targetOf } from './resolve'
|
|
18
|
+
|
|
19
|
+
export type Judgement =
|
|
20
|
+
/** The install goes through; `note` says why, for a toast. */
|
|
21
|
+
| { kind: 'allow'; note?: string }
|
|
22
|
+
/** The install is held: `text` answers the command, or tells Claude why its command was refused. */
|
|
23
|
+
| { kind: 'hold'; text: string }
|
|
24
|
+
|
|
25
|
+
/** What the guard does with one install, asked for as `request` says. */
|
|
26
|
+
export async function judge($: EngineInterface, reading: Exclude<Reading, { kind: 'pass' }>, request: GuardRequest, lookups: boolean): Promise<Judgement> {
|
|
27
|
+
// Install anyway, confirmed, on something nothing could pin: this exact command, once.
|
|
28
|
+
if (await takePass($, { origin: request.origin, args: request.args })) {
|
|
29
|
+
return { kind: 'allow', note: `AI Install Guard: installing ${request.label}, as you allowed` }
|
|
30
|
+
}
|
|
31
|
+
const options = { action: 'install' as const, request, lookups }
|
|
32
|
+
if (reading.kind === 'unreadable') {
|
|
33
|
+
const shown = await showUnverified($, reading.label, reading.reason, options)
|
|
34
|
+
return shown.superseded ? superseded(request) : hold(request, 'Could not verify', '', shown.placed)
|
|
35
|
+
}
|
|
36
|
+
const name = nameOf(reading.request)
|
|
37
|
+
const host = modHost($)
|
|
38
|
+
const target = await targetOf(host, reading.request)
|
|
39
|
+
switch (target.kind) {
|
|
40
|
+
case 'browse':
|
|
41
|
+
return { kind: 'allow', note: BROWSE_NOTE }
|
|
42
|
+
case 'unverifiable': {
|
|
43
|
+
const shown = await showUnverified($, name, target.reason, options)
|
|
44
|
+
return shown.superseded ? superseded(request) : hold(request, 'Could not verify', '', shown.placed)
|
|
45
|
+
}
|
|
46
|
+
case 'remote': {
|
|
47
|
+
// The commit the install would take now. A rejection of it stands
|
|
48
|
+
// without a new scan; an approval is checked against a new one, which
|
|
49
|
+
// may judge worse (a lookup turned on since may know it as malware).
|
|
50
|
+
const pin = await resolvePin(host, target.spec).catch(() => undefined)
|
|
51
|
+
if (pin !== undefined) {
|
|
52
|
+
const decided = await pinnedDecision($, { root: sourceRoot(target.spec.url, pin.subdir), sha: pin.sha }).catch(() => undefined)
|
|
53
|
+
if (decided?.decision === 'rejected') return rejected(request, name, ` at commit ${pin.sha.slice(0, 12)}`, decided.decidedAt)
|
|
54
|
+
}
|
|
55
|
+
// The scan fetches exactly the commit just looked up, so the verdict is about what installs.
|
|
56
|
+
const spec = pin === undefined ? target.spec : { url: target.spec.url, ref: pin.sha, ...(pin.subdir === undefined ? {} : { subdir: pin.subdir }) }
|
|
57
|
+
return afterScan(request, name, await showScan($, name, { remote: spec }, options))
|
|
58
|
+
}
|
|
59
|
+
case 'local':
|
|
60
|
+
return afterScan(request, name, await showScan($, name, { folder: target.path }, options))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What the plugins menu cannot promise yet. */
|
|
65
|
+
export const BROWSE_NOTE = 'AI Install Guard: plugins picked from this menu are not scanned yet. To scan one first, run /plugin install <name>@<marketplace>'
|
|
66
|
+
|
|
67
|
+
/** The name a decision is kept under, so /guard forget finds it: the plugin as typed, or the marketplace's source. */
|
|
68
|
+
function nameOf(request: InstallRequest): string {
|
|
69
|
+
if (request.kind === 'marketplace') return safeLine(request.source)
|
|
70
|
+
return safeLine(request.marketplace === undefined ? request.plugin : `${request.plugin}@${request.marketplace}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function afterScan(request: GuardRequest, name: string, shown: Shown): Judgement {
|
|
74
|
+
if (shown.kind === 'failed') return shown.superseded ? superseded(request) : hold(request, 'Could not verify', '', shown.placed)
|
|
75
|
+
const sha = shown.report.source?.sha
|
|
76
|
+
const where = sha === undefined ? '' : ` at commit ${sha.slice(0, 12)}`
|
|
77
|
+
// The same files approved before (through /guard scan, or an earlier install), and judged no
|
|
78
|
+
// worse now, need no second press.
|
|
79
|
+
if (shown.view.decision === 'approved') return { kind: 'allow', note: `AI Install Guard: you approved ${name}${where} on ${day(shown.view.decidedAt ?? Date.now())}; installing it` }
|
|
80
|
+
if (shown.view.decision === 'rejected') return rejected(request, name, where, shown.view.decidedAt)
|
|
81
|
+
if (shown.superseded) return superseded(request)
|
|
82
|
+
return hold(request, shown.report.verdict.recommendation, where, shown.placed, shown.text)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Another scan took the pane before this one finished: nothing to press, so nothing installs. */
|
|
86
|
+
function superseded(request: GuardRequest): Judgement {
|
|
87
|
+
const text = `AI Install Guard: another scan took the pane before the one for ${request.label} finished, so nothing was installed.`
|
|
88
|
+
return { kind: 'hold', text: request.origin === 'tool' ? `${text} Do not retry it now; tell the person, and run it again once they ask.` : `${text} Run it again to review it.` }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rejected(request: GuardRequest, name: string, where: string, decidedAt: number | undefined): Judgement {
|
|
92
|
+
const when = decidedAt === undefined ? '' : ` on ${day(decidedAt)}`
|
|
93
|
+
if (request.origin === 'tool') {
|
|
94
|
+
return { kind: 'hold', text: `AI Install Guard: the person rejected this exact version of ${name}${where}${when}. Nothing was installed. Do not install it or try another way.` }
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'hold', text: `AI Install Guard: you rejected ${name}${where}${when}. Nothing was installed. /guard forget ${name} judges it again.` }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The answer while an install waits for the person's decision in the pane. */
|
|
100
|
+
function hold(request: GuardRequest, recommendation: string, where: string, placed: boolean, report?: string): Judgement {
|
|
101
|
+
const what = `${recommendation} for ${request.label}${where}`
|
|
102
|
+
if (request.origin === 'tool') {
|
|
103
|
+
const next = placed
|
|
104
|
+
? 'The verdict and the Install and Reject buttons are in the guard\'s pane. Tell the person the install waits for their decision there, and run the same command again only after they say they approved it.'
|
|
105
|
+
: 'This session cannot show the guard\'s pane, so the person cannot approve it here.'
|
|
106
|
+
return { kind: 'hold', text: `AI Install Guard held this install for the person to review: ${what}. Nothing was installed. Do not retry it or install it another way. ${next}` }
|
|
107
|
+
}
|
|
108
|
+
if (!placed) {
|
|
109
|
+
return { kind: 'hold', text: `${report === undefined ? '' : `${report}\n\n`}AI Install Guard: ${what}. Nothing was installed: approving an install needs the guard's pane, which this session cannot show.` }
|
|
110
|
+
}
|
|
111
|
+
return { kind: 'hold', text: `AI Install Guard: ${what}. Nothing is installed yet: press Install in the guard's pane to install it, or Reject.` }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function day(time: number): string {
|
|
115
|
+
return new Date(time).toISOString().slice(0, 10)
|
|
116
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The last look before a held install runs: the version it would install now
|
|
2
|
+
// must still be the one the person approved. A plugin's own `$.command.run`
|
|
3
|
+
// does not pass through its own gate, so Install asks this itself.
|
|
4
|
+
|
|
5
|
+
import type { GuardView } from '../../types'
|
|
6
|
+
import { sourceRoot } from '../guard/view'
|
|
7
|
+
import type { Host } from '../scan/host'
|
|
8
|
+
import { takeInventory } from '../scan/inventory'
|
|
9
|
+
import { resolvePin } from '../scan/remote'
|
|
10
|
+
import { readPluginArgs } from './requests'
|
|
11
|
+
import { targetOf } from './resolve'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Undefined when the held install would still install what was scanned;
|
|
15
|
+
* otherwise why not. Something nothing could pin (Could not verify) has no
|
|
16
|
+
* version to compare, and the person allowed it as it is.
|
|
17
|
+
*/
|
|
18
|
+
export async function changedSinceScan(host: Host, view: GuardView): Promise<string | undefined> {
|
|
19
|
+
if (view.treeHash === undefined || view.request === undefined) return undefined
|
|
20
|
+
const reading = readPluginArgs(view.request.args)
|
|
21
|
+
if (reading.kind !== 'install') return 'the install no longer reads as one the guard can check'
|
|
22
|
+
const target = await targetOf(host, reading.request)
|
|
23
|
+
if (target.kind === 'remote') {
|
|
24
|
+
const pin = await resolvePin(host, target.spec)
|
|
25
|
+
if (view.sha !== pin.sha || view.root !== sourceRoot(target.spec.url, pin.subdir)) return `it now points at commit ${pin.sha.slice(0, 12)}, not the one scanned`
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
if (target.kind === 'local') {
|
|
29
|
+
const { inventory } = await takeInventory(host, target.path)
|
|
30
|
+
return inventory.treeHash === view.treeHash ? undefined : 'its files changed since the scan'
|
|
31
|
+
}
|
|
32
|
+
return target.kind === 'unverifiable' ? target.reason : 'it no longer names one plugin'
|
|
33
|
+
}
|