@rosetears/aili-pi 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/THIRD_PARTY_NOTICES.md +13 -13
- package/extensions/matrix/index.ts +13 -3
- package/extensions/zentui/config.ts +9 -2
- package/extensions/zentui/extension-status.ts +25 -1
- package/extensions/zentui/gradient.ts +6 -6
- package/licenses/pi-permission-modes-MIT.txt +21 -0
- package/manifests/adapter-evidence.json +10 -9
- package/manifests/live-verification.json +19 -11
- package/manifests/provenance.json +17 -17
- package/manifests/sbom.json +6 -6
- package/manifests/skill-compatibility.json +12 -12
- package/manifests/subagent-provenance.json +13 -3
- package/notices/pi-sakura-cyberdeck-NOTICE.txt +3 -2
- package/package.json +5 -3
- package/scripts/generate-provenance.ts +1 -1
- package/scripts/sync-permission-modes.ts +135 -0
- package/src/runtime/native-integrations.ts +5 -5
- package/src/runtime/package-resolution.ts +8 -0
- package/src/runtime/registry.ts +138 -9
- package/src/runtime/subagents.ts +163 -11
- package/src/vendor/pi-permission-modes/index.ts +692 -0
- package/src/vendor/pi-permission-modes/resolve.ts +142 -0
- package/upstream/pi-permission-modes.lock.json +47 -0
|
@@ -27,11 +27,21 @@
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"name": "@agwab/pi-subagent",
|
|
30
|
+
"version": "0.4.8",
|
|
30
31
|
"revision": "daa7b83819116a62008ad17aa65fcd50fefbafd0",
|
|
31
32
|
"license": "MIT",
|
|
32
|
-
"status": "
|
|
33
|
-
"reusedPatterns": [
|
|
34
|
-
|
|
33
|
+
"status": "dependency",
|
|
34
|
+
"reusedPatterns": [
|
|
35
|
+
"generic subagent tool schema and rendering",
|
|
36
|
+
"headless/tmux/inline backend and lifecycle envelopes",
|
|
37
|
+
"bounded parallel fan-out, worktree, sandbox, async, interrupt, and reconcile behavior"
|
|
38
|
+
],
|
|
39
|
+
"localAdaptation": [
|
|
40
|
+
"ordinary omitted/auto runs use headless for the Pi 0.81.1 compatibility window",
|
|
41
|
+
"explicit incompatible inline requests fail before model startup",
|
|
42
|
+
"AILI injects the immutable credential guard and a bounded requested-Agent heading"
|
|
43
|
+
],
|
|
44
|
+
"reason": "AILI depends on the exact package and applies a wrapper-only compatibility adaptation; no upstream source is copied."
|
|
35
45
|
}
|
|
36
46
|
]
|
|
37
47
|
}
|
|
@@ -2,5 +2,6 @@ This package includes a modified copy of pi-zentui by Luka.
|
|
|
2
2
|
Original project: https://github.com/lmilojevicc/pi-zentui
|
|
3
3
|
License: MIT. Full license text: licenses/pi-zentui-MIT.txt
|
|
4
4
|
|
|
5
|
-
Modifications include the
|
|
6
|
-
|
|
5
|
+
Modifications include the Rem shell palette, retained Sakura Matrix/reasoning
|
|
6
|
+
palette, responsive bounded Matrix track selection, package-specific defaults,
|
|
7
|
+
and package-specific configuration path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rosetears/aili-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "AILI and ROSE distribution for official Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,8 +68,10 @@
|
|
|
68
68
|
"validate:roles": "npm run verify:roles",
|
|
69
69
|
"sync:adapters": "node --experimental-strip-types scripts/apply-adapter-evidence.ts",
|
|
70
70
|
"validate:compatibility": "node --experimental-strip-types scripts/apply-adapter-evidence.ts --verify",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
71
|
+
"sync:permission-modes": "node --experimental-strip-types scripts/sync-permission-modes.ts",
|
|
72
|
+
"verify:permission-modes": "node --experimental-strip-types scripts/sync-permission-modes.ts --verify",
|
|
73
|
+
"generate:provenance": "node --experimental-strip-types scripts/sync-permission-modes.ts --verify && node --experimental-strip-types scripts/generate-provenance.ts",
|
|
74
|
+
"validate:provenance": "node --experimental-strip-types scripts/sync-permission-modes.ts --verify && node --experimental-strip-types scripts/generate-provenance.ts --verify",
|
|
73
75
|
"validate:capabilities": "node --experimental-strip-types scripts/validate-runtime.ts",
|
|
74
76
|
"validate:release": "node --experimental-strip-types scripts/validate-runtime.ts --release",
|
|
75
77
|
"test:doctor": "vitest run tests/unit/doctor.test.ts",
|
|
@@ -115,7 +115,7 @@ async function generate(): Promise<{ notices: string; sbom: object }> {
|
|
|
115
115
|
"",
|
|
116
116
|
`The exact ${packages.length}-entry package-lock inventory, versions, integrity values, dependency scope, and declared licenses is recorded in \`manifests/sbom.json\`.`,
|
|
117
117
|
"",
|
|
118
|
-
"Runtime dependencies are initialized through the single AILI Extension entry
|
|
118
|
+
"Runtime dependencies are initialized through the single AILI Extension entry. Package-owned third-party adaptations are copied only where their provenance sourceFiles explicitly name repository paths.",
|
|
119
119
|
"",
|
|
120
120
|
].join("\n");
|
|
121
121
|
return { notices, sbom };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolvePermissionModesPackageRoot } from "../src/runtime/package-resolution.ts";
|
|
6
|
+
|
|
7
|
+
const ROOT = resolve(import.meta.dirname, "..");
|
|
8
|
+
const SOURCE_ROOT = fileURLToPath(resolvePermissionModesPackageRoot());
|
|
9
|
+
const VERIFY = process.argv.includes("--verify");
|
|
10
|
+
const VERSION = "2.2.0";
|
|
11
|
+
const REVISION = "23d65d10a53b67043cae42322acf9044d6edb196";
|
|
12
|
+
|
|
13
|
+
const EXPECTED_UPSTREAM_HASHES: Record<string, string> = {
|
|
14
|
+
"src/index.ts": "fd4462a3b7ba986af734c2e17ba8ea7178df56c933e87ed444ba90ba24c2fd5b",
|
|
15
|
+
"src/resolve.ts": "13f52a4a9c08d7a55f5f9d03f97302d864768838fb3e9fca2051cb7d94a0ae82",
|
|
16
|
+
LICENSE: "d87cb99b43f6bf8771e57be83485db11b977b9dfa21b6bd201b8d3d370bdce43",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ADAPTATION_HEADER = [
|
|
20
|
+
"/**",
|
|
21
|
+
" * Generated AILI adaptation of pi-permission-modes@2.2.0.",
|
|
22
|
+
" * Source revision: 23d65d10a53b67043cae42322acf9044d6edb196.",
|
|
23
|
+
" * Regenerate with scripts/sync-permission-modes.ts; do not edit manually.",
|
|
24
|
+
" */",
|
|
25
|
+
"",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
const sha256 = (value: string): string => createHash("sha256").update(value).digest("hex");
|
|
29
|
+
|
|
30
|
+
function replaceExactlyOnce(source: string, before: string, after: string, label: string): string {
|
|
31
|
+
const first = source.indexOf(before);
|
|
32
|
+
if (first < 0 || source.indexOf(before, first + before.length) >= 0) {
|
|
33
|
+
throw new Error(`expected exactly one ${label} in pi-permission-modes@${VERSION}`);
|
|
34
|
+
}
|
|
35
|
+
return source.slice(0, first) + after + source.slice(first + before.length);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function adaptIndex(source: string): string {
|
|
39
|
+
let redirected = 0;
|
|
40
|
+
const body = source.replace(/from "\.\/([^"]+)";/g, (full, specifier: string) => {
|
|
41
|
+
if (specifier === "resolve.ts") return full;
|
|
42
|
+
redirected += 1;
|
|
43
|
+
return `from "pi-permission-modes/src/${specifier}";`;
|
|
44
|
+
});
|
|
45
|
+
if (redirected !== 12) {
|
|
46
|
+
throw new Error(`expected 12 redirected sibling imports in pi-permission-modes@${VERSION}, found ${redirected}`);
|
|
47
|
+
}
|
|
48
|
+
const documented = replaceExactlyOnce(
|
|
49
|
+
body,
|
|
50
|
+
"The logic lives in sibling\n * modules:",
|
|
51
|
+
"The adapted matcher lives locally; unchanged logic stays in pinned dependency\n * modules:",
|
|
52
|
+
"entry module ownership comment",
|
|
53
|
+
);
|
|
54
|
+
return ADAPTATION_HEADER + documented;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function adaptResolve(source: string): string {
|
|
58
|
+
const withSchemaRedirect = replaceExactlyOnce(
|
|
59
|
+
source,
|
|
60
|
+
'from "./schema.ts";',
|
|
61
|
+
'from "pi-permission-modes/src/schema.ts";',
|
|
62
|
+
"schema import",
|
|
63
|
+
);
|
|
64
|
+
const withDotAll = replaceExactlyOnce(
|
|
65
|
+
withSchemaRedirect,
|
|
66
|
+
"return new RegExp(re).test(t);",
|
|
67
|
+
'return new RegExp(re, "s").test(t);',
|
|
68
|
+
"glob RegExp compilation",
|
|
69
|
+
);
|
|
70
|
+
return ADAPTATION_HEADER + withDotAll;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function expectedOutputs(): Promise<Record<string, string>> {
|
|
74
|
+
const packageJsonText = await readFile(resolve(SOURCE_ROOT, "package.json"), "utf8");
|
|
75
|
+
const packageJson = JSON.parse(packageJsonText) as { name?: string; version?: string; license?: string };
|
|
76
|
+
if (packageJson.name !== "pi-permission-modes" || packageJson.version !== VERSION || packageJson.license !== "MIT") {
|
|
77
|
+
throw new Error(`expected pi-permission-modes@${VERSION} MIT baseline`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const upstream = Object.fromEntries(await Promise.all(
|
|
81
|
+
Object.keys(EXPECTED_UPSTREAM_HASHES).map(async (path) => [path, await readFile(resolve(SOURCE_ROOT, path), "utf8")]),
|
|
82
|
+
)) as Record<string, string>;
|
|
83
|
+
for (const [path, expected] of Object.entries(EXPECTED_UPSTREAM_HASHES)) {
|
|
84
|
+
const actual = sha256(upstream[path]!);
|
|
85
|
+
if (actual !== expected) throw new Error(`unexpected upstream drift for ${path}: ${actual}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const index = adaptIndex(upstream["src/index.ts"]!);
|
|
89
|
+
const matcher = adaptResolve(upstream["src/resolve.ts"]!);
|
|
90
|
+
const license = upstream.LICENSE!;
|
|
91
|
+
const adaptedFiles = {
|
|
92
|
+
"src/vendor/pi-permission-modes/index.ts": index,
|
|
93
|
+
"src/vendor/pi-permission-modes/resolve.ts": matcher,
|
|
94
|
+
"licenses/pi-permission-modes-MIT.txt": license,
|
|
95
|
+
};
|
|
96
|
+
const lock = {
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
package: {
|
|
99
|
+
name: "pi-permission-modes",
|
|
100
|
+
version: VERSION,
|
|
101
|
+
revision: REVISION,
|
|
102
|
+
license: "MIT",
|
|
103
|
+
},
|
|
104
|
+
upstreamFiles: Object.entries(EXPECTED_UPSTREAM_HASHES).map(([path, hash]) => ({ path, sha256: hash })),
|
|
105
|
+
adaptedFiles: Object.entries(adaptedFiles).map(([path, content]) => ({ path, sha256: sha256(content) })),
|
|
106
|
+
localChanges: [
|
|
107
|
+
"Package-owned adapted entry redirects all unchanged sibling modules to the exact pi-permission-modes dependency while owning resolve.ts locally.",
|
|
108
|
+
"matchPattern compiles its anchored glob RegExp with dotAll so * and ? include ECMAScript line terminators.",
|
|
109
|
+
],
|
|
110
|
+
generatedBy: "scripts/sync-permission-modes.ts",
|
|
111
|
+
verification: ["npm run verify:permission-modes", "tests/unit/permission-patterns.test.ts", "tests/integration/permission-modes.test.ts"],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...adaptedFiles,
|
|
116
|
+
"upstream/pi-permission-modes.lock.json": `${JSON.stringify(lock, null, 2)}\n`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main(): Promise<void> {
|
|
121
|
+
const outputs = await expectedOutputs();
|
|
122
|
+
for (const [path, expected] of Object.entries(outputs)) {
|
|
123
|
+
const absolute = resolve(ROOT, path);
|
|
124
|
+
if (VERIFY) {
|
|
125
|
+
const actual = await readFile(absolute, "utf8").catch(() => "");
|
|
126
|
+
if (actual !== expected) throw new Error(`permission-mode adaptation drifted: ${path}`);
|
|
127
|
+
} else {
|
|
128
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
129
|
+
await writeFile(absolute, expected, "utf8");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log(`${VERIFY ? "PASS" : "SYNCED"}: pi-permission-modes@${VERSION} adapted matcher (${Object.keys(outputs).length} artifacts)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await main();
|
|
@@ -3,7 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
type NativeExtension = (pi: ExtensionAPI) => void | Promise<void>;
|
|
4
4
|
|
|
5
5
|
export const NATIVE_INTEGRATIONS = [
|
|
6
|
-
"pi-permission-modes@2.2.0",
|
|
6
|
+
"pi-permission-modes@2.2.0 (AILI multiline-glob adaptation)",
|
|
7
7
|
"pi-quota-status@0.3.0",
|
|
8
8
|
"pi-web-access@0.13.0",
|
|
9
9
|
"pi-cache-optimizer@2.6.18",
|
|
@@ -12,7 +12,7 @@ export const NATIVE_INTEGRATIONS = [
|
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
14
|
const INTEGRATION_MODULES = [
|
|
15
|
-
"pi-permission-modes/
|
|
15
|
+
"../vendor/pi-permission-modes/index.ts",
|
|
16
16
|
"pi-quota-status",
|
|
17
17
|
"pi-web-access",
|
|
18
18
|
"pi-cache-optimizer/index.ts",
|
|
@@ -21,9 +21,9 @@ const INTEGRATION_MODULES = [
|
|
|
21
21
|
] as const;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Initialise
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Initialise pinned native integrations through AILI's single package entry.
|
|
25
|
+
* Permission modes uses the revision-bound adapted entry that changes only
|
|
26
|
+
* shared multiline glob matching; the remaining integrations stay upstream-owned.
|
|
27
27
|
*/
|
|
28
28
|
export async function registerNativeIntegrations(pi: ExtensionAPI): Promise<void> {
|
|
29
29
|
for (const moduleName of INTEGRATION_MODULES) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
/** Resolve the installed dependency from either a development tree or npm's hoisted scoped-package layout. */
|
|
5
|
+
export function resolvePermissionModesPackageRoot(from: string | URL = import.meta.url): URL {
|
|
6
|
+
const packageJson = createRequire(from).resolve("pi-permission-modes/package.json");
|
|
7
|
+
return new URL("./", pathToFileURL(packageJson));
|
|
8
|
+
}
|
package/src/runtime/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { loadRoleProfiles, validateRoleProfiles } from "./roles.ts";
|
|
4
|
+
import { resolvePermissionModesPackageRoot } from "./package-resolution.ts";
|
|
4
5
|
|
|
5
6
|
const ROOT = new URL("../../", import.meta.url);
|
|
6
7
|
|
|
@@ -143,30 +144,158 @@ export async function validateLiveVerification(): Promise<string[]> {
|
|
|
143
144
|
const errors: string[] = [];
|
|
144
145
|
try {
|
|
145
146
|
const evidence = await json<{
|
|
146
|
-
schemaVersion?: number; platform?: string; piVersion?: string; status?: string;
|
|
147
|
-
probes?: Array<{
|
|
147
|
+
schemaVersion?: number; platform?: string; piVersion?: string; subagentVersion?: string; status?: string;
|
|
148
|
+
probes?: Array<{
|
|
149
|
+
id?: string;
|
|
150
|
+
status?: string;
|
|
151
|
+
changedFiles?: number | null;
|
|
152
|
+
backendRequest?: string;
|
|
153
|
+
resolvedBackend?: string | null;
|
|
154
|
+
}>;
|
|
148
155
|
implementation?: Record<string, string>;
|
|
149
156
|
}>("manifests/live-verification.json");
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
if (
|
|
158
|
+
evidence.schemaVersion !== 2 ||
|
|
159
|
+
evidence.platform !== "linux" ||
|
|
160
|
+
evidence.piVersion !== "0.81.1" ||
|
|
161
|
+
evidence.subagentVersion !== "0.4.8" ||
|
|
162
|
+
evidence.status !== "passed"
|
|
163
|
+
) {
|
|
164
|
+
errors.push("live verification: compatibility identity is incomplete or non-pass");
|
|
155
165
|
}
|
|
166
|
+
const fixtureProbe = evidence.probes?.find((probe) => probe.id === "generic-subagent-fixtures");
|
|
167
|
+
const defaultProbe = evidence.probes?.find((probe) => probe.id === "generic-agentless-default-path");
|
|
168
|
+
const credentialProbe = evidence.probes?.find((probe) => probe.id === "generic-credential-guard-explicit-headless");
|
|
169
|
+
if (
|
|
170
|
+
evidence.probes?.length !== 3 ||
|
|
171
|
+
fixtureProbe?.status !== "passed" ||
|
|
172
|
+
defaultProbe?.status !== "passed" ||
|
|
173
|
+
defaultProbe.backendRequest !== "omitted" ||
|
|
174
|
+
defaultProbe.resolvedBackend !== "headless" ||
|
|
175
|
+
defaultProbe.changedFiles !== 0 ||
|
|
176
|
+
credentialProbe?.status !== "passed" ||
|
|
177
|
+
credentialProbe.backendRequest !== "explicit-headless" ||
|
|
178
|
+
credentialProbe.resolvedBackend !== "headless" ||
|
|
179
|
+
credentialProbe.changedFiles !== 0
|
|
180
|
+
) {
|
|
181
|
+
errors.push("live verification: default-path/headless probes are missing, ambiguous, non-pass, or mutated files");
|
|
182
|
+
}
|
|
183
|
+
const requiredImplementation = [
|
|
184
|
+
"src/runtime/subagents.ts",
|
|
185
|
+
"tests/unit/subagents.test.ts",
|
|
186
|
+
"tests/integration/live-subagent.test.ts",
|
|
187
|
+
];
|
|
156
188
|
for (const [filePath, expected] of Object.entries(evidence.implementation ?? {})) {
|
|
157
189
|
const content = await readFile(new URL(filePath, ROOT), "utf8");
|
|
158
190
|
const actual = createHash("sha256").update(content).digest("hex");
|
|
159
191
|
if (actual !== expected) errors.push(`live verification: implementation drift ${filePath}`);
|
|
160
192
|
}
|
|
161
|
-
if (Object.keys(evidence.implementation ?? {}).
|
|
193
|
+
if (JSON.stringify(Object.keys(evidence.implementation ?? {}).sort()) !== JSON.stringify(requiredImplementation.sort())) {
|
|
194
|
+
errors.push("live verification: implementation binding must contain the exact default-path files");
|
|
195
|
+
}
|
|
162
196
|
} catch (error) {
|
|
163
197
|
errors.push(`live verification: ${error instanceof Error ? error.message : String(error)}`);
|
|
164
198
|
}
|
|
165
199
|
return errors;
|
|
166
200
|
}
|
|
167
201
|
|
|
168
|
-
export async function
|
|
202
|
+
export async function validatePermissionModeAdaptation(): Promise<string[]> {
|
|
169
203
|
const errors: string[] = [];
|
|
204
|
+
try {
|
|
205
|
+
const lock = await json<{
|
|
206
|
+
schemaVersion?: number;
|
|
207
|
+
package?: { name?: string; version?: string; revision?: string; license?: string };
|
|
208
|
+
upstreamFiles?: Array<{ path?: string; sha256?: string }>;
|
|
209
|
+
adaptedFiles?: Array<{ path?: string; sha256?: string }>;
|
|
210
|
+
localChanges?: string[];
|
|
211
|
+
generatedBy?: string;
|
|
212
|
+
verification?: string[];
|
|
213
|
+
}>("upstream/pi-permission-modes.lock.json");
|
|
214
|
+
const expectedLocalChanges = [
|
|
215
|
+
"Package-owned adapted entry redirects all unchanged sibling modules to the exact pi-permission-modes dependency while owning resolve.ts locally.",
|
|
216
|
+
"matchPattern compiles its anchored glob RegExp with dotAll so * and ? include ECMAScript line terminators.",
|
|
217
|
+
];
|
|
218
|
+
const expectedVerification = [
|
|
219
|
+
"npm run verify:permission-modes",
|
|
220
|
+
"tests/unit/permission-patterns.test.ts",
|
|
221
|
+
"tests/integration/permission-modes.test.ts",
|
|
222
|
+
];
|
|
223
|
+
if (
|
|
224
|
+
lock.schemaVersion !== 1 ||
|
|
225
|
+
lock.package?.name !== "pi-permission-modes" ||
|
|
226
|
+
lock.package.version !== "2.2.0" ||
|
|
227
|
+
lock.package.revision !== "23d65d10a53b67043cae42322acf9044d6edb196" ||
|
|
228
|
+
lock.package.license !== "MIT" ||
|
|
229
|
+
lock.upstreamFiles?.length !== 3 ||
|
|
230
|
+
lock.adaptedFiles?.length !== 3 ||
|
|
231
|
+
JSON.stringify(lock.localChanges) !== JSON.stringify(expectedLocalChanges) ||
|
|
232
|
+
lock.generatedBy !== "scripts/sync-permission-modes.ts" ||
|
|
233
|
+
JSON.stringify(lock.verification) !== JSON.stringify(expectedVerification)
|
|
234
|
+
) {
|
|
235
|
+
errors.push("permission adaptation: lock identity or inventory is incomplete");
|
|
236
|
+
return errors;
|
|
237
|
+
}
|
|
238
|
+
const expectedUpstream = {
|
|
239
|
+
"src/index.ts": "fd4462a3b7ba986af734c2e17ba8ea7178df56c933e87ed444ba90ba24c2fd5b",
|
|
240
|
+
"src/resolve.ts": "13f52a4a9c08d7a55f5f9d03f97302d864768838fb3e9fca2051cb7d94a0ae82",
|
|
241
|
+
LICENSE: "d87cb99b43f6bf8771e57be83485db11b977b9dfa21b6bd201b8d3d370bdce43",
|
|
242
|
+
};
|
|
243
|
+
const upstreamIdentity = Object.fromEntries((lock.upstreamFiles ?? []).map((record) => [record.path, record.sha256]));
|
|
244
|
+
if (JSON.stringify(upstreamIdentity) !== JSON.stringify(expectedUpstream)) {
|
|
245
|
+
errors.push("permission adaptation: upstream baseline hashes do not match the accepted 2.2.0 revision");
|
|
246
|
+
}
|
|
247
|
+
const expectedAdapted = {
|
|
248
|
+
"src/vendor/pi-permission-modes/index.ts": "8bfa0364967a2b76e9900cc72b8f434ea1cfa6520899c7827488220f2e288ee8",
|
|
249
|
+
"src/vendor/pi-permission-modes/resolve.ts": "f71688f847495da5122724f75c5ebe3b41066b3d3cac74cbe99f66b9906404f6",
|
|
250
|
+
"licenses/pi-permission-modes-MIT.txt": "d87cb99b43f6bf8771e57be83485db11b977b9dfa21b6bd201b8d3d370bdce43",
|
|
251
|
+
};
|
|
252
|
+
const adaptedIdentity = Object.fromEntries((lock.adaptedFiles ?? []).map((record) => [record.path, record.sha256]));
|
|
253
|
+
if (JSON.stringify(adaptedIdentity) !== JSON.stringify(expectedAdapted)) {
|
|
254
|
+
errors.push("permission adaptation: adapted hashes or file inventory do not match the accepted generated output");
|
|
255
|
+
}
|
|
256
|
+
const permissionPackageRoot = resolvePermissionModesPackageRoot();
|
|
257
|
+
const installedPackage = JSON.parse(await readFile(new URL("package.json", permissionPackageRoot), "utf8")) as {
|
|
258
|
+
name?: string;
|
|
259
|
+
version?: string;
|
|
260
|
+
license?: string;
|
|
261
|
+
};
|
|
262
|
+
if (installedPackage.name !== "pi-permission-modes" || installedPackage.version !== "2.2.0" || installedPackage.license !== "MIT") {
|
|
263
|
+
errors.push("permission adaptation: resolved dependency identity is not exact pi-permission-modes@2.2.0 MIT");
|
|
264
|
+
}
|
|
265
|
+
for (const [kind, records, sourceRoot] of [
|
|
266
|
+
["upstream", lock.upstreamFiles, permissionPackageRoot],
|
|
267
|
+
["adapted", lock.adaptedFiles, ROOT],
|
|
268
|
+
] as const) {
|
|
269
|
+
for (const record of records ?? []) {
|
|
270
|
+
const path = record.path ?? "";
|
|
271
|
+
if (!path || path.startsWith("/") || path.split("/").includes("..") || !/^[0-9a-f]{64}$/.test(record.sha256 ?? "")) {
|
|
272
|
+
errors.push(`permission adaptation: unsafe or incomplete ${kind} record ${path || "(missing)"}`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const content = await readFile(new URL(path, sourceRoot), "utf8");
|
|
276
|
+
const actual = createHash("sha256").update(content).digest("hex");
|
|
277
|
+
if (actual !== record.sha256) errors.push(`permission adaptation: ${kind} drift ${path}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const [nativeIntegration, adaptedResolve] = await Promise.all([
|
|
281
|
+
readFile(new URL("src/runtime/native-integrations.ts", ROOT), "utf8"),
|
|
282
|
+
readFile(new URL("src/vendor/pi-permission-modes/resolve.ts", ROOT), "utf8"),
|
|
283
|
+
]);
|
|
284
|
+
const adaptedEntryCount = nativeIntegration.match(/"\.\.\/vendor\/pi-permission-modes\/index\.ts"/g)?.length ?? 0;
|
|
285
|
+
if (adaptedEntryCount !== 1 || nativeIntegration.includes('"pi-permission-modes/src/index.ts"')) {
|
|
286
|
+
errors.push("permission adaptation: native integration is not bound exactly once and exclusively to the adapted entry");
|
|
287
|
+
}
|
|
288
|
+
if (!adaptedResolve.includes('return new RegExp(re, "s").test(t);') || adaptedResolve.includes("return new RegExp(re).test(t);")) {
|
|
289
|
+
errors.push("permission adaptation: line-terminator-safe matcher semantic is missing");
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
errors.push(`permission adaptation: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
|
+
}
|
|
294
|
+
return errors;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function validateProvenance(): Promise<string[]> {
|
|
298
|
+
const errors: string[] = [...await validatePermissionModeAdaptation()];
|
|
170
299
|
try {
|
|
171
300
|
const provenance = await json<{ schemaVersion: number; sources: Array<{ name: string; revision: string; license: string; status: string; repository: string; sourceFiles: string[]; symbols: string[]; localChanges: string[]; verification: string[] }> }>("manifests/provenance.json");
|
|
172
301
|
const sbom = await json<{ spdxVersion?: string; packages?: Array<{ SPDXID?: string; name?: string; licenseDeclared?: string }> }>("manifests/sbom.json");
|
package/src/runtime/subagents.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Container, type Component, Text } from "@earendil-works/pi-tui";
|
|
3
5
|
import { validateRoleProfiles } from "./roles.js";
|
|
4
6
|
|
|
5
7
|
const CREDENTIAL_GUARD_EXTENSION = fileURLToPath(new URL("./credential-guard.ts", import.meta.url));
|
|
@@ -9,16 +11,80 @@ const CREDENTIAL_GUARD_EXTENSION = fileURLToPath(new URL("./credential-guard.ts"
|
|
|
9
11
|
const REQUIRED_CHILD_EXTENSIONS = [CREDENTIAL_GUARD_EXTENSION] as const;
|
|
10
12
|
|
|
11
13
|
type UnknownRecord = Record<string, unknown>;
|
|
14
|
+
type RenderTheme = {
|
|
15
|
+
fg(style: string, text: string): string;
|
|
16
|
+
bold(text: string): string;
|
|
17
|
+
};
|
|
12
18
|
type GenericTool = {
|
|
13
19
|
name?: unknown;
|
|
14
20
|
execute?: (...args: unknown[]) => unknown;
|
|
21
|
+
renderCall?: (args: unknown, theme: RenderTheme) => Component;
|
|
15
22
|
[key: string]: unknown;
|
|
16
23
|
};
|
|
17
24
|
|
|
25
|
+
type SubagentCompatibilityPlan =
|
|
26
|
+
| { kind: "forward"; params: unknown }
|
|
27
|
+
| { kind: "reject"; requestedBackend: "inline" | "auto"; error: string };
|
|
28
|
+
|
|
29
|
+
type AgentHeading = {
|
|
30
|
+
label: "Agent" | "Agents";
|
|
31
|
+
summary: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const MAX_AGENT_NAME_LENGTH = 48;
|
|
35
|
+
const MAX_AGENT_SUMMARY_LENGTH = 120;
|
|
36
|
+
const INLINE_COMPATIBILITY_ERROR =
|
|
37
|
+
"inline backend is incompatible with Pi 0.81.1 and @agwab/pi-subagent 0.4.8; use backend \"headless\"";
|
|
38
|
+
const MIXED_PARALLEL_COMPATIBILITY_ERROR =
|
|
39
|
+
"auto backend cannot safely combine visible tasks with ordinary non-visible, non-sandboxed tasks on Pi 0.81.1 and @agwab/pi-subagent 0.4.8; split the fan-out or choose one explicit compatible backend";
|
|
40
|
+
|
|
18
41
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
19
42
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
43
|
}
|
|
21
44
|
|
|
45
|
+
function truncateDisplayText(value: string, maxLength: number): string {
|
|
46
|
+
const characters = [...value];
|
|
47
|
+
return characters.length <= maxLength
|
|
48
|
+
? value
|
|
49
|
+
: `${characters.slice(0, Math.max(0, maxLength - 1)).join("")}…`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requestedAgentName(value: unknown): string {
|
|
53
|
+
if (typeof value !== "string") return "agentless";
|
|
54
|
+
const normalized = stripVTControlCharacters(value)
|
|
55
|
+
.replace(/[\r\n\t\f\v]+/g, " ")
|
|
56
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
|
|
57
|
+
.replace(/\p{Cf}/gu, "")
|
|
58
|
+
.replace(/\s+/g, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
return normalized
|
|
61
|
+
? truncateDisplayText(normalized, MAX_AGENT_NAME_LENGTH)
|
|
62
|
+
: "agentless";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requestedAgentHeading(params: unknown): AgentHeading | undefined {
|
|
66
|
+
if (!isRecord(params)) return undefined;
|
|
67
|
+
const action = typeof params.action === "string" ? params.action : "run";
|
|
68
|
+
if (action !== "run") return undefined;
|
|
69
|
+
|
|
70
|
+
const tasks = Array.isArray(params.tasks) ? params.tasks : undefined;
|
|
71
|
+
const isParallel = tasks !== undefined;
|
|
72
|
+
const names = tasks
|
|
73
|
+
? tasks.map((task: unknown) => requestedAgentName(isRecord(task) ? task.agent : undefined))
|
|
74
|
+
: [requestedAgentName(params.agent)];
|
|
75
|
+
if (names.length === 0) names.push("agentless");
|
|
76
|
+
|
|
77
|
+
const counts = new Map<string, number>();
|
|
78
|
+
for (const name of names) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
79
|
+
const summary = [...counts]
|
|
80
|
+
.map(([name, count]) => count > 1 ? `${name} ×${count}` : name)
|
|
81
|
+
.join(", ");
|
|
82
|
+
return {
|
|
83
|
+
label: isParallel ? "Agents" : "Agent",
|
|
84
|
+
summary: truncateDisplayText(summary, MAX_AGENT_SUMMARY_LENGTH),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
22
88
|
function withRequiredExtensions(value: unknown): unknown {
|
|
23
89
|
if (!isRecord(value)) return value;
|
|
24
90
|
if (value.extensions !== undefined && !Array.isArray(value.extensions)) return value;
|
|
@@ -30,6 +96,69 @@ function withRequiredExtensions(value: unknown): unknown {
|
|
|
30
96
|
};
|
|
31
97
|
}
|
|
32
98
|
|
|
99
|
+
function sandboxRequested(value: unknown): boolean {
|
|
100
|
+
return value !== undefined && value !== null && value !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function taskSelector(
|
|
104
|
+
task: UnknownRecord,
|
|
105
|
+
parent: UnknownRecord,
|
|
106
|
+
): { visible: boolean; sandboxed: boolean; plain: boolean } {
|
|
107
|
+
const visible = task.visible === undefined ? parent.visible === true : task.visible === true;
|
|
108
|
+
const sandbox = Object.hasOwn(task, "sandbox") ? task.sandbox : parent.sandbox;
|
|
109
|
+
const sandboxed = sandboxRequested(sandbox);
|
|
110
|
+
return { visible, sandboxed, plain: !visible && !sandboxed };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Select a Pi-0.81.1-compatible backend without changing compatible explicit,
|
|
115
|
+
* visible, sandboxed, or lifecycle requests. Parallel auto calls are evaluated
|
|
116
|
+
* per task because task-level visible/sandbox values override their parent.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeSubagentCompatibility(params: unknown): SubagentCompatibilityPlan {
|
|
119
|
+
if (!isRecord(params) || (params.action !== undefined && params.action !== "run")) {
|
|
120
|
+
return { kind: "forward", params };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (params.backend === "inline") {
|
|
124
|
+
return { kind: "reject", requestedBackend: "inline", error: INLINE_COMPATIBILITY_ERROR };
|
|
125
|
+
}
|
|
126
|
+
if (params.backend !== undefined && params.backend !== "auto") return { kind: "forward", params };
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(params.tasks)) {
|
|
129
|
+
if (params.tasks.length === 0 || params.tasks.some((task) => !isRecord(task))) {
|
|
130
|
+
return { kind: "forward", params };
|
|
131
|
+
}
|
|
132
|
+
const selectors = params.tasks.map((task) => taskSelector(task as UnknownRecord, params));
|
|
133
|
+
const hasPlain = selectors.some((selector) => selector.plain);
|
|
134
|
+
const hasVisible = selectors.some((selector) => selector.visible);
|
|
135
|
+
if (hasPlain && hasVisible) {
|
|
136
|
+
return { kind: "reject", requestedBackend: "auto", error: MIXED_PARALLEL_COMPATIBILITY_ERROR };
|
|
137
|
+
}
|
|
138
|
+
if (hasPlain) return { kind: "forward", params: { ...params, backend: "headless" } };
|
|
139
|
+
return { kind: "forward", params };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (params.visible === true || sandboxRequested(params.sandbox)) return { kind: "forward", params };
|
|
143
|
+
return { kind: "forward", params: { ...params, backend: "headless" } };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function compatibilityFailure(plan: Extract<SubagentCompatibilityPlan, { kind: "reject" }>): UnknownRecord {
|
|
147
|
+
const payload = {
|
|
148
|
+
tool: "subagent",
|
|
149
|
+
...(plan.requestedBackend === "inline" ? { backend: "inline" } : {}),
|
|
150
|
+
requestedBackend: plan.requestedBackend,
|
|
151
|
+
status: "failed",
|
|
152
|
+
failureKind: "validation",
|
|
153
|
+
error: plan.error,
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
157
|
+
details: { compatibility: payload },
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
33
162
|
/**
|
|
34
163
|
* Adds the immutable child credential guard to every run path while preserving
|
|
35
164
|
* the ambient AILI permission-mode extension and all caller options.
|
|
@@ -46,27 +175,50 @@ export function protectSubagentParams(params: unknown): unknown {
|
|
|
46
175
|
};
|
|
47
176
|
}
|
|
48
177
|
|
|
49
|
-
function
|
|
178
|
+
export function wrapSubagentTool(tool: GenericTool): GenericTool {
|
|
50
179
|
if (tool.name !== "subagent" || typeof tool.execute !== "function") return tool;
|
|
51
180
|
const execute = tool.execute;
|
|
181
|
+
const renderCall = tool.renderCall;
|
|
52
182
|
return {
|
|
53
183
|
...tool,
|
|
184
|
+
...(typeof renderCall === "function"
|
|
185
|
+
? {
|
|
186
|
+
renderCall(args: unknown, theme: RenderTheme): Component {
|
|
187
|
+
const plan = normalizeSubagentCompatibility(args);
|
|
188
|
+
const effectiveArgs = plan.kind === "forward" ? plan.params : args;
|
|
189
|
+
const upstream = renderCall(effectiveArgs, theme);
|
|
190
|
+
const heading = requestedAgentHeading(args);
|
|
191
|
+
if (!heading) return upstream;
|
|
192
|
+
const container = new Container();
|
|
193
|
+
container.addChild(new Text(
|
|
194
|
+
`${theme.fg("muted", `${heading.label}:`)} ${theme.fg("accent", heading.summary)}`,
|
|
195
|
+
0,
|
|
196
|
+
0,
|
|
197
|
+
));
|
|
198
|
+
container.addChild(upstream);
|
|
199
|
+
return container;
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
54
203
|
async execute(...args: unknown[]) {
|
|
55
|
-
// Pi
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const paramsIndex = args.length
|
|
204
|
+
// Current Pi uses execute(id, params, ...); older direct fixtures may call
|
|
205
|
+
// execute(params). Detect the object position rather than mistaking a
|
|
206
|
+
// signal/callback for params.
|
|
207
|
+
const paramsIndex = isRecord(args[0]) || args.length === 1 ? 0 : 1;
|
|
208
|
+
const plan = normalizeSubagentCompatibility(args[paramsIndex]);
|
|
209
|
+
if (plan.kind === "reject") return compatibilityFailure(plan);
|
|
59
210
|
const protectedArgs = [...args];
|
|
60
|
-
protectedArgs[paramsIndex] = protectSubagentParams(
|
|
211
|
+
protectedArgs[paramsIndex] = protectSubagentParams(plan.params);
|
|
61
212
|
return await execute(...protectedArgs);
|
|
62
213
|
},
|
|
63
214
|
};
|
|
64
215
|
}
|
|
65
216
|
|
|
66
217
|
/**
|
|
67
|
-
* Registers the upstream `subagent`
|
|
68
|
-
*
|
|
69
|
-
*
|
|
218
|
+
* Registers the upstream `subagent` name, schema, lifecycle, and worker engine.
|
|
219
|
+
* AILI wraps backend selection for the pinned SDK compatibility window, adds a
|
|
220
|
+
* bounded requested-Agent heading, and injects non-removable credential
|
|
221
|
+
* protection while each child loads permission modes exactly once.
|
|
70
222
|
*/
|
|
71
223
|
export async function registerSubagent(pi: ExtensionAPI): Promise<void> {
|
|
72
224
|
let genericToolRegistered = false;
|
|
@@ -75,7 +227,7 @@ export async function registerSubagent(pi: ExtensionAPI): Promise<void> {
|
|
|
75
227
|
if (property === "registerTool") {
|
|
76
228
|
return (tool: GenericTool) => {
|
|
77
229
|
if (tool?.name === "subagent") genericToolRegistered = true;
|
|
78
|
-
return target.registerTool(
|
|
230
|
+
return target.registerTool(wrapSubagentTool(tool) as never);
|
|
79
231
|
};
|
|
80
232
|
}
|
|
81
233
|
const value = Reflect.get(target, property, receiver);
|