ework-daemon 0.4.67 → 0.4.68
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 +1 -1
- package/src/opencode.ts +56 -9
package/package.json
CHANGED
package/src/opencode.ts
CHANGED
|
@@ -321,13 +321,16 @@ function createBackendFor(cfg: Config, runtime: string): RuntimeBackend {
|
|
|
321
321
|
// Wake policy shared by issue_opened and comment_created: blacklist wins,
|
|
322
322
|
// then an explicit login whitelist (which replaces the kind check), then
|
|
323
323
|
// author kind. Issue openers carry no kind and default to human.
|
|
324
|
+
// extraLogins extends the env whitelist with per-project entries fetched
|
|
325
|
+
// from the web config center; kinds still apply to them (bots never wake).
|
|
324
326
|
export function wakePolicySkips(
|
|
325
327
|
d: { nonWakingAuthors: string[]; noWakeLogins: string[]; wakeLogins: string[]; wakeKinds: string[] },
|
|
326
328
|
author: string,
|
|
327
329
|
authorKind: string,
|
|
330
|
+
extraLogins: string[] = [],
|
|
328
331
|
): string | null {
|
|
329
332
|
if ([...d.nonWakingAuthors, ...d.noWakeLogins].includes(author)) return `non-waking author ${author}`;
|
|
330
|
-
if (d.wakeLogins.length > 0 && !d.wakeLogins.includes(author)) return `author ${author} not in wakeLogins`;
|
|
333
|
+
if (d.wakeLogins.length > 0 && ![...d.wakeLogins, ...extraLogins].includes(author)) return `author ${author} not in wakeLogins`;
|
|
331
334
|
if (!d.wakeKinds.includes(authorKind)) return `author kind ${authorKind} not in wakeKinds [${d.wakeKinds.join(",")}]`;
|
|
332
335
|
return null;
|
|
333
336
|
}
|
|
@@ -414,6 +417,7 @@ export class Engine {
|
|
|
414
417
|
private envInitialized = new Set<string>();
|
|
415
418
|
private replyBurstCfg?: { max: number; windowMs: number };
|
|
416
419
|
private replyStamps = new Map<string, number[]>();
|
|
420
|
+
private wakeWhitelistCache = new Map<string, { at: number; logins: string[] }>();
|
|
417
421
|
|
|
418
422
|
private static MAX_INLINE_SIZE = 4000;
|
|
419
423
|
private static MAX_NUDGE_ROUNDS = 1;
|
|
@@ -495,6 +499,32 @@ export class Engine {
|
|
|
495
499
|
} catch { /* non-critical */ }
|
|
496
500
|
}
|
|
497
501
|
|
|
502
|
+
// Per-project wake whitelist from the web config center (admin-managed
|
|
503
|
+
// external GitHub users). Cached 60s; on fetch failure a stale cache is
|
|
504
|
+
// still honored (it was a prior web decision) but an empty first fetch
|
|
505
|
+
// fails closed.
|
|
506
|
+
private async projectWakeLogins(scopeKey: string): Promise<string[]> {
|
|
507
|
+
const hit = this.wakeWhitelistCache.get(scopeKey);
|
|
508
|
+
if (hit && Date.now() - hit.at < 60_000) return hit.logins;
|
|
509
|
+
const parts = scopeKey.split("/");
|
|
510
|
+
const owner = parts[0] ?? "";
|
|
511
|
+
const repo = parts.slice(1).join("/");
|
|
512
|
+
if (!owner || !repo) return [];
|
|
513
|
+
const url = `${this.cfg.gitea.url}/api/v1/wake-logins?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
|
|
514
|
+
try {
|
|
515
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(5000), headers: { Authorization: `token ${this.cfg.gitea.token}` } });
|
|
516
|
+
if (!resp.ok) throw new Error(`web returned ${resp.status}`);
|
|
517
|
+
const data = await resp.json() as { logins?: string[] };
|
|
518
|
+
const logins = (Array.isArray(data.logins) ? data.logins : [])
|
|
519
|
+
.map((s) => String(s).trim()).filter(Boolean);
|
|
520
|
+
this.wakeWhitelistCache.set(scopeKey, { at: Date.now(), logins });
|
|
521
|
+
return logins;
|
|
522
|
+
} catch (err) {
|
|
523
|
+
log.warn(`engine: wake whitelist query failed for ${scopeKey}: ${(err as Error).message}${hit ? " — using stale cache" : " — fail-closed"}`);
|
|
524
|
+
return hit?.logins ?? [];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
498
528
|
private async webGateAllows(issue: Issue): Promise<{ allowed: boolean; reason: string }> {
|
|
499
529
|
const parts = issue.trackerScopeKey.split("/");
|
|
500
530
|
const owner = parts[0] ?? "";
|
|
@@ -827,7 +857,19 @@ export class Engine {
|
|
|
827
857
|
const wakeAuthor = event.type === "comment_created" ? event.comment?.author : event.type === "issue_opened" ? event.issue?.author : undefined;
|
|
828
858
|
const wakeKind = event.type === "comment_created" ? event.comment?.authorKind ?? "human" : "human";
|
|
829
859
|
if (wakeAuthor) {
|
|
830
|
-
|
|
860
|
+
let skip = wakePolicySkips(this.cfg.daemon, wakeAuthor, wakeKind);
|
|
861
|
+
if (skip && skip.includes("not in wakeLogins")) {
|
|
862
|
+
// GitHub logins are case-insensitive; match the project whitelist that
|
|
863
|
+
// way, then inject the exact author string for the exact-match check.
|
|
864
|
+
const extra = (await this.projectWakeLogins(scopeKey))
|
|
865
|
+
.filter((l) => l.toLowerCase() === wakeAuthor.toLowerCase());
|
|
866
|
+
if (extra.length > 0) {
|
|
867
|
+
skip = wakePolicySkips(this.cfg.daemon, wakeAuthor, wakeKind, [wakeAuthor]);
|
|
868
|
+
if (!skip) {
|
|
869
|
+
log.info(`engine: author ${wakeAuthor} is in project wake whitelist — allowing ${event.type} for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
831
873
|
if (skip) {
|
|
832
874
|
log.info(`engine: ${skip} — skipping ${event.type} for ${ref.trackerType}:${scopeKey}#${ref.issueId}`);
|
|
833
875
|
return;
|
|
@@ -1024,7 +1066,8 @@ export class Engine {
|
|
|
1024
1066
|
const instructions = tracker.getTrackerInstructions(ref);
|
|
1025
1067
|
const prompt = this.buildForwardPrompt(
|
|
1026
1068
|
session.name, this.handleLargeContent(workdir, comment.body, `comment-${comment.id}.txt`),
|
|
1027
|
-
comment.author, comment.authorKind, issueData.title, workdir, instructions
|
|
1069
|
+
comment.author, comment.authorKind, issueData.title, workdir, instructions,
|
|
1070
|
+
this.wakeWhitelistCache.get(scopeKey)?.logins ?? []
|
|
1028
1071
|
);
|
|
1029
1072
|
|
|
1030
1073
|
// Immediate ack
|
|
@@ -1078,7 +1121,8 @@ export class Engine {
|
|
|
1078
1121
|
const instructions = tracker.getTrackerInstructions(ref);
|
|
1079
1122
|
const prompt = this.buildForwardPrompt(
|
|
1080
1123
|
session.name, this.handleLargeContent(workdir, comment.body, `comment-${comment.id}.txt`),
|
|
1081
|
-
comment.author, comment.authorKind, issueData.title, workdir, instructions
|
|
1124
|
+
comment.author, comment.authorKind, issueData.title, workdir, instructions,
|
|
1125
|
+
this.wakeWhitelistCache.get(scopeKey)?.logins ?? []
|
|
1082
1126
|
);
|
|
1083
1127
|
await tracker.createComment(ref, `[system] 🏷 ${this.sessionRef(session)} ✓ Message forwarded to **${session.name}**${this.running.has(this.sessionKey(session, issue)) ? " (running)" : ""}.\n> workdir: ${this.workdirLink(workdir)}`);
|
|
1084
1128
|
await this.enqueueOrRun(session, issue, prompt, comment.id, model);
|
|
@@ -1698,11 +1742,13 @@ export class Engine {
|
|
|
1698
1742
|
// Wake-policy whitelist mirrors the dispatch decision: an author outside
|
|
1699
1743
|
// wakeLogins cannot start work, but their comments still enter a running
|
|
1700
1744
|
// session's prompt — flag them so the model treats their text as data.
|
|
1701
|
-
|
|
1745
|
+
// The project whitelist (case-insensitive) counts as trusted: vetting a
|
|
1746
|
+
// user is an explicit operator trust decision.
|
|
1747
|
+
private isTrustedAuthor(login: string, extraTrusted: string[] = []): boolean {
|
|
1702
1748
|
const d = this.cfg.daemon;
|
|
1703
1749
|
if ([...d.nonWakingAuthors, ...d.noWakeLogins].includes(login)) return false;
|
|
1704
|
-
if (d.wakeLogins.length
|
|
1705
|
-
return
|
|
1750
|
+
if (d.wakeLogins.length === 0) return true;
|
|
1751
|
+
return [...d.wakeLogins, ...extraTrusted].some((l) => l.toLowerCase() === login.toLowerCase());
|
|
1706
1752
|
}
|
|
1707
1753
|
|
|
1708
1754
|
private buildForwardPrompt(
|
|
@@ -1712,10 +1758,11 @@ export class Engine {
|
|
|
1712
1758
|
authorKind: string | undefined,
|
|
1713
1759
|
issueTitle: string,
|
|
1714
1760
|
workdir: string,
|
|
1715
|
-
instructions: { issueRef: string }
|
|
1761
|
+
instructions: { issueRef: string },
|
|
1762
|
+
extraTrusted: string[] = [],
|
|
1716
1763
|
): string {
|
|
1717
1764
|
const who = authorKind === "bot" ? `@${commentUser} (bot)` : `@${commentUser} (user)`;
|
|
1718
|
-
const trusted = this.isTrustedAuthor(commentUser);
|
|
1765
|
+
const trusted = this.isTrustedAuthor(commentUser, extraTrusted);
|
|
1719
1766
|
return [
|
|
1720
1767
|
`[SYSTEM FORWARD] User ${who}${trusted ? "" : " (unverified outside user)"} posted a new comment on ${instructions.issueRef} "${issueTitle}".`,
|
|
1721
1768
|
trusted
|