dsh-plugin-coding-delegate 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +920 -2
- package/cordis.patch.example.yml +48 -0
- package/cordis.patch.yml +13 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +44 -0
- package/dist/containment.d.ts +33 -0
- package/dist/containment.js +63 -0
- package/dist/context-quality.d.ts +79 -0
- package/dist/context-quality.js +181 -0
- package/dist/context.d.ts +39 -0
- package/dist/context.js +136 -0
- package/dist/contracts.d.ts +174 -0
- package/dist/contracts.js +415 -0
- package/dist/declaration-egress-hook.d.ts +45 -0
- package/dist/declaration-egress-hook.js +125 -0
- package/dist/declaration-egress.d.ts +37 -0
- package/dist/declaration-egress.js +169 -0
- package/dist/delegation.d.ts +339 -0
- package/dist/delegation.js +933 -0
- package/dist/emission.d.ts +70 -0
- package/dist/emission.js +310 -0
- package/dist/guard.d.ts +162 -0
- package/dist/guard.js +873 -0
- package/dist/host-events.d.ts +15 -0
- package/dist/host-events.js +27 -0
- package/dist/index.d.ts +560 -0
- package/dist/index.js +1398 -0
- package/dist/local-classifier.d.ts +52 -0
- package/dist/local-classifier.js +159 -0
- package/dist/logging.d.ts +21 -0
- package/dist/logging.js +90 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +83 -0
- package/dist/profiles.d.ts +35 -0
- package/dist/profiles.js +95 -0
- package/dist/retry-context.d.ts +52 -0
- package/dist/retry-context.js +97 -0
- package/dist/roles.d.ts +129 -0
- package/dist/roles.js +363 -0
- package/dist/routing.d.ts +35 -0
- package/dist/routing.js +88 -0
- package/dist/savings-tracker.d.ts +128 -0
- package/dist/savings-tracker.js +340 -0
- package/dist/session-events.d.ts +17 -0
- package/dist/session-events.js +31 -0
- package/dist/verification.d.ts +104 -0
- package/dist/verification.js +560 -0
- package/package.json +66 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleDeclarationPostExecute = handleDeclarationPostExecute;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const guard_1 = require("./guard");
|
|
39
|
+
// CODE_EXTENSIONS is declared in ./paths and is deliberately not re-exported from ./guard, so it is
|
|
40
|
+
// imported from its own module. Importing it from './guard' fails to compile.
|
|
41
|
+
const paths_1 = require("./paths");
|
|
42
|
+
const declaration_egress_1 = require("./declaration-egress");
|
|
43
|
+
/**
|
|
44
|
+
* Decide whether this tool call may return its output under declarations mode.
|
|
45
|
+
*
|
|
46
|
+
* Returns a replacement decision when the call must be refused, or null when it may proceed. A null is
|
|
47
|
+
* the caller's signal to keep going, which is why every pass-through path returns null rather than a
|
|
48
|
+
* decision: the handler's contract is (exec, result, next) => decision, and a decision invented here
|
|
49
|
+
* would replace the tool's real output with nothing.
|
|
50
|
+
*/
|
|
51
|
+
function handleDeclarationPostExecute(exec, options) {
|
|
52
|
+
// The setting is the ceiling and is checked first, so this is a complete no-op for anyone who has not
|
|
53
|
+
// opted in. index.ts also gates registration on the same value; both checks are kept because a
|
|
54
|
+
// mis-ordered load must not be able to turn the control off.
|
|
55
|
+
if (options?.sourceReadEgress !== 'declarations')
|
|
56
|
+
return null;
|
|
57
|
+
const name = String(exec?.name || '');
|
|
58
|
+
if (!name)
|
|
59
|
+
return null;
|
|
60
|
+
let toolKind = 'other';
|
|
61
|
+
if (guard_1.READ_TOOLS.has(name))
|
|
62
|
+
toolKind = 'read';
|
|
63
|
+
else if (guard_1.SEARCH_TOOLS.has(name))
|
|
64
|
+
toolKind = 'search';
|
|
65
|
+
else if (guard_1.SHELL_TOOLS.has(name))
|
|
66
|
+
toolKind = 'shell';
|
|
67
|
+
// Not an egress route, and -- for a read -- handled by index.ts's own branch, which does more than
|
|
68
|
+
// this module can. See the note at the top.
|
|
69
|
+
if (toolKind !== 'search' && toolKind !== 'shell')
|
|
70
|
+
return null;
|
|
71
|
+
// Reads carry `path`, searches carry a scope, shell commands carry `command`. Reading the wrong field
|
|
72
|
+
// yields an empty target, which the evaluator treats as scope-free and refuses -- a fail-closed but
|
|
73
|
+
// wrong answer, so the extraction is per kind rather than guessed.
|
|
74
|
+
const args = exec?.arguments ?? {};
|
|
75
|
+
const target = toolKind === 'search'
|
|
76
|
+
? String(args.path ?? args.directory ?? args.target ?? '')
|
|
77
|
+
: String(args.command ?? args.cmd ?? '');
|
|
78
|
+
// CODE_EXTENSIONS is a Set of EXTENSIONS, not of paths. Testing a whole path against it returns false
|
|
79
|
+
// for every source file and would silently disable the gate, so the extension is derived here.
|
|
80
|
+
const isCodeExtension = (p) => paths_1.CODE_EXTENSIONS.has(path.extname(String(p)).toLowerCase());
|
|
81
|
+
// longestCodeReference returns the longest code-looking token, or undefined. The evaluator wants a
|
|
82
|
+
// predicate, so presence becomes a boolean here rather than a second vocabulary of source extensions.
|
|
83
|
+
const commandNamesSource = (cmd) => (0, guard_1.longestCodeReference)(cmd) !== undefined;
|
|
84
|
+
const verdict = (0, declaration_egress_1.evaluateDeclarationEgress)({
|
|
85
|
+
toolKind,
|
|
86
|
+
target,
|
|
87
|
+
sourceReadEgress: options?.sourceReadEgress,
|
|
88
|
+
declarationRoot: options?.declarationRoot,
|
|
89
|
+
isCodeExtension,
|
|
90
|
+
declarationPathFor: guard_1.declarationPathFor,
|
|
91
|
+
commandReadsContent: guard_1.commandReadsContent,
|
|
92
|
+
commandNamesSource,
|
|
93
|
+
});
|
|
94
|
+
if (verdict.action === 'block') {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'accept',
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: String(verdict.reason || 'Egress blocked by declarations policy.'),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// A search or a shell command has no single file to substitute a skeleton for: a search returns
|
|
106
|
+
// matched lines from wherever they matched. If the evaluator ever returns serve-declaration for one of
|
|
107
|
+
// these kinds, refusing is the safe reading -- serving a whole declaration in place of search output
|
|
108
|
+
// would answer a question the caller did not ask. Reads never reach this module.
|
|
109
|
+
if (verdict.action === 'serve-declaration') {
|
|
110
|
+
return {
|
|
111
|
+
kind: 'accept',
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: "Reading '" +
|
|
116
|
+
target +
|
|
117
|
+
"' under sourceReadEgress 'declarations' is refused on this route: only a direct file read " +
|
|
118
|
+
'can be redirected to a type skeleton, and a search or shell command cannot. Read the ' +
|
|
119
|
+
'declaration path directly instead.',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a declarations-mode egress check decided.
|
|
3
|
+
*
|
|
4
|
+
* `action` and `skeleton` are the field names the committed oracle asserts on
|
|
5
|
+
* (`plugin/tests/oracles/declaration-egress-scope.test.cjs`). They are not `decision`/`declarationPath`:
|
|
6
|
+
* the test file is the frozen contract for this unit, so the implementation follows it rather than the
|
|
7
|
+
* other way round.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeclarationEgressVerdict {
|
|
10
|
+
action: 'allow' | 'serve-declaration' | 'block';
|
|
11
|
+
skeleton?: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DeclarationEgressInput {
|
|
15
|
+
toolKind: 'read' | 'search' | 'shell' | string;
|
|
16
|
+
/** The path argument for a read or a search scope; the command text for a shell command. */
|
|
17
|
+
target: string;
|
|
18
|
+
sourceReadEgress?: 'source' | 'declarations' | string;
|
|
19
|
+
declarationRoot?: string;
|
|
20
|
+
/** Injected so this module does not depend on guard.ts internals. See `mappableSourceFile` below. */
|
|
21
|
+
isCodeExtension?: (filePath: string) => boolean;
|
|
22
|
+
declarationPathFor?: (filePath: string, root?: string) => string | null;
|
|
23
|
+
commandReadsContent?: (cmd: string) => boolean;
|
|
24
|
+
commandNamesSource?: (cmd: string) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether one tool call may proceed under declarations mode.
|
|
28
|
+
*
|
|
29
|
+
* Pure, with every environment-dependent question injected, so the whole policy is testable without a
|
|
30
|
+
* host -- the shape `containment.ts` and `attestation.ts` already use. Callers pass the real
|
|
31
|
+
* `declarationPathFor` and `commandReadsContent`; `SHELL_TOOLS` is not referenced here because it is
|
|
32
|
+
* module-private in `guard.ts` and the caller already knows which kind of tool it is holding.
|
|
33
|
+
*
|
|
34
|
+
* The setting is a ceiling checked first: when it is not the string 'declarations' this function is a
|
|
35
|
+
* complete no-op, so nobody who has not opted in is affected by anything below.
|
|
36
|
+
*/
|
|
37
|
+
export declare function evaluateDeclarationEgress(input: DeclarationEgressInput): DeclarationEgressVerdict;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.evaluateDeclarationEgress = evaluateDeclarationEgress;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
/**
|
|
39
|
+
* The extensions `tsc --declaration --emitDeclarationOnly` can actually produce a skeleton for.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS SET EXISTS, and why it is narrower than `CODE_EXTENSIONS`. The obvious implementation is to
|
|
42
|
+
* ask `declarationPathFor` whether it can map the file and treat `null` as "cannot". That does not work,
|
|
43
|
+
* and the reason is a trap: `declarationPathFor` ends with
|
|
44
|
+
*
|
|
45
|
+
* return path.join(declarationRoot, relative + '.d.ts')
|
|
46
|
+
*
|
|
47
|
+
* having fallen back to `path.basename` for any path that does not contain a `src/` segment
|
|
48
|
+
* (`guard.ts:583-587`). It therefore returns a NON-NULL path for every extension in its four-extension
|
|
49
|
+
* allowlist AND never consults the extension list of the thing being mapped. A `.py` file handed to it
|
|
50
|
+
* comes back as `<root>/deploy.d.ts` -- a path that no build will ever create.
|
|
51
|
+
*
|
|
52
|
+
* A check written that way would not refuse the Python read. It would redirect it to a skeleton that
|
|
53
|
+
* does not exist, which is the leak plus a misleading file reference. So the mappability decision is
|
|
54
|
+
* made here, explicitly, and `declarationPathFor` is only ever called for a file this set accepts.
|
|
55
|
+
*
|
|
56
|
+
* `.d.ts` is deliberately absent: a declaration is already a skeleton and is allowed earlier.
|
|
57
|
+
* `.mts` and `.cts` are absent because the build in this repository does not emit for them.
|
|
58
|
+
*/
|
|
59
|
+
const MAPPABLE_SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
60
|
+
/** True when this file's extension is one declarations mode can serve a skeleton for. */
|
|
61
|
+
function mappableSourceFile(filePath) {
|
|
62
|
+
return MAPPABLE_SOURCE_EXTENSIONS.has(path.extname(String(filePath || '')).toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decide whether one tool call may proceed under declarations mode.
|
|
66
|
+
*
|
|
67
|
+
* Pure, with every environment-dependent question injected, so the whole policy is testable without a
|
|
68
|
+
* host -- the shape `containment.ts` and `attestation.ts` already use. Callers pass the real
|
|
69
|
+
* `declarationPathFor` and `commandReadsContent`; `SHELL_TOOLS` is not referenced here because it is
|
|
70
|
+
* module-private in `guard.ts` and the caller already knows which kind of tool it is holding.
|
|
71
|
+
*
|
|
72
|
+
* The setting is a ceiling checked first: when it is not the string 'declarations' this function is a
|
|
73
|
+
* complete no-op, so nobody who has not opted in is affected by anything below.
|
|
74
|
+
*/
|
|
75
|
+
function evaluateDeclarationEgress(input) {
|
|
76
|
+
// 1. Not opted in. A total pass-through, for the reason above.
|
|
77
|
+
if (input.sourceReadEgress !== 'declarations')
|
|
78
|
+
return { action: 'allow' };
|
|
79
|
+
const target = String(input.target || '');
|
|
80
|
+
// 2. A declaration file is already the artifact this mode exists to serve, so reading one is ordinary.
|
|
81
|
+
// Checked before the tool kind because it is true for a read and for a search scope alike.
|
|
82
|
+
if (target.toLowerCase().endsWith('.d.ts'))
|
|
83
|
+
return { action: 'allow' };
|
|
84
|
+
// 3. READS.
|
|
85
|
+
if (input.toolKind === 'read') {
|
|
86
|
+
const isCode = input.isCodeExtension ? input.isCodeExtension(target) : false;
|
|
87
|
+
if (!isCode)
|
|
88
|
+
return { action: 'allow' };
|
|
89
|
+
// Only a mappable extension may be redirected. Anything else that is still code -- Python, Go, Rust,
|
|
90
|
+
// a shell script, SQL -- cannot be served as a skeleton by any build, so it is refused rather than
|
|
91
|
+
// leaked. See MAPPABLE_SOURCE_EXTENSIONS for why this is not delegated to `declarationPathFor`.
|
|
92
|
+
if (!mappableSourceFile(target)) {
|
|
93
|
+
return {
|
|
94
|
+
action: 'block',
|
|
95
|
+
reason: "Reading '" +
|
|
96
|
+
target +
|
|
97
|
+
"' was refused: sourceReadEgress is 'declarations' and no type skeleton can be produced for " +
|
|
98
|
+
path.extname(target).toLowerCase() +
|
|
99
|
+
'. Serving the body would be the implementation leak this setting exists to prevent. Read the ' +
|
|
100
|
+
'declaration path directly, or turn sourceReadEgress off in the profile patch (which needs a ' +
|
|
101
|
+
'DSH restart).',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const skeleton = input.declarationPathFor
|
|
105
|
+
? input.declarationPathFor(target, input.declarationRoot)
|
|
106
|
+
: null;
|
|
107
|
+
if (!skeleton) {
|
|
108
|
+
// Fail closed, which is the rule index.ts already applies to a read whose declaration is missing.
|
|
109
|
+
return {
|
|
110
|
+
action: 'block',
|
|
111
|
+
reason: "Reading '" +
|
|
112
|
+
target +
|
|
113
|
+
"' was refused: sourceReadEgress is 'declarations' and no declaration path could be derived " +
|
|
114
|
+
'for it. Implementation bodies are not served to this context.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { action: 'serve-declaration', skeleton };
|
|
118
|
+
}
|
|
119
|
+
// 4. SEARCHES. A search returns matching LINES, so there is no whole file to substitute: the matched
|
|
120
|
+
// bytes are implementation bodies wherever they land. The scope decides, because a search confined
|
|
121
|
+
// to prose cannot reach code. An UNSTATED scope cannot be shown to be harmless and is refused --
|
|
122
|
+
// the opposite of the read branch, where an unstated target is simply not a read.
|
|
123
|
+
if (input.toolKind === 'search') {
|
|
124
|
+
const scope = target.trim();
|
|
125
|
+
if (scope && !(input.isCodeExtension ? input.isCodeExtension(scope) : false)) {
|
|
126
|
+
return { action: 'allow' };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
action: 'block',
|
|
130
|
+
reason: scope
|
|
131
|
+
? "Searching '" +
|
|
132
|
+
scope +
|
|
133
|
+
"' was refused: sourceReadEgress is 'declarations' and a search returns matched source lines, " +
|
|
134
|
+
'which are implementation bodies. Read the declaration path directly instead.'
|
|
135
|
+
: "A search with no stated scope was refused: sourceReadEgress is 'declarations' and the search's " +
|
|
136
|
+
'reach cannot be shown to avoid source. Narrow it to a non-source path, or read a declaration ' +
|
|
137
|
+
'path directly.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 5. SHELL. Same leak by another door, with one difference that matters: a command that merely lists
|
|
141
|
+
// or tests reads no bytes, so there is nothing to leak and it is allowed through.
|
|
142
|
+
if (input.toolKind === 'shell') {
|
|
143
|
+
// An egress filter that cannot evaluate must not wave the read through. This is the rule
|
|
144
|
+
// index.ts:1496 already states for a failed evaluation.
|
|
145
|
+
if (!input.commandReadsContent || !input.commandNamesSource) {
|
|
146
|
+
return {
|
|
147
|
+
action: 'block',
|
|
148
|
+
reason: "Shell command '" +
|
|
149
|
+
target +
|
|
150
|
+
"' was refused: sourceReadEgress is 'declarations' and the command could not be inspected for " +
|
|
151
|
+
'content reads, so it cannot be cleared. Fail closed.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const reads = input.commandReadsContent(target) === true;
|
|
155
|
+
const names = input.commandNamesSource(target) === true;
|
|
156
|
+
if (reads && names) {
|
|
157
|
+
return {
|
|
158
|
+
action: 'block',
|
|
159
|
+
reason: "Shell command '" +
|
|
160
|
+
target +
|
|
161
|
+
"' was refused: sourceReadEgress is 'declarations' and this command reads source content, " +
|
|
162
|
+
'which is an implementation egress by another route. Read the declaration path directly instead.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { action: 'allow' };
|
|
166
|
+
}
|
|
167
|
+
// 6. Anything that neither reads, searches nor shells is not an egress route.
|
|
168
|
+
return { action: 'allow' };
|
|
169
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { SavingsTracker } from './savings-tracker';
|
|
2
|
+
import { ContextRequest } from './context';
|
|
3
|
+
import { VerificationPolicy } from './verification';
|
|
4
|
+
import type { DelegateWorkerParameter } from './contracts';
|
|
5
|
+
import type { LLMSession } from './index';
|
|
6
|
+
export declare const DELEGATE_WORKER_OPENAI_SCHEMA: {
|
|
7
|
+
type: string;
|
|
8
|
+
function: {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
parameters: {
|
|
12
|
+
type: string;
|
|
13
|
+
properties: {
|
|
14
|
+
taskName: {
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
instruction: {
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
targetFiles: {
|
|
23
|
+
type: string;
|
|
24
|
+
items: {
|
|
25
|
+
type: string;
|
|
26
|
+
};
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
runVerification: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
verificationRepeats: {
|
|
34
|
+
type: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
attestTargets: {
|
|
38
|
+
type: string;
|
|
39
|
+
items: {
|
|
40
|
+
type: string;
|
|
41
|
+
};
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
attestEvidence: {
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
};
|
|
48
|
+
attestOperator: {
|
|
49
|
+
type: string;
|
|
50
|
+
description: string;
|
|
51
|
+
};
|
|
52
|
+
contractFiles: {
|
|
53
|
+
type: string;
|
|
54
|
+
items: {
|
|
55
|
+
type: string;
|
|
56
|
+
};
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
contextFiles: {
|
|
60
|
+
type: string;
|
|
61
|
+
description: string;
|
|
62
|
+
items: {
|
|
63
|
+
type: string;
|
|
64
|
+
properties: {
|
|
65
|
+
path: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
startLine: {
|
|
69
|
+
type: string;
|
|
70
|
+
};
|
|
71
|
+
endLine: {
|
|
72
|
+
type: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
required: string[];
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
workspaceDir: {
|
|
79
|
+
type: string;
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
required: string[];
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
export declare const DELEGATE_WORKER_SCHEMA: {
|
|
88
|
+
type: string;
|
|
89
|
+
function: {
|
|
90
|
+
name: string;
|
|
91
|
+
description: string;
|
|
92
|
+
parameters: {
|
|
93
|
+
type: string;
|
|
94
|
+
properties: {
|
|
95
|
+
taskName: {
|
|
96
|
+
type: string;
|
|
97
|
+
description: string;
|
|
98
|
+
};
|
|
99
|
+
instruction: {
|
|
100
|
+
type: string;
|
|
101
|
+
description: string;
|
|
102
|
+
};
|
|
103
|
+
targetFiles: {
|
|
104
|
+
type: string;
|
|
105
|
+
items: {
|
|
106
|
+
type: string;
|
|
107
|
+
};
|
|
108
|
+
description: string;
|
|
109
|
+
};
|
|
110
|
+
runVerification: {
|
|
111
|
+
type: string;
|
|
112
|
+
description: string;
|
|
113
|
+
};
|
|
114
|
+
verificationRepeats: {
|
|
115
|
+
type: string;
|
|
116
|
+
description: string;
|
|
117
|
+
};
|
|
118
|
+
attestTargets: {
|
|
119
|
+
type: string;
|
|
120
|
+
items: {
|
|
121
|
+
type: string;
|
|
122
|
+
};
|
|
123
|
+
description: string;
|
|
124
|
+
};
|
|
125
|
+
attestEvidence: {
|
|
126
|
+
type: string;
|
|
127
|
+
description: string;
|
|
128
|
+
};
|
|
129
|
+
attestOperator: {
|
|
130
|
+
type: string;
|
|
131
|
+
description: string;
|
|
132
|
+
};
|
|
133
|
+
contractFiles: {
|
|
134
|
+
type: string;
|
|
135
|
+
items: {
|
|
136
|
+
type: string;
|
|
137
|
+
};
|
|
138
|
+
description: string;
|
|
139
|
+
};
|
|
140
|
+
contextFiles: {
|
|
141
|
+
type: string;
|
|
142
|
+
description: string;
|
|
143
|
+
items: {
|
|
144
|
+
type: string;
|
|
145
|
+
properties: {
|
|
146
|
+
path: {
|
|
147
|
+
type: string;
|
|
148
|
+
};
|
|
149
|
+
startLine: {
|
|
150
|
+
type: string;
|
|
151
|
+
};
|
|
152
|
+
endLine: {
|
|
153
|
+
type: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
required: string[];
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
workspaceDir: {
|
|
160
|
+
type: string;
|
|
161
|
+
description: string;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
required: string[];
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Compile-time assertion that the schema's parameters are EXACTLY `DelegateWorkerParameter`.
|
|
170
|
+
*
|
|
171
|
+
* TWO earlier versions of this did nothing, and both failures are worth recording because they are the same
|
|
172
|
+
* mistake in different clothes:
|
|
173
|
+
*
|
|
174
|
+
* 1. A type alias asserting the same thing. An exported type alias is never checked unless referenced, so
|
|
175
|
+
* it was decoration. Verified by mutating the schema and watching tsc pass.
|
|
176
|
+
* 2. A one-directional conditional. `Record<SchemaKeys, true> extends Record<Canonical, true>` only asks
|
|
177
|
+
* whether the canonical keys are all present; structural assignability permits EXTRA properties, so a
|
|
178
|
+
* schema key that is not canonical was still accepted. Verified by mutation again.
|
|
179
|
+
*
|
|
180
|
+
* This version requires set EQUALITY: both directions, expressed as one condition so there is no ordering to
|
|
181
|
+
* get wrong. It is a value, so it is always checked, and both mutations now fail the build.
|
|
182
|
+
*/
|
|
183
|
+
type SchemaKeys = Extract<keyof (typeof DELEGATE_WORKER_OPENAI_SCHEMA)['function']['parameters']['properties'], string>;
|
|
184
|
+
type SchemaMatchesCanonicalParameters = [SchemaKeys] extends [DelegateWorkerParameter] ? [DelegateWorkerParameter] extends [SchemaKeys] ? true : never : never;
|
|
185
|
+
export declare const DELEGATE_WORKER_SCHEMA_COVERS_EVERY_PARAMETER: SchemaMatchesCanonicalParameters;
|
|
186
|
+
/**
|
|
187
|
+
* How many times a contract should run, from a caller-supplied value that cannot be trusted.
|
|
188
|
+
*
|
|
189
|
+
* **The default is 3, and that is a deliberate break with the previous behaviour.** It used to be 1,
|
|
190
|
+
* chosen to guarantee no caller saw a change. That was backwards for an integrity boundary: it left a
|
|
191
|
+
* non-deterministic oracle unprotected unless the caller happened to KNOW the contract was flaky -- and
|
|
192
|
+
* the caller who does not know is exactly the one whose race condition gets promoted by a lucky pass.
|
|
193
|
+
* A guardrail that has to be opted into is not a guardrail.
|
|
194
|
+
*
|
|
195
|
+
* The cost is real and worth naming: every delegated verification now runs three subprocesses instead of
|
|
196
|
+
* one. A deterministic suite pays that to prove its own stability, which is not wasted -- three agreeing
|
|
197
|
+
* runs are strictly more evidence than one. A caller who has measured their suite and wants the speed
|
|
198
|
+
* back passes `verificationRepeats: 1` explicitly, which is the honest way round: the fast path is the
|
|
199
|
+
* one that requires a decision.
|
|
200
|
+
*
|
|
201
|
+
* The ceiling is not cosmetic. Each repeat is a real subprocess with the host process's authority, so an
|
|
202
|
+
* unbounded value would be a denial-of-service lever through a tool argument.
|
|
203
|
+
*/
|
|
204
|
+
export declare const DEFAULT_VERIFICATION_REPEATS = 3;
|
|
205
|
+
export declare const MAX_VERIFICATION_REPEATS = 20;
|
|
206
|
+
export declare function resolveVerificationRepeats(value: unknown): number;
|
|
207
|
+
export interface DelegateWorkerParams {
|
|
208
|
+
instruction?: string;
|
|
209
|
+
taskPrompt?: string;
|
|
210
|
+
prompt?: string;
|
|
211
|
+
taskName?: string;
|
|
212
|
+
targetFiles?: string[] | string;
|
|
213
|
+
fileContext?: string;
|
|
214
|
+
systemPrompt?: string;
|
|
215
|
+
runVerification?: string;
|
|
216
|
+
endpoint?: string;
|
|
217
|
+
model?: string;
|
|
218
|
+
turnId?: number;
|
|
219
|
+
timeoutMs?: number;
|
|
220
|
+
workspaceDir?: string;
|
|
221
|
+
workspaceSource?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Paths to the tests that constitute this unit's contract. They are hashed before the worker
|
|
224
|
+
* runs, refused as worker emission targets, and re-hashed afterwards: any change voids the
|
|
225
|
+
* verdict. The architect owns these files and the executor never may, which is what stops a
|
|
226
|
+
* unit from certifying itself.
|
|
227
|
+
*/
|
|
228
|
+
contractFiles?: string[];
|
|
229
|
+
/**
|
|
230
|
+
* Existing files the worker needs to see, named by path with optional 1-based line ranges. The
|
|
231
|
+
* plugin reads them and puts them in the worker's prompt; the architect never receives contents,
|
|
232
|
+
* only a record of what was injected. This is the transport that lets a unit close on existing
|
|
233
|
+
* code without the code entering the architect's context.
|
|
234
|
+
*/
|
|
235
|
+
contextFiles?: ContextRequest[];
|
|
236
|
+
/** Set false to return raw verification output. Raw output can carry source. */
|
|
237
|
+
redactVerification?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* How many times to run `runVerification`. Defaults to 3: a contract that runs three times and agrees
|
|
240
|
+
* with itself has proved its own stability, and one that disagrees produces `UNIT_FLAKY` instead of a
|
|
241
|
+
* pass. Raise it for a genuinely rare race; pass 1 only for a suite you have measured as deterministic
|
|
242
|
+
* and whose three-fold cost you are deliberately declining.
|
|
243
|
+
*/
|
|
244
|
+
verificationRepeats?: number;
|
|
245
|
+
/** Policy for the model-supplied `runVerification` command. */
|
|
246
|
+
verificationPolicy?: VerificationPolicy;
|
|
247
|
+
/**
|
|
248
|
+
* Approval callback used when the policy resolves to `ask`. Absent means no approver is
|
|
249
|
+
* reachable, which refuses the command rather than running it unattended.
|
|
250
|
+
*/
|
|
251
|
+
verificationApproval?: (command: string) => Promise<boolean>;
|
|
252
|
+
/** Extra roots the worker may write into beyond `workspaceDir`. */
|
|
253
|
+
emitAllowlist?: string[];
|
|
254
|
+
/**
|
|
255
|
+
* What to do when the worker writes a file the unit did not declare in `targetFiles`. `'enforce'`
|
|
256
|
+
* (the default) refuses it and reports why; `'off'` restores the permissive behaviour that predates
|
|
257
|
+
* the boundary.
|
|
258
|
+
*
|
|
259
|
+
* Enforced by default because declaring targets is what makes a later project-level failure
|
|
260
|
+
* attributable: without it, a unit can sprawl and no record says which unit broke the tree.
|
|
261
|
+
*/
|
|
262
|
+
unitScope?: 'enforce' | 'off';
|
|
263
|
+
/**
|
|
264
|
+
* Whether the previous failure's locations are offered to this attempt as context. `'auto'` (the
|
|
265
|
+
* default) adds a window around each file the last failure named, once; `'off'` disables it.
|
|
266
|
+
*
|
|
267
|
+
* Bounded three ways: it is consumed by a single attempt, it never overrides a file the caller already
|
|
268
|
+
* declared, and it is dropped entirely if adding it would push the injection over its byte budget.
|
|
269
|
+
*/
|
|
270
|
+
retryContext?: 'auto' | 'off';
|
|
271
|
+
}
|
|
272
|
+
/** Where the local worker is assumed to live when nothing else is configured. */
|
|
273
|
+
export declare const DEFAULT_LOCAL_ENDPOINT = "http://127.0.0.1:1234/v1";
|
|
274
|
+
/**
|
|
275
|
+
* Accept either a base URL or a full chat-completions URL and return the full one, so
|
|
276
|
+
* `localEndpoint: 'http://127.0.0.1:11434/v1'` (Ollama, vLLM, llama.cpp, …) works exactly as
|
|
277
|
+
* written without the operator having to know this plugin appends the path.
|
|
278
|
+
*/
|
|
279
|
+
export declare function resolveChatCompletionsUrl(base: string): string;
|
|
280
|
+
/** A delta block: the bytes to find, and what to put there instead. */
|
|
281
|
+
export interface SearchReplaceBlock {
|
|
282
|
+
search: string;
|
|
283
|
+
replace: string;
|
|
284
|
+
}
|
|
285
|
+
export interface PatchResult {
|
|
286
|
+
ok: boolean;
|
|
287
|
+
content?: string;
|
|
288
|
+
reason?: string;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A one- or two-character search is unique by accident rather than by intent, so it is refused even
|
|
292
|
+
* when the exactly-once rule would allow it. The property that matters is exactness, not cleverness.
|
|
293
|
+
*/
|
|
294
|
+
export declare const MIN_SEARCH_CHARS = 8;
|
|
295
|
+
/**
|
|
296
|
+
* Recognise a search/replace body. The fenced header is shared with whole-file emission, so the body
|
|
297
|
+
* decides the mode and the worker does not have to know which of the two it is producing.
|
|
298
|
+
*
|
|
299
|
+
* Returns null when there is no delta here, which tells the caller to treat the body as a whole file.
|
|
300
|
+
* A body that *starts* a search/replace block but never finishes it returns an empty list instead --
|
|
301
|
+
* never null -- so a malformed patch cannot fall through and be written over a real file.
|
|
302
|
+
*/
|
|
303
|
+
export declare function parseSearchReplaceBlocks(body: string): SearchReplaceBlock[] | null;
|
|
304
|
+
/**
|
|
305
|
+
* Apply every block, or none. A partially applied change is worse than no change: it leaves the tree
|
|
306
|
+
* in a state that no contract was written against.
|
|
307
|
+
*
|
|
308
|
+
* Everything is normalised to LF for matching and the file's own ending is restored at the end.
|
|
309
|
+
* Nothing else is normalised -- indentation is bytes -- because a near miss must fail loudly rather
|
|
310
|
+
* than be massaged into a match. Fuzzy patching is not a tuning choice here; it is the mechanism by
|
|
311
|
+
* which a wrong edit lands silently.
|
|
312
|
+
*/
|
|
313
|
+
export declare function applySearchReplaceBlocks(content: string, blocks: SearchReplaceBlock[]): PatchResult;
|
|
314
|
+
/**
|
|
315
|
+
* Status precedence, as a pure function so the ordering is testable without a server. Tampering
|
|
316
|
+
* outranks everything: a modified contract voids the run even when verification passed, because
|
|
317
|
+
* what passed was no longer the contract.
|
|
318
|
+
*/
|
|
319
|
+
export declare function resolveDelegateStatus(input: {
|
|
320
|
+
verificationGate?: string | null;
|
|
321
|
+
unverified: boolean;
|
|
322
|
+
contractViolations: string[];
|
|
323
|
+
isSuccess: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* The project's own check ran and failed, whatever the unit's own contract said. Absent means no
|
|
326
|
+
* coherence check was configured, or it was skipped -- never "it passed".
|
|
327
|
+
*/
|
|
328
|
+
coherenceFailed?: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* The contract disagreed with itself across repeated runs. A statement about the ORACLE, not the
|
|
331
|
+
* code, and it is checked before the coherence result for the same reason `verificationGate` is:
|
|
332
|
+
* there is no trustworthy unit verdict to compare the project against.
|
|
333
|
+
*/
|
|
334
|
+
flaky?: boolean;
|
|
335
|
+
}): string;
|
|
336
|
+
export declare function delegateWorker(params?: DelegateWorkerParams, tracker?: SavingsTracker): Promise<any>;
|
|
337
|
+
export declare function extractPromptText(session: LLMSession | any): string;
|
|
338
|
+
export declare function estimateTokenCount(text: string): number;
|
|
339
|
+
export {};
|