@yeaft/webchat-agent 1.0.525 → 1.0.527
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/local-runtime/server/index.js +2 -0
- package/local-runtime/server/routes/installer-routes.js +19 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +174 -175
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/installers/install.ps1 +264 -0
- package/local-runtime/web/installers/install.sh +150 -0
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/repo-workflow-cli.js +34 -26
- package/repo-workflow.js +0 -779
- package/service/linux.js +24 -11
- package/service/macos.js +23 -19
- package/service/windows.js +8 -7
- package/yeaft/routing/router.js +0 -135
- package/yeaft/tools/activation.js +0 -3
- package/yeaft/tools/index.js +0 -2
- package/yeaft/tools/route-forward.js +1 -19
- package/yeaft/web-bridge.js +2 -54
- package/skills/review-merge-tag/SKILL.md +0 -78
- package/yeaft/tools/repo-workflow.js +0 -80
package/service/linux.js
CHANGED
|
@@ -12,17 +12,30 @@ export function getSystemdServicePath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
|
12
12
|
return join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// systemd unit values use C-style quoting and percent specifier expansion.
|
|
16
|
+
function quoteUnit(value, { executable = false } = {}) {
|
|
17
|
+
let escaped = String(value).replace(/%/g, '%%');
|
|
18
|
+
if (executable) escaped = escaped.replace(/\$/g, '$$$$');
|
|
19
|
+
return JSON.stringify(escaped);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unitPath(value) {
|
|
23
|
+
const path = String(value);
|
|
24
|
+
if (/[\r\n\0]/.test(path)) throw new Error('Service paths cannot contain line breaks or NUL');
|
|
25
|
+
return path.replace(/%/g, '%%');
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export function generateSystemdUnit(config) {
|
|
16
29
|
const nodePath = getNodePath();
|
|
17
30
|
const cliPath = getCliPath();
|
|
18
31
|
const logDir = getLogDir(config.instanceId);
|
|
19
32
|
const envLines = [];
|
|
20
|
-
if (config.instanceId) envLines.push(`Environment
|
|
21
|
-
if (config.serverUrl) envLines.push(`Environment
|
|
22
|
-
if (config.agentName) envLines.push(`Environment
|
|
23
|
-
if (config.agentSecret) envLines.push(`Environment
|
|
24
|
-
if (config.workDir) envLines.push(`Environment
|
|
25
|
-
if (config.yeaftDir) envLines.push(`Environment
|
|
33
|
+
if (config.instanceId) envLines.push(`Environment=${quoteUnit(`YEAFT_AGENT_INSTANCE=${config.instanceId}`)}`);
|
|
34
|
+
if (config.serverUrl) envLines.push(`Environment=${quoteUnit(`SERVER_URL=${config.serverUrl}`)}`);
|
|
35
|
+
if (config.agentName) envLines.push(`Environment=${quoteUnit(`AGENT_NAME=${config.agentName}`)}`);
|
|
36
|
+
if (config.agentSecret) envLines.push(`Environment=${quoteUnit(`AGENT_SECRET=${config.agentSecret}`)}`);
|
|
37
|
+
if (config.workDir) envLines.push(`Environment=${quoteUnit(`WORK_DIR=${config.workDir}`)}`);
|
|
38
|
+
if (config.yeaftDir) envLines.push(`Environment=${quoteUnit(`YEAFT_DIR=${config.yeaftDir}`)}`);
|
|
26
39
|
|
|
27
40
|
// Include node's bin dir in PATH for claude CLI access
|
|
28
41
|
const nodeBinDir = dirname(nodePath);
|
|
@@ -34,17 +47,17 @@ Wants=network-online.target
|
|
|
34
47
|
|
|
35
48
|
[Service]
|
|
36
49
|
Type=simple
|
|
37
|
-
ExecStart=${nodePath} ${cliPath}
|
|
38
|
-
WorkingDirectory=${config.workDir || homedir()}
|
|
50
|
+
ExecStart=${quoteUnit(nodePath, { executable: true })} ${quoteUnit(cliPath, { executable: true })}
|
|
51
|
+
WorkingDirectory=${unitPath(config.workDir || homedir())}
|
|
39
52
|
Restart=on-failure
|
|
40
53
|
RestartSec=10
|
|
41
54
|
KillMode=mixed
|
|
42
55
|
TimeoutStopSec=15
|
|
43
56
|
${envLines.join('\n')}
|
|
44
|
-
Environment
|
|
57
|
+
Environment=${quoteUnit(`PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin`)}
|
|
45
58
|
|
|
46
|
-
StandardOutput=append:${logDir}/out.log
|
|
47
|
-
StandardError=append:${logDir}/error.log
|
|
59
|
+
StandardOutput=append:${unitPath(`${logDir}/out.log`)}
|
|
60
|
+
StandardError=append:${unitPath(`${logDir}/error.log`)}
|
|
48
61
|
|
|
49
62
|
[Install]
|
|
50
63
|
WantedBy=default.target
|
package/service/macos.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Service — macOS (launchd) platform implementation
|
|
3
3
|
*/
|
|
4
|
-
import { execSync } from 'child_process';
|
|
4
|
+
import { execFileSync, execSync } from 'child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
@@ -12,33 +12,37 @@ export function getLaunchdPlistPath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
|
12
12
|
return join(homedir(), 'Library', 'LaunchAgents', `${getLaunchdLabel(instanceId)}.plist`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const xmlText = value => String(value).replace(/[&<>"']/g, char => ({
|
|
16
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
17
|
+
})[char]);
|
|
18
|
+
|
|
19
|
+
export function generateLaunchdPlist(config) {
|
|
16
20
|
const nodePath = getNodePath();
|
|
17
21
|
const cliPath = getCliPath();
|
|
18
22
|
const logDir = getLogDir(config.instanceId);
|
|
19
23
|
const label = getLaunchdLabel(config.instanceId);
|
|
20
24
|
|
|
21
25
|
const envDict = [];
|
|
22
|
-
if (config.instanceId) envDict.push(` <key>YEAFT_AGENT_INSTANCE</key>\n <string>${config.instanceId}</string>`);
|
|
23
|
-
if (config.serverUrl) envDict.push(` <key>SERVER_URL</key>\n <string>${config.serverUrl}</string>`);
|
|
24
|
-
if (config.agentName) envDict.push(` <key>AGENT_NAME</key>\n <string>${config.agentName}</string>`);
|
|
25
|
-
if (config.agentSecret) envDict.push(` <key>AGENT_SECRET</key>\n <string>${config.agentSecret}</string>`);
|
|
26
|
-
if (config.workDir) envDict.push(` <key>WORK_DIR</key>\n <string>${config.workDir}</string>`);
|
|
27
|
-
if (config.yeaftDir) envDict.push(` <key>YEAFT_DIR</key>\n <string>${config.yeaftDir}</string>`);
|
|
26
|
+
if (config.instanceId) envDict.push(` <key>YEAFT_AGENT_INSTANCE</key>\n <string>${xmlText(config.instanceId)}</string>`);
|
|
27
|
+
if (config.serverUrl) envDict.push(` <key>SERVER_URL</key>\n <string>${xmlText(config.serverUrl)}</string>`);
|
|
28
|
+
if (config.agentName) envDict.push(` <key>AGENT_NAME</key>\n <string>${xmlText(config.agentName)}</string>`);
|
|
29
|
+
if (config.agentSecret) envDict.push(` <key>AGENT_SECRET</key>\n <string>${xmlText(config.agentSecret)}</string>`);
|
|
30
|
+
if (config.workDir) envDict.push(` <key>WORK_DIR</key>\n <string>${xmlText(config.workDir)}</string>`);
|
|
31
|
+
if (config.yeaftDir) envDict.push(` <key>YEAFT_DIR</key>\n <string>${xmlText(config.yeaftDir)}</string>`);
|
|
28
32
|
|
|
29
33
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
30
34
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
31
35
|
<plist version="1.0">
|
|
32
36
|
<dict>
|
|
33
37
|
<key>Label</key>
|
|
34
|
-
<string>${label}</string>
|
|
38
|
+
<string>${xmlText(label)}</string>
|
|
35
39
|
<key>ProgramArguments</key>
|
|
36
40
|
<array>
|
|
37
|
-
<string>${nodePath}</string>
|
|
38
|
-
<string>${cliPath}</string>
|
|
41
|
+
<string>${xmlText(nodePath)}</string>
|
|
42
|
+
<string>${xmlText(cliPath)}</string>
|
|
39
43
|
</array>
|
|
40
44
|
<key>WorkingDirectory</key>
|
|
41
|
-
<string>${config.workDir || homedir()}</string>
|
|
45
|
+
<string>${xmlText(config.workDir || homedir())}</string>
|
|
42
46
|
<key>EnvironmentVariables</key>
|
|
43
47
|
<dict>
|
|
44
48
|
${envDict.join('\n')}
|
|
@@ -53,9 +57,9 @@ ${envDict.join('\n')}
|
|
|
53
57
|
<key>ThrottleInterval</key>
|
|
54
58
|
<integer>10</integer>
|
|
55
59
|
<key>StandardOutPath</key>
|
|
56
|
-
<string>${logDir}/out.log</string>
|
|
60
|
+
<string>${xmlText(`${logDir}/out.log`)}</string>
|
|
57
61
|
<key>StandardErrorPath</key>
|
|
58
|
-
<string>${logDir}/error.log</string>
|
|
62
|
+
<string>${xmlText(`${logDir}/error.log`)}</string>
|
|
59
63
|
</dict>
|
|
60
64
|
</plist>
|
|
61
65
|
`;
|
|
@@ -68,10 +72,10 @@ export function macInstall(config) {
|
|
|
68
72
|
mkdirSync(getLogDir(config.instanceId), { recursive: true });
|
|
69
73
|
// Unload first if exists
|
|
70
74
|
if (existsSync(plistPath)) {
|
|
71
|
-
try {
|
|
75
|
+
try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'pipe' }); } catch {}
|
|
72
76
|
}
|
|
73
77
|
writeFileSync(plistPath, generateLaunchdPlist(config));
|
|
74
|
-
|
|
78
|
+
execFileSync('launchctl', ['load', plistPath]);
|
|
75
79
|
console.log(`Service installed and started: ${getLaunchdLabel(config.instanceId)}`);
|
|
76
80
|
console.log(`\nManage with:`);
|
|
77
81
|
console.log(` yeaft-agent status --name ${config.instanceId}`);
|
|
@@ -83,7 +87,7 @@ export function macInstall(config) {
|
|
|
83
87
|
export function macUninstall(instanceId = DEFAULT_INSTANCE_ID) {
|
|
84
88
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
85
89
|
if (existsSync(plistPath)) {
|
|
86
|
-
try {
|
|
90
|
+
try { execFileSync('launchctl', ['unload', plistPath]); } catch {}
|
|
87
91
|
unlinkSync(plistPath);
|
|
88
92
|
}
|
|
89
93
|
console.log(`Service uninstalled: ${getLaunchdLabel(instanceId)}`);
|
|
@@ -95,14 +99,14 @@ export function macStart(instanceId = DEFAULT_INSTANCE_ID) {
|
|
|
95
99
|
console.error('Service not installed. Run "yeaft-agent install" first.');
|
|
96
100
|
process.exit(1);
|
|
97
101
|
}
|
|
98
|
-
|
|
102
|
+
execFileSync('launchctl', ['load', plistPath]);
|
|
99
103
|
console.log(`Service started: ${getLaunchdLabel(instanceId)}`);
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
export function macStop(instanceId = DEFAULT_INSTANCE_ID) {
|
|
103
107
|
const plistPath = getLaunchdPlistPath(instanceId);
|
|
104
108
|
if (existsSync(plistPath)) {
|
|
105
|
-
|
|
109
|
+
execFileSync('launchctl', ['unload', plistPath]);
|
|
106
110
|
}
|
|
107
111
|
console.log(`Service stopped: ${getLaunchdLabel(instanceId)}`);
|
|
108
112
|
}
|
package/service/windows.js
CHANGED
|
@@ -26,7 +26,7 @@ export function getEcosystemPath(instanceId = DEFAULT_INSTANCE_ID) {
|
|
|
26
26
|
return join(getConfigDir(instanceId), 'ecosystem.config.cjs');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function generateEcosystem(config) {
|
|
29
|
+
export function generateEcosystem(config) {
|
|
30
30
|
const nodePath = getNodePath();
|
|
31
31
|
const cliPath = getCliPath();
|
|
32
32
|
const cliDir = dirname(cliPath);
|
|
@@ -43,18 +43,18 @@ function generateEcosystem(config) {
|
|
|
43
43
|
|
|
44
44
|
return `module.exports = {
|
|
45
45
|
apps: [{
|
|
46
|
-
name:
|
|
47
|
-
script:
|
|
48
|
-
interpreter:
|
|
49
|
-
cwd:
|
|
46
|
+
name: ${JSON.stringify(pm2AppName)},
|
|
47
|
+
script: ${JSON.stringify(cliPath)},
|
|
48
|
+
interpreter: ${JSON.stringify(nodePath)},
|
|
49
|
+
cwd: ${JSON.stringify(cliDir)},
|
|
50
50
|
env: ${JSON.stringify(env, null, 6)},
|
|
51
51
|
autorestart: true,
|
|
52
52
|
watch: false,
|
|
53
53
|
max_restarts: 10,
|
|
54
54
|
restart_delay: 5000,
|
|
55
55
|
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
56
|
-
error_file:
|
|
57
|
-
out_file:
|
|
56
|
+
error_file: ${JSON.stringify(join(logDir, 'error.log'))},
|
|
57
|
+
out_file: ${JSON.stringify(join(logDir, 'out.log'))},
|
|
58
58
|
merge_logs: true,
|
|
59
59
|
max_memory_restart: '500M',
|
|
60
60
|
}]
|
|
@@ -105,6 +105,7 @@ export function winInstall(config) {
|
|
|
105
105
|
// pm2-startup doesn't work well on Windows, use Startup folder approach
|
|
106
106
|
const trayScript = join(dirname(getCliPath()), 'scripts', 'agent-tray.ps1');
|
|
107
107
|
const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
108
|
+
mkdirSync(startupDir, { recursive: true });
|
|
108
109
|
const startupBat = join(startupDir, `${pm2AppName}.bat`);
|
|
109
110
|
// Resurrect pm2 processes + launch tray icon
|
|
110
111
|
let batContent = `@echo off\r\npm2 resurrect\r\n`;
|
package/yeaft/routing/router.js
CHANGED
|
@@ -28,122 +28,6 @@
|
|
|
28
28
|
import { resolveMemberId } from '../sessions/roster.js';
|
|
29
29
|
import { createLoopGuard, extendCausedBy } from './loop-guard.js';
|
|
30
30
|
|
|
31
|
-
const repoApprovals = new WeakMap();
|
|
32
|
-
const REPO_APPROVAL_ISSUER_IDS = new Set(['martin']);
|
|
33
|
-
export const REPO_APPROVAL_TTL_MS = 2 * 60 * 1000;
|
|
34
|
-
|
|
35
|
-
function normalizeId(value) {
|
|
36
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeSha(value) {
|
|
40
|
-
const sha = normalizeId(value).toLowerCase();
|
|
41
|
-
return /^[0-9a-f]{40}$/.test(sha) ? sha : '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeApprovalRepository(value) {
|
|
45
|
-
const repository = normalizeId(value).replace(/\.git$/i, '');
|
|
46
|
-
const segments = repository.split('/');
|
|
47
|
-
if (segments.length === 2) segments.unshift('github.com');
|
|
48
|
-
if (segments.length !== 3) return '';
|
|
49
|
-
const [host, owner, name] = segments;
|
|
50
|
-
const safePart = part => /^[a-z0-9_.-]+$/i.test(part) && part !== '.' && part !== '..';
|
|
51
|
-
if (!safePart(host) || !safePart(owner) || !safePart(name) || !host.includes('.')) return '';
|
|
52
|
-
return `${host}/${owner}/${name}`.toLowerCase();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isRepoApprovalIssuer(vpId) {
|
|
56
|
-
return REPO_APPROVAL_ISSUER_IDS.has(normalizeId(vpId));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Mint authority only inside the canonical Router path after roster identity
|
|
61
|
-
* resolution. Keeping this function module-private prevents callers from
|
|
62
|
-
* turning a claimed issuer string into landing authority.
|
|
63
|
-
*/
|
|
64
|
-
function issueRepoApprovalCapability(input = {}, { now = Date.now } = {}) {
|
|
65
|
-
const sessionId = normalizeId(input.sessionId);
|
|
66
|
-
const issuerVpId = normalizeId(input.issuerVpId);
|
|
67
|
-
const recipientVpId = normalizeId(input.recipientVpId);
|
|
68
|
-
const repository = normalizeApprovalRepository(input.repository);
|
|
69
|
-
const pr = Number(input.pr);
|
|
70
|
-
const baseBranch = normalizeId(input.baseBranch);
|
|
71
|
-
const baseSha = normalizeSha(input.baseSha);
|
|
72
|
-
const reviewedHead = normalizeSha(input.reviewedHead);
|
|
73
|
-
const reviewedSnapshot = normalizeSha(input.reviewedSnapshot);
|
|
74
|
-
const issuedAt = Number(now());
|
|
75
|
-
if (!sessionId || !isRepoApprovalIssuer(issuerVpId) || !recipientVpId || issuerVpId === recipientVpId
|
|
76
|
-
|| !repository || !Number.isSafeInteger(pr) || pr <= 0 || !baseBranch || !baseSha
|
|
77
|
-
|| !reviewedHead || !reviewedSnapshot || !Number.isFinite(issuedAt)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const capability = Object.freeze(Object.create(null));
|
|
81
|
-
repoApprovals.set(capability, {
|
|
82
|
-
sessionId,
|
|
83
|
-
issuerVpId,
|
|
84
|
-
recipientVpId,
|
|
85
|
-
repository,
|
|
86
|
-
pr,
|
|
87
|
-
baseBranch,
|
|
88
|
-
baseSha,
|
|
89
|
-
reviewedHead,
|
|
90
|
-
reviewedSnapshot,
|
|
91
|
-
issuedAt,
|
|
92
|
-
expiresAt: issuedAt + REPO_APPROVAL_TTL_MS,
|
|
93
|
-
turnId: null,
|
|
94
|
-
});
|
|
95
|
-
return capability;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function isRepoApprovalCapability(capability) {
|
|
99
|
-
return Boolean(capability && typeof capability === 'object' && repoApprovals.has(capability));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Bind a freshly issued capability to the exact Web execution that received it. */
|
|
103
|
-
export function bindRepoApprovalCapability(capability, expected = {}, { now = Date.now } = {}) {
|
|
104
|
-
if (!capability || typeof capability !== 'object') return false;
|
|
105
|
-
const grant = repoApprovals.get(capability);
|
|
106
|
-
const turnId = normalizeId(expected.turnId);
|
|
107
|
-
const currentTime = Number(now());
|
|
108
|
-
if (!grant || !turnId || !Number.isFinite(currentTime)) return false;
|
|
109
|
-
if (currentTime > grant.expiresAt
|
|
110
|
-
|| grant.turnId
|
|
111
|
-
|| grant.sessionId !== normalizeId(expected.sessionId)
|
|
112
|
-
|| grant.recipientVpId !== normalizeId(expected.recipientVpId)) {
|
|
113
|
-
repoApprovals.delete(capability);
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
grant.turnId = turnId;
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function revokeRepoApprovalCapability(capability) {
|
|
121
|
-
if (!capability || typeof capability !== 'object') return false;
|
|
122
|
-
return repoApprovals.delete(capability);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Consume a capability exactly once and verify its complete landing tuple. */
|
|
126
|
-
export function consumeRepoApprovalCapability(capability, expected = {}, { now = Date.now } = {}) {
|
|
127
|
-
if (!capability || typeof capability !== 'object') return null;
|
|
128
|
-
const grant = repoApprovals.get(capability);
|
|
129
|
-
repoApprovals.delete(capability);
|
|
130
|
-
if (!grant) return null;
|
|
131
|
-
const currentTime = Number(now());
|
|
132
|
-
const matches = Number.isFinite(currentTime)
|
|
133
|
-
&& currentTime <= grant.expiresAt
|
|
134
|
-
&& grant.turnId
|
|
135
|
-
&& grant.turnId === normalizeId(expected.turnId)
|
|
136
|
-
&& grant.sessionId === normalizeId(expected.sessionId)
|
|
137
|
-
&& grant.recipientVpId === normalizeId(expected.recipientVpId)
|
|
138
|
-
&& grant.repository === normalizeApprovalRepository(expected.repository)
|
|
139
|
-
&& grant.pr === Number(expected.pr)
|
|
140
|
-
&& grant.baseBranch === normalizeId(expected.baseBranch)
|
|
141
|
-
&& grant.baseSha === normalizeSha(expected.baseSha)
|
|
142
|
-
&& grant.reviewedHead === normalizeSha(expected.reviewedHead)
|
|
143
|
-
&& grant.reviewedSnapshot === normalizeSha(expected.reviewedSnapshot);
|
|
144
|
-
return matches ? Object.freeze({ ...grant }) : null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
31
|
function routeForwardParentFromEnvelope(envelope) {
|
|
148
32
|
const msg = envelope?.msg;
|
|
149
33
|
const meta = msg?.meta;
|
|
@@ -265,24 +149,6 @@ export function createRouter(deps = {}) {
|
|
|
265
149
|
// spec's intent ("depth of forwards already taken").
|
|
266
150
|
const chain = extendCausedBy(args.inboundEnvelope || null, null);
|
|
267
151
|
const routeForwardParent = routeForwardParentFromEnvelope(args.inboundEnvelope);
|
|
268
|
-
let repoApproval = null;
|
|
269
|
-
if (args.repoApproval !== undefined) {
|
|
270
|
-
if (targetVpId === 'all') {
|
|
271
|
-
return { ok: false, error: 'repo_approval_requires_single_target' };
|
|
272
|
-
}
|
|
273
|
-
if (!isRepoApprovalIssuer(senderVpId)) {
|
|
274
|
-
return { ok: false, error: 'repo_approval_issuer_forbidden' };
|
|
275
|
-
}
|
|
276
|
-
repoApproval = issueRepoApprovalCapability({
|
|
277
|
-
...args.repoApproval,
|
|
278
|
-
sessionId: meta.id,
|
|
279
|
-
issuerVpId: senderVpId,
|
|
280
|
-
recipientVpId: targetVpId,
|
|
281
|
-
}, { now });
|
|
282
|
-
if (!repoApproval) {
|
|
283
|
-
return { ok: false, error: 'invalid_repo_approval' };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
152
|
|
|
287
153
|
// Loop guard: for broadcast, use 'all' as the target key so one VP
|
|
288
154
|
// spamming @all still gets throttled even if each cycle hits different
|
|
@@ -319,7 +185,6 @@ export function createRouter(deps = {}) {
|
|
|
319
185
|
// of UI replay and future visible history so it doesn't render as a
|
|
320
186
|
// second assistant/user block after the target VP answers.
|
|
321
187
|
internal: true,
|
|
322
|
-
...(repoApproval ? { _repoApproval: repoApproval } : {}),
|
|
323
188
|
meta: {
|
|
324
189
|
synthetic: true,
|
|
325
190
|
injectedBy: 'route_forward',
|
|
@@ -55,7 +55,6 @@ export const CONDITIONAL_BUILTIN_TOOL_NAMES = new Set([
|
|
|
55
55
|
'JsRepl',
|
|
56
56
|
'NotebookEdit',
|
|
57
57
|
'ImageGeneration',
|
|
58
|
-
'RepoWorkflow',
|
|
59
58
|
]);
|
|
60
59
|
|
|
61
60
|
const HISTORY_INTENT_RE = /(?:\bhistory\b|\b(?:prior|previous) (?:chat|conversation|discussion)\b|\bprevious(?:ly)? discussed\b|\bwhat did we (?:decide|discuss|say|agree)\b|\b(?:our|the) (?:earlier|last) decision\b|历史|之前(?:的)?(?:对话|讨论|会话|决定)|过去(?:的)?会话|我们(?:之前|上次)(?:决定|讨论|说)了什么)/iu;
|
|
@@ -67,7 +66,6 @@ const WORK_ITEM_INTENT_RE = /(?:\bwork ?center\b|\bwork ?item\b|\bdurable tracki
|
|
|
67
66
|
const REPL_INTENT_RE = /(?:\bjs ?repl\b|\bjavascript (?:calculation|experiment|evaluation)\b|\bcalculate\b|\bdata transform\b|JavaScript (?:计算|实验|求值)|数据转换|快速计算)/iu;
|
|
68
67
|
const NOTEBOOK_INTENT_RE = /(?:\.ipynb\b|\bjupyter\b|\bnotebook (?:cell|file)\b|Jupyter|笔记本单元格)/iu;
|
|
69
68
|
const IMAGE_GENERATION_INTENT_RE = /(?:\b(?:generate|make|design|draw) (?:me |us )?(?:an? |the )?(?:image|picture|logo|icon|illustration|graphic)\b|\bcreate (?:me |us )?(?:an? |the )?(?:illustration|image|picture|logo|icon|graphic)\b|生成(?:一张)?(?:图片|图像|标志|图标|插图)|创建(?:一张)?(?:插图|图片|图像|标志|图标)|画(?:一张)?(?:图|图片|图标))/iu;
|
|
70
|
-
const REPO_WORKFLOW_INTENT_RE = /(?:\b(?:git |github )?(?:worktree|pull request|pr) (?:workflow|review|merge|landing|prepare)\b|\b(?:review|merge|land|prepare) (?:a |the )?(?:github )?(?:pull request|pr)\b|\bhead[- ]match(?:ed)? merge\b|\bmerge (?:and|\+) tag\b|\breview[- ]prep\b|\bye?aft-repo\b|仓库(?:工作流|流程)|准备(?:开发|审查|review) worktree|(?:审查|评审|合并|准备) ?(?:github )?(?:pr|pull request|拉取请求)|合并(?:并|和)?打 tag|精确 head 合并)/iu;
|
|
71
69
|
const MCP_INTENT_RE = /(?:\bmcp\b|model context protocol|模型上下文协议)/iu;
|
|
72
70
|
|
|
73
71
|
function messageText(message) {
|
|
@@ -159,7 +157,6 @@ export function resolveActiveToolNames({
|
|
|
159
157
|
if (REPL_INTENT_RE.test(intentText)) active.add('JsRepl');
|
|
160
158
|
if (NOTEBOOK_INTENT_RE.test(intentText)) active.add('NotebookEdit');
|
|
161
159
|
if (imageGenerationConfigured && IMAGE_GENERATION_INTENT_RE.test(intentText)) active.add('ImageGeneration');
|
|
162
|
-
if (REPO_WORKFLOW_INTENT_RE.test(intentText)) active.add('RepoWorkflow');
|
|
163
160
|
|
|
164
161
|
for (const name of matchedMcpTools(intentText, toolNames)) active.add(name);
|
|
165
162
|
|
package/yeaft/tools/index.js
CHANGED
|
@@ -22,7 +22,6 @@ import { ToolRegistry } from './registry.js';
|
|
|
22
22
|
import skillTool from './skill.js';
|
|
23
23
|
import enterWorktree from './enter-worktree.js';
|
|
24
24
|
import exitWorktree from './exit-worktree.js';
|
|
25
|
-
import repoWorkflow from './repo-workflow.js';
|
|
26
25
|
|
|
27
26
|
// --- P0 Core tools ---
|
|
28
27
|
import askUser from './ask-user.js';
|
|
@@ -89,7 +88,6 @@ export const allTools = [
|
|
|
89
88
|
skillTool,
|
|
90
89
|
enterWorktree,
|
|
91
90
|
exitWorktree,
|
|
92
|
-
repoWorkflow,
|
|
93
91
|
|
|
94
92
|
// P0 Core
|
|
95
93
|
askUser,
|
|
@@ -99,23 +99,6 @@ Returns JSON: { ok, dispatched?, error?, detail? }.`,
|
|
|
99
99
|
zh: '可选:转交的简短原因',
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
|
-
repoApproval: {
|
|
103
|
-
type: 'object',
|
|
104
|
-
description: {
|
|
105
|
-
en: 'Optional structured repository landing approval. The host binds it to the authenticated sender VP, target VP, and current Session, then issues a non-serializable one-time capability.',
|
|
106
|
-
zh: '可选的结构化仓库落地批准。宿主会将其绑定到真实发送 VP、目标 VP 和当前 Session,并签发不可序列化的一次性 capability。',
|
|
107
|
-
},
|
|
108
|
-
properties: {
|
|
109
|
-
repository: { type: 'string', description: 'GitHub owner/repository identity' },
|
|
110
|
-
pr: { type: 'integer', minimum: 1 },
|
|
111
|
-
baseBranch: { type: 'string', description: 'Exact approved destination branch' },
|
|
112
|
-
baseSha: { type: 'string', description: 'Exact approved 40-character base SHA' },
|
|
113
|
-
reviewedHead: { type: 'string', description: 'Exact approved 40-character PR head SHA' },
|
|
114
|
-
reviewedSnapshot: { type: 'string', description: 'Exact approved 40-character merge snapshot SHA' },
|
|
115
|
-
},
|
|
116
|
-
required: ['repository', 'pr', 'baseBranch', 'baseSha', 'reviewedHead', 'reviewedSnapshot'],
|
|
117
|
-
additionalProperties: false,
|
|
118
|
-
},
|
|
119
102
|
},
|
|
120
103
|
required: ['to', 'text'],
|
|
121
104
|
},
|
|
@@ -126,7 +109,7 @@ Returns JSON: { ok, dispatched?, error?, detail? }.`,
|
|
|
126
109
|
// dispatch is rejected; the cost is one fresh read, not stale workspace data.
|
|
127
110
|
mayMutateWorkspaceAfterReturn: () => true,
|
|
128
111
|
async execute(input, ctx = {}) {
|
|
129
|
-
const { to, text, reason
|
|
112
|
+
const { to, text, reason } = input || {};
|
|
130
113
|
if (!to || typeof to !== 'string') {
|
|
131
114
|
return JSON.stringify({ ok: false, error: 'to_required' });
|
|
132
115
|
}
|
|
@@ -151,7 +134,6 @@ Returns JSON: { ok, dispatched?, error?, detail? }.`,
|
|
|
151
134
|
taskId: ctx.taskId ?? null,
|
|
152
135
|
inboundEnvelope: ctx.inboundEnvelope ?? null,
|
|
153
136
|
sourceThreadId: ctx.threadId ?? null,
|
|
154
|
-
repoApproval,
|
|
155
137
|
},
|
|
156
138
|
{ taskMembers: ctx.taskMembers },
|
|
157
139
|
);
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -77,12 +77,7 @@ import { hydrateYeaftStatusFromSession } from './status-cache.js';
|
|
|
77
77
|
import { handleVpSubscribe } from './vp/vp-bridge.js';
|
|
78
78
|
import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
|
|
79
79
|
import { scanVpLibrary } from './vp/vp-store.js';
|
|
80
|
-
import {
|
|
81
|
-
bindRepoApprovalCapability,
|
|
82
|
-
createRouter,
|
|
83
|
-
isRepoApprovalCapability,
|
|
84
|
-
revokeRepoApprovalCapability,
|
|
85
|
-
} from './routing/router.js';
|
|
80
|
+
import { createRouter } from './routing/router.js';
|
|
86
81
|
import {
|
|
87
82
|
SessionCrudError,
|
|
88
83
|
createSessionFromSpec,
|
|
@@ -473,22 +468,6 @@ const turnAbortCtrls = new Map();
|
|
|
473
468
|
* @type {Map<string, { sessionId: string, vpId: string, threadId: string, key: string }>}
|
|
474
469
|
*/
|
|
475
470
|
const turnAbortMeta = new Map();
|
|
476
|
-
/** @type {Map<string, { capability: object, sessionId: string, vpId: string }>} Exact-turn repo approvals, never persisted or projected. */
|
|
477
|
-
const turnRepoApprovals = new Map();
|
|
478
|
-
|
|
479
|
-
function revokeTurnRepoApproval(turnId) {
|
|
480
|
-
const grant = turnRepoApprovals.get(turnId);
|
|
481
|
-
turnRepoApprovals.delete(turnId);
|
|
482
|
-
return grant ? revokeRepoApprovalCapability(grant.capability) : false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function revokeRepoApprovalsForRecipient(sessionId, vpId) {
|
|
486
|
-
for (const [turnId, grant] of turnRepoApprovals.entries()) {
|
|
487
|
-
if (grant.sessionId !== sessionId || grant.vpId !== vpId) continue;
|
|
488
|
-
revokeTurnRepoApproval(turnId);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
471
|
/**
|
|
493
472
|
* Per-VP status broker — the agent-side authority for VP timeline
|
|
494
473
|
* status. Lazy-initialized on first use because `sendSessionEvent` is
|
|
@@ -1175,7 +1154,6 @@ function invalidateGroupContext(sessionId, { abortRuntime = false } = {}) {
|
|
|
1175
1154
|
}
|
|
1176
1155
|
for (const [turnId, meta] of Array.from(turnAbortMeta.entries())) {
|
|
1177
1156
|
if (meta?.sessionId !== sessionId) continue;
|
|
1178
|
-
revokeTurnRepoApproval(turnId);
|
|
1179
1157
|
turnAbortCtrls.delete(turnId);
|
|
1180
1158
|
turnAbortMeta.delete(turnId);
|
|
1181
1159
|
}
|
|
@@ -2159,21 +2137,12 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2159
2137
|
let related = false;
|
|
2160
2138
|
|
|
2161
2139
|
const meta = envelope?.msg?.meta || {};
|
|
2162
|
-
const repoApproval = envelope?._repoApproval;
|
|
2163
|
-
const hasRepoApproval = isRepoApprovalCapability(repoApproval);
|
|
2164
|
-
if (envelope?.msg?.role === 'user' || envelope?.msg?.from === 'user') {
|
|
2165
|
-
revokeRepoApprovalsForRecipient(sessionId, vpId);
|
|
2166
|
-
}
|
|
2167
2140
|
const isTaskResult = meta.injectedBy === 'task_result';
|
|
2168
2141
|
const sourceThreadId = typeof meta.sourceThreadId === 'string' && meta.sourceThreadId.trim()
|
|
2169
2142
|
? meta.sourceThreadId.trim()
|
|
2170
2143
|
: null;
|
|
2171
2144
|
|
|
2172
|
-
if (
|
|
2173
|
-
// Approval is an execution capability, not text. It must never collapse
|
|
2174
|
-
// into a related thread's pendingQueries/rescue path.
|
|
2175
|
-
thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
|
|
2176
|
-
} else if (isTaskResult && sourceThreadId) {
|
|
2145
|
+
if (isTaskResult && sourceThreadId) {
|
|
2177
2146
|
thread = getOrCreateVpThread({ sessionId, vpId, threadId: sourceThreadId, title: fallbackTitle(text) });
|
|
2178
2147
|
} else if (runningThreads.length === 0) {
|
|
2179
2148
|
thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
|
|
@@ -2216,12 +2185,6 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
2216
2185
|
if (!thread) return null;
|
|
2217
2186
|
rememberThreadMessage(thread, envelope?.msg);
|
|
2218
2187
|
const turnId = `${randomUUID().slice(0, 8)}:${vpId}`;
|
|
2219
|
-
if (hasRepoApproval) {
|
|
2220
|
-
if (!bindRepoApprovalCapability(repoApproval, { sessionId, recipientVpId: vpId, turnId })) {
|
|
2221
|
-
return null;
|
|
2222
|
-
}
|
|
2223
|
-
turnRepoApprovals.set(turnId, { capability: repoApproval, sessionId, vpId });
|
|
2224
|
-
}
|
|
2225
2188
|
const perfTraceId = envelope?._perfTraceId || envelope?.perfTraceId || null;
|
|
2226
2189
|
if (perfTraceId) {
|
|
2227
2190
|
recordAgentPerfTrace(ctx.CONFIG, {
|
|
@@ -2397,7 +2360,6 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
2397
2360
|
} catch (err) {
|
|
2398
2361
|
console.warn('[Yeaft] driveVp: runVpTurn failed', vpId, err?.message || err);
|
|
2399
2362
|
} finally {
|
|
2400
|
-
revokeTurnRepoApproval(turnId);
|
|
2401
2363
|
turnAbortCtrls.delete(turnId);
|
|
2402
2364
|
turnAbortMeta.delete(turnId);
|
|
2403
2365
|
if (vpAborts.get(key) === vpAbort) vpAborts.delete(key);
|
|
@@ -2532,7 +2494,6 @@ export async function __testResetVpState() {
|
|
|
2532
2494
|
if (Array.isArray(inbox)) inbox.length = 0;
|
|
2533
2495
|
}
|
|
2534
2496
|
vpThreads.clear();
|
|
2535
|
-
for (const turnId of turnRepoApprovals.keys()) revokeTurnRepoApproval(turnId);
|
|
2536
2497
|
turnAbortCtrls.clear();
|
|
2537
2498
|
turnAbortMeta.clear();
|
|
2538
2499
|
await __testDrainVpDrivers();
|
|
@@ -5442,7 +5403,6 @@ async function runVpTurnWithEscalation(args) {
|
|
|
5442
5403
|
graceMs: ESCALATE_AFTER_ABORT_MS,
|
|
5443
5404
|
onEscalate: () => {
|
|
5444
5405
|
if (escalationState.escalated) return;
|
|
5445
|
-
revokeTurnRepoApproval(turnId);
|
|
5446
5406
|
escalationState.escalated = true;
|
|
5447
5407
|
escalationState.terminalEmitted = true;
|
|
5448
5408
|
console.error(
|
|
@@ -5661,7 +5621,6 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
5661
5621
|
queryTimer = setTimeout(() => {
|
|
5662
5622
|
if (!vpAbort.signal.aborted) {
|
|
5663
5623
|
console.error(`[Yeaft] query timeout after ${queryTimeoutMs / 1000}s of silence — aborting VP ${vpId}`);
|
|
5664
|
-
revokeTurnRepoApproval(turnId);
|
|
5665
5624
|
try { vpAbort.abort(); } catch { /* best-effort */ }
|
|
5666
5625
|
}
|
|
5667
5626
|
}, queryTimeoutMs);
|
|
@@ -6346,10 +6305,6 @@ function emitQueuedTurnAbort(meta, turnId) {
|
|
|
6346
6305
|
}
|
|
6347
6306
|
|
|
6348
6307
|
function abortAllVpRuntime(aborted, sessionId = null) {
|
|
6349
|
-
for (const [turnId, meta] of Array.from(turnAbortMeta.entries())) {
|
|
6350
|
-
if (sessionId && meta?.sessionId !== sessionId) continue;
|
|
6351
|
-
revokeTurnRepoApproval(turnId);
|
|
6352
|
-
}
|
|
6353
6308
|
for (const [key, ctrl] of vpAborts) {
|
|
6354
6309
|
if (sessionId && !key.startsWith(`${sessionId}::`)) continue;
|
|
6355
6310
|
try {
|
|
@@ -6392,7 +6347,6 @@ export function handleYeaftAbortThread(_msg = {}) {
|
|
|
6392
6347
|
const meta = turnAbortMeta.get(turnId);
|
|
6393
6348
|
if ((meta?.threadId || 'main') !== targetThreadId) continue;
|
|
6394
6349
|
try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
|
|
6395
|
-
revokeTurnRepoApproval(turnId);
|
|
6396
6350
|
turnAbortCtrls.delete(turnId);
|
|
6397
6351
|
turnAbortMeta.delete(turnId);
|
|
6398
6352
|
}
|
|
@@ -6411,7 +6365,6 @@ export function handleYeaftAbortThread(_msg = {}) {
|
|
|
6411
6365
|
for (const [turnId, ctrl] of turnAbortCtrls) {
|
|
6412
6366
|
try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
|
|
6413
6367
|
}
|
|
6414
|
-
for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
|
|
6415
6368
|
turnAbortCtrls.clear();
|
|
6416
6369
|
turnAbortMeta.clear();
|
|
6417
6370
|
abortAllVpRuntime(aborted);
|
|
@@ -6438,12 +6391,10 @@ export function handleYeaftAbortAll(msg = {}) {
|
|
|
6438
6391
|
const meta = turnAbortMeta.get(turnId);
|
|
6439
6392
|
if (sessionId && meta?.sessionId !== sessionId) continue;
|
|
6440
6393
|
try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
|
|
6441
|
-
revokeTurnRepoApproval(turnId);
|
|
6442
6394
|
turnAbortCtrls.delete(turnId);
|
|
6443
6395
|
turnAbortMeta.delete(turnId);
|
|
6444
6396
|
}
|
|
6445
6397
|
if (!sessionId) {
|
|
6446
|
-
for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
|
|
6447
6398
|
turnAbortCtrls.clear();
|
|
6448
6399
|
turnAbortMeta.clear();
|
|
6449
6400
|
}
|
|
@@ -6490,7 +6441,6 @@ export function handleYeaftAbortTurn(msg = {}) {
|
|
|
6490
6441
|
success = true;
|
|
6491
6442
|
abortedTurnIds.push(turnId);
|
|
6492
6443
|
}
|
|
6493
|
-
revokeTurnRepoApproval(turnId);
|
|
6494
6444
|
turnAbortCtrls.delete(turnId);
|
|
6495
6445
|
turnAbortMeta.delete(turnId);
|
|
6496
6446
|
}
|
|
@@ -7827,7 +7777,6 @@ export async function resetYeaftSession() {
|
|
|
7827
7777
|
try { if (!ctrl.signal.aborted) ctrl.abort(); } catch { /* best-effort */ }
|
|
7828
7778
|
}
|
|
7829
7779
|
vpAborts.clear();
|
|
7830
|
-
for (const turnId of turnAbortMeta.keys()) revokeTurnRepoApproval(turnId);
|
|
7831
7780
|
turnAbortCtrls.clear();
|
|
7832
7781
|
turnAbortMeta.clear();
|
|
7833
7782
|
vpInboxes.clear();
|
|
@@ -8306,7 +8255,6 @@ export const __testHooks = {
|
|
|
8306
8255
|
return loadAndBroadcastYeaftSkillSlashCommands();
|
|
8307
8256
|
},
|
|
8308
8257
|
resetAbortState() {
|
|
8309
|
-
for (const turnId of turnRepoApprovals.keys()) revokeTurnRepoApproval(turnId);
|
|
8310
8258
|
turnAbortCtrls.clear();
|
|
8311
8259
|
turnAbortMeta.clear();
|
|
8312
8260
|
vpAborts.clear();
|