sortie-dogs 0.1.1

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.
@@ -0,0 +1,173 @@
1
+ import { BUILT_IN_MODEL_CATALOG, DEDICATED_SOL_ROUTING, RECOMMENDED_LUNA_ROUTING, isDedicatedSolRole, parseModelRoutingConfig, } from "./model-routing.js";
2
+ export const DEFAULT_PLUGIN_OPTIONS = {
3
+ operationManifestPath: "operation-manifest.json",
4
+ handoffPaths: ["handoff.json"],
5
+ modelRouting: RECOMMENDED_LUNA_ROUTING,
6
+ modelCatalog: BUILT_IN_MODEL_CATALOG,
7
+ };
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function nonEmptyString(value) {
12
+ return typeof value === "string" && value.length > 0;
13
+ }
14
+ function parseCatalogModels(value) {
15
+ if (!Array.isArray(value))
16
+ return undefined;
17
+ const models = [];
18
+ for (const candidate of value) {
19
+ if (!isRecord(candidate) || Object.keys(candidate).some((key) => key !== "model" && key !== "variants")) {
20
+ return undefined;
21
+ }
22
+ if (!nonEmptyString(candidate.model))
23
+ return undefined;
24
+ if (candidate.variants !== undefined &&
25
+ (!Array.isArray(candidate.variants) || candidate.variants.some((variant) => !nonEmptyString(variant))))
26
+ return undefined;
27
+ models.push(candidate.variants === undefined
28
+ ? { model: candidate.model }
29
+ : { model: candidate.model, variants: candidate.variants });
30
+ }
31
+ return models;
32
+ }
33
+ function parseModelCatalog(value) {
34
+ if (!isRecord(value) || Object.keys(value).some((key) => key !== "project" && key !== "global")) {
35
+ return undefined;
36
+ }
37
+ const project = value.project === undefined ? undefined : parseCatalogModels(value.project);
38
+ const global = value.global === undefined ? undefined : parseCatalogModels(value.global);
39
+ if (value.project !== undefined && project === undefined)
40
+ return undefined;
41
+ if (value.global !== undefined && global === undefined)
42
+ return undefined;
43
+ return {
44
+ ...(project === undefined ? {} : { project }),
45
+ ...(global === undefined ? {} : { global }),
46
+ };
47
+ }
48
+ function mergeCatalogModels(builtIn, configured) {
49
+ const models = new Map();
50
+ for (const candidate of [...builtIn, ...configured]) {
51
+ if (!models.has(candidate.model)) {
52
+ models.set(candidate.model, candidate.variants === undefined
53
+ ? undefined
54
+ : new Set(candidate.variants));
55
+ continue;
56
+ }
57
+ if (candidate.variants === undefined)
58
+ continue;
59
+ const variants = models.get(candidate.model);
60
+ if (variants === undefined) {
61
+ models.set(candidate.model, new Set(candidate.variants));
62
+ }
63
+ else {
64
+ for (const variant of candidate.variants)
65
+ variants.add(variant);
66
+ }
67
+ }
68
+ return [...models].map(([model, variants]) => variants === undefined
69
+ ? { model }
70
+ : { model, variants: [...variants] });
71
+ }
72
+ function parseLayer(value) {
73
+ if (value === undefined)
74
+ return {};
75
+ if (!isRecord(value))
76
+ return undefined;
77
+ if (Object.keys(value).some((key) => key !== "operationManifestPath" && key !== "handoffPaths" && key !== "modelRouting" && key !== "modelCatalog")) {
78
+ return undefined;
79
+ }
80
+ const manifestPath = value.operationManifestPath;
81
+ const handoffPaths = value.handoffPaths;
82
+ const modelRouting = value.modelRouting === undefined
83
+ ? undefined
84
+ : parseModelRoutingConfig(value.modelRouting);
85
+ const modelCatalog = value.modelCatalog === undefined
86
+ ? undefined
87
+ : parseModelCatalog(value.modelCatalog);
88
+ if (manifestPath !== undefined && (typeof manifestPath !== "string" || manifestPath.length === 0)) {
89
+ return undefined;
90
+ }
91
+ if (handoffPaths !== undefined &&
92
+ (!Array.isArray(handoffPaths) || handoffPaths.some((path) => typeof path !== "string" || path.length === 0))) {
93
+ return undefined;
94
+ }
95
+ if (value.modelRouting !== undefined && modelRouting === undefined)
96
+ return undefined;
97
+ if (value.modelCatalog !== undefined && modelCatalog === undefined)
98
+ return undefined;
99
+ return {
100
+ operationManifestPath: manifestPath,
101
+ handoffPaths: handoffPaths,
102
+ modelRouting,
103
+ modelCatalog,
104
+ };
105
+ }
106
+ /** Merge defaults, optional project/env configuration, then the host override. */
107
+ export function resolvePluginConfiguration(...values) {
108
+ let operationManifestPath = DEFAULT_PLUGIN_OPTIONS.operationManifestPath;
109
+ let handoffPaths = DEFAULT_PLUGIN_OPTIONS.handoffPaths;
110
+ let modelRouting = DEFAULT_PLUGIN_OPTIONS.modelRouting;
111
+ let modelCatalog = DEFAULT_PLUGIN_OPTIONS.modelCatalog;
112
+ for (const value of values) {
113
+ const layer = parseLayer(value);
114
+ if (layer === undefined)
115
+ return { kind: "invalid" };
116
+ if (layer.operationManifestPath !== undefined)
117
+ operationManifestPath = layer.operationManifestPath;
118
+ if (layer.handoffPaths !== undefined)
119
+ handoffPaths = layer.handoffPaths;
120
+ if (layer.modelRouting !== undefined) {
121
+ modelRouting = { ...modelRouting, ...layer.modelRouting };
122
+ }
123
+ if (layer.modelCatalog !== undefined) {
124
+ modelCatalog = {
125
+ ...modelCatalog,
126
+ ...layer.modelCatalog,
127
+ ...(layer.modelCatalog.global === undefined ? {} : {
128
+ global: mergeCatalogModels(BUILT_IN_MODEL_CATALOG.global ?? [], layer.modelCatalog.global),
129
+ }),
130
+ };
131
+ }
132
+ }
133
+ const hasRouting = Object.keys(modelRouting).length > 0;
134
+ const hasCatalogEntries = (modelCatalog.project?.length ?? 0) + (modelCatalog.global?.length ?? 0) > 0;
135
+ if (hasRouting && !hasCatalogEntries)
136
+ return { kind: "invalid" };
137
+ return {
138
+ kind: "configured",
139
+ operationManifestPath,
140
+ handoffPaths,
141
+ modelRouting,
142
+ modelCatalog,
143
+ };
144
+ }
145
+ /** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
146
+ export function resolvePluginConfigurationSources(projectValue, environmentValue, hostValue) {
147
+ const configured = resolvePluginConfiguration(projectValue, environmentValue, hostValue);
148
+ if (configured.kind === "invalid")
149
+ return configured;
150
+ const projectLayer = parseLayer(projectValue);
151
+ const environmentLayer = parseLayer(environmentValue);
152
+ const hostLayer = parseLayer(hostValue);
153
+ if (projectLayer === undefined || environmentLayer === undefined || hostLayer === undefined) {
154
+ return { kind: "invalid" };
155
+ }
156
+ const globalModelRouting = Object.fromEntries(Object.entries({
157
+ ...RECOMMENDED_LUNA_ROUTING,
158
+ ...(environmentLayer.modelRouting ?? {}),
159
+ ...(hostLayer.modelRouting ?? {}),
160
+ }).filter(([role]) => !isDedicatedSolRole(role)));
161
+ const modelRouting = {
162
+ ...Object.fromEntries(Object.entries(configured.modelRouting)
163
+ .filter(([role]) => !isDedicatedSolRole(role))),
164
+ ...DEDICATED_SOL_ROUTING,
165
+ };
166
+ return {
167
+ ...configured,
168
+ modelRouting,
169
+ // Mk2A2 worker policy is authoritative over every configurable layer.
170
+ localModelRouting: { ...(projectLayer.modelRouting ?? {}), ...DEDICATED_SOL_ROUTING },
171
+ globalModelRouting,
172
+ };
173
+ }
@@ -0,0 +1,38 @@
1
+ export interface ToolExecuteBeforeInput {
2
+ tool: string;
3
+ sessionID: string;
4
+ callID: string;
5
+ }
6
+ export interface ToolExecuteBeforeOutput {
7
+ args: unknown;
8
+ }
9
+ export type WriteDenialReason = "manifest-unavailable" | "path-required" | "project-boundary" | "manifest-scope";
10
+ export declare class WriteDeniedError extends Error {
11
+ readonly reason: WriteDenialReason;
12
+ constructor(reason: WriteDenialReason, path: string, options?: ErrorOptions);
13
+ }
14
+ export interface ProjectPaths {
15
+ readonly root: string;
16
+ absolute(relativePath: string): string;
17
+ toRelativePath(path: string): Promise<string>;
18
+ }
19
+ export interface WriteGate {
20
+ check(input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput): Promise<void>;
21
+ checkPath(path: string): Promise<void>;
22
+ toRelativePath(path: string): Promise<string>;
23
+ }
24
+ interface Extraction {
25
+ applies: boolean;
26
+ ambiguous: boolean;
27
+ paths: string[];
28
+ }
29
+ export declare function safePath(path: string): string;
30
+ export declare function resolveProjectRoot(input: {
31
+ directory: string;
32
+ worktree?: string;
33
+ }): string;
34
+ /** Extract known write destinations; unknown shell executables fail closed as ambiguous. */
35
+ export declare function extractWritePaths(tool: string, args: unknown): Extraction;
36
+ export declare function createProjectPaths(rootCandidate: string): Promise<ProjectPaths>;
37
+ export declare function createWriteGate(project: ProjectPaths, value: unknown): Promise<WriteGate>;
38
+ export {};
@@ -0,0 +1,320 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { RelativePathError, normalizeRelativePath } from "../core/path.js";
4
+ import { validateOperationManifestSchema } from "../core/validate-schema.js";
5
+ export class WriteDeniedError extends Error {
6
+ reason;
7
+ constructor(reason, path, options) {
8
+ const messages = {
9
+ "manifest-unavailable": "operation manifest unavailable.",
10
+ "path-required": "write path must be explicit.",
11
+ "project-boundary": "project-root-relative path required.",
12
+ "manifest-scope": "operation manifest write scope.",
13
+ };
14
+ super(`Write denied for "${safePath(path)}": ${messages[reason]}`, options);
15
+ this.name = "WriteDeniedError";
16
+ this.reason = reason;
17
+ }
18
+ }
19
+ const DIRECT_PATH_KEYS = new Set([
20
+ "file", "filepath", "file_path", "path", "destination", "target",
21
+ ]);
22
+ const ALL_OPERAND_COMMANDS = new Set(["mkdir", "rm", "rmdir", "touch", "truncate", "unlink"]);
23
+ const LAST_OPERAND_COMMANDS = new Set(["cp", "install"]);
24
+ const READ_ONLY_COMMANDS = new Set([
25
+ "cat", "echo", "false", "get-childitem", "get-content", "grep", "head", "ls", "pwd",
26
+ "rg", "stat", "tail", "test-path", "true", "type", "wc",
27
+ ]);
28
+ const READ_ONLY_GIT_COMMANDS = new Set(["diff", "log", "ls-files", "rev-parse", "show", "status"]);
29
+ const POWERSHELL_WRITE_COMMANDS = new Set([
30
+ "add-content", "copy-item", "move-item", "new-item", "out-file", "remove-item",
31
+ "rename-item", "set-content",
32
+ ]);
33
+ const READ_ONLY_OPTIONS_WITH_VALUES = new Set(["-c", "--directory", "--exclude", "--include"]);
34
+ function isRecord(value) {
35
+ return value !== null && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+ export function safePath(path) {
38
+ return [...path].map((character) => {
39
+ const code = character.codePointAt(0);
40
+ return code <= 0x1f || (code >= 0x7f && code <= 0x9f) ? "?" : character;
41
+ }).join("").slice(0, 512);
42
+ }
43
+ export function resolveProjectRoot(input) {
44
+ return resolve(input.directory);
45
+ }
46
+ function directPaths(args) {
47
+ if (!isRecord(args))
48
+ return [];
49
+ const paths = [];
50
+ for (const [key, value] of Object.entries(args)) {
51
+ if (DIRECT_PATH_KEYS.has(key.toLowerCase()) && typeof value === "string")
52
+ paths.push(value);
53
+ if (/^(?:files|paths|destinations|targets)$/iu.test(key) && Array.isArray(value)) {
54
+ paths.push(...value.filter((item) => typeof item === "string"));
55
+ }
56
+ }
57
+ return paths;
58
+ }
59
+ function patchPaths(patch) {
60
+ const paths = [];
61
+ for (const line of patch.split(/\r?\n/u)) {
62
+ const envelope = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+?)\s*$/u.exec(line) ??
63
+ /^\*\*\* Move to:\s*(.+?)\s*$/u.exec(line);
64
+ if (envelope !== null)
65
+ paths.push(envelope[1]);
66
+ const unified = /^\+\+\+\s+(?:b\/)?(.+?)\s*$/u.exec(line);
67
+ if (unified !== null && unified[1] !== "/dev/null")
68
+ paths.push(unified[1]);
69
+ }
70
+ return paths;
71
+ }
72
+ function unquote(value) {
73
+ if ((value.startsWith('"') && value.endsWith('"')) ||
74
+ (value.startsWith("'") && value.endsWith("'")))
75
+ return value.slice(1, -1);
76
+ return value;
77
+ }
78
+ function words(command) {
79
+ return command.match(/"(?:\\.|[^"])*"|'[^']*'|[^\s]+/gu)?.map(unquote) ?? [];
80
+ }
81
+ function operands(tokens) {
82
+ const result = [];
83
+ for (let index = 1; index < tokens.length; index += 1) {
84
+ const token = tokens[index];
85
+ if (/^(?:\d*>>?|>\||[|&])(?:.*)?$/u.test(token))
86
+ break;
87
+ if (READ_ONLY_OPTIONS_WITH_VALUES.has(token))
88
+ index += 1;
89
+ else if (!token.startsWith("-"))
90
+ result.push(token.replace(/[|&;]+$/u, ""));
91
+ }
92
+ return result.filter(Boolean);
93
+ }
94
+ function unwrapEnvironmentCommand(tokens) {
95
+ const executable = tokens[0]?.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase();
96
+ if (executable !== "env")
97
+ return tokens;
98
+ let index = 1;
99
+ while (index < tokens.length) {
100
+ const token = tokens[index];
101
+ if (token === "-u" || token === "--unset") {
102
+ index += 2;
103
+ }
104
+ else if (token.startsWith("--unset=") || /^[A-Za-z_][A-Za-z0-9_]*=/u.test(token)) {
105
+ index += 1;
106
+ }
107
+ else {
108
+ break;
109
+ }
110
+ }
111
+ return tokens.slice(index);
112
+ }
113
+ function isRemoteOnlyGitHubCommand(tokens) {
114
+ const command = tokens[1]?.toLowerCase();
115
+ return command === "project" || (command === "api" && tokens[2]?.toLowerCase() === "graphql");
116
+ }
117
+ function isReadOnlyGitCommand(tokens) {
118
+ const command = tokens[1]?.toLowerCase() ?? "";
119
+ return READ_ONLY_GIT_COMMANDS.has(command) ||
120
+ (command === "branch" && tokens.length === 3 && tokens[2] === "--show-current");
121
+ }
122
+ function shellPaths(command) {
123
+ const paths = [];
124
+ let applies = false;
125
+ let ambiguous = false;
126
+ const redirection = /(?:^|[\s;&|])(?:\d*)(?:>>?|>\|)\s*("(?:\\.|[^"])*"|'[^']*'|[^\s;&|]+)/gu;
127
+ for (const match of command.matchAll(redirection)) {
128
+ applies = true;
129
+ paths.push(unquote(match[1]));
130
+ }
131
+ for (const segment of command.split(/(?:&&|\|\||(?<!>)\|(?!\|)|;|\r?\n)/u)) {
132
+ const tokens = unwrapEnvironmentCommand(words(segment.trim()));
133
+ if (tokens.length === 0)
134
+ continue;
135
+ const executable = tokens[0].replaceAll("\\", "/").split("/").at(-1).toLowerCase();
136
+ const commandOperands = operands(tokens);
137
+ if (ALL_OPERAND_COMMANDS.has(executable)) {
138
+ applies = true;
139
+ paths.push(...commandOperands);
140
+ }
141
+ else if (LAST_OPERAND_COMMANDS.has(executable)) {
142
+ applies = true;
143
+ const destination = commandOperands.at(-1);
144
+ if (destination === undefined)
145
+ ambiguous = true;
146
+ else
147
+ paths.push(destination);
148
+ }
149
+ else if (executable === "mv") {
150
+ applies = true;
151
+ if (commandOperands.length < 2)
152
+ ambiguous = true;
153
+ paths.push(...commandOperands);
154
+ }
155
+ else if (executable === "tee") {
156
+ applies = true;
157
+ if (commandOperands.length === 0)
158
+ ambiguous = true;
159
+ paths.push(...commandOperands);
160
+ }
161
+ else if (POWERSHELL_WRITE_COMMANDS.has(executable)) {
162
+ applies = true;
163
+ const named = [];
164
+ for (let index = 1; index < tokens.length - 1; index += 1) {
165
+ if (/^-(?:path|literalpath|destination)$/iu.test(tokens[index]))
166
+ named.push(tokens[index + 1]);
167
+ }
168
+ const selected = named.length > 0 ? named : commandOperands.slice(0, 1);
169
+ if (selected.length === 0)
170
+ ambiguous = true;
171
+ paths.push(...selected);
172
+ }
173
+ else if (/^(?:apply_?patch)$/iu.test(executable)) {
174
+ applies = true;
175
+ const selected = patchPaths(segment);
176
+ if (selected.length === 0)
177
+ ambiguous = true;
178
+ paths.push(...selected);
179
+ }
180
+ else if (executable === "git" && isReadOnlyGitCommand(tokens)) {
181
+ // Explicitly read-only git subcommands.
182
+ }
183
+ else if (/^gh(?:\.exe)?$/u.test(executable) && isRemoteOnlyGitHubCommand(tokens)) {
184
+ // GitHub Project commands mutate remote state, not project files. Redirections remain gated above.
185
+ }
186
+ else if (!READ_ONLY_COMMANDS.has(executable)) {
187
+ applies = true;
188
+ ambiguous = true;
189
+ }
190
+ }
191
+ return { applies, ambiguous, paths };
192
+ }
193
+ /** Extract known write destinations; unknown shell executables fail closed as ambiguous. */
194
+ export function extractWritePaths(tool, args) {
195
+ const name = tool.toLowerCase();
196
+ const paths = directPaths(args);
197
+ if (/^(?:write|edit)(?:$|[_-])/u.test(name))
198
+ return { applies: true, ambiguous: paths.length === 0, paths };
199
+ if (/patch/u.test(name)) {
200
+ const patch = isRecord(args) && typeof args.patchText === "string"
201
+ ? args.patchText
202
+ : isRecord(args) && typeof args.patch === "string" ? args.patch : undefined;
203
+ const extracted = patch === undefined ? [] : patchPaths(patch);
204
+ return { applies: true, ambiguous: extracted.length === 0, paths: [...paths, ...extracted] };
205
+ }
206
+ if (/^(?:bash|shell|powershell|pwsh)(?:$|[_-])/u.test(name)) {
207
+ const command = isRecord(args) && typeof args.command === "string" ? args.command : undefined;
208
+ if (command === undefined)
209
+ return { applies: paths.length > 0, ambiguous: paths.length === 0, paths };
210
+ const extracted = shellPaths(command);
211
+ return {
212
+ applies: extracted.applies || paths.length > 0,
213
+ ambiguous: extracted.ambiguous,
214
+ paths: [...paths, ...extracted.paths],
215
+ };
216
+ }
217
+ return { applies: false, ambiguous: false, paths: [] };
218
+ }
219
+ async function nearestExistingRealPath(path) {
220
+ let candidate = path;
221
+ while (true) {
222
+ try {
223
+ await lstat(candidate);
224
+ }
225
+ catch (error) {
226
+ if (!isRecord(error) || error.code !== "ENOENT")
227
+ throw error;
228
+ const parent = resolve(candidate, "..");
229
+ if (parent === candidate)
230
+ throw error;
231
+ candidate = parent;
232
+ continue;
233
+ }
234
+ return await realpath(candidate);
235
+ }
236
+ }
237
+ function isWithin(root, target) {
238
+ const path = relative(root, target);
239
+ return path === "" || (!path.startsWith("..") && !isAbsolute(path));
240
+ }
241
+ export async function createProjectPaths(rootCandidate) {
242
+ const root = resolve(rootCandidate);
243
+ const realRoot = await realpath(root);
244
+ return {
245
+ root,
246
+ absolute: (relativePath) => resolve(root, relativePath),
247
+ async toRelativePath(path) {
248
+ const relativePath = isAbsolute(path) ? relative(root, resolve(path)) : path;
249
+ const normalized = normalizeRelativePath(relativePath);
250
+ const absolute = resolve(root, normalized);
251
+ if (!isWithin(root, absolute) || !isWithin(realRoot, await nearestExistingRealPath(absolute))) {
252
+ throw new WriteDeniedError("project-boundary", normalized);
253
+ }
254
+ return normalized;
255
+ },
256
+ };
257
+ }
258
+ export async function createWriteGate(project, value) {
259
+ const validated = validateOperationManifestSchema(value);
260
+ if (!validated.ok)
261
+ throw new WriteDeniedError("manifest-unavailable", "<unknown>");
262
+ const manifest = validated.value;
263
+ const writable = new Set(manifest.write.map((path) => normalizeRelativePath(path)));
264
+ const writableDirectories = [];
265
+ for (const path of writable) {
266
+ try {
267
+ const metadata = await lstat(project.absolute(path));
268
+ if (metadata.isDirectory()) {
269
+ writableDirectories.push({ path, realPath: await realpath(project.absolute(path)) });
270
+ }
271
+ }
272
+ catch {
273
+ // Missing, inaccessible, and concurrently changed paths remain exact-only scopes.
274
+ }
275
+ }
276
+ const isWritable = async (normalized) => {
277
+ if (writable.has(normalized))
278
+ return true;
279
+ const scopes = writableDirectories.filter((scope) => normalized.startsWith(`${scope.path}/`));
280
+ if (scopes.length === 0)
281
+ return false;
282
+ try {
283
+ const realTarget = await nearestExistingRealPath(project.absolute(normalized));
284
+ return scopes.some((scope) => isWithin(scope.realPath, realTarget));
285
+ }
286
+ catch {
287
+ return false;
288
+ }
289
+ };
290
+ const checkPath = async (path) => {
291
+ let normalized;
292
+ try {
293
+ normalized = await project.toRelativePath(path);
294
+ }
295
+ catch (error) {
296
+ if (error instanceof WriteDeniedError)
297
+ throw error;
298
+ if (error instanceof RelativePathError) {
299
+ throw new WriteDeniedError("project-boundary", path, { cause: error });
300
+ }
301
+ throw error;
302
+ }
303
+ if (!await isWritable(normalized))
304
+ throw new WriteDeniedError("manifest-scope", normalized);
305
+ };
306
+ return {
307
+ checkPath,
308
+ toRelativePath: project.toRelativePath,
309
+ async check(_input, output) {
310
+ const extracted = extractWritePaths(_input.tool, output.args);
311
+ if (!extracted.applies)
312
+ return;
313
+ if (extracted.ambiguous || extracted.paths.length === 0) {
314
+ throw new WriteDeniedError("path-required", "<unknown>");
315
+ }
316
+ for (const path of extracted.paths)
317
+ await checkPath(path);
318
+ },
319
+ };
320
+ }
@@ -0,0 +1,36 @@
1
+ import { type SortieDogsPluginOptions } from "./config.js";
2
+ import { type ToolExecuteBeforeInput, type ToolExecuteBeforeOutput } from "./gate.js";
3
+ import { type OpenCodeChatMessageHook } from "./model-routing-hook.js";
4
+ export interface OpenCodePluginInput {
5
+ directory: string;
6
+ worktree?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface OpenCodeEvent {
10
+ type: string;
11
+ properties?: Record<string, unknown>;
12
+ }
13
+ export interface OpenCodeHooks {
14
+ event?: (input: {
15
+ event: OpenCodeEvent;
16
+ }) => Promise<void>;
17
+ "permission.ask"?: (input: {
18
+ permission: string;
19
+ patterns: string[];
20
+ sessionID?: string;
21
+ }, output: {
22
+ status: "ask" | "deny" | "allow";
23
+ }) => Promise<void>;
24
+ "tool.execute.before"?: (input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput) => Promise<void>;
25
+ "chat.message"?: OpenCodeChatMessageHook;
26
+ }
27
+ export type OpenCodePlugin = (input: OpenCodePluginInput, options?: SortieDogsPluginOptions | Record<string, unknown>) => Promise<OpenCodeHooks>;
28
+ export type HandoffDenialReason = "configuration-unavailable" | "path-invalid" | "input-unavailable" | "schema-invalid" | "contract-invalid";
29
+ export declare class HandoffDeniedError extends Error {
30
+ readonly reason: HandoffDenialReason;
31
+ constructor(reason: HandoffDenialReason, path: string, options?: ErrorOptions);
32
+ }
33
+ /** Named OpenCode plugin export. Importing the package has no side effects; invoking it installs active gates. */
34
+ export declare const SortieDogsPlugin: OpenCodePlugin;
35
+ export type { SortieDogsPluginOptions } from "./config.js";
36
+ export { InvalidModelTargetError, ModelRoutingDeniedError } from "./model-routing-hook.js";