mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-12 00:03:26 +08:00
feat: add unified IM access policies
This commit is contained in:
parent
075d2713c1
commit
840e5aa857
89 changed files with 5573 additions and 1676 deletions
|
|
@ -1,4 +1,8 @@
|
|||
import { t } from './i18n.mjs';
|
||||
import {
|
||||
COMMAND_PERMISSION_DENIED_MESSAGE,
|
||||
evaluateInboundAccess,
|
||||
} from './inbound-access.mjs';
|
||||
import { captureContextEnhancement, enhanceContextContent } from './context-enhancement.mjs';
|
||||
import { runWorkspaceCommand } from './workspace-command.mjs';
|
||||
import { runCompactCommand } from './compact-command.mjs';
|
||||
|
|
@ -129,6 +133,7 @@ export class TextHarnessBridge {
|
|||
#harness;
|
||||
#state;
|
||||
#contextEnhancement;
|
||||
#accessPolicy;
|
||||
#status;
|
||||
#logger;
|
||||
#replyTimeoutMs;
|
||||
|
|
@ -149,6 +154,7 @@ export class TextHarnessBridge {
|
|||
harness,
|
||||
state,
|
||||
contextEnhancement,
|
||||
accessPolicy,
|
||||
status = createTextBridgeStatus(),
|
||||
logger = console,
|
||||
replyTimeoutMs = 600_000,
|
||||
|
|
@ -162,6 +168,7 @@ export class TextHarnessBridge {
|
|||
this.#harness = harness;
|
||||
this.#state = state;
|
||||
this.#contextEnhancement = contextEnhancement;
|
||||
this.#accessPolicy = accessPolicy;
|
||||
this.#status = status;
|
||||
this.#logger = logger;
|
||||
this.#replyTimeoutMs = replyTimeoutMs;
|
||||
|
|
@ -176,7 +183,7 @@ export class TextHarnessBridge {
|
|||
return structuredClone(this.#status);
|
||||
}
|
||||
|
||||
accept(message, { contextSnapshot } = {}) {
|
||||
accept(message, { contextSnapshot, accessDecision } = {}) {
|
||||
if (this.#signal?.aborted) return Promise.resolve();
|
||||
const conversationId = cleanText(message?.conversationId);
|
||||
const kind = message?.kind === 'group' ? 'group' : 'direct';
|
||||
|
|
@ -187,6 +194,40 @@ export class TextHarnessBridge {
|
|||
|| this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Preserve the channel trigger boundary. Access policy never turns an
|
||||
// unaddressed group message into a denial reply.
|
||||
if (kind === 'group' && normalized.addressed !== true) {
|
||||
this.#status.messagesRejected += 1;
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
this.#acceptedMessageIds.set(messageId, null);
|
||||
return this.#finishLocalMessage(normalized, messageId, null);
|
||||
}
|
||||
if (this.#accessPolicy || accessDecision) {
|
||||
const hasImages = hasInboundImages(normalized);
|
||||
const hasFiles = hasInboundFiles(normalized);
|
||||
const decision = accessDecision ?? evaluateInboundAccess(this.#accessPolicy, {
|
||||
conversationType: kind,
|
||||
senderIds: [senderId, cleanText(normalized.senderAlternateId)].filter(Boolean),
|
||||
text: normalized.content,
|
||||
hasImages,
|
||||
hasFiles,
|
||||
});
|
||||
if (!decision.allowed) {
|
||||
this.#status.messagesRejected += 1;
|
||||
this.#status.lastRejectedAt = new Date().toISOString();
|
||||
// Mark policy denials so a webhook replay cannot repeat local work or
|
||||
// a command-permission notice.
|
||||
this.#acceptedMessageIds.set(messageId, null);
|
||||
return this.#finishLocalMessage(
|
||||
normalized,
|
||||
messageId,
|
||||
decision.reason === 'command-not-allowed'
|
||||
? t(COMMAND_PERMISSION_DENIED_MESSAGE)
|
||||
: null,
|
||||
{ recordReceived: decision.reason === 'command-not-allowed' },
|
||||
);
|
||||
}
|
||||
}
|
||||
this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined
|
||||
? captureContextEnhancement(this.#contextEnhancement, message?.kind)
|
||||
: contextSnapshot);
|
||||
|
|
@ -350,13 +391,15 @@ export class TextHarnessBridge {
|
|||
return this.#enqueueMessage(normalized, messageId, senderId, key);
|
||||
}
|
||||
|
||||
#finishLocalMessage(message, messageId, reply) {
|
||||
#finishLocalMessage(message, messageId, reply, { recordReceived = true } = {}) {
|
||||
let task;
|
||||
task = (async () => {
|
||||
if (this.#state.hasSeen(messageId)) return;
|
||||
await this.#state.markSeen(messageId);
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
if (recordReceived) {
|
||||
this.#status.messagesReceived += 1;
|
||||
this.#status.lastMessageAt = new Date().toISOString();
|
||||
}
|
||||
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
||||
this.#status.lastError = null;
|
||||
})().catch(async (error) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue