@doist/doistbot-cli 1.0.14 → 1.0.15
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/package.json +2 -2
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +2 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +8 -2
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +59 -0
- package/sandbox/dist/core/pi.js +17 -14
- package/sandbox/dist/core/tool-environment.js +22 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +4 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +53 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +3 -1
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +2 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +8 -2
- package/sandbox/src/review/prompts/service-production-readiness.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/doistbot-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Local self-review CLI for Doistbot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"commander": "14.0.3",
|
|
52
52
|
"js-yaml": "4.3.2",
|
|
53
53
|
"octokit": "5.0.5",
|
|
54
|
-
"undici": "8.
|
|
54
|
+
"undici": "8.11.2",
|
|
55
55
|
"yocto-spinner": "1.2.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -7,6 +7,8 @@ export type LoggerOptions = {
|
|
|
7
7
|
suppressLogs?: (level: LogLevel) => boolean;
|
|
8
8
|
suppressInfoLogs?: () => boolean;
|
|
9
9
|
includeLogField?: boolean;
|
|
10
|
+
/** Receives serialized JSON without a trailing newline. The writer must add one. */
|
|
11
|
+
write?: (line: string) => void;
|
|
10
12
|
buildInfo?: {
|
|
11
13
|
version: number;
|
|
12
14
|
commit: string;
|
|
@@ -33,8 +33,14 @@ export function createLogger(options = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
Object.assign(payload, traceInfo);
|
|
35
35
|
payload.meta = metaInfo;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const line = JSON.stringify(payload);
|
|
37
|
+
if (options.write) {
|
|
38
|
+
options.write(line);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(line);
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
return {
|
|
40
46
|
info: (message, meta) => log('info', message, meta),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writeSync } from 'node:fs';
|
|
2
|
+
import { scrubPii } from './pii-scrubber.js';
|
|
3
|
+
// Pi applies tool_result changes before sending the result to the model. Filter
|
|
4
|
+
// all text tools so redirecting CLI output to a file cannot bypass filtering.
|
|
5
|
+
// eslint-disable-next-line import/no-default-export
|
|
6
|
+
export default function toolOutputFilterExtension(pi) {
|
|
7
|
+
const secrets = Object.entries(process.env)
|
|
8
|
+
.filter(([key, value]) => /(?:KEY|TOKEN|PASSWORD|SECRET)$/.test(key) && value && value.length >= 8)
|
|
9
|
+
.map(([, value]) => value);
|
|
10
|
+
function filterText(value) {
|
|
11
|
+
for (const secret of secrets)
|
|
12
|
+
value = value.split(secret).join('[REDACTED_SECRET]');
|
|
13
|
+
return scrubPii(value);
|
|
14
|
+
}
|
|
15
|
+
function filterValue(value) {
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return filterText(value);
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(filterValue);
|
|
20
|
+
if (value && typeof value === 'object')
|
|
21
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [filterText(key), filterValue(entry)]));
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
pi.on('tool_call', (event) => {
|
|
25
|
+
try {
|
|
26
|
+
// Pi redirects process.stdout writes to stderr in JSON mode.
|
|
27
|
+
// Write this protocol record directly to its stdout pipe.
|
|
28
|
+
writeSync(1, `${JSON.stringify({
|
|
29
|
+
type: 'doistbot_tool_audit',
|
|
30
|
+
tool: event.toolName,
|
|
31
|
+
// Audit queries/commands, never service response bodies.
|
|
32
|
+
input: JSON.stringify(filterValue(event.input)).slice(0, 8_000),
|
|
33
|
+
})}\n`);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { block: true, reason: 'Tool query could not be safely recorded.' };
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
});
|
|
40
|
+
pi.on('tool_result', (event) => {
|
|
41
|
+
try {
|
|
42
|
+
return {
|
|
43
|
+
content: event.content.map((item) => item.type === 'text' ? { ...item, text: filterText(item.text) } : item),
|
|
44
|
+
details: filterValue(event.details ?? null),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Pi otherwise ignores extension errors and keeps the original
|
|
49
|
+
// result. Explicitly replace it so a filter failure cannot leak text.
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{ type: 'text', text: 'Tool output withheld: filtering failed.' },
|
|
53
|
+
],
|
|
54
|
+
details: null,
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -3,7 +3,9 @@ import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { logger } from './logging.js';
|
|
6
7
|
import { PI_EXECUTION_LIMIT_ENV } from './pi-execution-limits-extension.js';
|
|
8
|
+
import { approvedSkillArgs, buildToolEnvironment } from './tool-environment.js';
|
|
7
9
|
export function toPiInvocationTelemetryFields(telemetry) {
|
|
8
10
|
return {
|
|
9
11
|
turn_count: telemetry.turnCount,
|
|
@@ -54,7 +56,7 @@ export const DEFAULT_PI_MODELS = {
|
|
|
54
56
|
gemini: 'gemini-3.1-pro-preview',
|
|
55
57
|
grok: 'grok-4.6',
|
|
56
58
|
zai: 'glm5.2',
|
|
57
|
-
openai: 'gpt-6-sol',
|
|
59
|
+
openai: 'gpt-6.1-sol',
|
|
58
60
|
};
|
|
59
61
|
export const DEFAULT_OPENROUTER_MODELS = {
|
|
60
62
|
deepseek: 'deepseek/deepseek-v4-pro-0813',
|
|
@@ -95,6 +97,7 @@ function resolvePiCliPath() {
|
|
|
95
97
|
}
|
|
96
98
|
const PI_CLI_PATH = resolvePiCliPath();
|
|
97
99
|
const PI_EXECUTION_LIMIT_EXTENSION_PATH = fileURLToPath(new URL('./pi-execution-limits-extension.js', import.meta.url));
|
|
100
|
+
const PI_TOOL_OUTPUT_FILTER_EXTENSION_PATH = fileURLToPath(new URL('./pi-tool-output-filter-extension.js', import.meta.url));
|
|
98
101
|
let cachedNodeExecutable;
|
|
99
102
|
let cachedOpenRouterSessionExtensionPath;
|
|
100
103
|
export function resolveNodeExecutable({ execPath = process.execPath, argv0 = process.argv0, canExecute = canExecuteNode, } = {}) {
|
|
@@ -416,7 +419,11 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
416
419
|
'json',
|
|
417
420
|
'--no-session',
|
|
418
421
|
'--no-extensions',
|
|
422
|
+
...(process.env.SANDBOX_PROFILE
|
|
423
|
+
? ['--extension', PI_TOOL_OUTPUT_FILTER_EXTENSION_PATH]
|
|
424
|
+
: []),
|
|
419
425
|
'--no-skills',
|
|
426
|
+
...approvedSkillArgs(),
|
|
420
427
|
'--no-prompt-templates',
|
|
421
428
|
...(noContextFiles ? ['--no-context-files'] : []),
|
|
422
429
|
'--model',
|
|
@@ -428,21 +435,11 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
428
435
|
...executionLimitExtension.args,
|
|
429
436
|
...imageArgs,
|
|
430
437
|
];
|
|
431
|
-
const
|
|
432
|
-
...process.env,
|
|
433
|
-
};
|
|
434
|
-
// Repository-scoped credentials must be passed explicitly through
|
|
435
|
-
// extraEnv by an authorized task invocation.
|
|
436
|
-
delete inheritedEnv.GH_PACKAGES_TOKEN;
|
|
437
|
-
const childEnv = {
|
|
438
|
-
...inheritedEnv,
|
|
439
|
-
HOME: process.env.HOME,
|
|
440
|
-
PATH: process.env.PATH,
|
|
441
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
438
|
+
const childEnv = buildToolEnvironment({
|
|
442
439
|
...extraEnv,
|
|
443
440
|
...openRouterSessionExtension.extraEnv,
|
|
444
441
|
...executionLimitExtension.extraEnv,
|
|
445
|
-
};
|
|
442
|
+
});
|
|
446
443
|
const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
|
|
447
444
|
if (piAuthProviderId) {
|
|
448
445
|
delete childEnv[apiKeyEnvVar];
|
|
@@ -450,7 +447,6 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
450
447
|
else {
|
|
451
448
|
childEnv[apiKeyEnvVar] = apiKey;
|
|
452
449
|
}
|
|
453
|
-
delete childEnv.GITHUB_TOKEN;
|
|
454
450
|
try {
|
|
455
451
|
const child = spawn(resolveNodeExecutable(), args, {
|
|
456
452
|
cwd,
|
|
@@ -576,6 +572,13 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
576
572
|
}
|
|
577
573
|
try {
|
|
578
574
|
const parsed = JSON.parse(trimmed);
|
|
575
|
+
if (parsed.type === 'doistbot_tool_audit') {
|
|
576
|
+
logger.info('Sandbox tool query', {
|
|
577
|
+
tool: parsed.tool,
|
|
578
|
+
input: parsed.input,
|
|
579
|
+
});
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
579
582
|
if (parsed.type === 'session') {
|
|
580
583
|
sessionHeaderSeen = true;
|
|
581
584
|
continue;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Prepare credentials for Pi shell tools. */
|
|
2
|
+
export function buildToolEnvironment(extraEnv = {}, inherited = process.env) {
|
|
3
|
+
const env = { ...inherited };
|
|
4
|
+
delete env.GH_PACKAGES_TOKEN;
|
|
5
|
+
Object.assign(env, extraEnv);
|
|
6
|
+
delete env.GITHUB_TOKEN;
|
|
7
|
+
delete env.PRIVATE_KEY;
|
|
8
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
9
|
+
env.DD_API_KEY = env.DD_API_KEY ?? env.DATADOG_API_KEY;
|
|
10
|
+
env.DD_APP_KEY = env.DD_APP_KEY ?? env.DATADOG_APP_KEY;
|
|
11
|
+
env.DD_SITE = env.DD_SITE?.trim() || env.DATADOG_SITE?.trim() || 'datadoghq.com';
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
export function approvedSkillArgs(env = process.env) {
|
|
15
|
+
if (!env.SANDBOX_PROFILE)
|
|
16
|
+
return [];
|
|
17
|
+
const skills = ['/app/apps/sandbox/skills/investigation/SKILL.md'];
|
|
18
|
+
if (env.SANDBOX_PROFILE === 'android') {
|
|
19
|
+
skills.push('/app/apps/sandbox/skills/agent-device/SKILL.md');
|
|
20
|
+
}
|
|
21
|
+
return skills.flatMap((skill) => ['--skill', skill]);
|
|
22
|
+
}
|
|
@@ -9,6 +9,7 @@ import { CX_HERO_MANUAL_HANDLE, resolveHeroGroup } from './hero-group-map.js';
|
|
|
9
9
|
import { AUTOFIX_FAILURE_MARKER, FIX_LOOP_START_MARKER, HERO_AUTOFIX_MARKER } from './markers.js';
|
|
10
10
|
import { invokeTriageModel } from './model.js';
|
|
11
11
|
import { commitAndPushFix, findPRTemplate, openDraftFixPR } from './pr-creator.js';
|
|
12
|
+
import { sandboxEnvironmentError } from './sandbox-environment.js';
|
|
12
13
|
const FIX_PROMPT_FILE = 'issue-triage-fix-prompt.md';
|
|
13
14
|
// Maximum time allowed for the Codex fix-generation invocation before we abort
|
|
14
15
|
const AUTO_FIX_CODEX_TIMEOUT_MS = 45 * 60 * 1000;
|
|
@@ -330,6 +331,9 @@ export async function runFixCore(input) {
|
|
|
330
331
|
reason: buildFixAssessmentSkipReason(fixAssessment),
|
|
331
332
|
};
|
|
332
333
|
}
|
|
334
|
+
const environmentError = sandboxEnvironmentError(targetRepo.repository.fullName);
|
|
335
|
+
if (environmentError)
|
|
336
|
+
return { ok: false, reason: environmentError };
|
|
333
337
|
const capabilityEnvResult = resolveAutofixCapabilityEnv(targetRepo.repository.fullName);
|
|
334
338
|
if (!capabilityEnvResult.ok) {
|
|
335
339
|
return capabilityEnvResult;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { repositorySandboxProfile, SANDBOX_PROFILES } from 'doistbot-issue-routing';
|
|
2
|
+
function readSelection() {
|
|
3
|
+
if (!process.env.SANDBOX_SELECTION)
|
|
4
|
+
return undefined;
|
|
5
|
+
const selection = JSON.parse(process.env.SANDBOX_SELECTION);
|
|
6
|
+
function strings(value) {
|
|
7
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
8
|
+
}
|
|
9
|
+
if (!selection ||
|
|
10
|
+
!strings(selection.repositories) ||
|
|
11
|
+
!strings(selection.labels) ||
|
|
12
|
+
!strings(selection.verificationLimits) ||
|
|
13
|
+
typeof selection.reason !== 'string' ||
|
|
14
|
+
(selection.profile !== 'legacy' &&
|
|
15
|
+
(!SANDBOX_PROFILES.includes(selection.profile) || typeof selection.image !== 'string')))
|
|
16
|
+
throw new Error('Invalid sandbox selection.');
|
|
17
|
+
return selection;
|
|
18
|
+
}
|
|
19
|
+
/** Check image identity only. Repo setup and required checks belong to issue #524. */
|
|
20
|
+
export function sandboxEnvironmentError(repository) {
|
|
21
|
+
let selection;
|
|
22
|
+
try {
|
|
23
|
+
selection = readSelection();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 'Invalid sandbox selection.';
|
|
27
|
+
}
|
|
28
|
+
if (!selection)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!selection.repositories.some((candidate) => candidate.toLowerCase() === repository.toLowerCase()))
|
|
31
|
+
return 'Fix target is outside the recorded sandbox scope.';
|
|
32
|
+
// Mixed scopes deliberately retain the existing image and autofix behavior.
|
|
33
|
+
if (selection.profile === 'legacy')
|
|
34
|
+
return undefined;
|
|
35
|
+
const profile = repositorySandboxProfile(repository, selection.labels);
|
|
36
|
+
if (selection.profile !== profile ||
|
|
37
|
+
process.env.SANDBOX_PROFILE !== profile ||
|
|
38
|
+
process.env.SANDBOX_IMAGE_ID !== selection.image)
|
|
39
|
+
return 'Unsupported fix environment: target needs a different image. Cross-image fix dispatch is not enabled.';
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
export function describeSandboxEnvironment() {
|
|
43
|
+
let selection;
|
|
44
|
+
try {
|
|
45
|
+
selection = readSelection();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return '\n\nSandbox selection is invalid; automatic fixes are unavailable.';
|
|
49
|
+
}
|
|
50
|
+
if (!selection)
|
|
51
|
+
return '';
|
|
52
|
+
return `\n\n## Sandbox environment\nProfile: ${selection.profile}. ${selection.reason}\n${selection.verificationLimits.join('\n')}\nInstalled tools do not prove that repository builds or tests passed. Report only checks actually performed.`;
|
|
53
|
+
}
|
|
@@ -17,6 +17,7 @@ import { fillTemplate, loadPromptTemplate } from './prompt.js';
|
|
|
17
17
|
import { allRepositoriesAllow, createRepoConfigLoader, isAutoFixEnabled, isAutoTriageEnabled, } from './repo-config.js';
|
|
18
18
|
import { resolveTriageScope } from './resolution.js';
|
|
19
19
|
import { buildHeroRoutingComment, postTriageRouting, shouldPostHeroRoutingComment, } from './routing.js';
|
|
20
|
+
import { describeSandboxEnvironment } from './sandbox-environment.js';
|
|
20
21
|
import { createGitHubClients, getIssueUrl, prepareWorkspaceReposDirectory } from './shared.js';
|
|
21
22
|
import { deleteAutoTriagePlaceholder, postTriageOutput } from './side-effects.js';
|
|
22
23
|
const MAX_ISSUE_BODY_CHARS = 8_000;
|
|
@@ -238,7 +239,7 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
238
239
|
type: 'installation',
|
|
239
240
|
installationId: ctx.installationId,
|
|
240
241
|
}));
|
|
241
|
-
|
|
242
|
+
let prompt = buildPrompt({
|
|
242
243
|
ctx,
|
|
243
244
|
issue,
|
|
244
245
|
comments,
|
|
@@ -247,6 +248,7 @@ async function runIssueTriagePipeline(ctx, mode) {
|
|
|
247
248
|
decisionPath: resolution.decisionPath,
|
|
248
249
|
enableAutoFix,
|
|
249
250
|
});
|
|
251
|
+
prompt += describeSandboxEnvironment();
|
|
250
252
|
const triageResult = await invokeTriageModel({
|
|
251
253
|
prompt,
|
|
252
254
|
apiKey: ctx.openaiApiKey,
|
|
@@ -7,6 +7,8 @@ export type LoggerOptions = {
|
|
|
7
7
|
suppressLogs?: (level: LogLevel) => boolean;
|
|
8
8
|
suppressInfoLogs?: () => boolean;
|
|
9
9
|
includeLogField?: boolean;
|
|
10
|
+
/** Receives serialized JSON without a trailing newline. The writer must add one. */
|
|
11
|
+
write?: (line: string) => void;
|
|
10
12
|
buildInfo?: {
|
|
11
13
|
version: number;
|
|
12
14
|
commit: string;
|
|
@@ -33,8 +33,14 @@ export function createLogger(options = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
Object.assign(payload, traceInfo);
|
|
35
35
|
payload.meta = metaInfo;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const line = JSON.stringify(payload);
|
|
37
|
+
if (options.write) {
|
|
38
|
+
options.write(line);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(line);
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
return {
|
|
40
46
|
info: (message, meta) => log('info', message, meta),
|
|
@@ -73,6 +73,8 @@ Any state that must survive restarts must be stored externally, e.g.:
|
|
|
73
73
|
|
|
74
74
|
Local disk may be used only for temporary data.
|
|
75
75
|
|
|
76
|
+
Durable state must be backed up. DynamoDB tables must have [point-in-time recovery](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Point-in-time-recovery.html) enabled and carry the account's `Backup` tag so the AWS Backup plan selects them. Caches, lock tables and short-lived session tables with a TTL are exempt.
|
|
77
|
+
|
|
76
78
|
## Resource Limits
|
|
77
79
|
|
|
78
80
|
Define expected CPU, memory, network, and local disk usage.
|