@preapexis/piexis 0.1.2 → 0.2.4
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/.github/workflows/publish.yml +35 -0
- package/AGENTS.md +21 -41
- package/README.md +75 -142
- package/extensions/agents.ts +205 -0
- package/extensions/ask-question.ts +54 -95
- package/extensions/recommended-tools.ts +62 -0
- package/extensions/ui.ts +204 -58
- package/lib/agent-config.ts +55 -0
- package/lib/agents.ts +208 -0
- package/lib/question-model.ts +98 -0
- package/lib/question-ui.ts +214 -0
- package/lib/recommended-tools.ts +119 -0
- package/lib/subagent-runner.ts +146 -0
- package/lib/ui-tips.json +41 -0
- package/package.json +9 -12
- package/prompts/init.md +10 -2
- package/prompts/summary.md +7 -6
- package/schemas/agent.schema.json +48 -0
- package/scripts/release.mjs +32 -0
- package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
- package/skills/caveman/LICENSE-MIT +21 -0
- package/skills/caveman/NOTICE +10 -0
- package/skills/caveman/SKILL.md +89 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/ponytail/LICENSE +21 -0
- package/skills/ponytail/NOTICE +9 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/skills-lock.json +6 -0
- package/tests/agents.test.mjs +605 -0
- package/tests/ask-question-pty.test.mjs +16 -0
- package/tests/ask-question.test.mjs +269 -0
- package/tests/fixtures/agents-provider.ts +55 -0
- package/tests/fixtures/ask-question-pty.py +91 -0
- package/tests/fixtures/ask-question-terminal.mjs +69 -0
- package/tests/fixtures/recommended-tools-terminal.mjs +103 -0
- package/tests/init-workflow.test.mjs +31 -0
- package/tests/recommended-tools-pty.test.mjs +16 -0
- package/tests/recommended-tools.test.mjs +423 -0
- package/tests/resources.test.mjs +66 -0
- package/tests/skills-routing.test.mjs +7 -4
- package/tests/ui-tip-reload.test.mjs +70 -0
- package/tests/ui.test.mjs +450 -0
- package/tsconfig.json +1 -0
- package/.github/workflows/npm-publish.yml +0 -63
- package/PERMISSION_MODES_PLAN.md +0 -279
- package/WHAT_YOU_GET.md +0 -153
- package/eval/README.md +0 -72
- package/eval/permission-cases.json +0 -28
- package/eval/permissions.mjs +0 -103
- package/extensions/background-tasks.ts +0 -433
- package/extensions/background-worker.ts +0 -50
- package/extensions/glob.ts +0 -32
- package/extensions/mode.ts +0 -397
- package/lib/bash-approvals.ts +0 -113
- package/lib/bash-policy.ts +0 -164
- package/lib/mode-policy.ts +0 -174
- package/lib/path-policy.ts +0 -182
- package/lib/permission-classifier.ts +0 -128
- package/lib/permission-config.ts +0 -91
- package/lib/permission-denials.ts +0 -32
- package/lib/permission-intent.ts +0 -128
- package/lib/permission-taxonomy.ts +0 -15
- package/lib/task-broker.ts +0 -65
- package/lib/task-checkout.ts +0 -39
- package/lib/task-git.ts +0 -44
- package/lib/task-patch.ts +0 -106
- package/lib/task-permissions.ts +0 -51
- package/lib/task-report.ts +0 -62
- package/lib/task-worktree-policy.ts +0 -56
- package/skills/claude-api/SKILL.md +0 -572
- package/skills/claude-api/csharp/claude-api/README.md +0 -362
- package/skills/claude-api/csharp/claude-api/batches.md +0 -14
- package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
- package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
- package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
- package/skills/claude-api/curl/examples.md +0 -256
- package/skills/claude-api/curl/managed-agents.md +0 -371
- package/skills/claude-api/go/claude-api/README.md +0 -188
- package/skills/claude-api/go/claude-api/files-api.md +0 -23
- package/skills/claude-api/go/claude-api/streaming.md +0 -43
- package/skills/claude-api/go/claude-api/tool-use.md +0 -220
- package/skills/claude-api/go/managed-agents/README.md +0 -564
- package/skills/claude-api/java/claude-api/README.md +0 -238
- package/skills/claude-api/java/claude-api/files-api.md +0 -27
- package/skills/claude-api/java/claude-api/streaming.md +0 -24
- package/skills/claude-api/java/claude-api/tool-use.md +0 -228
- package/skills/claude-api/java/managed-agents/README.md +0 -443
- package/skills/claude-api/php/claude-api/README.md +0 -174
- package/skills/claude-api/php/claude-api/batches.md +0 -15
- package/skills/claude-api/php/claude-api/files-api.md +0 -13
- package/skills/claude-api/php/claude-api/streaming.md +0 -27
- package/skills/claude-api/php/claude-api/tool-use.md +0 -253
- package/skills/claude-api/php/managed-agents/README.md +0 -443
- package/skills/claude-api/python/claude-api/README.md +0 -565
- package/skills/claude-api/python/claude-api/batches.md +0 -198
- package/skills/claude-api/python/claude-api/files-api.md +0 -170
- package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
- package/skills/claude-api/python/claude-api/streaming.md +0 -179
- package/skills/claude-api/python/claude-api/tool-use.md +0 -627
- package/skills/claude-api/python/managed-agents/README.md +0 -335
- package/skills/claude-api/ruby/claude-api/README.md +0 -134
- package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
- package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
- package/skills/claude-api/ruby/managed-agents/README.md +0 -394
- package/skills/claude-api/shared/admin-api.md +0 -179
- package/skills/claude-api/shared/agent-design.md +0 -101
- package/skills/claude-api/shared/anthropic-cli.md +0 -269
- package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
- package/skills/claude-api/shared/cost-optimization.md +0 -233
- package/skills/claude-api/shared/error-codes.md +0 -262
- package/skills/claude-api/shared/live-sources.md +0 -166
- package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
- package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
- package/skills/claude-api/shared/managed-agents-core.md +0 -359
- package/skills/claude-api/shared/managed-agents-environments.md +0 -226
- package/skills/claude-api/shared/managed-agents-events.md +0 -290
- package/skills/claude-api/shared/managed-agents-memory.md +0 -201
- package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
- package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
- package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
- package/skills/claude-api/shared/managed-agents-overview.md +0 -77
- package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
- package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
- package/skills/claude-api/shared/managed-agents-tools.md +0 -504
- package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
- package/skills/claude-api/shared/model-migration.md +0 -1872
- package/skills/claude-api/shared/models.md +0 -142
- package/skills/claude-api/shared/platform-availability.md +0 -53
- package/skills/claude-api/shared/prompt-audit.md +0 -223
- package/skills/claude-api/shared/prompt-caching.md +0 -292
- package/skills/claude-api/shared/token-counting.md +0 -56
- package/skills/claude-api/shared/tool-use-concepts.md +0 -526
- package/skills/claude-api/typescript/claude-api/README.md +0 -405
- package/skills/claude-api/typescript/claude-api/batches.md +0 -106
- package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
- package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
- package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
- package/skills/claude-api/typescript/managed-agents/README.md +0 -359
- package/tests/auto-permissions.test.mjs +0 -315
- package/tests/background-tasks.test.mjs +0 -27
- package/tests/background-worker.test.mjs +0 -60
- package/tests/bash-permissions.test.mjs +0 -276
- package/tests/glob.test.mjs +0 -31
- package/tests/mode.test.mjs +0 -62
- package/tests/permission-evaluation.test.mjs +0 -70
- package/tests/permission-foundation.test.mjs +0 -194
- package/tests/permission-policy.test.mjs +0 -158
- package/tests/runtime/README.md +0 -51
- package/tests/runtime/harness.mjs +0 -190
- package/tests/runtime/permissions.test.mjs +0 -192
- package/tests/runtime/tui.test.mjs +0 -60
- package/tests/task-integration.test.mjs +0 -324
- package/tests/task-permissions.test.mjs +0 -178
- package/tests/task-worktree-policy.test.mjs +0 -53
package/extensions/mode.ts
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
ALT_M_CYCLE,
|
|
4
|
-
FLAG_ONLY_MODES,
|
|
5
|
-
MODE_LABELS,
|
|
6
|
-
evaluatePermission,
|
|
7
|
-
type SessionApproval,
|
|
8
|
-
type Mode
|
|
9
|
-
} from "../lib/mode-policy.ts";
|
|
10
|
-
import { classifierAction, createClassifier, type Classifier } from "../lib/permission-classifier.ts";
|
|
11
|
-
import { resolveWorkspaceRoot } from "../lib/path-policy.ts";
|
|
12
|
-
import { initializeProjectPermissionConfig } from "../lib/permission-config.ts";
|
|
13
|
-
import { INTENT_PROVENANCE, IntentTracker, redactIntentSecrets, userIntentFromBranch, type UserIntent } from "../lib/permission-intent.ts";
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
-
import { MANAGED_TASK_TOOLS, TASK_INVALIDATED, TASK_PERMISSION_SERVICE, validateTaskScope, type TaskPermissionService, type TaskSpec } from "../lib/task-permissions.ts";
|
|
16
|
-
import { bashApprovalFingerprint } from "../lib/bash-approvals.ts";
|
|
17
|
-
import { DEFAULT_DENIAL_LIMITS, DenialTracker, denialLimit } from "../lib/permission-denials.ts";
|
|
18
|
-
|
|
19
|
-
const STATE = "piexis-permission-mode";
|
|
20
|
-
const modes = Object.keys(MODE_LABELS) as Mode[];
|
|
21
|
-
const AUTO_PRIVACY_NOTICE = "Auto review sends bounded user requests, action details, and narrow approval scopes to the configured classifier provider.";
|
|
22
|
-
|
|
23
|
-
const MODE_INSTRUCTIONS: Record<Mode, string> = {
|
|
24
|
-
default: "You are in Manual mode. Follow the user's request normally; permission prompts are handled by the host.",
|
|
25
|
-
acceptEdits: "You are in Accept Edits mode. Make requested file edits directly; do not behave as if you are in Plan mode.",
|
|
26
|
-
plan: "You are in Plan mode. Explore and propose a concrete implementation plan. Do not make edits or run mutating commands; explain what you would change and why.",
|
|
27
|
-
auto: "You are in Auto mode. Execute the user's requested work directly; do not behave as if you are in Plan mode.",
|
|
28
|
-
dontAsk: "You are in Don't Ask mode. Execute pre-approved actions directly; do not behave as if you are in Plan mode.",
|
|
29
|
-
bypassPermissions: "You are in Bypass Permissions mode. Execute the user's requested work directly; do not behave as if you are in Plan mode."
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export default function (pi: ExtensionAPI) {
|
|
33
|
-
let mode: Mode = "default";
|
|
34
|
-
const sessionApprovals: SessionApproval[] = [];
|
|
35
|
-
const approvalRequests = new WeakMap<SessionApproval, string>();
|
|
36
|
-
const intentTracker = new IntentTracker();
|
|
37
|
-
let authorizationRevision = 0;
|
|
38
|
-
const denials = new DenialTracker();
|
|
39
|
-
let nextApprovalId = 1;
|
|
40
|
-
let workspaceRoot = "";
|
|
41
|
-
let permissionContext: ExtensionContext | undefined;
|
|
42
|
-
const taskControllers = new Set<AbortController>();
|
|
43
|
-
function invalidateTasks(reason = "authorization") {
|
|
44
|
-
authorizationRevision++;
|
|
45
|
-
pi.events.emit(TASK_INVALIDATED, { reason });
|
|
46
|
-
for (const controller of taskControllers) controller.abort();
|
|
47
|
-
taskControllers.clear();
|
|
48
|
-
}
|
|
49
|
-
let classifier: Classifier = {
|
|
50
|
-
available: false,
|
|
51
|
-
review: async () => ({ decision: "deny", category: "unavailable", reason: "Classifier is unavailable." })
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
pi.registerFlag("permission-repeat-limit", { description: "Stop after this many identical denied actions (1–50; default 3)", type: "string" });
|
|
55
|
-
pi.registerFlag("permission-denial-limit", { description: "Stop after this many total denials per user request (1–50; default 6)", type: "string" });
|
|
56
|
-
pi.registerFlag("permission-mode", { description: "Start in a permission mode", type: "string" });
|
|
57
|
-
pi.registerFlag("dangerously-skip-permissions", { description: "Enable bypassPermissions mode", type: "boolean", default: false });
|
|
58
|
-
|
|
59
|
-
function setStatus(ctx: ExtensionContext) {
|
|
60
|
-
if (ctx.hasUI) ctx.ui.setStatus(STATE, MODE_LABELS[mode]);
|
|
61
|
-
}
|
|
62
|
-
async function choose(next: Mode, ctx: ExtensionContext, allowDuringAgent = false) {
|
|
63
|
-
if (FLAG_ONLY_MODES.has(next) && next !== mode)
|
|
64
|
-
throw new Error(`${next} can only be selected at launch with --permission-mode.`);
|
|
65
|
-
if (next === "auto" && !classifier.available)
|
|
66
|
-
throw new Error("Auto mode requires a configured classifier model.");
|
|
67
|
-
if (!allowDuringAgent && !ctx.isIdle())
|
|
68
|
-
throw new Error("Wait until the agent is idle before changing permission modes");
|
|
69
|
-
invalidateTasks();
|
|
70
|
-
mode = next;
|
|
71
|
-
pi.events.emit("piexis:permission-mode", mode);
|
|
72
|
-
pi.appendEntry(STATE, { version: 2, mode });
|
|
73
|
-
setStatus(ctx);
|
|
74
|
-
if (ctx.hasUI)
|
|
75
|
-
ctx.ui.notify(`Permission mode: ${MODE_LABELS[mode]}${mode === "auto" ? `\n${AUTO_PRIVACY_NOTICE}` : ""}`, mode === "bypassPermissions" ? "warning" : "info");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
pi.registerCommand("piexis-init", {
|
|
79
|
-
description: "Create Piexis project configuration files",
|
|
80
|
-
handler: async (_args, ctx) => {
|
|
81
|
-
invalidateTasks();
|
|
82
|
-
await initializeProjectPermissionConfig(ctx.cwd, ctx.modelRegistry, ctx.model);
|
|
83
|
-
classifier = await createClassifier(ctx.cwd, ctx.modelRegistry);
|
|
84
|
-
if (ctx.hasUI) ctx.ui.notify("Piexis project configuration initialized.", "info");
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
pi.registerCommand("permissions", {
|
|
88
|
-
description: "Inspect session grants, revoke <id>, or clear all grants",
|
|
89
|
-
handler: async (args, ctx) => {
|
|
90
|
-
const [verb = "list", id, ...extra] = args.trim().split(/\s+/).filter(Boolean);
|
|
91
|
-
if (!["list", "revoke", "clear"].includes(verb) || extra.length ||
|
|
92
|
-
(verb === "revoke" ? !id : Boolean(id))) throw new Error("Usage: /permissions [list | revoke <id> | clear]");
|
|
93
|
-
if (verb !== "list") {
|
|
94
|
-
if (!ctx.isIdle()) throw new Error("Wait until the agent is idle before changing permissions.");
|
|
95
|
-
if (verb === "clear") sessionApprovals.length = 0;
|
|
96
|
-
else {
|
|
97
|
-
const index = sessionApprovals.findIndex((approval) => approval.id === id);
|
|
98
|
-
if (index < 0) throw new Error(`Unknown permission grant ${id}.`);
|
|
99
|
-
sessionApprovals.splice(index, 1);
|
|
100
|
-
}
|
|
101
|
-
invalidateTasks();
|
|
102
|
-
}
|
|
103
|
-
const grants = sessionApprovals.map((approval) => redactIntentSecrets(JSON.stringify(approval,
|
|
104
|
-
(key, value) => key === "fingerprint" ? undefined : value))).join("\n") || "No session grants.";
|
|
105
|
-
const text = `Permission mode: ${mode}\nDenials: ${denials.total}/${denials.limits.total}${denials.stopped ? " (run stopped; submit a new request)" : ""}\n${grants}`;
|
|
106
|
-
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
107
|
-
else process.stderr.write(`${text}\n`);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
pi.registerCommand("plan", {
|
|
111
|
-
description: "Enter read-only Plan mode and optionally start a planning task",
|
|
112
|
-
handler: async (args, ctx) => {
|
|
113
|
-
if (!ctx.isIdle()) throw new Error("Wait until the agent is idle before entering Plan mode");
|
|
114
|
-
if (mode !== "plan") await choose("plan", ctx);
|
|
115
|
-
if (args.trim()) pi.sendUserMessage(args.trim());
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
async function cycleMode(ctx: ExtensionContext) {
|
|
120
|
-
const available = ALT_M_CYCLE.filter((candidate) => candidate !== "auto" || classifier.available);
|
|
121
|
-
const index = available.indexOf(mode);
|
|
122
|
-
await choose(available[index < 0 ? 0 : (index + 1) % available.length]!, ctx);
|
|
123
|
-
}
|
|
124
|
-
pi.registerShortcut("alt+m", { description: "Cycle permission mode", handler: cycleMode });
|
|
125
|
-
|
|
126
|
-
async function restoreMode(ctx: ExtensionContext) {
|
|
127
|
-
mode = "default";
|
|
128
|
-
const saved = [...ctx.sessionManager.getBranch()].reverse()
|
|
129
|
-
.find((entry: any) => entry.type === "custom" && entry.customType === STATE) as any;
|
|
130
|
-
const restored = saved?.data?.mode as Mode | undefined;
|
|
131
|
-
if (restored && modes.includes(restored) && !FLAG_ONLY_MODES.has(restored) && (restored !== "auto" || classifier.available)) mode = restored;
|
|
132
|
-
setStatus(ctx);
|
|
133
|
-
pi.events.emit("piexis:permission-mode", mode);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
pi.on("input", (event) => {
|
|
137
|
-
invalidateTasks();
|
|
138
|
-
if (!event.streamingBehavior) intentTracker.reset();
|
|
139
|
-
intentTracker.input(event.text, event.source);
|
|
140
|
-
});
|
|
141
|
-
pi.on("agent_settled", () => { intentTracker.reset(); });
|
|
142
|
-
pi.on("message_end", (event) => {
|
|
143
|
-
const provenance = intentTracker.message(event.message);
|
|
144
|
-
if (provenance) {
|
|
145
|
-
invalidateTasks();
|
|
146
|
-
if (provenance.source === "interactive" || provenance.source === "rpc") denials.reset();
|
|
147
|
-
pi.appendEntry(INTENT_PROVENANCE, provenance);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
pi.on("session_shutdown", () => { permissionContext = undefined; invalidateTasks("session"); intentTracker.reset(); });
|
|
151
|
-
|
|
152
|
-
pi.on("session_start", async (event, ctx) => {
|
|
153
|
-
permissionContext = undefined;
|
|
154
|
-
invalidateTasks("session");
|
|
155
|
-
intentTracker.reset();
|
|
156
|
-
sessionApprovals.length = 0;
|
|
157
|
-
denials.reset();
|
|
158
|
-
denials.limits = {
|
|
159
|
-
repeated: denialLimit(pi.getFlag("permission-repeat-limit"), DEFAULT_DENIAL_LIMITS.repeated),
|
|
160
|
-
total: denialLimit(pi.getFlag("permission-denial-limit"), DEFAULT_DENIAL_LIMITS.total)
|
|
161
|
-
};
|
|
162
|
-
workspaceRoot = await resolveWorkspaceRoot(ctx.cwd);
|
|
163
|
-
const isStartup = event.reason === "startup";
|
|
164
|
-
const bypassRequested = isStartup && pi.getFlag("dangerously-skip-permissions") === true;
|
|
165
|
-
classifier = await createClassifier(ctx.cwd, ctx.modelRegistry);
|
|
166
|
-
const requested = pi.getFlag("permission-mode");
|
|
167
|
-
if (isStartup && typeof requested === "string" && !modes.includes(requested as Mode))
|
|
168
|
-
throw new Error(`Unknown permission mode: ${requested}`);
|
|
169
|
-
if (isStartup && bypassRequested && requested && requested !== "bypassPermissions")
|
|
170
|
-
throw new Error("--dangerously-skip-permissions conflicts with --permission-mode.");
|
|
171
|
-
await restoreMode(ctx);
|
|
172
|
-
const selected = bypassRequested ? "bypassPermissions" : isStartup && typeof requested === "string" ? requested as Mode : undefined;
|
|
173
|
-
if (selected === "auto" && !classifier.available) throw new Error("Auto mode requires a configured classifier model.");
|
|
174
|
-
if (selected) mode = selected;
|
|
175
|
-
if (mode === "auto" && ctx.hasUI) ctx.ui.notify(AUTO_PRIVACY_NOTICE, "info");
|
|
176
|
-
setStatus(ctx);
|
|
177
|
-
pi.events.emit("piexis:permission-mode", mode);
|
|
178
|
-
permissionContext = ctx;
|
|
179
|
-
if (ctx.mode === "tui") ctx.ui.onTerminalInput((data) => {
|
|
180
|
-
if (data !== "\u001bm") return;
|
|
181
|
-
void cycleMode(ctx).catch((error) => ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning"));
|
|
182
|
-
return { consume: true };
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
187
|
-
invalidateTasks();
|
|
188
|
-
intentTracker.reset();
|
|
189
|
-
sessionApprovals.length = 0;
|
|
190
|
-
denials.reset();
|
|
191
|
-
await restoreMode(ctx);
|
|
192
|
-
});
|
|
193
|
-
pi.on("before_agent_start", async (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${MODE_INSTRUCTIONS[mode]}` }));
|
|
194
|
-
|
|
195
|
-
async function checkTool(event: ToolCallEvent, ctx: ExtensionContext, delegated?: { spec: TaskSpec; intent: UserIntent; worktree?: string }) {
|
|
196
|
-
const worker = Boolean(delegated?.worktree);
|
|
197
|
-
if (mode === "bypassPermissions" && !worker || event.toolName === "AskQuestion") return;
|
|
198
|
-
const root = delegated?.worktree ?? (workspaceRoot || await resolveWorkspaceRoot(ctx.cwd));
|
|
199
|
-
const evaluationRevision = authorizationRevision;
|
|
200
|
-
const activeIntent = mode === "auto" ? userIntentFromBranch(ctx.sessionManager.getBranch()) : undefined;
|
|
201
|
-
const eligibleApprovals = delegated ? [] : mode !== "auto" ? sessionApprovals : sessionApprovals.filter((approval) =>
|
|
202
|
-
activeIntent?.complete && !intentTracker.hasPendingInput && approvalRequests.get(approval) === activeIntent.currentRequestId);
|
|
203
|
-
const decision = await evaluatePermission({
|
|
204
|
-
mode, tool: event.toolName, input: event.input, cwd: ctx.cwd, workspaceRoot: root,
|
|
205
|
-
authorization: { approvals: eligibleApprovals }, actor: worker
|
|
206
|
-
? { kind: "worker", scope: delegated!.spec } : { kind: "foreground" }
|
|
207
|
-
});
|
|
208
|
-
if (decision.decision === "allow") {
|
|
209
|
-
if (decision.reason === "session-approval" && evaluationRevision !== authorizationRevision)
|
|
210
|
-
return { block: true, reason: "Authorization changed while checking the session grant." };
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (decision.decision === "deny")
|
|
214
|
-
return { block: true, reason: `Permission policy denied action (${decision.reason}).` };
|
|
215
|
-
const pathPolicy = decision.pathPolicy?.decision === "deny" ? undefined : decision.pathPolicy;
|
|
216
|
-
|
|
217
|
-
if (decision.decision === "require-model-review") {
|
|
218
|
-
if (!classifier.available)
|
|
219
|
-
return { block: true, reason: "Auto mode is unavailable: configure a classifier model." };
|
|
220
|
-
try {
|
|
221
|
-
const revision = authorizationRevision;
|
|
222
|
-
const intent = delegated?.intent ?? userIntentFromBranch(ctx.sessionManager.getBranch());
|
|
223
|
-
const intentSnapshot = JSON.stringify(intent);
|
|
224
|
-
if (intentTracker.hasPendingInput) {
|
|
225
|
-
intent.complete = false;
|
|
226
|
-
intent.issues.push("undelivered-user-input");
|
|
227
|
-
}
|
|
228
|
-
// Only valid reusable grants could have bypassed review above. Stale or
|
|
229
|
-
// ineligible execution grants must not influence the model as permission.
|
|
230
|
-
const approvals = eligibleApprovals.filter((approval) => approval.tool === event.toolName &&
|
|
231
|
-
(approval.scope === "file" || approval.scope === "directory"));
|
|
232
|
-
const verdict = await classifier.review(classifierAction(event.toolName, event.input), {
|
|
233
|
-
cwd: ctx.cwd, workspaceRoot: root, intent, approvals, delegation: delegated?.spec,
|
|
234
|
-
policy: { reason: decision.reason, pathPolicy: decision.pathPolicy },
|
|
235
|
-
findings: { shellSemantics: decision.bash?.status ?? "not-applicable", bash: decision.bash, fileContents: "excluded" }
|
|
236
|
-
}, ctx.signal);
|
|
237
|
-
if (revision !== authorizationRevision || !delegated && intentSnapshot !== JSON.stringify(userIntentFromBranch(ctx.sessionManager.getBranch())))
|
|
238
|
-
return { block: true, reason: "Auto authorization changed during review; the action was not executed." };
|
|
239
|
-
if (verdict.decision === "deny")
|
|
240
|
-
return { block: true, reason: `Auto review denied action (${verdict.category}): ${verdict.reason}` };
|
|
241
|
-
return;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
return { block: true, reason: `Auto classifier failed closed: ${error instanceof Error ? error.message : String(error)}` };
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!ctx.hasUI) return { block: true, reason: "Approval required but no permission UI is available." };
|
|
248
|
-
|
|
249
|
-
const request = JSON.stringify(event.input).slice(0, 1_000);
|
|
250
|
-
const allowOnce = `Allow once: ${event.toolName}`;
|
|
251
|
-
const allowAll = `Allow ALL ${event.toolName} actions for this session`;
|
|
252
|
-
const options = [allowOnce];
|
|
253
|
-
const isBash = event.toolName === "bash" && event.input && typeof event.input === "object";
|
|
254
|
-
const command = isBash ? String((event.input as { command?: unknown }).command ?? "").trim() : "";
|
|
255
|
-
if (!delegated && isBash && decision.bashFingerprint) {
|
|
256
|
-
options.push("Allow this exact Bash command in this directory for this session");
|
|
257
|
-
options.push("Allow Git status queries in this directory for this session");
|
|
258
|
-
}
|
|
259
|
-
if (!delegated && pathPolicy && pathPolicy.reason !== "recursive-boundary") {
|
|
260
|
-
options.push("Allow this file for this session");
|
|
261
|
-
if (pathPolicy.scope !== "sensitive") options.push("Allow this directory for this session");
|
|
262
|
-
if (pathPolicy.scope === "project") options.push("Allow project files for this tool for this session");
|
|
263
|
-
}
|
|
264
|
-
if (!delegated && !MANAGED_TASK_TOOLS.has(event.toolName) && (!pathPolicy || pathPolicy.scope !== "sensitive")) options.push(allowAll);
|
|
265
|
-
options.push("Deny");
|
|
266
|
-
const approvalRequestId = userIntentFromBranch(ctx.sessionManager?.getBranch() ?? []).currentRequestId;
|
|
267
|
-
const revision = authorizationRevision;
|
|
268
|
-
const choice = await ctx.ui.select(`${delegated ? `Delegated task data: ${delegated.spec.task.slice(0, 500)}\nDirectory: ${ctx.cwd}\nPaths: ${JSON.stringify(delegated.spec.paths)}; Bash: ${delegated.spec.bash}; Model: ${delegated.spec.model ?? "parent"}\n` : ""}Permission required: ${event.toolName} (${decision.reason})\nRequested: ${request}${decision.bash ? `\nShell findings: ${decision.bash.findings.join(", ") || "literal command"}` : ""}`, options, { signal: ctx.signal });
|
|
269
|
-
if (revision !== authorizationRevision) return { block: true, reason: "Permission request changed while awaiting approval." };
|
|
270
|
-
if (choice?.includes("in this directory") && decision.bashFingerprint !== await bashApprovalFingerprint(command, ctx.cwd, root))
|
|
271
|
-
return { block: true, reason: "Command environment/configuration changed while awaiting approval." };
|
|
272
|
-
if (revision !== authorizationRevision || ctx.signal?.aborted || denials.stopped) return { block: true, reason: "Permission request changed, was cancelled, or the run stopped while awaiting approval." };
|
|
273
|
-
const approvalCount = sessionApprovals.length;
|
|
274
|
-
if (!choice || !options.includes(choice)) return { block: true, reason: "Blocked by user" };
|
|
275
|
-
if (choice === allowAll) sessionApprovals.push({ tool: event.toolName, scope: "tool" });
|
|
276
|
-
else if (choice === "Allow this exact Bash command in this directory for this session" && decision.bashFingerprint && decision.canonicalCwd)
|
|
277
|
-
sessionApprovals.push({ tool: "bash", scope: "command", command, cwd: decision.canonicalCwd, fingerprint: decision.bashFingerprint });
|
|
278
|
-
else if (choice === "Allow Git status queries in this directory for this session" && decision.bashFingerprint && decision.canonicalCwd)
|
|
279
|
-
sessionApprovals.push({ tool: "bash", scope: "subcommand", family: "git-status", cwd: decision.canonicalCwd, fingerprint: decision.bashFingerprint });
|
|
280
|
-
else if (pathPolicy && choice === "Allow this file for this session") sessionApprovals.push({ tool: event.toolName, scope: "file", path: pathPolicy.path });
|
|
281
|
-
else if (pathPolicy && choice === "Allow this directory for this session") sessionApprovals.push({ tool: event.toolName, scope: "directory", path: pathPolicy.directory });
|
|
282
|
-
else if (pathPolicy && choice === "Allow project files for this tool for this session") sessionApprovals.push({ tool: event.toolName, scope: "project" });
|
|
283
|
-
else if (choice !== allowOnce) return { block: true, reason: "Blocked by user" };
|
|
284
|
-
if (sessionApprovals.length > approvalCount) {
|
|
285
|
-
const approval = sessionApprovals[sessionApprovals.length - 1]!;
|
|
286
|
-
approval.id = String(nextApprovalId++);
|
|
287
|
-
if (approvalRequestId) approvalRequests.set(approval, approvalRequestId);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function recordDenial(tool: string, input: unknown, reason: string, ctx: ExtensionContext) {
|
|
292
|
-
const state = denials.deny(tool, input);
|
|
293
|
-
if (state.stopped) {
|
|
294
|
-
invalidateTasks();
|
|
295
|
-
pi.appendEntry("piexis-permission-stop", { total: state.total, repeated: state.repeated, mode });
|
|
296
|
-
if (ctx.hasUI) ctx.ui.notify("Permission denial limit reached; the agent run was stopped.", "warning");
|
|
297
|
-
else process.stderr.write(`Piexis permission_blocked: denial limit reached (${state.total} total, ${state.repeated} repeated).\n`);
|
|
298
|
-
ctx.abort();
|
|
299
|
-
return "Permission denial limit reached. This run is stopped; wait for a new user request. Do not switch tools or modes to retry the denied effect.";
|
|
300
|
-
}
|
|
301
|
-
return `${reason} Choose a genuinely narrower action, ask for clarification, or wait for explicit authorization. Do not use another tool to achieve the same denied effect. (${state.total}/${denials.limits.total} denials)`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// These task handlers perform authorization with their complete task/patch
|
|
305
|
-
// metadata through the service below; do not charge/review them twice.
|
|
306
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
307
|
-
if (mode === "bypassPermissions" || event.toolName === "AskQuestion" || MANAGED_TASK_TOOLS.has(event.toolName) && !["plan", "dontAsk"].includes(mode)) return;
|
|
308
|
-
const stop = () => {
|
|
309
|
-
ctx.abort();
|
|
310
|
-
return { block: true, reason: "Permission denial limit reached. This run is stopped; wait for a new user request. Do not switch tools or modes to retry the denied effect." };
|
|
311
|
-
};
|
|
312
|
-
if (denials.stopped) return stop();
|
|
313
|
-
if (ctx.signal?.aborted) return { block: true, reason: "Permission request cancelled." };
|
|
314
|
-
const revision = denials.revision;
|
|
315
|
-
let outcome;
|
|
316
|
-
try { outcome = await checkTool(event, ctx); }
|
|
317
|
-
catch { outcome = { block: true, reason: "Permission evaluation failed closed." }; }
|
|
318
|
-
if (revision !== denials.revision || ctx.signal?.aborted) return { block: true, reason: "Permission request changed or was cancelled during evaluation." };
|
|
319
|
-
if (denials.stopped) return stop();
|
|
320
|
-
if (!outcome?.block) return outcome;
|
|
321
|
-
return { block: true, reason: recordDenial(event.toolName, event.input, outcome.reason, ctx) };
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
pi.events.on?.(TASK_PERMISSION_SERVICE, (message) => {
|
|
325
|
-
const ctx = permissionContext;
|
|
326
|
-
if (!ctx) return;
|
|
327
|
-
const revision = authorizationRevision;
|
|
328
|
-
const current = () => permissionContext === ctx && authorizationRevision === revision && !denials.stopped;
|
|
329
|
-
const requireCurrent = () => { if (!current()) throw new Error("Task authorization is no longer current."); };
|
|
330
|
-
const service: TaskPermissionService = {
|
|
331
|
-
declined(tool, id, reason = "Task operation was not approved.") {
|
|
332
|
-
requireCurrent();
|
|
333
|
-
return recordDenial(tool, { id }, reason, ctx);
|
|
334
|
-
},
|
|
335
|
-
async control(tool, id) {
|
|
336
|
-
requireCurrent();
|
|
337
|
-
const decision = await evaluatePermission({ mode, tool, input: { id }, cwd: ctx.cwd, workspaceRoot });
|
|
338
|
-
requireCurrent();
|
|
339
|
-
if (decision.decision !== "allow") throw new Error(`Task operation denied (${decision.reason}).`);
|
|
340
|
-
},
|
|
341
|
-
async open(proposed, userCommand, signal) {
|
|
342
|
-
requireCurrent();
|
|
343
|
-
if (typeof proposed.task !== "string" || !proposed.task.trim() || proposed.task.length > 20000)
|
|
344
|
-
throw new Error("Task description must contain 1–20,000 characters.");
|
|
345
|
-
const spec: TaskSpec = { task: proposed.task, model: proposed.model, ...validateTaskScope(proposed.paths, proposed.bash) };
|
|
346
|
-
let intent = userIntentFromBranch(ctx.sessionManager.getBranch());
|
|
347
|
-
if (userCommand) {
|
|
348
|
-
const id = `task-command-${randomUUID()}`;
|
|
349
|
-
const text = redactIntentSecrets(spec.task);
|
|
350
|
-
const messages = [...intent.messages, { id, source: ctx.mode === "rpc" ? "rpc" as const : "interactive" as const, text }];
|
|
351
|
-
intent = { currentRequestId: id, messages, issues: intent.issues,
|
|
352
|
-
complete: (!intent.messages.length || intent.complete) && text === spec.task && text.length <= 6000 &&
|
|
353
|
-
messages.length <= 16 && messages.reduce((sum, item) => sum + item.text.length, 0) <= 24000 };
|
|
354
|
-
}
|
|
355
|
-
const input = { ...spec };
|
|
356
|
-
const policy = await evaluatePermission({ mode, tool: "background_task", input, cwd: ctx.cwd, workspaceRoot });
|
|
357
|
-
if (policy.decision === "deny") throw new Error(`Delegation denied (${policy.reason}).`);
|
|
358
|
-
const outcome = userCommand && mode !== "auto" ? undefined : await checkTool(
|
|
359
|
-
{ type: "tool_call", toolCallId: randomUUID(), toolName: "background_task", input } as ToolCallEvent,
|
|
360
|
-
{ ...ctx, signal }, { spec, intent });
|
|
361
|
-
requireCurrent();
|
|
362
|
-
if (signal?.aborted) throw new Error("Task launch cancelled.");
|
|
363
|
-
if (outcome?.block) throw new Error(recordDenial("background_task", input, outcome.reason, ctx));
|
|
364
|
-
const controller = new AbortController();
|
|
365
|
-
const workerDenials = new DenialTracker(denials.limits);
|
|
366
|
-
const abort = () => controller.abort();
|
|
367
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
368
|
-
taskControllers.add(controller);
|
|
369
|
-
const valid = () => current() && !controller.signal.aborted && !signal?.aborted;
|
|
370
|
-
return {
|
|
371
|
-
signal: controller.signal,
|
|
372
|
-
limits: { ...workerDenials.limits },
|
|
373
|
-
valid,
|
|
374
|
-
close() { signal?.removeEventListener("abort", abort); taskControllers.delete(controller); controller.abort(); },
|
|
375
|
-
async authorize(tool, action, worktree, actionSignal) {
|
|
376
|
-
if (!valid()) return { allow: false, reason: "Task authorization was cancelled or invalidated." };
|
|
377
|
-
const signals = [controller.signal, ...(actionSignal ? [actionSignal] : [])];
|
|
378
|
-
const combined = AbortSignal.any(signals);
|
|
379
|
-
try {
|
|
380
|
-
const result = await checkTool({ type: "tool_call", toolCallId: randomUUID(), toolName: tool, input: action } as ToolCallEvent,
|
|
381
|
-
{ ...ctx, cwd: worktree, signal: combined }, { spec, intent, worktree });
|
|
382
|
-
if (!valid() || combined.aborted) return { allow: false, reason: "Task authorization changed during review." };
|
|
383
|
-
if (!result?.block) return { allow: true };
|
|
384
|
-
const state = workerDenials.deny(tool, action);
|
|
385
|
-
if (state.stopped) controller.abort();
|
|
386
|
-
return { allow: false, reason: state.stopped ? "Worker denial limit reached; task stopped." : result.reason };
|
|
387
|
-
} catch {
|
|
388
|
-
controller.abort();
|
|
389
|
-
return { allow: false, reason: "Worker permission evaluation failed closed." };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
(message as { provide(service: TaskPermissionService): void }).provide(service);
|
|
396
|
-
});
|
|
397
|
-
}
|
package/lib/bash-approvals.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { parseBash } from "./bash-policy.ts";
|
|
7
|
-
import { resolveWorkspaceRoot } from "./path-policy.ts";
|
|
8
|
-
|
|
9
|
-
// Only configurations whose Git status effects we understand are reusable.
|
|
10
|
-
const CONFIG_KEYS: Record<string, Set<string>> = {
|
|
11
|
-
core: new Set(["repositoryformatversion", "filemode", "bare", "logallrefupdates", "ignorecase", "precomposeunicode"]),
|
|
12
|
-
user: new Set(["name", "email"]),
|
|
13
|
-
remote: new Set(["url", "fetch"]),
|
|
14
|
-
branch: new Set(["remote", "merge", "rebase"])
|
|
15
|
-
};
|
|
16
|
-
function inertGitConfig(text: string): boolean {
|
|
17
|
-
let section = "";
|
|
18
|
-
for (const raw of text.split("\n")) {
|
|
19
|
-
const line = raw.trim();
|
|
20
|
-
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
21
|
-
if (line.includes("\\")) return false;
|
|
22
|
-
const header = line.match(/^\[([A-Za-z]+)(?:\s+"[^"\n]*")?\]$/);
|
|
23
|
-
if (header) { section = header[1]!.toLowerCase(); if (!CONFIG_KEYS[section]) return false; continue; }
|
|
24
|
-
const field = line.match(/^([A-Za-z][A-Za-z0-9]*)\s*=/);
|
|
25
|
-
if (!field || !CONFIG_KEYS[section]?.has(field[1]!.toLowerCase())) return false;
|
|
26
|
-
}
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Fingerprint for the deliberately tiny reusable execution grammar (Git status).
|
|
31
|
-
* Unknown configs, linked worktrees, shell startup overrides, scripts, or missing
|
|
32
|
-
* dependencies decline reuse. No config contents are sent to a model or persisted.
|
|
33
|
-
* Assumes a trusted shell/Git installation; this is not process containment.
|
|
34
|
-
*/
|
|
35
|
-
export async function bashApprovalFingerprint(
|
|
36
|
-
command: string,
|
|
37
|
-
cwd: string,
|
|
38
|
-
workspaceRoot: string,
|
|
39
|
-
env: NodeJS.ProcessEnv = process.env
|
|
40
|
-
): Promise<string | undefined> {
|
|
41
|
-
if (!parseBash(command).grantFamily || process.platform === "win32") return undefined;
|
|
42
|
-
if (env.BASH_ENV || env.ENV || env.SHELLOPTS || env.BASHOPTS || env.PS4 || env.GIT_DIR || env.GIT_WORK_TREE || env.GIT_COMMON_DIR || env.GIT_CONFIG ||
|
|
43
|
-
Object.keys(env).some((key) => key.startsWith("BASH_FUNC_") || key.startsWith("GIT_CONFIG_") &&
|
|
44
|
-
!["GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM"].includes(key))) return undefined;
|
|
45
|
-
try {
|
|
46
|
-
const root = await realpath(workspaceRoot);
|
|
47
|
-
const directory = await realpath(cwd);
|
|
48
|
-
if (await resolveWorkspaceRoot(directory) !== root) return undefined;
|
|
49
|
-
if (!(await lstat(join(root, ".git"))).isDirectory()) return undefined;
|
|
50
|
-
try { await lstat(join(root, ".git", "commondir")); return undefined; }
|
|
51
|
-
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined; }
|
|
52
|
-
const digest = createHash("sha256").update("piexis-shell-grant-v1\0").update(root).update(directory);
|
|
53
|
-
// Bind executable lookup and relevant shell/Git environment without disclosure.
|
|
54
|
-
digest.update(JSON.stringify(Object.entries(env).filter(([key]) =>
|
|
55
|
-
/^(PATH|HOME|SHELL|SHELLOPTS|BASHOPTS|CDPATH|GIT_.*|XDG_CONFIG_HOME|PAGER|PI_PERMISSION_CLASSIFIER)$/.test(key)).sort()));
|
|
56
|
-
let executableFound = false;
|
|
57
|
-
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
58
|
-
if (!dir.startsWith("/")) return undefined;
|
|
59
|
-
const candidate = join(dir, "git");
|
|
60
|
-
try {
|
|
61
|
-
const path = await realpath(candidate);
|
|
62
|
-
const stat = await lstat(path, { bigint: true });
|
|
63
|
-
if (!stat.isFile() || !(stat.mode & 0o111n)) return undefined;
|
|
64
|
-
const file = await open(path, "r");
|
|
65
|
-
try {
|
|
66
|
-
const magic = Buffer.alloc(4);
|
|
67
|
-
await file.read(magic, 0, 4, 0);
|
|
68
|
-
// Reject shell-script wrappers named git. ELF and common Mach-O formats.
|
|
69
|
-
if (!["7f454c46", "cffaedfe", "feedfacf", "cafebabe"].includes(magic.toString("hex"))) return undefined;
|
|
70
|
-
} finally { await file.close(); }
|
|
71
|
-
digest.update(`${path}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`);
|
|
72
|
-
executableFound = true;
|
|
73
|
-
break;
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (!executableFound) return undefined;
|
|
79
|
-
const home = env.HOME ?? homedir();
|
|
80
|
-
const files: [string, boolean][] = [
|
|
81
|
-
[join(root, ".git", "config"), true], [join(root, ".git", "config.worktree"), true],
|
|
82
|
-
[join(root, CONFIG_DIR_NAME, "settings.json"), false],
|
|
83
|
-
[join(root, CONFIG_DIR_NAME, "permission-modes.json"), false],
|
|
84
|
-
[join(directory, CONFIG_DIR_NAME, "settings.json"), false],
|
|
85
|
-
[join(directory, CONFIG_DIR_NAME, "permission-modes.json"), false]
|
|
86
|
-
];
|
|
87
|
-
if (!["1", "true", "yes", "on"].includes(env.GIT_CONFIG_NOSYSTEM?.toLowerCase() ?? "")) files.push([env.GIT_CONFIG_SYSTEM ?? "/etc/gitconfig", true]);
|
|
88
|
-
if (env.GIT_CONFIG_GLOBAL) files.push([env.GIT_CONFIG_GLOBAL, true]);
|
|
89
|
-
else files.push([join(home, ".gitconfig"), true], [join(env.XDG_CONFIG_HOME ?? join(home, ".config"), "git", "config"), true]);
|
|
90
|
-
for (const [path, git] of files) {
|
|
91
|
-
if (!isAbsolute(path)) return undefined;
|
|
92
|
-
digest.update(path);
|
|
93
|
-
try {
|
|
94
|
-
const stat = await lstat(path);
|
|
95
|
-
if (!stat.isFile() || stat.size > 64000) return undefined;
|
|
96
|
-
const file = await open(path, "r");
|
|
97
|
-
let text: string;
|
|
98
|
-
try {
|
|
99
|
-
const buffer = Buffer.alloc(64001);
|
|
100
|
-
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
101
|
-
if (bytesRead > 64000) return undefined;
|
|
102
|
-
text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
103
|
-
} finally { await file.close(); }
|
|
104
|
-
if (git && !inertGitConfig(text)) return undefined;
|
|
105
|
-
digest.update(text);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined;
|
|
108
|
-
digest.update("<missing>");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return digest.digest("hex");
|
|
112
|
-
} catch { return undefined; }
|
|
113
|
-
}
|