portable-agent-layer 0.66.1 → 0.68.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/assets/skills/pal-analyze/SKILL.md +1 -1
- package/assets/skills/pal-reflect/SKILL.md +1 -1
- package/assets/skills/projects/SKILL.md +1 -1
- package/assets/skills/telos/SKILL.md +1 -1
- package/assets/templates/hooks.codex.json +18 -4
- package/assets/templates/hooks.copilot.json +31 -12
- package/assets/templates/hooks.cursor.json +23 -7
- package/assets/templates/settings.claude.json +50 -8
- package/package.json +3 -2
- package/src/cli/index.ts +7 -3
- package/src/hooks/LedgerCommit.ts +29 -0
- package/src/hooks/LedgerSnapshot.ts +30 -0
- package/src/hooks/LedgerUnapplied.ts +69 -0
- package/src/hooks/LoadContext.ts +4 -2
- package/src/hooks/lib/actor.ts +4 -4
- package/src/hooks/lib/agent.ts +31 -7
- package/src/hooks/lib/ledger-hook.ts +276 -0
- package/src/hooks/lib/ledger.ts +348 -0
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/sensitive-path.ts +117 -0
- package/src/hooks/lib/settings.ts +5 -0
- package/src/targets/lib.ts +37 -7
- package/src/targets/opencode/plugin.ts +22 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths whose contents the ledger notes the change of but never keeps.
|
|
3
|
+
*
|
|
4
|
+
* The floor below is fixed in code rather than configured: a denylist a user can
|
|
5
|
+
* shrink is a suggestion, and settings may only add to this one. That direction
|
|
6
|
+
* also makes a malformed user pattern harmless — the worst it can do is redact
|
|
7
|
+
* something it did not need to.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { raw } from "./settings";
|
|
11
|
+
|
|
12
|
+
const ENV_TEMPLATE_SUFFIXES = [".sample", ".example", ".template", ".dist", ".defaults"];
|
|
13
|
+
|
|
14
|
+
/** Matched whole: `id_*` would catch `id_generator.ts`, `credentials*` a test file. */
|
|
15
|
+
const SECRET_FILENAMES = new Set([
|
|
16
|
+
".npmrc",
|
|
17
|
+
".netrc",
|
|
18
|
+
"_netrc",
|
|
19
|
+
".pgpass",
|
|
20
|
+
".htpasswd",
|
|
21
|
+
".envrc",
|
|
22
|
+
"credentials",
|
|
23
|
+
"id_rsa",
|
|
24
|
+
"id_dsa",
|
|
25
|
+
"id_ecdsa",
|
|
26
|
+
"id_ecdsa_sk",
|
|
27
|
+
"id_ed25519",
|
|
28
|
+
"id_ed25519_sk",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const SECRET_EXTENSIONS = [
|
|
32
|
+
".pem",
|
|
33
|
+
".key",
|
|
34
|
+
".p12",
|
|
35
|
+
".pfx",
|
|
36
|
+
".keystore",
|
|
37
|
+
".jks",
|
|
38
|
+
".asc",
|
|
39
|
+
".gpg",
|
|
40
|
+
".kdbx",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const SECRET_DIRECTORIES = new Set([
|
|
44
|
+
".ssh",
|
|
45
|
+
".gnupg",
|
|
46
|
+
".aws",
|
|
47
|
+
".docker",
|
|
48
|
+
".kube",
|
|
49
|
+
".gcloud",
|
|
50
|
+
".azure",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const SECRET_DIRECTORY_PAIRS = [
|
|
54
|
+
[".config", "gh"],
|
|
55
|
+
[".config", "gcloud"],
|
|
56
|
+
[".local", "share/keyrings"],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function segmentsOf(path: string): string[] {
|
|
60
|
+
return path.split(/[/\\]/).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isLiveDotenv(name: string): boolean {
|
|
64
|
+
if (name !== ".env" && !name.startsWith(".env.")) return false;
|
|
65
|
+
return !ENV_TEMPLATE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSecretFilename(name: string): boolean {
|
|
69
|
+
return SECRET_FILENAMES.has(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasSecretExtension(name: string): boolean {
|
|
73
|
+
return SECRET_EXTENSIONS.some((ext) => name.toLowerCase().endsWith(ext));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inSecretDirectory(dirs: string[]): boolean {
|
|
77
|
+
if (dirs.some((dir) => SECRET_DIRECTORIES.has(dir))) return true;
|
|
78
|
+
return SECRET_DIRECTORY_PAIRS.some(([parent, child]) =>
|
|
79
|
+
dirs.some(
|
|
80
|
+
(dir, i) =>
|
|
81
|
+
dir === parent &&
|
|
82
|
+
dirs
|
|
83
|
+
.slice(i + 1)
|
|
84
|
+
.join("/")
|
|
85
|
+
.startsWith(child)
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function userPatterns(): string[] {
|
|
91
|
+
const configured = raw().ledger?.redactPaths;
|
|
92
|
+
if (!Array.isArray(configured)) return [];
|
|
93
|
+
return configured.filter(
|
|
94
|
+
(pattern) => typeof pattern === "string" && pattern.length > 0
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function matchesUserPattern(path: string, name: string): boolean {
|
|
99
|
+
return userPatterns().some((pattern) => {
|
|
100
|
+
const glob = new Bun.Glob(pattern);
|
|
101
|
+
return glob.match(path) || glob.match(name);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Should this file's contents be withheld from the ledger? */
|
|
106
|
+
export function isSensitivePath(path: string): boolean {
|
|
107
|
+
const segments = segmentsOf(path);
|
|
108
|
+
const name = segments.at(-1) ?? "";
|
|
109
|
+
const dirs = segments.slice(0, -1);
|
|
110
|
+
return (
|
|
111
|
+
inSecretDirectory(dirs) ||
|
|
112
|
+
isLiveDotenv(name) ||
|
|
113
|
+
isSecretFilename(name) ||
|
|
114
|
+
hasSecretExtension(name) ||
|
|
115
|
+
matchesUserPattern(path, name)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -25,6 +25,11 @@ export interface PalSettingsData {
|
|
|
25
25
|
dynamicContext?: Record<string, boolean>;
|
|
26
26
|
/** Git co-author attribution opt-in. `decided` gates the one-time prompt. */
|
|
27
27
|
attribution?: { enabled?: boolean; decided?: boolean };
|
|
28
|
+
/**
|
|
29
|
+
* Action-ledger user extension. `redactPaths` adds to the built-in set of
|
|
30
|
+
* paths whose contents are never stored; it cannot shrink it.
|
|
31
|
+
*/
|
|
32
|
+
ledger?: { redactPaths?: string[] };
|
|
28
33
|
/** Contextual-steering user extension: personal rules + shipped rules to suppress by tag. */
|
|
29
34
|
steering?: {
|
|
30
35
|
disable?: string[];
|
package/src/targets/lib.ts
CHANGED
|
@@ -23,11 +23,27 @@ import { declaredTriggers } from "../hooks/lib/skill-triggers";
|
|
|
23
23
|
|
|
24
24
|
// --- Colored logging ---
|
|
25
25
|
|
|
26
|
+
function runningUnderTest(): boolean {
|
|
27
|
+
return process.env.PAL_TEST_SANDBOX === "1";
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
export const log = {
|
|
27
31
|
info: (msg: string) => console.log(`\x1b[34m[pal]\x1b[0m ${msg}`),
|
|
28
32
|
success: (msg: string) => console.log(`\x1b[32m[pal]\x1b[0m ${msg}`),
|
|
29
33
|
warn: (msg: string) => console.log(`\x1b[33m[pal]\x1b[0m ${msg}`),
|
|
30
34
|
error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Per-item narration from inside a loop, where the caller already reports the
|
|
38
|
+
* total. Silent under the test runner: the suite drives these installers by
|
|
39
|
+
* the hundred against temp directories, so the lines name files that were
|
|
40
|
+
* never on this machine — and they are the only output no caller asserts on,
|
|
41
|
+
* precisely because the summary is what carries the result.
|
|
42
|
+
*/
|
|
43
|
+
detail: (msg: string) => {
|
|
44
|
+
if (runningUnderTest()) return;
|
|
45
|
+
console.log(`\x1b[34m[pal]\x1b[0m ${msg}`);
|
|
46
|
+
},
|
|
31
47
|
};
|
|
32
48
|
|
|
33
49
|
// --- JSON helpers ---
|
|
@@ -456,7 +472,21 @@ export function unmergeCursorHooks(
|
|
|
456
472
|
|
|
457
473
|
type CodexHookCommand = { type: string; command: string; timeout?: number };
|
|
458
474
|
type CodexHookGroup = { matcher?: string; hooks: CodexHookCommand[] };
|
|
459
|
-
type CodexHooks = {
|
|
475
|
+
type CodexHooks = {
|
|
476
|
+
hooks?: Record<string, CodexHookGroup[]>;
|
|
477
|
+
description?: string;
|
|
478
|
+
version?: unknown;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Codex parses hooks.json strictly and accepts only `description` and `hooks`.
|
|
483
|
+
* A stale `version` makes it reject the whole file — every PAL hook silently
|
|
484
|
+
* stops — and merging preserves what it finds, so it must be dropped by name.
|
|
485
|
+
*/
|
|
486
|
+
function withoutRejectedFields(config: CodexHooks): CodexHooks {
|
|
487
|
+
const { version: _version, ...accepted } = config;
|
|
488
|
+
return accepted;
|
|
489
|
+
}
|
|
460
490
|
|
|
461
491
|
/**
|
|
462
492
|
* Normalize a PAL hook command for cross-path deduplication.
|
|
@@ -526,7 +556,7 @@ function stripPalHooks(
|
|
|
526
556
|
|
|
527
557
|
/** Merge PAL hooks into an existing Codex hooks.json. Deduplicates by canonical command path. */
|
|
528
558
|
export function mergeCodexHooks(existing: CodexHooks, template: CodexHooks): CodexHooks {
|
|
529
|
-
const result: CodexHooks =
|
|
559
|
+
const result: CodexHooks = withoutRejectedFields(existing);
|
|
530
560
|
if (!template.hooks) return result;
|
|
531
561
|
result.hooks ??= {};
|
|
532
562
|
|
|
@@ -545,7 +575,7 @@ export function unmergeCodexHooks(
|
|
|
545
575
|
existing: CodexHooks,
|
|
546
576
|
template: CodexHooks
|
|
547
577
|
): CodexHooks {
|
|
548
|
-
const result: CodexHooks =
|
|
578
|
+
const result: CodexHooks = withoutRejectedFields(existing);
|
|
549
579
|
if (!template.hooks || !result.hooks) return result;
|
|
550
580
|
|
|
551
581
|
stripPalHooks(result.hooks, collectPalCanonical(template));
|
|
@@ -784,7 +814,7 @@ export function copySkills(claudeSkillsDir: string): number {
|
|
|
784
814
|
let count = 0;
|
|
785
815
|
|
|
786
816
|
for (const name of pruneStaleSkillLinks(claudeSkillsDir)) {
|
|
787
|
-
log.
|
|
817
|
+
log.detail(`Removed stale skill link: ${name}`);
|
|
788
818
|
}
|
|
789
819
|
|
|
790
820
|
for (const name of readdirSync(skillsDir)) {
|
|
@@ -994,7 +1024,7 @@ export function removeSkills(claudeSkillsDir: string): string[] {
|
|
|
994
1024
|
}
|
|
995
1025
|
}
|
|
996
1026
|
removed.push(name);
|
|
997
|
-
log.
|
|
1027
|
+
log.detail(`Removed skill: ${name}`);
|
|
998
1028
|
}
|
|
999
1029
|
|
|
1000
1030
|
// Remove ~/.agents/skills/ → ~/.pal/skills/ symlink
|
|
@@ -1031,7 +1061,7 @@ export function removeAgents(): string[] {
|
|
|
1031
1061
|
unlinkSync(dst);
|
|
1032
1062
|
const name = file.replace(/\.md$/, "");
|
|
1033
1063
|
removed.push(name);
|
|
1034
|
-
log.
|
|
1064
|
+
log.detail(`Removed agent: ${name}`);
|
|
1035
1065
|
}
|
|
1036
1066
|
}
|
|
1037
1067
|
return removed;
|
|
@@ -1141,7 +1171,7 @@ function uninstallAgents(targetDir: string, label: string): string[] {
|
|
|
1141
1171
|
if (existsSync(dst)) {
|
|
1142
1172
|
unlinkSync(dst);
|
|
1143
1173
|
removed.push(file.replace(/\.md$/, ""));
|
|
1144
|
-
log.
|
|
1174
|
+
log.detail(`Removed ${label} agent: ${file.replace(/\.md$/, "")}`);
|
|
1145
1175
|
}
|
|
1146
1176
|
}
|
|
1147
1177
|
return removed;
|
|
@@ -106,6 +106,14 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
106
106
|
const { isPalSpawnedInference } =
|
|
107
107
|
await lib<typeof import("../../hooks/lib/spawn-guard")>("spawn-guard.ts");
|
|
108
108
|
|
|
109
|
+
const { commitApplied, ledgeredTarget, snapshotCall } =
|
|
110
|
+
await lib<typeof import("../../hooks/lib/ledger-hook")>("ledger-hook.ts");
|
|
111
|
+
|
|
112
|
+
const ledgeredOpencodeCall = (tool: string, callID: string, args: unknown) => {
|
|
113
|
+
const target = ledgeredTarget(tool, (args ?? {}) as Record<string, unknown>);
|
|
114
|
+
return target ? { toolUseId: callID, tool, target } : null;
|
|
115
|
+
};
|
|
116
|
+
|
|
109
117
|
return {
|
|
110
118
|
// --- Per-message: Inject dynamic system reminder ---
|
|
111
119
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
@@ -213,6 +221,20 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
213
221
|
throw new Error(`PAL Security: ${fileReason}`);
|
|
214
222
|
}
|
|
215
223
|
}
|
|
224
|
+
|
|
225
|
+
const call = ledgeredOpencodeCall(toolName, _input.callID, output.args);
|
|
226
|
+
if (call) snapshotCall(call);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
"tool.execute.after": async (
|
|
230
|
+
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
231
|
+
_output: { title: string; output: string; metadata: unknown }
|
|
232
|
+
) => {
|
|
233
|
+
const call = ledgeredOpencodeCall(input.tool, input.callID, input.args);
|
|
234
|
+
if (!call) return;
|
|
235
|
+
|
|
236
|
+
const entry = commitApplied(call);
|
|
237
|
+
if (entry) logDebug("opencode:ledger", `recorded ${entry.id} ${entry.target}`);
|
|
216
238
|
},
|
|
217
239
|
|
|
218
240
|
// --- Inject PAL_DIR into shell environment ---
|