praxis-agent 0.40.0 → 0.42.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/README.md +5 -2
- package/dist/cli/controls.js +2 -0
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +6 -0
- package/dist/cli/workspace-trust-prompt.d.ts +10 -0
- package/dist/cli/workspace-trust-prompt.js +53 -0
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/cli-runtime.js +168 -27
- package/dist/core/resources.d.ts +5 -0
- package/dist/plugins/claude-plugin-mcpb.d.ts +2 -0
- package/dist/plugins/claude-plugin-mcpb.js +11 -2
- package/dist/plugins/claude-plugin-runtime.d.ts +2 -0
- package/dist/plugins/claude-plugin-runtime.js +99 -9
- package/dist/providers/deadline-provider.d.ts +16 -0
- package/dist/providers/deadline-provider.js +226 -0
- package/dist/providers/environment.d.ts +1 -0
- package/dist/providers/environment.js +12 -0
- package/dist/providers/fallback-provider.js +35 -2
- package/dist/security/workspace-trust.d.ts +30 -0
- package/dist/security/workspace-trust.js +278 -0
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
1
3
|
import { cp, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
4
|
import { execFile } from 'node:child_process';
|
|
3
5
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
@@ -5,8 +7,34 @@ import { promisify } from 'node:util';
|
|
|
5
7
|
import { parse as parseYaml } from 'yaml';
|
|
6
8
|
import { countTokens } from '@anthropic-ai/tokenizer';
|
|
7
9
|
import { claudePluginDataPath, materializeClaudePluginSource, readClaudePluginOptions, readClaudePluginMcpServerOptions, readClaudeSkillsDirectoryPlugins, replaceClaudePluginDirectory, readClaudeInstalledPlugins, validateClaudePluginUserConfig, } from './claude-plugin-marketplace.js';
|
|
8
|
-
import { loadClaudePluginMcpb } from './claude-plugin-mcpb.js';
|
|
10
|
+
import { CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES, loadClaudePluginMcpb, } from './claude-plugin-mcpb.js';
|
|
9
11
|
const execFileAsync = promisify(execFile);
|
|
12
|
+
async function boundedFileSha256(path) {
|
|
13
|
+
const metadata = await stat(path);
|
|
14
|
+
if (!metadata.isFile()) {
|
|
15
|
+
throw new Error(`MCPB archive must be a regular file: ${path}`);
|
|
16
|
+
}
|
|
17
|
+
if (metadata.size > CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES) {
|
|
18
|
+
throw new Error(`MCPB archive exceeds ${CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES} bytes`);
|
|
19
|
+
}
|
|
20
|
+
const source = createReadStream(path);
|
|
21
|
+
const hash = createHash('sha256');
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
try {
|
|
24
|
+
for await (const chunk of source) {
|
|
25
|
+
const value = chunk;
|
|
26
|
+
bytes += value.byteLength;
|
|
27
|
+
if (bytes > CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES) {
|
|
28
|
+
throw new Error(`MCPB archive exceeds ${CLAUDE_PLUGIN_MCPB_ARCHIVE_BYTES} bytes`);
|
|
29
|
+
}
|
|
30
|
+
hash.update(value);
|
|
31
|
+
}
|
|
32
|
+
return hash.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
source.destroy();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
10
38
|
const PLUGIN_MANIFEST = join('.claude-plugin', 'plugin.json');
|
|
11
39
|
const LEGACY_MANIFEST = 'plugin.json';
|
|
12
40
|
const MAX_PLUGIN_FILES = 2_000;
|
|
@@ -139,6 +167,16 @@ function safePluginPath(pluginPath, candidate) {
|
|
|
139
167
|
}
|
|
140
168
|
return resolved;
|
|
141
169
|
}
|
|
170
|
+
async function canonicalPluginPath(pluginPath, candidate) {
|
|
171
|
+
const [root, resolved] = await Promise.all([
|
|
172
|
+
realpath(pluginPath),
|
|
173
|
+
realpath(candidate),
|
|
174
|
+
]);
|
|
175
|
+
if (resolved !== root && !resolved.startsWith(`${root}/`)) {
|
|
176
|
+
throw new Error(`Plugin path escapes plugin root: ${candidate}`);
|
|
177
|
+
}
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
142
180
|
async function assertPluginPath(pluginPath, candidate) {
|
|
143
181
|
let resolved;
|
|
144
182
|
try {
|
|
@@ -462,7 +500,7 @@ async function loadLspServers(root, manifest, source, configRoot, environment, u
|
|
|
462
500
|
}
|
|
463
501
|
return [...definitions.values()];
|
|
464
502
|
}
|
|
465
|
-
async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = false, resourceScope, configRoot, environment = process.env, configId, readOnlyHooks = false, dataPlane) {
|
|
503
|
+
async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = false, resourceScope, configRoot, environment = process.env, configId, readOnlyHooks = false, readOnlyExecutables = false, allowWorkspaceMcpb = true, dataPlane) {
|
|
466
504
|
const canonical = await realpath(pluginPath);
|
|
467
505
|
if (!(await isDirectory(canonical)))
|
|
468
506
|
throw new Error(`Plugin path is not a directory: ${pluginPath}`);
|
|
@@ -480,7 +518,8 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
480
518
|
: [])
|
|
481
519
|
.filter((value) => value.length > 0);
|
|
482
520
|
const pluginData = claudePluginDataPath(configRoot ?? canonical, source);
|
|
483
|
-
|
|
521
|
+
const readOnlyComponents = readOnlyHooks || readOnlyExecutables;
|
|
522
|
+
if (configRoot !== undefined && !readOnlyComponents) {
|
|
484
523
|
await mkdir(pluginData, { recursive: true });
|
|
485
524
|
}
|
|
486
525
|
const pluginEnvironment = pluginOptionEnvironment(canonical, pluginData, userConfig);
|
|
@@ -491,7 +530,7 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
491
530
|
environment: pluginEnvironment,
|
|
492
531
|
...(sensitiveValues.length === 0 ? {} : { sensitiveValues }),
|
|
493
532
|
};
|
|
494
|
-
const scope = resourceScope ?? scopeForPath(canonical, cwd);
|
|
533
|
+
const scope = resourceScope ?? scopeForPath(canonical, await realpath(resolve(cwd)));
|
|
495
534
|
const commandDefinitions = isRecord(manifest.commands)
|
|
496
535
|
? manifest.commands
|
|
497
536
|
: undefined;
|
|
@@ -506,7 +545,7 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
506
545
|
? [join(canonical, 'skills'), canonical]
|
|
507
546
|
: pathList(manifest.skills, 'skills').map((path) => safePluginPath(canonical, path));
|
|
508
547
|
const agentsRoots = pathList(manifest.agents, 'agents').map((path) => safePluginPath(canonical, path));
|
|
509
|
-
const [commands, skills, agents, lsp] =
|
|
548
|
+
const [commands, skills, agents, lsp] = readOnlyComponents
|
|
510
549
|
? [[], [], [], []]
|
|
511
550
|
: await (async () => {
|
|
512
551
|
const [commandFiles, skillFiles, agentFiles, loadedLsp] = await Promise.all([
|
|
@@ -524,7 +563,7 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
524
563
|
]);
|
|
525
564
|
return [...loadedText, loadedLsp];
|
|
526
565
|
})();
|
|
527
|
-
if (commandDefinitions && !
|
|
566
|
+
if (commandDefinitions && !readOnlyComponents) {
|
|
528
567
|
for (const [commandName, definition] of Object.entries(commandDefinitions)) {
|
|
529
568
|
if (definition.content !== undefined) {
|
|
530
569
|
commands.push({
|
|
@@ -649,17 +688,61 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
649
688
|
if (typeof spec === 'string') {
|
|
650
689
|
const urlReference = isUrlReference(spec);
|
|
651
690
|
if ((urlReference &&
|
|
652
|
-
(!/^https?:\/\//u.test(spec) ||
|
|
691
|
+
(!/^https?:\/\//u.test(spec) ||
|
|
692
|
+
!isMcpbReference(spec) ||
|
|
693
|
+
(scope !== 'user' && !/^https:\/\//u.test(spec)))) ||
|
|
653
694
|
(!urlReference && isMcpbReference(spec) && !spec.startsWith('./'))) {
|
|
654
695
|
mcpErrors.push(`Invalid plugin MCPB reference at index ${index}: expected ./file.mcpb, ./file.dxt, or an exact-suffix HTTP(S) URL`);
|
|
655
696
|
continue;
|
|
656
697
|
}
|
|
657
698
|
if (isMcpbReference(spec)) {
|
|
699
|
+
let mcpbPath;
|
|
700
|
+
let sourceFingerprint;
|
|
701
|
+
try {
|
|
702
|
+
mcpbPath = urlReference
|
|
703
|
+
? join(canonical, '.claude-plugin', `plugin-mcpb-${index}.json`)
|
|
704
|
+
: await canonicalPluginPath(canonical, safePluginPath(canonical, spec));
|
|
705
|
+
sourceFingerprint = createHash('sha256')
|
|
706
|
+
.update(urlReference ? spec : await boundedFileSha256(mcpbPath))
|
|
707
|
+
.digest('hex');
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
mcpErrors.push(`Invalid plugin MCPB reference at index ${index}: ${error instanceof Error ? error.message : String(error)}`);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const pluginExecutableSource = {
|
|
714
|
+
kind: 'mcpb',
|
|
715
|
+
source: spec,
|
|
716
|
+
fingerprint: sourceFingerprint,
|
|
717
|
+
};
|
|
718
|
+
if (readOnlyExecutables) {
|
|
719
|
+
mcp.push({
|
|
720
|
+
path: mcpbPath,
|
|
721
|
+
scope,
|
|
722
|
+
plugin: true,
|
|
723
|
+
value: {
|
|
724
|
+
mcpServers: {
|
|
725
|
+
[`plugin:${manifest.name}:mcpb-${index}`]: {
|
|
726
|
+
type: 'mcpb-reference',
|
|
727
|
+
source: spec,
|
|
728
|
+
fingerprint: sourceFingerprint,
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
environment: pluginEnvironment,
|
|
733
|
+
pluginExecutableSource,
|
|
734
|
+
sensitiveValues,
|
|
735
|
+
});
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (scope !== 'user' && !allowWorkspaceMcpb)
|
|
739
|
+
continue;
|
|
658
740
|
try {
|
|
659
741
|
const loaded = await loadClaudePluginMcpb({
|
|
660
742
|
pluginRoot: canonical,
|
|
661
743
|
pluginData,
|
|
662
744
|
source: spec,
|
|
745
|
+
requireHttps: scope !== 'user',
|
|
663
746
|
...(configRoot === undefined ? {} : { configRoot }),
|
|
664
747
|
...(dataPlane === undefined ? {} : { dataPlane }),
|
|
665
748
|
environment,
|
|
@@ -667,8 +750,14 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
667
750
|
? readClaudePluginMcpServerOptions(configRoot, cwd, configId, bundleManifest.name, bundleManifest.user_config, dataPlane)
|
|
668
751
|
: {},
|
|
669
752
|
});
|
|
753
|
+
if (!urlReference &&
|
|
754
|
+
createHash('sha256')
|
|
755
|
+
.update(await boundedFileSha256(mcpbPath))
|
|
756
|
+
.digest('hex') !== sourceFingerprint) {
|
|
757
|
+
throw new Error(`Plugin MCPB source changed while loading: ${mcpbPath}`);
|
|
758
|
+
}
|
|
670
759
|
mcp.push({
|
|
671
|
-
path:
|
|
760
|
+
path: mcpbPath,
|
|
672
761
|
scope,
|
|
673
762
|
plugin: true,
|
|
674
763
|
value: {
|
|
@@ -677,6 +766,7 @@ async function loadPlugin(pluginPath, source, enabled, cwd, requireManifest = fa
|
|
|
677
766
|
},
|
|
678
767
|
},
|
|
679
768
|
environment: pluginEnvironment,
|
|
769
|
+
pluginExecutableSource,
|
|
680
770
|
sensitiveValues: [...sensitiveValues, ...loaded.sensitiveValues],
|
|
681
771
|
});
|
|
682
772
|
}
|
|
@@ -840,7 +930,7 @@ export async function loadClaudePlugins(options) {
|
|
|
840
930
|
if (seen.has(canonical))
|
|
841
931
|
return null;
|
|
842
932
|
seen.add(canonical);
|
|
843
|
-
return await loadPlugin(canonical, candidate.source, candidate.enabled, options.cwd, false, candidate.resourceScope, options.configRoot, options.environment ?? process.env, candidate.configId, options.readOnlyHooks, options.dataPlane);
|
|
933
|
+
return await loadPlugin(canonical, candidate.source, candidate.enabled, options.cwd, false, candidate.resourceScope, options.configRoot, options.environment ?? process.env, candidate.configId, options.readOnlyHooks, options.readOnlyExecutables, options.allowWorkspaceMcpb, options.dataPlane);
|
|
844
934
|
}
|
|
845
935
|
catch (error) {
|
|
846
936
|
if (options.strictPluginDirectories &&
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ModelProvider, type ModelRequest, type ModelStreamEvent } from '../core/runtime.js';
|
|
2
|
+
export declare const DEFAULT_PROVIDER_DEADLINE_MS = 90000;
|
|
3
|
+
export interface DeadlineModelProviderOptions {
|
|
4
|
+
provider: ModelProvider;
|
|
5
|
+
deadlineMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Bounds each streamed completion while preserving the wrapped provider's API. */
|
|
8
|
+
export declare class DeadlineModelProvider implements ModelProvider {
|
|
9
|
+
private readonly provider;
|
|
10
|
+
private readonly deadlineMs;
|
|
11
|
+
readonly model?: string;
|
|
12
|
+
constructor(options: DeadlineModelProviderOptions);
|
|
13
|
+
get capabilities(): ModelProvider['capabilities'];
|
|
14
|
+
complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=deadline-provider.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ModelProviderError, } from '../core/runtime.js';
|
|
2
|
+
export const DEFAULT_PROVIDER_DEADLINE_MS = 90_000;
|
|
3
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
4
|
+
function deferred() {
|
|
5
|
+
let resolve;
|
|
6
|
+
const promise = new Promise((promiseResolve) => {
|
|
7
|
+
resolve = promiseResolve;
|
|
8
|
+
});
|
|
9
|
+
return { promise, resolve };
|
|
10
|
+
}
|
|
11
|
+
/** Bounds each streamed completion while preserving the wrapped provider's API. */
|
|
12
|
+
export class DeadlineModelProvider {
|
|
13
|
+
provider;
|
|
14
|
+
deadlineMs;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
const deadlineMs = options.deadlineMs === undefined
|
|
17
|
+
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
18
|
+
: options.deadlineMs;
|
|
19
|
+
if (!Number.isFinite(deadlineMs) ||
|
|
20
|
+
!Number.isInteger(deadlineMs) ||
|
|
21
|
+
deadlineMs <= 0) {
|
|
22
|
+
throw new Error('Provider deadline must be a positive integer');
|
|
23
|
+
}
|
|
24
|
+
this.provider = options.provider;
|
|
25
|
+
this.deadlineMs = deadlineMs;
|
|
26
|
+
Object.defineProperty(this, 'model', {
|
|
27
|
+
configurable: false,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
get: () => this.provider.model,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
get capabilities() {
|
|
33
|
+
return this.provider.capabilities;
|
|
34
|
+
}
|
|
35
|
+
complete(request) {
|
|
36
|
+
let started = false;
|
|
37
|
+
let closed = false;
|
|
38
|
+
let interruption;
|
|
39
|
+
let controller;
|
|
40
|
+
let callerSignal;
|
|
41
|
+
let timer;
|
|
42
|
+
let deadlineStartedAt;
|
|
43
|
+
let underlying;
|
|
44
|
+
let cleanupStarted = false;
|
|
45
|
+
const interruptionDeferred = deferred();
|
|
46
|
+
const interruptionError = () => {
|
|
47
|
+
if (interruption?.kind === 'timeout') {
|
|
48
|
+
return new ModelProviderError('Provider request timed out', {
|
|
49
|
+
kind: 'timeout',
|
|
50
|
+
retryable: true,
|
|
51
|
+
cause: interruption.cause,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return new ModelProviderError('Provider request cancelled', {
|
|
55
|
+
kind: 'cancelled',
|
|
56
|
+
retryable: false,
|
|
57
|
+
cause: interruption?.cause,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const bestEffortUnderlyingReturn = () => {
|
|
61
|
+
const iterator = underlying;
|
|
62
|
+
if (!iterator || cleanupStarted)
|
|
63
|
+
return;
|
|
64
|
+
cleanupStarted = true;
|
|
65
|
+
let returnMethod;
|
|
66
|
+
try {
|
|
67
|
+
returnMethod = iterator.return;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!returnMethod)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
const result = returnMethod.call(iterator);
|
|
76
|
+
if (result && typeof result.then === 'function') {
|
|
77
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Cleanup is best effort; preserve the original completion outcome.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const clearResources = () => {
|
|
85
|
+
if (timer !== undefined)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
timer = undefined;
|
|
88
|
+
callerSignal?.removeEventListener('abort', onCallerAbort);
|
|
89
|
+
};
|
|
90
|
+
const finish = (outcome) => {
|
|
91
|
+
if (interruption !== undefined)
|
|
92
|
+
return;
|
|
93
|
+
interruption = outcome;
|
|
94
|
+
closed = true;
|
|
95
|
+
interruptionDeferred.resolve(outcome);
|
|
96
|
+
controller?.abort(outcome.cause);
|
|
97
|
+
clearResources();
|
|
98
|
+
if (outcome.kind !== 'return')
|
|
99
|
+
bestEffortUnderlyingReturn();
|
|
100
|
+
};
|
|
101
|
+
const onCallerAbort = () => finish({ kind: 'cancelled', cause: callerSignal?.reason });
|
|
102
|
+
const timeoutCause = new Error('Provider request timed out');
|
|
103
|
+
timeoutCause.name = 'TimeoutError';
|
|
104
|
+
const scheduleDeadline = () => {
|
|
105
|
+
if (deadlineStartedAt === undefined || closed)
|
|
106
|
+
return;
|
|
107
|
+
const elapsedMs = Math.max(0, performance.now() - deadlineStartedAt);
|
|
108
|
+
const remainingMs = this.deadlineMs - elapsedMs;
|
|
109
|
+
if (remainingMs <= 0) {
|
|
110
|
+
finish({ kind: 'timeout', cause: timeoutCause });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
timer = setTimeout(scheduleDeadline, Math.min(remainingMs, MAX_TIMER_DELAY_MS));
|
|
114
|
+
};
|
|
115
|
+
const start = () => {
|
|
116
|
+
if (started || closed)
|
|
117
|
+
return;
|
|
118
|
+
started = true;
|
|
119
|
+
controller = new AbortController();
|
|
120
|
+
callerSignal = request.signal;
|
|
121
|
+
if (callerSignal?.aborted)
|
|
122
|
+
onCallerAbort();
|
|
123
|
+
else
|
|
124
|
+
callerSignal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
125
|
+
if (interruption !== undefined)
|
|
126
|
+
return;
|
|
127
|
+
deadlineStartedAt = performance.now();
|
|
128
|
+
scheduleDeadline();
|
|
129
|
+
const iterable = this.provider.complete({
|
|
130
|
+
...request,
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
});
|
|
133
|
+
underlying = iterable[Symbol.asyncIterator]();
|
|
134
|
+
if (interruption !== undefined)
|
|
135
|
+
bestEffortUnderlyingReturn();
|
|
136
|
+
};
|
|
137
|
+
const next = async () => {
|
|
138
|
+
try {
|
|
139
|
+
start();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (interruption !== undefined)
|
|
143
|
+
throw interruptionError();
|
|
144
|
+
closed = true;
|
|
145
|
+
clearResources();
|
|
146
|
+
bestEffortUnderlyingReturn();
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (interruption?.kind === 'return')
|
|
150
|
+
return { done: true, value: undefined };
|
|
151
|
+
if (interruption !== undefined)
|
|
152
|
+
throw interruptionError();
|
|
153
|
+
if (closed || !underlying)
|
|
154
|
+
return { done: true, value: undefined };
|
|
155
|
+
let nextPromise;
|
|
156
|
+
try {
|
|
157
|
+
const result = underlying.next();
|
|
158
|
+
nextPromise = Promise.resolve(result);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (interruption !== undefined)
|
|
162
|
+
throw interruptionError();
|
|
163
|
+
closed = true;
|
|
164
|
+
clearResources();
|
|
165
|
+
bestEffortUnderlyingReturn();
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (interruption !== undefined) {
|
|
169
|
+
void nextPromise.catch(() => undefined);
|
|
170
|
+
throw interruptionError();
|
|
171
|
+
}
|
|
172
|
+
const result = await Promise.race([
|
|
173
|
+
nextPromise.then((value) => ({ type: 'result', value }), (error) => ({ type: 'error', error })),
|
|
174
|
+
interruptionDeferred.promise.then((outcome) => ({
|
|
175
|
+
type: 'interruption',
|
|
176
|
+
outcome,
|
|
177
|
+
})),
|
|
178
|
+
]);
|
|
179
|
+
const observedInterruption = interruption ??
|
|
180
|
+
(result.type === 'interruption' ? result.outcome : undefined);
|
|
181
|
+
if (observedInterruption !== undefined) {
|
|
182
|
+
if (result.type === 'result')
|
|
183
|
+
void nextPromise.catch(() => undefined);
|
|
184
|
+
if (observedInterruption.kind === 'return')
|
|
185
|
+
return { done: true, value: undefined };
|
|
186
|
+
throw interruptionError();
|
|
187
|
+
}
|
|
188
|
+
if (result.type === 'error') {
|
|
189
|
+
closed = true;
|
|
190
|
+
clearResources();
|
|
191
|
+
bestEffortUnderlyingReturn();
|
|
192
|
+
throw result.error;
|
|
193
|
+
}
|
|
194
|
+
if (result.type === 'interruption') {
|
|
195
|
+
if (result.outcome.kind === 'return')
|
|
196
|
+
return { done: true, value: undefined };
|
|
197
|
+
throw interruptionError();
|
|
198
|
+
}
|
|
199
|
+
if (result.value.done) {
|
|
200
|
+
closed = true;
|
|
201
|
+
clearResources();
|
|
202
|
+
}
|
|
203
|
+
return result.value;
|
|
204
|
+
};
|
|
205
|
+
const returnFromConsumer = async () => {
|
|
206
|
+
if (!started) {
|
|
207
|
+
closed = true;
|
|
208
|
+
return { done: true, value: undefined };
|
|
209
|
+
}
|
|
210
|
+
if (!closed) {
|
|
211
|
+
finish({ kind: 'return', cause: undefined });
|
|
212
|
+
bestEffortUnderlyingReturn();
|
|
213
|
+
}
|
|
214
|
+
return { done: true, value: undefined };
|
|
215
|
+
};
|
|
216
|
+
const result = {
|
|
217
|
+
[Symbol.asyncIterator]() {
|
|
218
|
+
return result;
|
|
219
|
+
},
|
|
220
|
+
next,
|
|
221
|
+
return: returnFromConsumer,
|
|
222
|
+
};
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=deadline-provider.js.map
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { createAnthropicPromptCachePolicyResolver } from './anthropic-prompt-cache.js';
|
|
2
|
+
import { DEFAULT_PROVIDER_DEADLINE_MS } from './deadline-provider.js';
|
|
2
3
|
export function parseProviderEnvironment(environment) {
|
|
3
4
|
const provider = environment.PRAXIS_PROVIDER ?? 'openai';
|
|
4
5
|
if (provider !== 'openai' && provider !== 'anthropic') {
|
|
5
6
|
throw new Error('PRAXIS_PROVIDER must be openai or anthropic');
|
|
6
7
|
}
|
|
8
|
+
const rawDeadlineMs = environment.PRAXIS_PROVIDER_DEADLINE_MS;
|
|
9
|
+
if (rawDeadlineMs !== undefined &&
|
|
10
|
+
(!/^\d+$/.test(rawDeadlineMs) ||
|
|
11
|
+
Number(rawDeadlineMs) <= 0 ||
|
|
12
|
+
!Number.isSafeInteger(Number(rawDeadlineMs)))) {
|
|
13
|
+
throw new Error('PRAXIS_PROVIDER_DEADLINE_MS must be a positive safe integer');
|
|
14
|
+
}
|
|
15
|
+
const deadlineMs = rawDeadlineMs === undefined
|
|
16
|
+
? DEFAULT_PROVIDER_DEADLINE_MS
|
|
17
|
+
: Number(rawDeadlineMs);
|
|
7
18
|
const maxOutputTokens = environment.PRAXIS_MAX_OUTPUT_TOKENS;
|
|
8
19
|
if (maxOutputTokens !== undefined &&
|
|
9
20
|
(!/^\d+$/.test(maxOutputTokens) ||
|
|
@@ -47,6 +58,7 @@ export function parseProviderEnvironment(environment) {
|
|
|
47
58
|
(provider === 'anthropic'
|
|
48
59
|
? 'https://api.anthropic.com/v1'
|
|
49
60
|
: 'https://api.openai.com/v1'),
|
|
61
|
+
deadlineMs,
|
|
50
62
|
...(maxOutputTokens === undefined
|
|
51
63
|
? {}
|
|
52
64
|
: { maxOutputTokens: Number(maxOutputTokens) }),
|
|
@@ -6,6 +6,38 @@ function retryable(error) {
|
|
|
6
6
|
error.retryable &&
|
|
7
7
|
modelProviderErrorKind(error) !== 'prompt_too_long');
|
|
8
8
|
}
|
|
9
|
+
function cancellationError(signal) {
|
|
10
|
+
return new ModelProviderError('Provider request cancelled', {
|
|
11
|
+
kind: 'cancelled',
|
|
12
|
+
retryable: false,
|
|
13
|
+
cause: signal.reason,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function throwIfAborted(signal) {
|
|
17
|
+
if (signal?.aborted)
|
|
18
|
+
throw cancellationError(signal);
|
|
19
|
+
}
|
|
20
|
+
async function waitForRetry(delayMs, signal) {
|
|
21
|
+
throwIfAborted(signal);
|
|
22
|
+
if (delayMs <= 0)
|
|
23
|
+
return;
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
const finish = (result) => {
|
|
27
|
+
if (settled)
|
|
28
|
+
return;
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
signal?.removeEventListener('abort', onAbort);
|
|
32
|
+
result();
|
|
33
|
+
};
|
|
34
|
+
const onAbort = () => finish(() => reject(cancellationError(signal)));
|
|
35
|
+
const timer = setTimeout(() => finish(resolve), delayMs);
|
|
36
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
37
|
+
if (signal?.aborted)
|
|
38
|
+
onAbort();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
9
41
|
/** Routes each complete request through Claude's retry-then-fallback policy. */
|
|
10
42
|
export class FallbackModelProvider {
|
|
11
43
|
active;
|
|
@@ -30,6 +62,7 @@ export class FallbackModelProvider {
|
|
|
30
62
|
let lastError;
|
|
31
63
|
for (const provider of this.providers()) {
|
|
32
64
|
for (let attempt = 0; attempt < MAX_ATTEMPTS_PER_MODEL; attempt += 1) {
|
|
65
|
+
throwIfAborted(request.signal);
|
|
33
66
|
this.active = provider;
|
|
34
67
|
const attemptStartedAt = performance.now();
|
|
35
68
|
try {
|
|
@@ -73,6 +106,7 @@ export class FallbackModelProvider {
|
|
|
73
106
|
return;
|
|
74
107
|
}
|
|
75
108
|
catch (error) {
|
|
109
|
+
throwIfAborted(request.signal);
|
|
76
110
|
lastError = error;
|
|
77
111
|
if (!retryable(error))
|
|
78
112
|
throw error;
|
|
@@ -89,8 +123,7 @@ export class FallbackModelProvider {
|
|
|
89
123
|
error: modelProviderErrorKind(error),
|
|
90
124
|
};
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
126
|
+
await waitForRetry(delay, request.signal);
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { JsonResource } from '../core/resources.js';
|
|
2
|
+
export interface WorkspaceExecutableOrigin {
|
|
3
|
+
readonly kind: 'hook' | 'mcp';
|
|
4
|
+
readonly scope: 'project' | 'local';
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WorkspaceTrustInventory {
|
|
9
|
+
readonly canonicalPath: string;
|
|
10
|
+
readonly fingerprint: string;
|
|
11
|
+
readonly origins: readonly WorkspaceExecutableOrigin[];
|
|
12
|
+
}
|
|
13
|
+
export type WorkspaceTrustStatus = 'not-required' | 'trusted' | 'untrusted';
|
|
14
|
+
export interface WorkspaceTrustAssessment extends WorkspaceTrustInventory {
|
|
15
|
+
readonly status: WorkspaceTrustStatus;
|
|
16
|
+
}
|
|
17
|
+
export declare function canonicalizeWorkspaceTrust(value: unknown, ancestors?: Set<object>): string;
|
|
18
|
+
export declare function hasWorkspaceHooks(resource: JsonResource): boolean;
|
|
19
|
+
export declare function hasWorkspaceMcpServers(resource: JsonResource): boolean;
|
|
20
|
+
export declare function allowedWorkspaceHookSettings(resources: readonly JsonResource[], trusted: boolean): JsonResource[];
|
|
21
|
+
export declare function allowedWorkspaceMcpResources(resources: readonly JsonResource[], trusted: boolean): JsonResource[];
|
|
22
|
+
export declare function workspaceTrustInventory(options: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
settings?: readonly JsonResource[];
|
|
25
|
+
mcp?: readonly JsonResource[];
|
|
26
|
+
}): Promise<WorkspaceTrustInventory>;
|
|
27
|
+
export declare function workspaceTrustDecisionKey(inventory: Pick<WorkspaceTrustInventory, 'canonicalPath' | 'fingerprint'>): string;
|
|
28
|
+
export declare function assessWorkspaceTrust(inventory: WorkspaceTrustInventory, statePath: string): Promise<WorkspaceTrustAssessment>;
|
|
29
|
+
export declare function persistWorkspaceTrust(assessment: WorkspaceTrustAssessment, statePath: string): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=workspace-trust.d.ts.map
|