@pinet/slack-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type FollowerRuntimeDiagnostic, type FollowerThreadState, type InboxMessage, type PinetControlCommand, type PinetRemoteControlRequestResult, type SlackBridgeSettings } from "./helpers.js";
|
|
3
|
+
import { type FollowerDeliveryState } from "./follower-delivery.js";
|
|
4
|
+
import { BrokerClient } from "./broker/client.js";
|
|
5
|
+
export type BrokerClientRef = {
|
|
6
|
+
client: BrokerClient;
|
|
7
|
+
pollInterval: ReturnType<typeof setInterval> | null;
|
|
8
|
+
};
|
|
9
|
+
type SharedFollowerThreadState = Pick<FollowerThreadState, "channelId" | "threadTs" | "userId" | "source" | "owner">;
|
|
10
|
+
export interface FollowerRuntimeDeps {
|
|
11
|
+
getSettings: () => SlackBridgeSettings;
|
|
12
|
+
refreshSettings: () => void;
|
|
13
|
+
getPinetEnabled: () => boolean;
|
|
14
|
+
getAgentIdentity: () => {
|
|
15
|
+
name: string;
|
|
16
|
+
emoji: string;
|
|
17
|
+
};
|
|
18
|
+
getAgentStableId: () => string;
|
|
19
|
+
getAgentOwnerToken: () => string;
|
|
20
|
+
setAgentOwnerToken: (ownerToken: string) => void;
|
|
21
|
+
getDesiredAgentStatus: () => "working" | "idle";
|
|
22
|
+
getAgentAliases: () => Iterable<string>;
|
|
23
|
+
getThreads: () => Map<string, SharedFollowerThreadState>;
|
|
24
|
+
getLastDmChannel: () => string | null;
|
|
25
|
+
setLastDmChannel: (channelId: string | null) => void;
|
|
26
|
+
pushInboxMessages: (messages: InboxMessage[]) => void;
|
|
27
|
+
getAgentMetadata: (role: "broker" | "worker") => Promise<Record<string, unknown>>;
|
|
28
|
+
applyRegistrationIdentity: (registration: {
|
|
29
|
+
name: string;
|
|
30
|
+
emoji: string;
|
|
31
|
+
metadata?: Record<string, unknown> | null;
|
|
32
|
+
}) => void;
|
|
33
|
+
persistState: () => void;
|
|
34
|
+
updateBadge: () => void;
|
|
35
|
+
maybeDrainInboxIfIdle: (ctx: ExtensionContext) => boolean;
|
|
36
|
+
requestRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => PinetRemoteControlRequestResult;
|
|
37
|
+
deferControlAck: (command: PinetControlCommand, inboxId: number) => void;
|
|
38
|
+
runRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => void;
|
|
39
|
+
deliverFollowUpMessage: (text: string) => boolean;
|
|
40
|
+
setExtStatus: (ctx: ExtensionContext, state: "ok" | "reconnecting" | "error" | "off") => void;
|
|
41
|
+
getRuntimeDiagnostic: () => FollowerRuntimeDiagnostic | null;
|
|
42
|
+
setRuntimeDiagnostic: (diagnostic: FollowerRuntimeDiagnostic | null) => void;
|
|
43
|
+
handleTerminalReconnectFailure: (ctx: ExtensionContext, error: Error) => Promise<void> | void;
|
|
44
|
+
formatError: (error: unknown) => string;
|
|
45
|
+
deliveryState: FollowerDeliveryState;
|
|
46
|
+
}
|
|
47
|
+
export interface FollowerRuntime {
|
|
48
|
+
connect: (ctx: ExtensionContext) => Promise<BrokerClientRef>;
|
|
49
|
+
disconnect: (ctx: ExtensionContext, options?: {
|
|
50
|
+
releaseIdentity?: boolean;
|
|
51
|
+
}) => Promise<{
|
|
52
|
+
unregisterError: string | null;
|
|
53
|
+
}>;
|
|
54
|
+
syncDesiredStatus: (desiredStatus: "working" | "idle", options?: {
|
|
55
|
+
force?: boolean;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
flushDeliveredAcks: () => Promise<void>;
|
|
58
|
+
getClientRef: () => BrokerClientRef | null;
|
|
59
|
+
}
|
|
60
|
+
export declare function createFollowerRuntime(deps: FollowerRuntimeDeps): FollowerRuntime;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { buildFollowerRuntimeDiagnostic, buildPinetOwnerToken, extractPinetControlCommand, formatPinetInboxMessages, getFollowerOwnedThreadReclaims, getFollowerReconnectUiUpdate, partitionFollowerInboxEntries, resolvePinetMeshAuth, resolveRuntimeAgentIdentity, syncFollowerInboxEntries, syncTransferredSlackThreadContexts, } from "./helpers.js";
|
|
2
|
+
import { drainFollowerAckBatches, hasDeliveredFollowerInboxIds, isFollowerInboxIdTracked, markFollowerInboxIdsDelivered, queueFollowerInboxIds, resetFollowerDeliveryState, } from "./follower-delivery.js";
|
|
3
|
+
import { BrokerClient, DEFAULT_SOCKET_PATH } from "./broker/client.js";
|
|
4
|
+
function getInboxIds(entries) {
|
|
5
|
+
return entries.flatMap((entry) => (typeof entry.inboxId === "number" ? [entry.inboxId] : []));
|
|
6
|
+
}
|
|
7
|
+
function mergeFollowerThreadUpdates(threads, updates) {
|
|
8
|
+
for (const nextThread of updates) {
|
|
9
|
+
const existing = threads.get(nextThread.threadTs);
|
|
10
|
+
if (!existing) {
|
|
11
|
+
threads.set(nextThread.threadTs, { ...nextThread });
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
existing.channelId = nextThread.channelId;
|
|
15
|
+
existing.threadTs = nextThread.threadTs;
|
|
16
|
+
existing.userId = nextThread.userId;
|
|
17
|
+
existing.owner = nextThread.owner;
|
|
18
|
+
existing.source = nextThread.source ?? existing.source;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function createFollowerRuntime(deps) {
|
|
22
|
+
let clientRef = null;
|
|
23
|
+
let followerPollRunning = false;
|
|
24
|
+
let wasDisconnected = false;
|
|
25
|
+
let followerAckPromise = null;
|
|
26
|
+
let syncedFollowerStatus = null;
|
|
27
|
+
let followerStatusSyncPromise = null;
|
|
28
|
+
let followerStatusSyncTarget = null;
|
|
29
|
+
async function flushDeliveredAcks() {
|
|
30
|
+
if (followerAckPromise) {
|
|
31
|
+
await followerAckPromise;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!clientRef?.client) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const client = clientRef.client;
|
|
38
|
+
const promise = drainFollowerAckBatches(deps.deliveryState, async (ids) => {
|
|
39
|
+
await client.ackMessages(ids);
|
|
40
|
+
}).finally(() => {
|
|
41
|
+
if (followerAckPromise === promise) {
|
|
42
|
+
followerAckPromise = null;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
followerAckPromise = promise;
|
|
46
|
+
await promise;
|
|
47
|
+
}
|
|
48
|
+
async function syncDesiredStatus(desiredStatus, options = {}) {
|
|
49
|
+
if (!clientRef) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!options.force && syncedFollowerStatus === desiredStatus) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (followerStatusSyncPromise && followerStatusSyncTarget === desiredStatus) {
|
|
56
|
+
await followerStatusSyncPromise;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const targetStatus = desiredStatus;
|
|
60
|
+
const request = clientRef.client.updateStatus(targetStatus).then(() => {
|
|
61
|
+
syncedFollowerStatus = targetStatus;
|
|
62
|
+
});
|
|
63
|
+
followerStatusSyncTarget = targetStatus;
|
|
64
|
+
const inFlight = request.finally(() => {
|
|
65
|
+
if (followerStatusSyncPromise === inFlight) {
|
|
66
|
+
followerStatusSyncPromise = null;
|
|
67
|
+
followerStatusSyncTarget = null;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
followerStatusSyncPromise = inFlight;
|
|
71
|
+
await inFlight;
|
|
72
|
+
}
|
|
73
|
+
function stopPolling() {
|
|
74
|
+
if (clientRef?.pollInterval) {
|
|
75
|
+
clearInterval(clientRef.pollInterval);
|
|
76
|
+
clientRef.pollInterval = null;
|
|
77
|
+
}
|
|
78
|
+
followerPollRunning = false;
|
|
79
|
+
}
|
|
80
|
+
function resetFollowerRuntimeState() {
|
|
81
|
+
clientRef = null;
|
|
82
|
+
followerPollRunning = false;
|
|
83
|
+
wasDisconnected = false;
|
|
84
|
+
followerAckPromise = null;
|
|
85
|
+
syncedFollowerStatus = null;
|
|
86
|
+
followerStatusSyncPromise = null;
|
|
87
|
+
followerStatusSyncTarget = null;
|
|
88
|
+
resetFollowerDeliveryState(deps.deliveryState);
|
|
89
|
+
}
|
|
90
|
+
async function connect(ctx) {
|
|
91
|
+
deps.refreshSettings();
|
|
92
|
+
const meshAuth = resolvePinetMeshAuth(deps.getSettings());
|
|
93
|
+
const client = new BrokerClient({
|
|
94
|
+
path: DEFAULT_SOCKET_PATH,
|
|
95
|
+
...(meshAuth.meshSecret ? { meshSecret: meshAuth.meshSecret } : {}),
|
|
96
|
+
...(meshAuth.meshSecretPath ? { meshSecretPath: meshAuth.meshSecretPath } : {}),
|
|
97
|
+
});
|
|
98
|
+
async function registerFollowerRuntime() {
|
|
99
|
+
deps.refreshSettings();
|
|
100
|
+
const settings = deps.getSettings();
|
|
101
|
+
const { name, emoji } = deps.getAgentIdentity();
|
|
102
|
+
const workerIdentity = resolveRuntimeAgentIdentity({ name, emoji }, settings, process.env.PI_NICKNAME, ctx.sessionManager.getSessionFile() ?? deps.getAgentStableId(), "worker");
|
|
103
|
+
const hasExplicitIdentityRequest = Boolean(settings.agentName?.trim() && settings.agentEmoji?.trim()) ||
|
|
104
|
+
Boolean(process.env.PI_NICKNAME?.trim());
|
|
105
|
+
deps.setAgentOwnerToken(buildPinetOwnerToken(deps.getAgentStableId()));
|
|
106
|
+
const registration = await client.register(hasExplicitIdentityRequest ? workerIdentity.name : "", hasExplicitIdentityRequest ? workerIdentity.emoji : "", await deps.getAgentMetadata("worker"), deps.getAgentStableId());
|
|
107
|
+
deps.applyRegistrationIdentity(registration);
|
|
108
|
+
client.setHeartbeatMetadataProvider(() => deps.getAgentMetadata("worker"));
|
|
109
|
+
}
|
|
110
|
+
async function resumeThreadClaims() {
|
|
111
|
+
for (const thread of getFollowerOwnedThreadReclaims(deps.getThreads(), deps.getAgentIdentity().name, deps.getAgentAliases(), deps.getAgentOwnerToken())) {
|
|
112
|
+
try {
|
|
113
|
+
await client.claimThread(thread.threadTs, thread.channelId, thread.source);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function startPolling() {
|
|
121
|
+
if (!clientRef || clientRef.pollInterval) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
clientRef.pollInterval = setInterval(async () => {
|
|
125
|
+
if (!deps.getPinetEnabled() || followerPollRunning || clientRef?.client !== client) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
followerPollRunning = true;
|
|
129
|
+
try {
|
|
130
|
+
const entries = await client.pollInbox();
|
|
131
|
+
if (deps.getRuntimeDiagnostic()?.kind === "poll_failure") {
|
|
132
|
+
deps.setRuntimeDiagnostic(null);
|
|
133
|
+
}
|
|
134
|
+
const newEntries = entries.filter((entry) => !isFollowerInboxIdTracked(deps.deliveryState, entry.inboxId));
|
|
135
|
+
if (newEntries.length === 0) {
|
|
136
|
+
if (hasDeliveredFollowerInboxIds(deps.deliveryState)) {
|
|
137
|
+
void flushDeliveredAcks();
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const controlEntries = [];
|
|
142
|
+
const remainingEntries = [];
|
|
143
|
+
for (const entry of newEntries) {
|
|
144
|
+
const command = extractPinetControlCommand({
|
|
145
|
+
threadId: entry.message.threadId,
|
|
146
|
+
body: entry.message.body,
|
|
147
|
+
metadata: entry.message.metadata,
|
|
148
|
+
});
|
|
149
|
+
if (command) {
|
|
150
|
+
controlEntries.push({ inboxId: entry.inboxId, command });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
remainingEntries.push(entry);
|
|
154
|
+
}
|
|
155
|
+
if (controlEntries.length > 0) {
|
|
156
|
+
const immediateAckIds = [];
|
|
157
|
+
const commandsToStart = [];
|
|
158
|
+
for (const entry of controlEntries) {
|
|
159
|
+
const queued = deps.requestRemoteControl(entry.command, ctx);
|
|
160
|
+
if (queued.ackDisposition === "immediate") {
|
|
161
|
+
immediateAckIds.push(entry.inboxId);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
deps.deferControlAck(queued.scheduledCommand, entry.inboxId);
|
|
165
|
+
}
|
|
166
|
+
if (queued.shouldStartNow) {
|
|
167
|
+
commandsToStart.push(entry.command);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (immediateAckIds.length > 0) {
|
|
171
|
+
await client.ackMessages(immediateAckIds);
|
|
172
|
+
}
|
|
173
|
+
for (const command of commandsToStart) {
|
|
174
|
+
deps.runRemoteControl(command, ctx);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { nudges, agentMessages, regular } = partitionFollowerInboxEntries(remainingEntries);
|
|
179
|
+
if (nudges.length > 0) {
|
|
180
|
+
const nudgeText = nudges
|
|
181
|
+
.map((nudge) => nudge.message.body ?? "")
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
.join("\n");
|
|
184
|
+
if (nudgeText && deps.deliverFollowUpMessage(nudgeText)) {
|
|
185
|
+
markFollowerInboxIdsDelivered(deps.deliveryState, getInboxIds(nudges));
|
|
186
|
+
void flushDeliveredAcks();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (agentMessages.length > 0) {
|
|
190
|
+
const transferredThreads = syncTransferredSlackThreadContexts(agentMessages, deps.getThreads(), deps.getAgentOwnerToken());
|
|
191
|
+
if (transferredThreads.threadUpdates.length > 0) {
|
|
192
|
+
mergeFollowerThreadUpdates(deps.getThreads(), transferredThreads.threadUpdates);
|
|
193
|
+
if (transferredThreads.changed) {
|
|
194
|
+
deps.persistState();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const pinetPrompt = formatPinetInboxMessages(agentMessages);
|
|
198
|
+
if (deps.deliverFollowUpMessage(pinetPrompt)) {
|
|
199
|
+
markFollowerInboxIdsDelivered(deps.deliveryState, getInboxIds(agentMessages));
|
|
200
|
+
void flushDeliveredAcks();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (regular.length > 0) {
|
|
204
|
+
const synced = syncFollowerInboxEntries(regular, deps.getThreads(), deps.getAgentOwnerToken(), deps.getLastDmChannel());
|
|
205
|
+
mergeFollowerThreadUpdates(deps.getThreads(), synced.threadUpdates);
|
|
206
|
+
deps.setLastDmChannel(synced.lastDmChannel);
|
|
207
|
+
deps.pushInboxMessages(synced.inboxMessages);
|
|
208
|
+
queueFollowerInboxIds(deps.deliveryState, getInboxIds(regular));
|
|
209
|
+
if (synced.changed) {
|
|
210
|
+
deps.persistState();
|
|
211
|
+
}
|
|
212
|
+
deps.updateBadge();
|
|
213
|
+
deps.maybeDrainInboxIfIdle(ctx);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
deps.setRuntimeDiagnostic(buildFollowerRuntimeDiagnostic("poll_failure", {
|
|
218
|
+
detail: deps.formatError(error),
|
|
219
|
+
connected: client.isConnected(),
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
void syncDesiredStatus(deps.getDesiredAgentStatus()).catch(() => {
|
|
224
|
+
/* best effort */
|
|
225
|
+
});
|
|
226
|
+
followerPollRunning = false;
|
|
227
|
+
}
|
|
228
|
+
}, 2000);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
await client.connect();
|
|
232
|
+
await registerFollowerRuntime();
|
|
233
|
+
deps.setRuntimeDiagnostic(null);
|
|
234
|
+
syncedFollowerStatus = "idle";
|
|
235
|
+
clientRef = {
|
|
236
|
+
client,
|
|
237
|
+
pollInterval: null,
|
|
238
|
+
};
|
|
239
|
+
followerAckPromise = null;
|
|
240
|
+
wasDisconnected = false;
|
|
241
|
+
followerPollRunning = false;
|
|
242
|
+
resetFollowerDeliveryState(deps.deliveryState);
|
|
243
|
+
client.onDisconnect(() => {
|
|
244
|
+
if (clientRef?.client !== client) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
stopPolling();
|
|
248
|
+
deps.setRuntimeDiagnostic(buildFollowerRuntimeDiagnostic("broker_disconnect"));
|
|
249
|
+
deps.setExtStatus(ctx, "reconnecting");
|
|
250
|
+
const uiUpdate = getFollowerReconnectUiUpdate("disconnect", wasDisconnected);
|
|
251
|
+
wasDisconnected = uiUpdate.nextWasDisconnected;
|
|
252
|
+
if (uiUpdate.notify) {
|
|
253
|
+
ctx.ui.notify(uiUpdate.notify.message, uiUpdate.notify.level);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
client.onReconnect(() => {
|
|
257
|
+
void (async () => {
|
|
258
|
+
if (clientRef?.client !== client) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
await registerFollowerRuntime();
|
|
263
|
+
deps.setRuntimeDiagnostic(null);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error(`[slack-bridge] follower reconnect registration refresh failed: ${deps.formatError(error)}`);
|
|
267
|
+
deps.setRuntimeDiagnostic(buildFollowerRuntimeDiagnostic("registration_refresh_failure", {
|
|
268
|
+
detail: deps.formatError(error),
|
|
269
|
+
}));
|
|
270
|
+
const registration = client.getRegisteredIdentity();
|
|
271
|
+
if (registration) {
|
|
272
|
+
deps.applyRegistrationIdentity(registration);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await resumeThreadClaims();
|
|
276
|
+
syncedFollowerStatus = "idle";
|
|
277
|
+
void syncDesiredStatus(deps.getDesiredAgentStatus()).catch(() => {
|
|
278
|
+
/* best effort */
|
|
279
|
+
});
|
|
280
|
+
startPolling();
|
|
281
|
+
if (hasDeliveredFollowerInboxIds(deps.deliveryState)) {
|
|
282
|
+
void flushDeliveredAcks();
|
|
283
|
+
}
|
|
284
|
+
deps.setExtStatus(ctx, "ok");
|
|
285
|
+
const uiUpdate = getFollowerReconnectUiUpdate("reconnect", wasDisconnected);
|
|
286
|
+
wasDisconnected = uiUpdate.nextWasDisconnected;
|
|
287
|
+
if (uiUpdate.notify) {
|
|
288
|
+
ctx.ui.notify(uiUpdate.notify.message, uiUpdate.notify.level);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
});
|
|
292
|
+
client.onReconnectFailed((error) => {
|
|
293
|
+
if (clientRef?.client !== client) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const reconnectError = error instanceof Error ? error : new Error(String(error));
|
|
297
|
+
deps.setRuntimeDiagnostic(buildFollowerRuntimeDiagnostic("reconnect_stopped", {
|
|
298
|
+
detail: deps.formatError(reconnectError),
|
|
299
|
+
}));
|
|
300
|
+
void deps.handleTerminalReconnectFailure(ctx, reconnectError);
|
|
301
|
+
});
|
|
302
|
+
await resumeThreadClaims();
|
|
303
|
+
startPolling();
|
|
304
|
+
return clientRef;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
await client.unregister().catch(() => {
|
|
308
|
+
/* best effort */
|
|
309
|
+
});
|
|
310
|
+
client.disconnect();
|
|
311
|
+
resetFollowerRuntimeState();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function disconnect(_ctx, options = {}) {
|
|
316
|
+
const current = clientRef;
|
|
317
|
+
stopPolling();
|
|
318
|
+
await flushDeliveredAcks().catch(() => {
|
|
319
|
+
/* best effort */
|
|
320
|
+
});
|
|
321
|
+
let unregisterError = null;
|
|
322
|
+
if (current) {
|
|
323
|
+
if (options.releaseIdentity === false) {
|
|
324
|
+
try {
|
|
325
|
+
current.client.disconnect();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* best effort */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
try {
|
|
333
|
+
await current.client.disconnectGracefully();
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
unregisterError = deps.formatError(error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
resetFollowerRuntimeState();
|
|
341
|
+
return { unregisterError };
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
connect,
|
|
345
|
+
disconnect,
|
|
346
|
+
syncDesiredStatus,
|
|
347
|
+
flushDeliveredAcks,
|
|
348
|
+
getClientRef: () => clientRef,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type ExecFileResult = {
|
|
2
|
+
stdout?: string | Buffer;
|
|
3
|
+
};
|
|
4
|
+
export type ExecFileAsyncLike = (file: string, args: string[], options: {
|
|
5
|
+
cwd: string;
|
|
6
|
+
encoding: "utf-8";
|
|
7
|
+
}) => Promise<ExecFileResult>;
|
|
8
|
+
export interface GitContext {
|
|
9
|
+
cwd: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
repoRoot?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
dirty?: boolean;
|
|
14
|
+
dirtyFileCount?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface GitDynamicState {
|
|
17
|
+
/** Current branch, omitted when detached/unknown/not in a repo. */
|
|
18
|
+
branch?: string;
|
|
19
|
+
/** Dirty flag, omitted when `git status` could not be read. */
|
|
20
|
+
dirty?: boolean;
|
|
21
|
+
/** Number of changed/untracked entries when dirty state is known. */
|
|
22
|
+
dirtyFileCount?: number;
|
|
23
|
+
/** True when any git probe failed; callers should treat omitted values as unknown. */
|
|
24
|
+
probeFailed: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function probeGitBranch(cwd?: string, runner?: ExecFileAsyncLike): Promise<string | undefined>;
|
|
27
|
+
export declare function probeGitDynamic(cwd?: string, runner?: ExecFileAsyncLike): Promise<GitDynamicState>;
|
|
28
|
+
export declare function probeGitContext(cwd?: string, runner?: ExecFileAsyncLike): Promise<GitContext>;
|
|
29
|
+
export interface GitContextCache {
|
|
30
|
+
get(): Promise<GitContext>;
|
|
31
|
+
peek(): GitContext | null;
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare function createGitContextCache(loader: () => Promise<GitContext>): GitContextCache;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
async function runGitCommand(args, cwd, runner) {
|
|
6
|
+
try {
|
|
7
|
+
const result = await runner("git", args, { cwd, encoding: "utf-8" });
|
|
8
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout?.toString();
|
|
9
|
+
return { stdout, ok: true };
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { stdout: undefined, ok: false };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function readGitTrimmed(args, cwd, runner) {
|
|
16
|
+
const { stdout, ok } = await runGitCommand(args, cwd, runner);
|
|
17
|
+
const trimmed = stdout?.trim();
|
|
18
|
+
return { value: trimmed ? trimmed : undefined, ok };
|
|
19
|
+
}
|
|
20
|
+
export async function probeGitBranch(cwd = process.cwd(), runner = execFileAsync) {
|
|
21
|
+
return (await readGitTrimmed(["branch", "--show-current"], cwd, runner)).value;
|
|
22
|
+
}
|
|
23
|
+
async function probeGitDirty(cwd, runner) {
|
|
24
|
+
const { stdout, ok } = await runGitCommand(["status", "--porcelain", "--untracked-files=normal"], cwd, runner);
|
|
25
|
+
if (!ok) {
|
|
26
|
+
return { ok: false };
|
|
27
|
+
}
|
|
28
|
+
const dirtyFileCount = (stdout ?? "")
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((line) => line.replace(/\r$/, ""))
|
|
31
|
+
.filter((line) => line.length > 0).length;
|
|
32
|
+
return { dirty: dirtyFileCount > 0, dirtyFileCount, ok: true };
|
|
33
|
+
}
|
|
34
|
+
export async function probeGitDynamic(cwd = process.cwd(), runner = execFileAsync) {
|
|
35
|
+
const branchProbe = await readGitTrimmed(["branch", "--show-current"], cwd, runner);
|
|
36
|
+
const dirtyProbe = await probeGitDirty(cwd, runner);
|
|
37
|
+
return {
|
|
38
|
+
...(branchProbe.value ? { branch: branchProbe.value } : {}),
|
|
39
|
+
...(typeof dirtyProbe.dirty === "boolean" ? { dirty: dirtyProbe.dirty } : {}),
|
|
40
|
+
...(typeof dirtyProbe.dirtyFileCount === "number"
|
|
41
|
+
? { dirtyFileCount: dirtyProbe.dirtyFileCount }
|
|
42
|
+
: {}),
|
|
43
|
+
probeFailed: !branchProbe.ok || !dirtyProbe.ok,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function probeGitContext(cwd = process.cwd(), runner = execFileAsync) {
|
|
47
|
+
const repoRoot = (await readGitTrimmed(["rev-parse", "--show-toplevel"], cwd, runner)).value;
|
|
48
|
+
const dynamic = await probeGitDynamic(cwd, runner);
|
|
49
|
+
const resolvedRepoRoot = repoRoot ?? cwd;
|
|
50
|
+
return {
|
|
51
|
+
cwd,
|
|
52
|
+
repo: path.basename(resolvedRepoRoot),
|
|
53
|
+
repoRoot,
|
|
54
|
+
...(dynamic.branch ? { branch: dynamic.branch } : {}),
|
|
55
|
+
...(typeof dynamic.dirty === "boolean" ? { dirty: dynamic.dirty } : {}),
|
|
56
|
+
...(typeof dynamic.dirtyFileCount === "number"
|
|
57
|
+
? { dirtyFileCount: dynamic.dirtyFileCount }
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function createGitContextCache(loader) {
|
|
62
|
+
let cached = null;
|
|
63
|
+
let inflight = null;
|
|
64
|
+
return {
|
|
65
|
+
async get() {
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
if (inflight)
|
|
69
|
+
return inflight;
|
|
70
|
+
inflight = loader()
|
|
71
|
+
.then((result) => {
|
|
72
|
+
cached = result;
|
|
73
|
+
return result;
|
|
74
|
+
})
|
|
75
|
+
.finally(() => {
|
|
76
|
+
inflight = null;
|
|
77
|
+
});
|
|
78
|
+
return inflight;
|
|
79
|
+
},
|
|
80
|
+
peek() {
|
|
81
|
+
return cached;
|
|
82
|
+
},
|
|
83
|
+
clear() {
|
|
84
|
+
cached = null;
|
|
85
|
+
inflight = null;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface SecurityGuardrails {
|
|
2
|
+
readOnly?: boolean;
|
|
3
|
+
requireConfirmation?: string[];
|
|
4
|
+
blockedTools?: string[];
|
|
5
|
+
}
|
|
6
|
+
/** Well-known read-only tools that are always safe */
|
|
7
|
+
export declare const READ_ONLY_TOOLS: Set<string>;
|
|
8
|
+
/** Well-known write tools (used when readOnly mode is active) */
|
|
9
|
+
export declare const WRITE_TOOLS: Set<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Match a tool name against glob patterns (supports `*` wildcard).
|
|
12
|
+
* Slack and Pinet dispatcher actions use `<namespace>:<action>` names; legacy
|
|
13
|
+
* `<namespace>_<action>` guardrail patterns still match during migration. Pinet
|
|
14
|
+
* direct tools are no longer registered, but keeping alias matching prevents old
|
|
15
|
+
* security configs from silently failing open.
|
|
16
|
+
* Returns true if the tool name matches any of the patterns.
|
|
17
|
+
*/
|
|
18
|
+
export declare function matchesToolPattern(toolName: string, patterns: string[]): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a tool is blocked by the guardrails.
|
|
21
|
+
* Returns true if:
|
|
22
|
+
* - tool matches any blockedTools pattern
|
|
23
|
+
* - guardrails.readOnly is true AND tool is in WRITE_TOOLS
|
|
24
|
+
*/
|
|
25
|
+
export declare function isToolBlocked(toolName: string, guardrails: SecurityGuardrails): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Check if a tool needs confirmation.
|
|
28
|
+
* Returns true if tool matches any requireConfirmation pattern.
|
|
29
|
+
* Returns false if tool is already blocked (blocked takes priority).
|
|
30
|
+
*/
|
|
31
|
+
export declare function toolNeedsConfirmation(toolName: string, guardrails: SecurityGuardrails): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Build security instructions to prepend to Slack-triggered messages.
|
|
34
|
+
* Returns a clear, structured prompt with active guardrails.
|
|
35
|
+
* Returns empty string if no guardrails are active.
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasEffectivelyEmptyRuntimeGuardrails(guardrails: SecurityGuardrails | null | undefined): boolean;
|
|
38
|
+
export declare function formatRuntimeGuardrailsPosture(guardrails: SecurityGuardrails | null | undefined): string;
|
|
39
|
+
export declare function getEmptyRuntimeGuardrailsWarning(guardrails: SecurityGuardrails | null | undefined): string | null;
|
|
40
|
+
export declare function buildSecurityPrompt(guardrails: SecurityGuardrails): string;
|
|
41
|
+
/**
|
|
42
|
+
* Tools that the broker agent must NEVER use.
|
|
43
|
+
* The broker is infrastructure — it coordinates, not codes.
|
|
44
|
+
* Spawning local subagents (Agent) is forbidden because they
|
|
45
|
+
* have no Slack/Pinet connectivity and can't be monitored.
|
|
46
|
+
* Direct file mutation tools are also blocked so the runtime
|
|
47
|
+
* enforces the coordination-only role even if prompt guidance drifts.
|
|
48
|
+
*/
|
|
49
|
+
export declare const BROKER_FORBIDDEN_TOOLS: Set<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if a tool is forbidden for the broker role.
|
|
52
|
+
* Returns true if the broker must not use this tool.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isBrokerForbiddenTool(toolName: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Build a prompt snippet describing broker tool restrictions.
|
|
57
|
+
* Injected into the system prompt when the broker role is active.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildBrokerToolGuardrailsPrompt(): string;
|
|
60
|
+
/**
|
|
61
|
+
* Parse whether a user message is approving a confirmation request.
|
|
62
|
+
* Case-insensitive, trimmed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isConfirmationApproval(text: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Parse whether a user message is rejecting a confirmation request.
|
|
67
|
+
* Case-insensitive, trimmed.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isConfirmationRejection(text: string): boolean;
|