@fitlab-ai/agent-infra 0.11.10 → 0.11.11
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/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +1 -3
- package/dist/lib/internal/task-operation-registry.js +4 -138
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/pull-requests.js +79 -14
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +46 -12
- package/dist/lib/sandbox/control/executor.js +4 -11
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +7 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -1
- package/dist/lib/task/capabilities.js +4 -1
- package/dist/lib/task/control-authority.js +0 -2
- package/dist/lib/task/events.js +1 -1
- package/dist/lib/task/finalization.js +37 -54
- package/dist/lib/task/frontmatter.js +24 -1
- package/dist/lib/task/ledger-intents.js +2 -1
- package/dist/lib/task/platform-operation-journal.js +12 -0
- package/dist/lib/task/platform-operation-recovery.js +14 -7
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +1 -3
- package/lib/internal/task-operation-registry.ts +4 -150
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/pull-requests.ts +74 -13
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +52 -15
- package/lib/sandbox/control/executor.ts +5 -12
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +9 -2
- package/lib/task/artifact-lifecycle.ts +2 -1
- package/lib/task/capabilities.ts +5 -2
- package/lib/task/control-authority.ts +0 -4
- package/lib/task/events.ts +1 -1
- package/lib/task/finalization.ts +34 -51
- package/lib/task/frontmatter.ts +25 -1
- package/lib/task/ledger-intents.ts +2 -1
- package/lib/task/platform-operation-journal.ts +29 -2
- package/lib/task/platform-operation-recovery.ts +12 -6
- package/lib/task/resolve-ref.ts +8 -3
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/lib/task/finalization-handoff.ts +0 -105
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { AGENT_CLIENT_CAPABILITY_IDS, AGENT_CLIENT_SUPPORT_LEVELS, isAgentClientId } from './types.js';
|
|
2
3
|
import { SANDBOX_HOOK_PHASES } from '../sandbox/tool-types.js';
|
|
3
4
|
const PROJECT_ASSET_CATEGORIES = ['managed', 'merged', 'ejected'];
|
|
@@ -65,6 +66,24 @@ function freezeTool(tool) {
|
|
|
65
66
|
if (!tool.containerMount.startsWith('/')) {
|
|
66
67
|
throw new Error(`Sandbox tool '${tool.id}' containerMount must be absolute`);
|
|
67
68
|
}
|
|
69
|
+
if (tool.hostStateMounts !== undefined && !Array.isArray(tool.hostStateMounts)) {
|
|
70
|
+
throw new Error(`Agent Client '${tool.id}' hostStateMounts must be an array`);
|
|
71
|
+
}
|
|
72
|
+
const hostStateMounts = tool.hostStateMounts?.map((entry) => {
|
|
73
|
+
if (!entry || typeof entry.hostSubdir !== 'string' || typeof entry.containerSubpath !== 'string') {
|
|
74
|
+
throw new Error(`Agent Client '${tool.id}' hostStateMounts must contain relative path pairs`);
|
|
75
|
+
}
|
|
76
|
+
for (const value of [entry.hostSubdir, entry.containerSubpath]) {
|
|
77
|
+
const normalized = path.posix.normalize(value);
|
|
78
|
+
if (value === ''
|
|
79
|
+
|| path.posix.isAbsolute(value)
|
|
80
|
+
|| normalized === '..'
|
|
81
|
+
|| normalized.startsWith('../')) {
|
|
82
|
+
throw new Error(`Agent Client '${tool.id}' hostStateMounts must stay within the tool directories`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze({ ...entry });
|
|
86
|
+
});
|
|
68
87
|
if (!tool.versionCmd) {
|
|
69
88
|
throw new Error(`Sandbox tool '${tool.id}' requires versionCmd`);
|
|
70
89
|
}
|
|
@@ -82,6 +101,7 @@ function freezeTool(tool) {
|
|
|
82
101
|
...(tool.hostLiveMounts ? {
|
|
83
102
|
hostLiveMounts: Object.freeze(tool.hostLiveMounts.map((entry) => Object.freeze({ ...entry })))
|
|
84
103
|
} : {}),
|
|
104
|
+
...(hostStateMounts ? { hostStateMounts: Object.freeze(hostStateMounts) } : {}),
|
|
85
105
|
...(tool.postSetupCmds ? { postSetupCmds: Object.freeze([...tool.postSetupCmds]) } : {}),
|
|
86
106
|
...(tool.tmpfs ? {
|
|
87
107
|
tmpfs: Object.freeze({
|
|
@@ -241,6 +241,19 @@ const codexRecoveryChecks = [
|
|
|
241
241
|
path: '/home/devuser/.codex'
|
|
242
242
|
}
|
|
243
243
|
},
|
|
244
|
+
{
|
|
245
|
+
id: 'app-server-package-writable',
|
|
246
|
+
probe: {
|
|
247
|
+
script: 'probe="$1/.agent-infra-app-server-package-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
|
|
248
|
+
args: ['/home/devuser/.codex/packages/app-server-daemon'],
|
|
249
|
+
user: 'devuser'
|
|
250
|
+
},
|
|
251
|
+
finding: {
|
|
252
|
+
repairKind: 'hard-failure',
|
|
253
|
+
message: 'Codex app-server package directory is not writable by devuser.',
|
|
254
|
+
path: '/home/devuser/.codex/packages/app-server-daemon'
|
|
255
|
+
}
|
|
256
|
+
},
|
|
244
257
|
{
|
|
245
258
|
id: 'prompts-link',
|
|
246
259
|
when: {
|
|
@@ -51,6 +51,10 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
51
51
|
versionCmd: 'codex --version',
|
|
52
52
|
setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
|
|
53
53
|
tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
|
|
54
|
+
hostStateMounts: [{
|
|
55
|
+
hostSubdir: 'packages/app-server-daemon',
|
|
56
|
+
containerSubpath: 'packages/app-server-daemon'
|
|
57
|
+
}],
|
|
54
58
|
hostLiveMounts: [
|
|
55
59
|
{
|
|
56
60
|
hostPath: hostJoin(home, '.codex', 'auth.json'),
|
|
@@ -196,7 +196,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
|
|
|
196
196
|
if (transaction.phase === 'prepared') {
|
|
197
197
|
const staged = await syncPullRequestSummary(taskRef, {
|
|
198
198
|
cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
|
|
199
|
-
manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
|
|
199
|
+
manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
|
|
200
200
|
});
|
|
201
201
|
if (!['applied', 'no-op'].includes(staged.status))
|
|
202
202
|
transactionFailure(staged.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_FAILED', message: 'pending summary staging failed' });
|
|
@@ -261,7 +261,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
|
|
|
261
261
|
try {
|
|
262
262
|
await syncPullRequestSummary(taskRef, {
|
|
263
263
|
cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
|
|
264
|
-
manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
|
|
264
|
+
manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
catch {
|
|
@@ -10,7 +10,7 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
|
|
|
10
10
|
agent-infra-internal platform-pr bind <task-ref> --pr <token> --agent <agent> [--dry-run] [--cwd <path>]
|
|
11
11
|
agent-infra-internal platform-pr skip <task-ref> --agent <agent> [--dry-run] [--cwd <path>]
|
|
12
12
|
agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
|
|
13
|
-
agent-infra-internal platform-pr sync-in-labels --pr <
|
|
13
|
+
agent-infra-internal platform-pr sync-in-labels --pr <token> [--dry-run] [--cwd <path>]
|
|
14
14
|
agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
|
|
15
15
|
agent-infra-internal platform-pr change-report <task-ref> --agent <agent> --mechanical-file <path> --precheck-file <path> [--dry-run] [--cwd <path>]
|
|
16
16
|
agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> --change-report-file <path> --result <pr_created|pr_reused|no_op> [--manual-phase pending|final ...] [--strict] [--dry-run] [--cwd <path>]
|
|
@@ -91,13 +91,12 @@ async function platformPr(args = []) {
|
|
|
91
91
|
fail(`sync-in-labels does not accept --${unexpected}`);
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
fail('sync-in-labels requires a positive --pr');
|
|
94
|
+
if (typeof values.pr !== 'string' || !values.pr || values.pr.trim() !== values.pr) {
|
|
95
|
+
fail('sync-in-labels requires a non-empty --pr token');
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
98
|
const cwd = path.resolve(typeof values.cwd === 'string' ? values.cwd : process.cwd());
|
|
100
|
-
finish(await syncPlatformPullRequestInLabels(pr, { cwd, dryRun: values.dryRun === true }));
|
|
99
|
+
finish(await syncPlatformPullRequestInLabels(values.pr, { cwd, dryRun: values.dryRun === true }));
|
|
101
100
|
return;
|
|
102
101
|
}
|
|
103
102
|
const taskRef = args[1];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
|
|
2
2
|
import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
|
|
3
|
-
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
|
|
3
|
+
const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--reason-code <reason>] [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
|
|
4
4
|
function taskArtifact(args = []) {
|
|
5
5
|
if (!ensureInternalHandlerRoute('task-artifact', args))
|
|
6
6
|
return;
|
|
@@ -54,7 +54,21 @@ async function taskCreate(args) {
|
|
|
54
54
|
throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
|
|
55
55
|
if (stat.size > SANDBOX_CONTROL_MAX_BYTES)
|
|
56
56
|
throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
|
|
57
|
-
const
|
|
57
|
+
const inputContent = fs.readFileSync(inputPath, 'utf8');
|
|
58
|
+
let candidate;
|
|
59
|
+
try {
|
|
60
|
+
candidate = validateTaskCreateCandidate(JSON.parse(inputContent));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
|
|
65
|
+
output(failed(code, message, false, {
|
|
66
|
+
requestId: null,
|
|
67
|
+
accepted: false,
|
|
68
|
+
recovery: 'new-request-id'
|
|
69
|
+
}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
58
72
|
const environment = resolveSandboxControlTransport();
|
|
59
73
|
if (environment.kind === 'fail-closed') {
|
|
60
74
|
output(failed(environment.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID', 'sandbox client control configuration is invalid'));
|
|
@@ -65,9 +65,7 @@ async function taskFinalization(args = []) {
|
|
|
65
65
|
const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
|
|
66
66
|
if (transport.kind === 'sandbox-local') {
|
|
67
67
|
try {
|
|
68
|
-
const response = requestSandboxTaskFinalization({
|
|
69
|
-
agent: boundOperation.request.agent
|
|
70
|
-
});
|
|
68
|
+
const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
|
|
71
69
|
process.stdout.write(response.stdout);
|
|
72
70
|
process.stderr.write(response.stderr);
|
|
73
71
|
process.exitCode = response.exitCode ?? 1;
|
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { accessSandboxTaskView, parseSandboxTaskView, taskViewFromStatus } from '../sandbox/control/task-view.js';
|
|
4
4
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
5
|
import { PUBLIC_CLI_COMMAND_ALIASES, PUBLIC_CLI_SELECTOR_ALIASES, internalRouteSelector } from './cli-route-inventory.js';
|
|
6
|
-
import {
|
|
6
|
+
import { hasSandboxControlMarker, resolveSandboxControlTransport, sandboxTaskMarkerState } from '../sandbox/environment.js';
|
|
7
|
+
export { resolveSandboxControlTransport } from '../sandbox/environment.js';
|
|
7
8
|
const descriptor = (dispatcher, command, selector, scope, effect, taskRefSource = scope === 'non-task' ? 'none' : 'argv') => ({ dispatcher, command, selector, scope, effect, taskRefSource, guardBeforeImport: true });
|
|
8
9
|
function internalTaskRoutes() {
|
|
9
10
|
return [
|
|
@@ -232,141 +233,6 @@ export function resolveDelegatedTaskOperation(args) {
|
|
|
232
233
|
return null;
|
|
233
234
|
return resolveTaskOperation('internal', rest[0], rest.slice(1));
|
|
234
235
|
}
|
|
235
|
-
const TASK_MARKER_KEYS = [
|
|
236
|
-
'AGENT_INFRA_TASK_ID',
|
|
237
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
238
|
-
'AGENT_INFRA_CONTROL_GENERATION',
|
|
239
|
-
'AGENT_INFRA_CONTROL_DIR',
|
|
240
|
-
'AGENT_INFRA_CONTROL_STATUS_DIR',
|
|
241
|
-
'AGENT_INFRA_RUNTIME_DIR'
|
|
242
|
-
];
|
|
243
|
-
const TASK_CONTROL_MARKER_KEYS = [
|
|
244
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
245
|
-
'AGENT_INFRA_CONTROL_GENERATION',
|
|
246
|
-
'AGENT_INFRA_CONTROL_DIR',
|
|
247
|
-
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
248
|
-
];
|
|
249
|
-
const TASK_CONTROL_CONFIG_KEYS = [
|
|
250
|
-
'AGENT_INFRA_CONTROL_TOKEN',
|
|
251
|
-
'AGENT_INFRA_CONTROL_GENERATION',
|
|
252
|
-
'AGENT_INFRA_CONTROL_ROOT_ID',
|
|
253
|
-
'AGENT_INFRA_CONTROL_DIR',
|
|
254
|
-
'AGENT_INFRA_CONTROL_STATUS_DIR'
|
|
255
|
-
];
|
|
256
|
-
export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
|
|
257
|
-
function nativeDirectoryProbe(candidate) {
|
|
258
|
-
try {
|
|
259
|
-
// The fixed mount is a trust anchor. Do not use a mutable node:fs export:
|
|
260
|
-
// a preload must not turn a mounted sandbox into the direct-host path by
|
|
261
|
-
// replacing the ordinary filesystem probe.
|
|
262
|
-
const binding = process.binding('fs');
|
|
263
|
-
if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
|
|
264
|
-
return 'unknown';
|
|
265
|
-
const result = binding.internalModuleStat(candidate);
|
|
266
|
-
if (result === 1)
|
|
267
|
-
return 'present';
|
|
268
|
-
if (result === -2)
|
|
269
|
-
return 'absent';
|
|
270
|
-
return 'unknown';
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
return 'unknown';
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
function taskMarkerState(env) {
|
|
277
|
-
const present = (key) => Boolean(env[key]);
|
|
278
|
-
const taskId = present('AGENT_INFRA_TASK_ID');
|
|
279
|
-
const controls = TASK_CONTROL_MARKER_KEYS.every(present);
|
|
280
|
-
const runtime = present('AGENT_INFRA_RUNTIME_DIR');
|
|
281
|
-
const any = TASK_MARKER_KEYS.some(present);
|
|
282
|
-
if (!any)
|
|
283
|
-
return 'none';
|
|
284
|
-
if (taskId && controls && runtime)
|
|
285
|
-
return 'task-bound';
|
|
286
|
-
if (!taskId && controls && !runtime)
|
|
287
|
-
return 'branch-only';
|
|
288
|
-
return 'incomplete';
|
|
289
|
-
}
|
|
290
|
-
export function resolveSandboxControlTransport(env = process.env, options = {}) {
|
|
291
|
-
const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
|
|
292
|
-
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
293
|
-
|| Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
|
|
294
|
-
const fixedStatusDir = options.statusMountPath
|
|
295
|
-
?? SANDBOX_CONTROL_STATUS_MOUNT;
|
|
296
|
-
const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
|
|
297
|
-
? nativeDirectoryProbe(fixedStatusDir) : 'absent';
|
|
298
|
-
if (fixedStatusProbe === 'unknown') {
|
|
299
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
300
|
-
}
|
|
301
|
-
const fixedStatusMounted = fixedStatusProbe === 'present';
|
|
302
|
-
// A fixed status mount is installed only in a task-bound sandbox. Its
|
|
303
|
-
// presence is not mutable by the sandbox process, so marker removal must
|
|
304
|
-
// never make that process look like a direct host invocation.
|
|
305
|
-
if (!hasAnyMarker) {
|
|
306
|
-
return fixedStatusMounted
|
|
307
|
-
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
|
|
308
|
-
: { kind: 'direct-host', reasonCode: null };
|
|
309
|
-
}
|
|
310
|
-
const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
|
|
311
|
-
if (!hasCompleteConfig) {
|
|
312
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
313
|
-
}
|
|
314
|
-
const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
315
|
-
const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
316
|
-
? nativeDirectoryProbe(configuredStatusDir) : 'absent';
|
|
317
|
-
if (configuredStatusProbe === 'unknown') {
|
|
318
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
|
|
319
|
-
}
|
|
320
|
-
const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
|
|
321
|
-
? configuredStatusProbe === 'present' ? configuredStatusDir : null
|
|
322
|
-
: fixedStatusMounted ? fixedStatusDir : null;
|
|
323
|
-
const statusMounted = statusDir !== null;
|
|
324
|
-
if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
|
|
325
|
-
return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
|
|
326
|
-
}
|
|
327
|
-
const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
|
|
328
|
-
const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
|
|
329
|
-
if (statusMounted) {
|
|
330
|
-
let sentinel;
|
|
331
|
-
try {
|
|
332
|
-
sentinel = readSandboxControlIdentitySentinel(statusDir);
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
const message = error instanceof Error ? error.message : '';
|
|
336
|
-
return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
|
|
337
|
-
? 'SANDBOX_CONTROL_IDENTITY_MISSING'
|
|
338
|
-
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
|
|
339
|
-
}
|
|
340
|
-
if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
|
|
341
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
|
|
342
|
-
}
|
|
343
|
-
if (env.AGENT_INFRA_CONTROL_ROOT_ID && sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
344
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
|
|
345
|
-
}
|
|
346
|
-
if (sentinel.mode === 'task-bound'
|
|
347
|
-
&& (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
|
|
348
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
349
|
-
}
|
|
350
|
-
if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
|
|
351
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
|
|
352
|
-
}
|
|
353
|
-
if (!hasCompleteConfig) {
|
|
354
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
355
|
-
}
|
|
356
|
-
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
357
|
-
}
|
|
358
|
-
if (!hasAnyMarker)
|
|
359
|
-
return { kind: 'direct-host', reasonCode: null };
|
|
360
|
-
if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
|
|
361
|
-
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
362
|
-
}
|
|
363
|
-
return options.localWorkflow
|
|
364
|
-
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
|
|
365
|
-
: { kind: 'broker-client', reasonCode: null };
|
|
366
|
-
}
|
|
367
|
-
export function hasTaskBoundMarker(env = process.env) {
|
|
368
|
-
return taskMarkerState(env) === 'task-bound';
|
|
369
|
-
}
|
|
370
236
|
function explicitTaskRefForOperation(dispatcher, command, args, selected) {
|
|
371
237
|
if (selected.taskRefSource === 'none' || selected.taskRefSource === 'environment')
|
|
372
238
|
return null;
|
|
@@ -446,7 +312,7 @@ function isSandboxExecutorRoute(dispatcher, command, args, env) {
|
|
|
446
312
|
&& first(args) === 'execute'
|
|
447
313
|
&& Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST)
|
|
448
314
|
&& Boolean(env.AGENT_INFRA_RUNTIME_DIR)
|
|
449
|
-
&& !
|
|
315
|
+
&& !hasSandboxControlMarker(env);
|
|
450
316
|
}
|
|
451
317
|
function viewFromEnvironment(env) {
|
|
452
318
|
const statusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
@@ -492,7 +358,7 @@ export function guardTaskOperation(dispatcher, command, args = [], options = {})
|
|
|
492
358
|
taskView: null
|
|
493
359
|
};
|
|
494
360
|
}
|
|
495
|
-
const markerState =
|
|
361
|
+
const markerState = sandboxTaskMarkerState(env);
|
|
496
362
|
if (markerState === 'incomplete' && !isSandboxExecutorRoute(dispatcher, command, args, env)) {
|
|
497
363
|
throw new TaskViewOperationError('SANDBOX_TASK_VIEW_MARKER_INVALID', 'sandbox task markers are incomplete; refusing to infer host-direct authority');
|
|
498
364
|
}
|
|
@@ -16,6 +16,7 @@ import { resourceIdentityEquals } from './resource-identity.js';
|
|
|
16
16
|
import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
|
|
17
17
|
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
|
|
18
18
|
import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
|
|
19
|
+
import { readManualValidationTransaction } from '../task/manual-validation-transaction.js';
|
|
19
20
|
function summaryMarker(taskId) {
|
|
20
21
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
21
22
|
}
|
|
@@ -119,6 +120,20 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
|
|
|
119
120
|
return { ok: false, status: inspectionError(error), error };
|
|
120
121
|
}
|
|
121
122
|
const remote = inspected.value;
|
|
123
|
+
if (!remote.identity) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
status: 'failed',
|
|
127
|
+
error: { code: 'PR_IDENTITY_MISSING', message: 'Provider pull-request inspection did not return a canonical identity', retryable: false }
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!resourceIdentityEquals(remote.identity, prIdentity)) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
status: 'failed',
|
|
134
|
+
error: { code: 'PR_IDENTITY_MISMATCH', message: 'Provider returned a different pull request identity than requested', retryable: false }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
122
137
|
const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
|
|
123
138
|
const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
|
|
124
139
|
const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
|
|
@@ -127,7 +142,7 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
|
|
|
127
142
|
value: {
|
|
128
143
|
repository: context.platform.repository,
|
|
129
144
|
number,
|
|
130
|
-
identity: remote.identity
|
|
145
|
+
identity: remote.identity,
|
|
131
146
|
nodeId: remote.id,
|
|
132
147
|
url: remote.displayUrl || '',
|
|
133
148
|
state: remote.state === 'closed' ? 'closed' : 'open',
|
|
@@ -261,14 +276,18 @@ async function summaryContext(taskRef, options = {}) {
|
|
|
261
276
|
artifacts: []
|
|
262
277
|
};
|
|
263
278
|
const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
|
|
264
|
-
|
|
279
|
+
let prNumber = resourceIdentityNumber(prIdentity);
|
|
265
280
|
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
266
281
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
267
282
|
let pullRequest = null;
|
|
268
283
|
if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
|
|
269
284
|
const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
270
|
-
if (inspected.ok)
|
|
285
|
+
if (inspected.ok) {
|
|
271
286
|
pullRequest = inspected.value;
|
|
287
|
+
if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
|
|
288
|
+
prNumber = inspected.value.number;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
272
291
|
}
|
|
273
292
|
const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
|
|
274
293
|
return {
|
|
@@ -493,7 +512,7 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
493
512
|
const prIdentity = boundFact.identity.resource;
|
|
494
513
|
const boundPrNumber = resourceIdentityNumber(prIdentity);
|
|
495
514
|
knownPrNumber = boundPrNumber;
|
|
496
|
-
|
|
515
|
+
let prNumber = boundPrNumber;
|
|
497
516
|
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
498
517
|
if (!initial.ok)
|
|
499
518
|
return fail(initial.status, context, initial.error);
|
|
@@ -505,9 +524,23 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
505
524
|
if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
|
|
506
525
|
return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
|
|
507
526
|
if (manual) {
|
|
508
|
-
if (
|
|
509
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a
|
|
510
|
-
|
|
527
|
+
if (!Number.isSafeInteger(initial.value.number) || initial.value.number <= 0)
|
|
528
|
+
return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot', retryable: false }, prNumber);
|
|
529
|
+
prNumber = initial.value.number;
|
|
530
|
+
knownPrNumber = prNumber;
|
|
531
|
+
if (!manual.transactionId)
|
|
532
|
+
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'manual-validation summary requires a transaction ID', retryable: false }, prNumber);
|
|
533
|
+
if (manual.phase === 'pending') {
|
|
534
|
+
const transaction = readManualValidationTransaction(resolved.taskDir, {
|
|
535
|
+
transactionId: manual.transactionId,
|
|
536
|
+
taskId: resolved.taskId,
|
|
537
|
+
prNumber,
|
|
538
|
+
prHeadSha: initial.value.head.sha
|
|
539
|
+
});
|
|
540
|
+
if (!transaction.ok)
|
|
541
|
+
return fail('failed', context, platformError(transaction.error), prNumber);
|
|
542
|
+
}
|
|
543
|
+
if (manual.phase === 'final' && (!manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
|
|
511
544
|
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
|
|
512
545
|
if (manual.phase === 'final') {
|
|
513
546
|
if (manual.authority !== 'coordinator')
|
|
@@ -688,7 +721,11 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
688
721
|
target: JSON.stringify(prIdentity),
|
|
689
722
|
expectedDigest: createHash('sha256').update(desired).digest('hex'),
|
|
690
723
|
dependency: 'deferred',
|
|
691
|
-
pullRequestSummary: {
|
|
724
|
+
pullRequestSummary: {
|
|
725
|
+
body: options.body,
|
|
726
|
+
changeReportFile: options.changeReportFile,
|
|
727
|
+
...(manual?.transactionId ? { manualValidation: { ...manual, transactionId: manual.transactionId } } : {})
|
|
728
|
+
}
|
|
692
729
|
};
|
|
693
730
|
if (options.skipQueue)
|
|
694
731
|
return publish();
|
|
@@ -317,6 +317,21 @@ function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallba
|
|
|
317
317
|
Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
|
|
318
318
|
return snapshot;
|
|
319
319
|
}
|
|
320
|
+
function inspectedPullRequestIdentityError(remote, requested) {
|
|
321
|
+
if (!remote.identity)
|
|
322
|
+
return {
|
|
323
|
+
code: 'PR_IDENTITY_MISSING',
|
|
324
|
+
message: 'Provider pull-request inspection did not return a canonical identity',
|
|
325
|
+
retryable: false
|
|
326
|
+
};
|
|
327
|
+
if (!resourceIdentityEquals(remote.identity, requested))
|
|
328
|
+
return {
|
|
329
|
+
code: 'PR_IDENTITY_MISMATCH',
|
|
330
|
+
message: 'Provider returned a different pull request identity than requested',
|
|
331
|
+
retryable: false
|
|
332
|
+
};
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
320
335
|
function pullRequestIdentity(pullRequest) {
|
|
321
336
|
return pullRequest.identity || { kind: 'number', value: pullRequest.number };
|
|
322
337
|
}
|
|
@@ -339,6 +354,9 @@ async function inspectExternalPullRequest(base, prIdentity) {
|
|
|
339
354
|
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
340
355
|
if (!inspected.ok)
|
|
341
356
|
return providerPullRequestError(base, inspected.error, prNumber);
|
|
357
|
+
const identityError = inspectedPullRequestIdentityError(inspected.value, prIdentity);
|
|
358
|
+
if (identityError)
|
|
359
|
+
return providerPullRequestError(base, identityError, prNumber);
|
|
342
360
|
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
|
|
343
361
|
return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
|
|
344
362
|
platform: base.context.platform,
|
|
@@ -392,6 +410,12 @@ async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
|
|
|
392
410
|
platform: context.platform, capabilities: context.capabilities,
|
|
393
411
|
resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
|
|
394
412
|
});
|
|
413
|
+
const identityError = inspectedPullRequestIdentityError(inspected.value, identity);
|
|
414
|
+
if (identityError)
|
|
415
|
+
return result('failed', null, null, number, {
|
|
416
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
417
|
+
resource: { kind: 'pull-request', number, identity }, error: identityError
|
|
418
|
+
});
|
|
395
419
|
return result('no-op', null, null, number, {
|
|
396
420
|
platform: context.platform, capabilities: context.capabilities,
|
|
397
421
|
resource: { kind: 'pull-request', number, identity },
|
|
@@ -448,27 +472,38 @@ function resourceIdentityLabel(identity) {
|
|
|
448
472
|
const number = resourceIdentityNumber(identity);
|
|
449
473
|
return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
|
|
450
474
|
}
|
|
451
|
-
function inLabelResource(kind, identity, before, expected, after, effect) {
|
|
452
|
-
return { kind, number:
|
|
475
|
+
function inLabelResource(kind, identity, before, expected, after, effect, displayNumber = resourceIdentityNumber(identity)) {
|
|
476
|
+
return { kind, number: displayNumber, identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
|
|
453
477
|
}
|
|
454
|
-
async function syncPlatformPullRequestInLabels(
|
|
478
|
+
async function syncPlatformPullRequestInLabels(prToken, options = {}) {
|
|
455
479
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
480
|
+
const token = String(prToken);
|
|
456
481
|
const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
|
|
457
482
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
458
483
|
const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
|
|
459
484
|
if (!loaded.ok || !usable)
|
|
460
|
-
return result(context.status, null, null,
|
|
485
|
+
return result(context.status, null, null, null, {
|
|
461
486
|
platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
|
|
462
487
|
});
|
|
463
|
-
if (!
|
|
464
|
-
return result('failed', null, null,
|
|
488
|
+
if (!token || token.trim() !== token)
|
|
489
|
+
return result('failed', null, null, null, {
|
|
465
490
|
platform: context.platform, capabilities: context.capabilities,
|
|
466
|
-
error: { code: '
|
|
491
|
+
error: { code: 'PR_IDENTITY_INVALID', message: 'PR identity token must be non-empty and trimmed', retryable: false }
|
|
467
492
|
});
|
|
468
493
|
const provider = loaded.value.provider;
|
|
469
494
|
const operationContext = providerOperationContext(loaded.value);
|
|
470
495
|
const repository = context.platform.repository;
|
|
471
|
-
|
|
496
|
+
let pullRequestIdentity;
|
|
497
|
+
try {
|
|
498
|
+
pullRequestIdentity = providerResourceToken(provider, 'pull-request', token);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
return result('failed', null, null, null, {
|
|
502
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
503
|
+
error: { code: 'PR_IDENTITY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
let prNumber = resourceIdentityNumber(pullRequestIdentity);
|
|
472
507
|
const providerFailure = (error) => result(error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error), null, null, prNumber, {
|
|
473
508
|
platform: context.platform,
|
|
474
509
|
capabilities: context.capabilities,
|
|
@@ -480,7 +515,13 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
|
480
515
|
: unsupportedProviderOperation(provider, 'changeRequests.inspect');
|
|
481
516
|
if (!inspected.ok)
|
|
482
517
|
return providerFailure(inspected.error);
|
|
483
|
-
const
|
|
518
|
+
const identityError = inspectedPullRequestIdentityError(inspected.value, pullRequestIdentity);
|
|
519
|
+
if (identityError)
|
|
520
|
+
return providerFailure(identityError);
|
|
521
|
+
if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
|
|
522
|
+
prNumber = inspected.value.number;
|
|
523
|
+
}
|
|
524
|
+
const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber ?? 0);
|
|
484
525
|
const files = provider.changeRequests?.listFiles
|
|
485
526
|
? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
|
|
486
527
|
: unsupportedProviderOperation(provider, 'changeRequests.listFiles');
|
|
@@ -549,7 +590,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
|
549
590
|
}))
|
|
550
591
|
];
|
|
551
592
|
const willWrite = context.capabilities.triage && !options.dryRun;
|
|
552
|
-
const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
|
|
593
|
+
const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op', item.number));
|
|
553
594
|
if (options.dryRun || !context.capabilities.triage)
|
|
554
595
|
return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
|
|
555
596
|
platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
|
|
@@ -611,6 +652,16 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
|
611
652
|
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
|
|
612
653
|
error: { ...providerError(reread.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
|
|
613
654
|
});
|
|
655
|
+
if (item.kind === 'pull-request') {
|
|
656
|
+
const identityError = inspectedPullRequestIdentityError(reread.value, item.identity);
|
|
657
|
+
if (identityError)
|
|
658
|
+
return result('blocked', null, issueNumber, prNumber, {
|
|
659
|
+
changed: true, platform: context.platform, capabilities: context.capabilities,
|
|
660
|
+
pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
|
|
661
|
+
evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
|
|
662
|
+
error: { ...identityError, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${identityError.message}` }
|
|
663
|
+
});
|
|
664
|
+
}
|
|
614
665
|
const after = reread.value.labels || [];
|
|
615
666
|
const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
|
|
616
667
|
state.after = after;
|
|
@@ -623,7 +674,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
|
|
|
623
674
|
error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
|
|
624
675
|
});
|
|
625
676
|
if (item.kind === 'pull-request')
|
|
626
|
-
currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber, currentPullRequest);
|
|
677
|
+
currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber ?? currentPullRequest.number, currentPullRequest);
|
|
627
678
|
}
|
|
628
679
|
return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
|
|
629
680
|
changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
|
|
@@ -856,9 +907,13 @@ async function resolveExternalPullRequest(taskRef, options) {
|
|
|
856
907
|
? await base.provider.changeRequests.inspect({
|
|
857
908
|
context: providerOperationContext(base.loadedContext),
|
|
858
909
|
target: pullRequestIdentity(selected.selected)
|
|
859
|
-
}).then((response) =>
|
|
860
|
-
|
|
861
|
-
|
|
910
|
+
}).then((response) => {
|
|
911
|
+
if (!response.ok)
|
|
912
|
+
return response;
|
|
913
|
+
const identityError = inspectedPullRequestIdentityError(response.value, pullRequestIdentity(selected.selected));
|
|
914
|
+
return identityError ? { ok: false, error: identityError }
|
|
915
|
+
: { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) };
|
|
916
|
+
})
|
|
862
917
|
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
863
918
|
if (!rechecked.ok || !rechecked.value)
|
|
864
919
|
return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
@@ -1152,6 +1207,9 @@ async function createExternalPullRequest(taskRef, options, base) {
|
|
|
1152
1207
|
: unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
|
|
1153
1208
|
if (!inspected.ok)
|
|
1154
1209
|
return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1210
|
+
const identityError = inspectedPullRequestIdentityError(inspected.value, createdIdentity);
|
|
1211
|
+
if (identityError)
|
|
1212
|
+
return withCreation(providerPullRequestError(base, identityError, number), { kind: 'created', createdByCurrentOperation: true });
|
|
1155
1213
|
const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
|
|
1156
1214
|
if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
|
|
1157
1215
|
return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -1354,6 +1412,13 @@ async function syncPlatformPullRequestImpl(taskRef, options) {
|
|
|
1354
1412
|
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1355
1413
|
message: `In-label synchronization is partial or unknown: ${reread.error.message}`
|
|
1356
1414
|
}, base.prNumber));
|
|
1415
|
+
const identityError = inspectedPullRequestIdentityError(reread.value, base.prIdentity);
|
|
1416
|
+
if (identityError)
|
|
1417
|
+
return softenFailure(providerPullRequestError(base, {
|
|
1418
|
+
...identityError,
|
|
1419
|
+
code: 'IN_LABEL_SYNC_PARTIAL',
|
|
1420
|
+
message: `Pull-request metadata synchronization is partial or unknown: ${identityError.message}`
|
|
1421
|
+
}, base.prNumber));
|
|
1357
1422
|
finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
|
|
1358
1423
|
const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
|
|
1359
1424
|
.filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
|