pi-profile-switch 0.9.1 → 0.9.2
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 +1 -1
- package/bin/pi-profile.ts +9 -1
- package/bin/postinstall.js +2 -2
- package/package.json +1 -1
- package/skills/profile-config/SKILL.md +104 -104
- package/src/launcher/initial-profile.ts +8 -4
- package/src/launcher/untrusted-project-diagnostic.ts +84 -0
- package/src/project-trust.ts +10 -7
- package/src/starter-assets.ts +6 -6
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ All commands work in non-interactive modes (`--mode rpc|print|json`); CRUD wizar
|
|
|
121
121
|
|
|
122
122
|
## Docs
|
|
123
123
|
|
|
124
|
-
- [Architecture](docs/architecture/overview.md) · [ADRs](docs/adr/) · [Glossary](CONTEXT.md)
|
|
124
|
+
- [Architecture](docs/architecture/overview.md) · [ADRs](docs/adr/) · [Glossary](CONTEXT.md)
|
|
125
125
|
- JSON Schemas: [`schemas/`](schemas/)
|
|
126
126
|
|
|
127
127
|
## License
|
package/bin/pi-profile.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { ExtensionError } from "../src/extension-discovery.ts";
|
|
17
17
|
import { parseLauncherArgs } from "../src/launcher/args.ts";
|
|
18
18
|
import { UnknownProfileError, resolveInitialProfile } from "../src/launcher/initial-profile.ts";
|
|
19
|
+
import { untrustedProjectDiagnostic } from "../src/launcher/untrusted-project-diagnostic.ts";
|
|
19
20
|
import { sweepStaleInstances } from "../src/launcher/runtime-cleanup.ts";
|
|
20
21
|
import { spawnPi } from "../src/launcher/spawn.ts";
|
|
21
22
|
import { McpConfigError, MissingMcpAdapterError } from "../src/mcp-config.ts";
|
|
@@ -37,7 +38,7 @@ try {
|
|
|
37
38
|
}
|
|
38
39
|
// Fails before spawning when the profile is unknown or cannot activate.
|
|
39
40
|
// --approve/--no-approve are consumed here as a one-run trust input.
|
|
40
|
-
const { plan, discovery, projectDir, warnings } = await resolveInitialProfile(args.profile, {
|
|
41
|
+
const { plan, discovery, projectDir, projectTrusted, warnings } = await resolveInitialProfile(args.profile, {
|
|
41
42
|
agentDir,
|
|
42
43
|
cwd: process.cwd(),
|
|
43
44
|
trustOverride: args.trustOverride,
|
|
@@ -45,6 +46,13 @@ try {
|
|
|
45
46
|
for (const warning of warnings) {
|
|
46
47
|
console.error(`pi-profile: warning: ${warning}`);
|
|
47
48
|
}
|
|
49
|
+
// Untrusted-project notice: fires identically for every profile (default
|
|
50
|
+
// included) because it is driven by the trust determination, not by which
|
|
51
|
+
// profile branch resolution took. Startup continues either way.
|
|
52
|
+
const untrustedNotice = untrustedProjectDiagnostic(process.cwd(), projectTrusted);
|
|
53
|
+
if (untrustedNotice) {
|
|
54
|
+
console.error(`pi-profile: warning: ${untrustedNotice}`);
|
|
55
|
+
}
|
|
48
56
|
// Stale per-launch instance dirs (dead pid, or no pid past the grace
|
|
49
57
|
// window) are swept before this launch materializes its own. Unrecognized
|
|
50
58
|
// entries are dispositioned per content scan: kept + reported when they
|
package/bin/postinstall.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* launcher instead: `bin/pi-profile.ts` calls the runtime ensure
|
|
13
13
|
* (src/starter-assets.ts) on every launch, before resolving the initial
|
|
14
14
|
* profile. The authoritative behavior contract for both assets lives in
|
|
15
|
-
* openspec/specs/profile-catalog/spec.md ("
|
|
16
|
-
* "
|
|
15
|
+
* openspec/specs/profile-catalog/spec.md ("Seeding the starter profile" and
|
|
16
|
+
* "Distributing the profile-config skill"); keep this script's rules in sync with
|
|
17
17
|
* src/starter-assets.ts, which is the single TS implementation.
|
|
18
18
|
*
|
|
19
19
|
* Idempotent and conservative:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-profile-switch",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting. Install: npm install -g pi-profile-switch (not pi install).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -1,67 +1,67 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: profile-config
|
|
3
|
-
description:
|
|
3
|
+
description: Guides the creation, modification, and deletion of pi-profile-switch profiles. Trigger when the user wants to create, modify, configure, or delete a profile.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# profile-config
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
This skill guides the agent in understanding the user's vague requirements or explicit instructions, and assists in creating, modifying, or deleting `pi-profile-switch` profile files.
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> **Notice**: This skill file is distributed by the `pi-profile-switch` package install and is automatically overwritten on package upgrades. Do not modify this file manually.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## 1.
|
|
14
|
+
## 1. Core concepts and constraints
|
|
15
15
|
|
|
16
|
-
### 1.1
|
|
17
|
-
- **Profile
|
|
18
|
-
- **Catalog
|
|
19
|
-
- **default profile
|
|
16
|
+
### 1.1 Profiles and catalogs
|
|
17
|
+
- **Profile**: a named capability definition referencing skills, extensions, MCP servers, and tools, optionally declaring model, thinking level, and instructions.
|
|
18
|
+
- **Catalog**: the `profiles/` directory holding profile definitions. Each profile corresponds to one standalone JSON file in the directory: `<name>.json`.
|
|
19
|
+
- **default profile**: provided by Pi; cannot be deleted or edited; loads every resource Pi can discover. **Strictly forbidden** to create `default.json` in any profiles directory.
|
|
20
20
|
|
|
21
|
-
### 1.2
|
|
22
|
-
|
|
21
|
+
### 1.2 Name charset rules
|
|
22
|
+
A profile name must fully match the regex:
|
|
23
23
|
```regex
|
|
24
24
|
^[A-Za-z0-9][A-Za-z0-9._-]*$
|
|
25
25
|
```
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
26
|
+
- Must start with an ASCII letter or digit.
|
|
27
|
+
- Only ASCII letters, digits, dots (`.`), underscores (`_`), and hyphens (`-`) are allowed.
|
|
28
|
+
- Spaces, CJK characters, and special symbols are not allowed.
|
|
29
29
|
|
|
30
|
-
### 1.3
|
|
31
|
-
Profile
|
|
30
|
+
### 1.3 Scopes (source scope) and storage locations
|
|
31
|
+
Profile files live in one of two locations:
|
|
32
32
|
|
|
33
|
-
|
|
|
33
|
+
| Scope | Path | Notes |
|
|
34
34
|
| --- | --- | --- |
|
|
35
|
-
|
|
|
36
|
-
|
|
|
35
|
+
| **Global** | `$PI_PROFILE_SWITCH_DIR/profiles/<name>.json`<br>(default `~/.pi-profile-switch/profiles/<name>.json`) | Applies to all projects. If the `PI_PROFILE_SWITCH_DIR` environment variable exists and is non-empty, the `profiles/` directory under it is authoritative. |
|
|
36
|
+
| **Project** | `<projectDir>/.pi/profiles/<name>.json` | Effective only in the current project, and only when the project is trusted. |
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
- **Override rule**: a same-named profile in project scope completely replaces the global entry; fields are **not** merged with the global configuration.
|
|
39
|
+
- **Project trust gate**: if the current project is untrusted, profiles in project scope cannot resolve, and writes to project scope fail. Before writing to project scope while the project is untrusted, you must tell the user to run `/trust` in the session and restart Pi.
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
## 2. Profile
|
|
43
|
+
## 2. Profile file format and fields
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The file content must be formatted JSON, with **the bare definition object at the top level** — strictly no `schemaVersion`, `profiles`, or other outer envelope fields.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
All fields are optional. Undeclared fields keep native Pi behavior or current state and produce no side effects.
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### Field semantics
|
|
50
50
|
|
|
51
|
-
|
|
|
51
|
+
| Field | Type | Semantics and constraints |
|
|
52
52
|
| --- | --- | --- |
|
|
53
|
-
| `label` | `string` |
|
|
54
|
-
| `description` | `string` |
|
|
55
|
-
| `skills` | `string[]` |
|
|
56
|
-
| `extensions` | `string[]` |
|
|
57
|
-
| `mcps` | `string[]` |
|
|
58
|
-
| `tools` | `string[]` |
|
|
59
|
-
| `defaultProvider` | `string` |
|
|
60
|
-
| `defaultModel` | `string` |
|
|
61
|
-
| `defaultThinkingLevel` | `string` |
|
|
62
|
-
| `instructions` | `string` |
|
|
63
|
-
|
|
64
|
-
###
|
|
53
|
+
| `label` | `string` | Human-readable display name (e.g. `"Code Review"`). |
|
|
54
|
+
| `description` | `string` | Short description of the profile (e.g. `"Read-only review profile"`). |
|
|
55
|
+
| `skills` | `string[]` | Skill names or globs to reference. When undeclared, available skills are not narrowed. |
|
|
56
|
+
| `extensions` | `string[]` | Extension identifiers or globs to reference. When undeclared, available extensions are not narrowed. |
|
|
57
|
+
| `mcps` | `string[]` | MCP server names or globs to reference. When undeclared, there is no dependency on `pi-mcp-adapter`. |
|
|
58
|
+
| `tools` | `string[]` | Whitelisted tool names or globs. When undeclared, tools are not narrowed and Pi's native tool set is kept. |
|
|
59
|
+
| `defaultProvider` | `string` | Default model provider (e.g. `"anthropic"`, `"openai"`). Effective only when declared together with `defaultModel`. |
|
|
60
|
+
| `defaultModel` | `string` | Default model name (e.g. `"claude-sonnet-4-5"`). Effective only when declared together with `defaultProvider`. |
|
|
61
|
+
| `defaultThinkingLevel` | `string` | Default thinking level; allowed values: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. Effective only when the model declaration holds. |
|
|
62
|
+
| `instructions` | `string` | Instruction text appended to the system prompt when this profile is active. |
|
|
63
|
+
|
|
64
|
+
### Example
|
|
65
65
|
```json
|
|
66
66
|
{
|
|
67
67
|
"label": "Review Mode",
|
|
@@ -86,82 +86,82 @@ Profile 文件存放在两个位置之一:
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
-
## 3.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
1. **Skills
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
2. **Extensions
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
3. **MCP
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
4. **Tools
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
5.
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
89
|
+
## 3. Discovering referenceable resources
|
|
90
|
+
|
|
91
|
+
When helping the user configure a profile, check or consult the following locations to discover the resources the user currently has:
|
|
92
|
+
|
|
93
|
+
1. **Skills**:
|
|
94
|
+
- Discovery locations: `<agentDir>/skills/` and the global `~/.agents/skills/`.
|
|
95
|
+
- Reference identity: Pi's skill name (the `name` declared in the frontmatter of the skill directory's `SKILL.md`, or the directory name).
|
|
96
|
+
- Globs supported (e.g. `"git-*"`).
|
|
97
|
+
2. **Extensions**:
|
|
98
|
+
- Discovery locations: installed packages declared in `<agentDir>/settings.json`, and loose files (`.ts` or `.js`) under `<agentDir>/extensions/`.
|
|
99
|
+
- Reference forms:
|
|
100
|
+
- An installed package's package name or source alias.
|
|
101
|
+
- Entry ID of a multi-entry package: `<package>:<relative path>`.
|
|
102
|
+
- Loose-file ID: the path relative to the extension directory minus the `.ts`/`.js` suffix (e.g. `sub/index.ts` is referenced as `sub`).
|
|
103
|
+
- Absolute paths or `~/` paths.
|
|
104
|
+
- Glob matching.
|
|
105
|
+
3. **MCP servers**:
|
|
106
|
+
- Discovery locations: the standard configuration locations recognized by `pi-mcp-adapter` — on the global side `~/.config/mcp/mcp.json`, `~/.agents/mcp.json`, `~/.agents/mcp/mcp.json`, `<agentDir>/mcp.json`; trusted projects additionally have `<projectDir>/.mcp.json` and `<projectDir>/.pi/mcp.json`.
|
|
107
|
+
- Reference identity: the server key names under the `mcpServers` object in those configuration files.
|
|
108
|
+
- A profile declaring `mcps` must also ensure the `pi-mcp-adapter` extension is available.
|
|
109
|
+
4. **Tools**:
|
|
110
|
+
- Reference identity: tool names in Pi's live tool registry.
|
|
111
|
+
- Includes built-in tools (`read`, `write`, `edit`, `bash`, etc.), extension-contributed tools, and tools exposed by MCP servers (the proxy tool `mcp__<server>` and direct tools `<server>_<tool>`).
|
|
112
|
+
- Globs supported (e.g. `"mcp__*"`, `"github_*"`).
|
|
113
|
+
5. **Narrowing boundary of project-level resources (important)**:
|
|
114
|
+
- A profile's resource selection (`skills`, `extensions`) **applies only to user-level resources** (the real agentDir and `~/.agents/skills`).
|
|
115
|
+
- The visibility of project-level resources (project `.pi/skills`, project `.pi/extensions`, ancestor `.agents/skills`) is decided by Pi's project-trust determination: in a trusted project they are always visible under **any** profile; in an untrusted project they are never visible.
|
|
116
|
+
- Therefore project-level resource visibility **does not narrow with profiles** — there is no need, and no guidance, to declare project-level resources in a profile.
|
|
117
117
|
|
|
118
118
|
---
|
|
119
119
|
|
|
120
|
-
## 4.
|
|
120
|
+
## 4. Default injection rule at authoring time
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
When creating or generating a new profile that declares `skills` for the user, follow these conventions:
|
|
123
123
|
|
|
124
|
-
1.
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
2.
|
|
129
|
-
-
|
|
124
|
+
1. **Inject `"profile-config"` by default**:
|
|
125
|
+
- If the generated profile declares a `skills` array, include `"profile-config"` in the `skills` list by default, so that after switching into the profile the user can still configure profiles through this skill.
|
|
126
|
+
- **Exception 1**: the user explicitly asks not to include `"profile-config"`.
|
|
127
|
+
- **Exception 2**: the `skills` list already contains a covering glob (e.g. `"*"`), so there is no need to add `"profile-config"` explicitly again.
|
|
128
|
+
2. **Do nothing when `skills` is undeclared**:
|
|
129
|
+
- If the profile does not declare the `skills` field, skills are not narrowed and every skill (including `profile-config`) is naturally available — never proactively add a `skills` field in that case.
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
133
|
-
## 5.
|
|
134
|
-
|
|
135
|
-
### 5.1
|
|
136
|
-
1.
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
2.
|
|
141
|
-
3.
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
4.
|
|
146
|
-
|
|
147
|
-
### 5.2
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
151
|
-
4.
|
|
152
|
-
|
|
153
|
-
### 5.3
|
|
154
|
-
1.
|
|
155
|
-
2.
|
|
156
|
-
3.
|
|
133
|
+
## 5. Interaction and execution flows
|
|
134
|
+
|
|
135
|
+
### 5.1 Create
|
|
136
|
+
1. **Clarify requirements**: from the user's natural-language description (e.g. "set up a read-only profile for security auditing"), determine:
|
|
137
|
+
- The target name (validate against `^[A-Za-z0-9][A-Za-z0-9._-]*$`, and not `default`).
|
|
138
|
+
- The target scope (global or project).
|
|
139
|
+
- The tools, skills, extensions, MCP servers, or specific model settings to narrow.
|
|
140
|
+
2. **Check resources and environment**: construct a legal JSON definition per the rules above, applying the default injection rule.
|
|
141
|
+
3. **Write the file**:
|
|
142
|
+
- Global path: `$PI_PROFILE_SWITCH_DIR/profiles/<name>.json` (default `~/.pi-profile-switch/profiles/<name>.json`).
|
|
143
|
+
- Project path: `<projectDir>/.pi/profiles/<name>.json`.
|
|
144
|
+
- Ensure the directory exists and write formatted JSON.
|
|
145
|
+
4. **Tell the user how to take effect**: the new profile is immediately usable via `/profile reload` or `/profile use <name>`.
|
|
146
|
+
|
|
147
|
+
### 5.2 Edit
|
|
148
|
+
1. Read the existing content of the target profile file.
|
|
149
|
+
2. Adjust the requested fields per the user's requirements, keeping all other fields intact.
|
|
150
|
+
3. Validate and write back formatted JSON.
|
|
151
|
+
4. Tell the user to run `/profile reload`.
|
|
152
|
+
|
|
153
|
+
### 5.3 Delete
|
|
154
|
+
1. Confirm the profile to delete exists in the specified scope.
|
|
155
|
+
2. Never attempt to delete the `default` profile.
|
|
156
|
+
3. Delete the corresponding `<name>.json` file. If the profile is currently in use, remind the user to switch to another profile first (e.g. `/profile use default`).
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
## 6.
|
|
160
|
+
## 6. Boundaries and degradation
|
|
161
161
|
|
|
162
|
-
1.
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
2.
|
|
166
|
-
-
|
|
167
|
-
-
|
|
162
|
+
1. **Read-only / no `write` tool environments**:
|
|
163
|
+
- If the current session is in a profile with narrowed tools (e.g. a read-only mode without the `write` tool):
|
|
164
|
+
- Degrade to outputting the complete formatted JSON content in the reply along with the suggested absolute file path, and suggest the user save it manually or switch to a profile with file-write capability (e.g. `/profile use default`) before saving.
|
|
165
|
+
2. **Untrusted projects**:
|
|
166
|
+
- If a write to project scope (`<projectDir>/.pi/profiles/`) is needed while the current project is not yet trusted:
|
|
167
|
+
- You must explain to the user that an untrusted project cannot take effect, and suggest running `/trust` and restarting Pi, or saving the profile to global scope instead.
|
|
@@ -55,6 +55,10 @@ export interface InitialProfile {
|
|
|
55
55
|
discovery?: LauncherDiscovery;
|
|
56
56
|
/** The trusted project directory, when trusted. */
|
|
57
57
|
projectDir?: string;
|
|
58
|
+
/** The launcher's project-trust determination. Drives the untrusted-
|
|
59
|
+
* project diagnostic; not inferable from projectDir, which the dangling
|
|
60
|
+
* saved-profile fallback returns unset while the project is trusted. */
|
|
61
|
+
projectTrusted: boolean;
|
|
58
62
|
/** Non-fatal notices for the user (e.g. a dangling restored profile that
|
|
59
63
|
* fell back to default). The launcher prints them. */
|
|
60
64
|
warnings: string[];
|
|
@@ -116,7 +120,7 @@ export async function resolveInitialProfile(
|
|
|
116
120
|
throw new UnknownProfileError(selected);
|
|
117
121
|
}
|
|
118
122
|
warnings.push(`saved profile "${selected}" no longer exists; starting the default profile`);
|
|
119
|
-
return { plan: defaultPlan(), warnings };
|
|
123
|
+
return { plan: defaultPlan(), projectTrusted, warnings };
|
|
120
124
|
}
|
|
121
125
|
if (profile.source === "builtin") {
|
|
122
126
|
// The default profile is normally unfiltered. With an overlay it becomes
|
|
@@ -131,7 +135,7 @@ export async function resolveInitialProfile(
|
|
|
131
135
|
(overlay.disabledMcps?.length ?? 0) > 0 ||
|
|
132
136
|
overlay.tools !== undefined);
|
|
133
137
|
if (!narrowed) {
|
|
134
|
-
return { plan: defaultPlan(), warnings };
|
|
138
|
+
return { plan: defaultPlan(), projectTrusted, warnings };
|
|
135
139
|
}
|
|
136
140
|
if ((overlay.disabledMcps?.length ?? 0) > 0) {
|
|
137
141
|
throw new ActivationError(
|
|
@@ -151,7 +155,7 @@ export async function resolveInitialProfile(
|
|
|
151
155
|
overlay,
|
|
152
156
|
});
|
|
153
157
|
warnings.push(...discovery.extensions.warnings(), ...unmatchedWarnings(plan));
|
|
154
|
-
return { plan, discovery, projectDir, warnings };
|
|
158
|
+
return { plan, discovery, projectDir, projectTrusted, warnings };
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
const discovery = await discoverLauncherResources({ ...context, projectTrusted });
|
|
@@ -172,5 +176,5 @@ export async function resolveInitialProfile(
|
|
|
172
176
|
// silently do nothing or leak through unfiltered.
|
|
173
177
|
throw new MissingMcpAdapterError(plan.profile);
|
|
174
178
|
}
|
|
175
|
-
return { plan, discovery, projectDir, warnings };
|
|
179
|
+
return { plan, discovery, projectDir, projectTrusted, warnings };
|
|
176
180
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Untrusted-project launch diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* After the launcher's project-trust determination judges the project
|
|
5
|
+
* untrusted, the project catalog, project runtime state, project MCP
|
|
6
|
+
* configuration, and Pi's own project resources are skipped silently
|
|
7
|
+
* (ADR-0011). This module names the skipped content and how to authorize it
|
|
8
|
+
* so the result of the determination is observable. Detection mirrors the
|
|
9
|
+
* trust-requiring set: pi-profile's project files (see project-trust.ts) and
|
|
10
|
+
* Pi's project resources (see pi-coding-agent's trust manager).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
import { PI_PROFILE_PROJECT_FILES } from "../project-trust.ts";
|
|
19
|
+
|
|
20
|
+
/** The project's MCP configuration files (the project sources recognized by
|
|
21
|
+
* mcp-config.ts). */
|
|
22
|
+
const PROJECT_MCP_CONFIGS = [".mcp.json", path.join(".pi", "mcp.json")] as const;
|
|
23
|
+
|
|
24
|
+
/** Pi's trust-requiring `.pi` entries (mirrors
|
|
25
|
+
* TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES in pi-coding-agent's trust
|
|
26
|
+
* manager, which doesn't export the list). */
|
|
27
|
+
const PI_PROJECT_RESOURCES = [
|
|
28
|
+
"settings.json",
|
|
29
|
+
"extensions",
|
|
30
|
+
"skills",
|
|
31
|
+
"prompts",
|
|
32
|
+
"themes",
|
|
33
|
+
"SYSTEM.md",
|
|
34
|
+
"APPEND_SYSTEM.md",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** Relative paths of project content skipped while the project is
|
|
38
|
+
* untrusted; empty when the project contains nothing trust-requiring. */
|
|
39
|
+
function skippedProjectContent(cwd: string): string[] {
|
|
40
|
+
const skipped: string[] = [];
|
|
41
|
+
for (const name of PI_PROFILE_PROJECT_FILES) {
|
|
42
|
+
if (existsSync(path.join(cwd, ".pi", name))) {
|
|
43
|
+
skipped.push(path.join(".pi", name));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const mcpConfig of PROJECT_MCP_CONFIGS) {
|
|
47
|
+
if (existsSync(path.join(cwd, mcpConfig))) {
|
|
48
|
+
skipped.push(mcpConfig);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const resource of PI_PROJECT_RESOURCES) {
|
|
52
|
+
if (existsSync(path.join(cwd, ".pi", resource))) {
|
|
53
|
+
skipped.push(path.join(".pi", resource));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (skipped.length === 0 && hasTrustRequiringProjectResources(cwd)) {
|
|
57
|
+
// Pi gated something this list doesn't name: a cwd-local
|
|
58
|
+
// .agents/skills, or one in a parent directory.
|
|
59
|
+
skipped.push(
|
|
60
|
+
existsSync(path.join(cwd, ".agents", "skills"))
|
|
61
|
+
? path.join(".agents", "skills")
|
|
62
|
+
: "Pi project resources in a parent directory",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return skipped;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The untrusted-project diagnostic for this launch, or undefined when there
|
|
69
|
+
* is nothing to report (trusted project, or untrusted with no
|
|
70
|
+
* trust-requiring content). The launcher prints the result to stderr. */
|
|
71
|
+
export function untrustedProjectDiagnostic(cwd: string, projectTrusted: boolean): string | undefined {
|
|
72
|
+
if (projectTrusted) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const skipped = skippedProjectContent(cwd);
|
|
76
|
+
if (skipped.length === 0) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
`project is untrusted; skipped project content is invisible to this launch: ${skipped.join(", ")}. ` +
|
|
81
|
+
'To authorize it, run "/trust" in Pi to persist the decision (effective on the next launch), ' +
|
|
82
|
+
'or relaunch with "-- --approve" to grant one-shot trust for this launch.'
|
|
83
|
+
);
|
|
84
|
+
}
|
package/src/project-trust.ts
CHANGED
|
@@ -76,11 +76,14 @@ export function resolveProjectTrust(input: ProjectTrustInput): boolean {
|
|
|
76
76
|
return input.userDefaultProjectTrust === "always";
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** pi-profile's own project files
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
/** pi-profile's own project files, relative to the project's `.pi`
|
|
80
|
+
* directory. They are trust-requiring even though Pi's native list doesn't
|
|
81
|
+
* know them: a committed catalog/state file would otherwise inject profile
|
|
82
|
+
* definitions (and instructions) unguarded. Exported so the launcher's
|
|
83
|
+
* untrusted-project diagnostic names exactly the files the trust gate
|
|
84
|
+
* skips — the two must never drift apart. */
|
|
85
|
+
export const PI_PROFILE_PROJECT_FILES = ["profiles", "pi-profile-state.json"] as const;
|
|
86
|
+
|
|
87
|
+
export function hasPiProfileProjectFiles(cwd: string): boolean {
|
|
88
|
+
return PI_PROFILE_PROJECT_FILES.some((name) => existsSync(path.join(cwd, ".pi", name)));
|
|
86
89
|
}
|
package/src/starter-assets.ts
CHANGED
|
@@ -17,25 +17,25 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import { getGlobalProfilesDir } from "./workspace.ts";
|
|
18
18
|
|
|
19
19
|
export interface StarterAssetFileResult {
|
|
20
|
-
/**
|
|
20
|
+
/** Absolute path of the target file */
|
|
21
21
|
path: string;
|
|
22
|
-
/**
|
|
22
|
+
/** Whether this call performed a write */
|
|
23
23
|
written: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface StarterAssetsResult {
|
|
27
27
|
profile: StarterAssetFileResult;
|
|
28
28
|
skill: StarterAssetFileResult;
|
|
29
|
-
/**
|
|
29
|
+
/** Human-readable degradation warnings; empty means all succeeded or no-op */
|
|
30
30
|
warnings: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface EnsureStarterAssetsOptions {
|
|
34
|
-
/**
|
|
34
|
+
/** Defaults to getGlobalProfilesDir(); injected by tests */
|
|
35
35
|
globalProfilesDir?: string;
|
|
36
|
-
/**
|
|
36
|
+
/** Defaults to Pi's getAgentDir(); injected by tests */
|
|
37
37
|
agentDir?: string;
|
|
38
|
-
/**
|
|
38
|
+
/** Defaults to locating the package root from import.meta.url; injected by tests */
|
|
39
39
|
packageRoot?: string;
|
|
40
40
|
}
|
|
41
41
|
|