@vellumai/assistant 0.12.2-staging.7 → 0.12.2
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/docs/architecture/memory.md +2 -11
- package/docs/desktop-browser-cli.md +2 -4
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/openapi.yaml +2 -6
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +0 -124
- package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
- package/src/__tests__/channel-approval.test.ts +14 -9
- package/src/__tests__/conversation-agent-loop.test.ts +0 -25
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
- package/src/__tests__/terminal-tools.test.ts +0 -8
- package/src/__tests__/unicode.test.ts +0 -36
- package/src/agent/loop.ts +0 -19
- package/src/api/index.ts +0 -6
- package/src/approvals/approval-primitive.ts +2 -5
- package/src/approvals/scoped-approval-grants.ts +2 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
- package/src/daemon/conversation-agent-loop.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +1 -57
- package/src/daemon/conversation.ts +0 -17
- package/src/daemon/daemon-control.ts +6 -2
- package/src/daemon/orphan-reaper.ts +3 -4
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-conversation-ops.ts +15 -38
- package/src/desktop/desktop-automation-lease.test.ts +0 -143
- package/src/desktop/desktop-automation-lease.ts +3 -39
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +0 -70
- package/src/notifications/copy-composer.ts +3 -11
- package/src/oauth/seed-providers.ts +30 -0
- package/src/persistence/conversation-plugin-facade.ts +0 -13
- package/src/persistence/schema/index.ts +0 -1
- package/src/persistence/steps.ts +0 -2
- package/src/plugin-api/conversation-turn.ts +7 -31
- package/src/plugin-api/index.ts +1 -9
- package/src/plugins/defaults/memory/AGENTS.md +2 -14
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
- package/src/plugins/defaults/memory/buffer-format.ts +0 -40
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
- package/src/plugins/defaults/memory/context-search/format.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +3 -4
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
- package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
- package/src/plugins/defaults/memory/v3/card.ts +1 -2
- package/src/plugins/defaults/memory/v3/injector.ts +178 -212
- package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
- package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
- package/src/plugins/defaults/memory/v3/sections.ts +1 -2
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +1 -86
- package/src/runtime/agent-wake.ts +4 -20
- package/src/runtime/guardian-action-service.ts +17 -2
- package/src/runtime/guardian-reply-router.ts +8 -1
- package/src/runtime/routes/channel-route-shared.ts +9 -1
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +3 -7
- package/src/runtime/routes/guardian-approval-interception.ts +0 -24
- package/src/runtime/routes/inbound-message-handler.ts +3 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +6 -12
- package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
- package/src/tools/shared/shell-output.test.ts +0 -10
- package/src/tools/shared/shell-output.ts +2 -14
- package/src/tools/skills/sandbox-runner.ts +2 -13
- package/src/tools/skills/scaffold-managed.ts +1 -2
- package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
- package/src/tools/terminal/safe-env.ts +1 -30
- package/src/tools/terminal/sanitized-bash.ts +2 -15
- package/src/tools/terminal/shell.test.ts +0 -29
- package/src/tools/terminal/shell.ts +7 -13
- package/src/util/host-process.test.ts +1 -17
- package/src/util/host-process.ts +0 -24
- package/src/util/unicode.ts +0 -29
- package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
- package/src/api/events/desktop-activity-changed.ts +0 -10
- package/src/persistence/conversation-tool-surface.ts +0 -86
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
- package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
- package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
- package/src/plugins/defaults/memory/buffer-file.ts +0 -354
- package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
- package/src/tools/terminal/shell-launch.test.ts +0 -162
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { eq, sql } from "drizzle-orm";
|
|
4
|
-
|
|
5
|
-
import type { ToolDefinition } from "../providers/types.js";
|
|
6
|
-
import { getDb } from "./db-connection.js";
|
|
7
|
-
import { conversationToolSurfaces } from "./schema/index.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Per-conversation record of the tool definitions the most recent live turn
|
|
11
|
-
* sent to the provider (`conversation_tool_surfaces`).
|
|
12
|
-
*
|
|
13
|
-
* The provider prompt cache is a byte-exact prefix match over
|
|
14
|
-
* `tools -> system -> messages`, so a background wake that forks a
|
|
15
|
-
* conversation can only reuse the source's cached prefix by sending the SAME
|
|
16
|
-
* tools array. Re-deriving the array on the fork cannot guarantee that: the
|
|
17
|
-
* fork may run in another process (a different tool registry), with no
|
|
18
|
-
* connected clients (different host-tool gates), or under a presence the
|
|
19
|
-
* persisted message stamps do not encode. Recording the resolved array and
|
|
20
|
-
* replaying it verbatim can.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** Content hash of the serialized tools array. */
|
|
24
|
-
export function hashConversationToolSurface(
|
|
25
|
-
tools: readonly ToolDefinition[],
|
|
26
|
-
): string {
|
|
27
|
-
return hashToolsJson(JSON.stringify(tools));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function hashToolsJson(toolsJson: string): string {
|
|
31
|
-
return createHash("sha256").update(toolsJson).digest("hex").slice(0, 32);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Persist `tools` as the conversation's current wire tool surface and return
|
|
36
|
-
* its hash. `knownHash` is the hash the caller last recorded for this
|
|
37
|
-
* conversation (undefined when it has recorded nothing this process
|
|
38
|
-
* lifetime): a matching hash skips the statement outright. Otherwise the
|
|
39
|
-
* upsert only rewrites a row whose stored hash differs, so a conversation
|
|
40
|
-
* reloaded from disk never rewrites an unchanged surface either.
|
|
41
|
-
*/
|
|
42
|
-
export function recordConversationToolSurface(
|
|
43
|
-
conversationId: string,
|
|
44
|
-
tools: readonly ToolDefinition[],
|
|
45
|
-
knownHash?: string,
|
|
46
|
-
): string {
|
|
47
|
-
const toolsJson = JSON.stringify(tools);
|
|
48
|
-
const toolsHash = hashToolsJson(toolsJson);
|
|
49
|
-
if (knownHash === toolsHash) {
|
|
50
|
-
return toolsHash;
|
|
51
|
-
}
|
|
52
|
-
const updatedAt = Date.now();
|
|
53
|
-
getDb()
|
|
54
|
-
.insert(conversationToolSurfaces)
|
|
55
|
-
.values({ conversationId, toolsJson, toolsHash, updatedAt })
|
|
56
|
-
.onConflictDoUpdate({
|
|
57
|
-
target: conversationToolSurfaces.conversationId,
|
|
58
|
-
set: { toolsJson, toolsHash, updatedAt },
|
|
59
|
-
setWhere: sql`${conversationToolSurfaces.toolsHash} <> excluded.tools_hash`,
|
|
60
|
-
})
|
|
61
|
-
.run();
|
|
62
|
-
return toolsHash;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The tool definitions the conversation's most recent live turn sent, or
|
|
67
|
-
* `null` when no turn has recorded one (or the stored JSON is unreadable).
|
|
68
|
-
*/
|
|
69
|
-
export function getConversationToolSurface(
|
|
70
|
-
conversationId: string,
|
|
71
|
-
): ToolDefinition[] | null {
|
|
72
|
-
const row = getDb()
|
|
73
|
-
.select({ toolsJson: conversationToolSurfaces.toolsJson })
|
|
74
|
-
.from(conversationToolSurfaces)
|
|
75
|
-
.where(eq(conversationToolSurfaces.conversationId, conversationId))
|
|
76
|
-
.get();
|
|
77
|
-
if (!row) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const parsed: unknown = JSON.parse(row.toolsJson);
|
|
82
|
-
return Array.isArray(parsed) ? (parsed as ToolDefinition[]) : null;
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import { describe, expect, test } from "bun:test";
|
|
3
|
-
|
|
4
|
-
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
-
|
|
6
|
-
import * as schema from "../schema.js";
|
|
7
|
-
import { migrateCreateConversationToolSurfaces } from "./378-create-conversation-tool-surfaces.js";
|
|
8
|
-
|
|
9
|
-
function createTestDb() {
|
|
10
|
-
const sqlite = new Database(":memory:");
|
|
11
|
-
sqlite.run("PRAGMA foreign_keys = ON");
|
|
12
|
-
// Only the parent table the FK points at.
|
|
13
|
-
sqlite.run(/*sql*/ `CREATE TABLE conversations (id TEXT PRIMARY KEY)`);
|
|
14
|
-
return { sqlite, db: drizzle(sqlite, { schema }) };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function tableDdl(sqlite: Database): string | undefined {
|
|
18
|
-
const row = sqlite
|
|
19
|
-
.query(
|
|
20
|
-
`SELECT sql FROM sqlite_master WHERE type='table' AND name='conversation_tool_surfaces'`,
|
|
21
|
-
)
|
|
22
|
-
.get() as { sql: string } | null;
|
|
23
|
-
return row?.sql;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function insertSurface(sqlite: Database, conversationId: string): void {
|
|
27
|
-
sqlite
|
|
28
|
-
.query(
|
|
29
|
-
/*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
|
|
30
|
-
)
|
|
31
|
-
.run(conversationId, "[]", "hash", 1);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function surfaceCount(sqlite: Database): number {
|
|
35
|
-
const row = sqlite
|
|
36
|
-
.query(`SELECT COUNT(*) AS n FROM conversation_tool_surfaces`)
|
|
37
|
-
.get() as { n: number };
|
|
38
|
-
return row.n;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe("migration 378: conversation_tool_surfaces", () => {
|
|
42
|
-
test("creates the table keyed by conversation with a cascading FK", () => {
|
|
43
|
-
const { sqlite, db } = createTestDb();
|
|
44
|
-
expect(tableDdl(sqlite)).toBeUndefined();
|
|
45
|
-
|
|
46
|
-
migrateCreateConversationToolSurfaces(db);
|
|
47
|
-
|
|
48
|
-
const ddl = tableDdl(sqlite);
|
|
49
|
-
expect(ddl).toContain("conversation_id TEXT PRIMARY KEY");
|
|
50
|
-
expect(ddl).toContain("REFERENCES conversations(id) ON DELETE CASCADE");
|
|
51
|
-
expect(ddl).toContain("tools_json TEXT NOT NULL");
|
|
52
|
-
expect(ddl).toContain("tools_hash TEXT NOT NULL");
|
|
53
|
-
expect(ddl).toContain("updated_at INTEGER NOT NULL");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("deleting a conversation cascades to its surface row", () => {
|
|
57
|
-
const { sqlite, db } = createTestDb();
|
|
58
|
-
migrateCreateConversationToolSurfaces(db);
|
|
59
|
-
sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
|
|
60
|
-
insertSurface(sqlite, "conv-1");
|
|
61
|
-
expect(surfaceCount(sqlite)).toBe(1);
|
|
62
|
-
|
|
63
|
-
sqlite.query(`DELETE FROM conversations WHERE id = ?`).run("conv-1");
|
|
64
|
-
|
|
65
|
-
expect(surfaceCount(sqlite)).toBe(0);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("is idempotent: a second run keeps the table and its rows", () => {
|
|
69
|
-
const { sqlite, db } = createTestDb();
|
|
70
|
-
migrateCreateConversationToolSurfaces(db);
|
|
71
|
-
sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
|
|
72
|
-
insertSurface(sqlite, "conv-1");
|
|
73
|
-
|
|
74
|
-
expect(() => migrateCreateConversationToolSurfaces(db)).not.toThrow();
|
|
75
|
-
|
|
76
|
-
expect(surfaceCount(sqlite)).toBe(1);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
-
|
|
3
|
-
const TABLE = "conversation_tool_surfaces";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Create `conversation_tool_surfaces`: one row per conversation holding the
|
|
7
|
-
* tool definitions its most recent live turn sent to the provider, keyed by
|
|
8
|
-
* conversation with a content hash so an unchanged surface is never rewritten.
|
|
9
|
-
*
|
|
10
|
-
* Its own table rather than a column on `conversations` because the payload
|
|
11
|
-
* is a JSON array of every tool definition (tens of kilobytes) that only fork
|
|
12
|
-
* wakes read, while `conversations` rows are read whole on every hot path.
|
|
13
|
-
* The row cascades with its conversation, so a deleted conversation leaves
|
|
14
|
-
* no surface behind.
|
|
15
|
-
*
|
|
16
|
-
* Idempotent via IF NOT EXISTS.
|
|
17
|
-
*/
|
|
18
|
-
export function migrateCreateConversationToolSurfaces(
|
|
19
|
-
database: DrizzleDb,
|
|
20
|
-
): void {
|
|
21
|
-
database.run(
|
|
22
|
-
`CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
23
|
-
conversation_id TEXT PRIMARY KEY REFERENCES conversations(id) ON DELETE CASCADE,
|
|
24
|
-
tools_json TEXT NOT NULL,
|
|
25
|
-
tools_hash TEXT NOT NULL,
|
|
26
|
-
updated_at INTEGER NOT NULL
|
|
27
|
-
)`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
-
|
|
3
|
-
import { conversations } from "./conversations.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The tool definitions a conversation's most recent live turn sent to the
|
|
7
|
-
* provider, serialized exactly as resolved. Written by the per-turn tool
|
|
8
|
-
* resolver when the array changes; read by fork wakes that replay it so their
|
|
9
|
-
* provider prompt-cache prefix matches the source's live turns. Cascades with
|
|
10
|
-
* its conversation.
|
|
11
|
-
*/
|
|
12
|
-
export const conversationToolSurfaces = sqliteTable(
|
|
13
|
-
"conversation_tool_surfaces",
|
|
14
|
-
{
|
|
15
|
-
conversationId: text("conversation_id")
|
|
16
|
-
.primaryKey()
|
|
17
|
-
.references(() => conversations.id, { onDelete: "cascade" }),
|
|
18
|
-
toolsJson: text("tools_json").notNull(),
|
|
19
|
-
toolsHash: text("tools_hash").notNull(),
|
|
20
|
-
updatedAt: integer("updated_at").notNull(),
|
|
21
|
-
},
|
|
22
|
-
);
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
type InboundTrustReadResult =
|
|
4
|
-
| {
|
|
5
|
-
ok: true;
|
|
6
|
-
verdict: {
|
|
7
|
-
trustClass: string;
|
|
8
|
-
canonicalSenderId: string | null;
|
|
9
|
-
contactId?: string;
|
|
10
|
-
status?: string;
|
|
11
|
-
resolutionFailed?: boolean;
|
|
12
|
-
};
|
|
13
|
-
admissionPolicy: string | null;
|
|
14
|
-
}
|
|
15
|
-
| { ok: false };
|
|
16
|
-
|
|
17
|
-
let nextRead: InboundTrustReadResult = { ok: false };
|
|
18
|
-
|
|
19
|
-
mock.module("../calls/inbound-trust-reader.js", () => ({
|
|
20
|
-
readInboundTrust: async () => nextRead,
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
PluginTurnNotAdmittedError,
|
|
25
|
-
resolvePluginChannelTurnTrust,
|
|
26
|
-
} from "./plugin-channel-turn-trust.js";
|
|
27
|
-
|
|
28
|
-
const CHANNEL = {
|
|
29
|
-
sourceChannel: "plugin" as const,
|
|
30
|
-
externalChatId: "imessage:+12025550142",
|
|
31
|
-
externalUserId: "imessage:+12025550142",
|
|
32
|
-
displayName: "Ada",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
nextRead = { ok: false };
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("resolvePluginChannelTurnTrust", () => {
|
|
40
|
-
test("returns the gateway verdict as the turn trust context", async () => {
|
|
41
|
-
nextRead = {
|
|
42
|
-
ok: true,
|
|
43
|
-
verdict: {
|
|
44
|
-
trustClass: "trusted_contact",
|
|
45
|
-
canonicalSenderId: "imessage:+12025550142",
|
|
46
|
-
contactId: "c-ada",
|
|
47
|
-
status: "active",
|
|
48
|
-
},
|
|
49
|
-
admissionPolicy: "trusted_contacts",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const trust = await resolvePluginChannelTurnTrust(CHANNEL);
|
|
53
|
-
expect(trust.trustClass).toBe("trusted_contact");
|
|
54
|
-
expect(trust.sourceChannel).toBe("plugin");
|
|
55
|
-
expect(trust.requesterChatId).toBe(CHANNEL.externalChatId);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("fails closed when the gateway could not vouch for the sender", async () => {
|
|
59
|
-
nextRead = {
|
|
60
|
-
ok: true,
|
|
61
|
-
verdict: {
|
|
62
|
-
trustClass: "unknown",
|
|
63
|
-
canonicalSenderId: null,
|
|
64
|
-
resolutionFailed: true,
|
|
65
|
-
},
|
|
66
|
-
admissionPolicy: "strangers",
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
await expect(resolvePluginChannelTurnTrust(CHANNEL)).rejects.toMatchObject({
|
|
70
|
-
name: "PluginTurnNotAdmittedError",
|
|
71
|
-
reason: "trust_resolution_failed",
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("fails closed when the gateway trust read fails", async () => {
|
|
76
|
-
nextRead = { ok: false };
|
|
77
|
-
await expect(resolvePluginChannelTurnTrust(CHANNEL)).rejects.toMatchObject({
|
|
78
|
-
name: "PluginTurnNotAdmittedError",
|
|
79
|
-
reason: "trust_resolution_failed",
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("rejects a sender below the channel admission floor", async () => {
|
|
84
|
-
nextRead = {
|
|
85
|
-
ok: true,
|
|
86
|
-
verdict: { trustClass: "unknown", canonicalSenderId: null },
|
|
87
|
-
admissionPolicy: "guardian_only",
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
await expect(resolvePluginChannelTurnTrust(CHANNEL)).rejects.toEqual(
|
|
91
|
-
expect.objectContaining({
|
|
92
|
-
name: "PluginTurnNotAdmittedError",
|
|
93
|
-
reason: "admission_policy_guardian_only",
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("rejects a blocked member regardless of floor", async () => {
|
|
99
|
-
nextRead = {
|
|
100
|
-
ok: true,
|
|
101
|
-
verdict: {
|
|
102
|
-
trustClass: "trusted_contact",
|
|
103
|
-
canonicalSenderId: "imessage:+12025550142",
|
|
104
|
-
contactId: "c-ada",
|
|
105
|
-
status: "blocked",
|
|
106
|
-
},
|
|
107
|
-
admissionPolicy: "strangers",
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
await expect(resolvePluginChannelTurnTrust(CHANNEL)).rejects.toMatchObject({
|
|
111
|
-
name: "PluginTurnNotAdmittedError",
|
|
112
|
-
reason: "member_blocked",
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("admits when the gateway reports no admission policy", async () => {
|
|
117
|
-
nextRead = {
|
|
118
|
-
ok: true,
|
|
119
|
-
verdict: { trustClass: "unknown", canonicalSenderId: null },
|
|
120
|
-
admissionPolicy: null,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const trust = await resolvePluginChannelTurnTrust(CHANNEL);
|
|
124
|
-
expect(trust.trustClass).toBe("unknown");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("PluginTurnNotAdmittedError names the deny reason", () => {
|
|
128
|
-
const err = new PluginTurnNotAdmittedError("admission_policy_no_one");
|
|
129
|
-
expect(err).toBeInstanceOf(Error);
|
|
130
|
-
expect(err.reason).toBe("admission_policy_no_one");
|
|
131
|
-
expect(err.message).toContain("admission_policy_no_one");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gateway admission and trust for plugin turns addressed by a channel chat.
|
|
3
|
-
*
|
|
4
|
-
* A channel address means an inbound sender, not an internal plugin job.
|
|
5
|
-
* Trust comes from the gateway verdict (`resolve_inbound_trust`); the turn
|
|
6
|
-
* runs only when that sender clears the channel admission floor.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { meetsAdmissionFloor } from "@vellumai/gateway-client";
|
|
10
|
-
|
|
11
|
-
import { readInboundTrust } from "../calls/inbound-trust-reader.js";
|
|
12
|
-
import type { ChannelId } from "../channels/types.js";
|
|
13
|
-
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
14
|
-
import { trustContextFromVerdict } from "../runtime/trust-verdict-consumer.js";
|
|
15
|
-
|
|
16
|
-
interface PluginChannelTurnAddress {
|
|
17
|
-
sourceChannel: ChannelId;
|
|
18
|
-
externalChatId: string;
|
|
19
|
-
externalUserId?: string | null;
|
|
20
|
-
displayName?: string | null;
|
|
21
|
-
username?: string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class PluginTurnNotAdmittedError extends Error {
|
|
25
|
-
readonly reason: string;
|
|
26
|
-
|
|
27
|
-
constructor(reason: string) {
|
|
28
|
-
super(`This inbound plugin turn was not admitted (${reason}).`);
|
|
29
|
-
this.name = "PluginTurnNotAdmittedError";
|
|
30
|
-
this.reason = reason;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function resolvePluginChannelTurnTrust(
|
|
35
|
-
channel: PluginChannelTurnAddress,
|
|
36
|
-
): Promise<TrustContext> {
|
|
37
|
-
const actorExternalId = channel.externalUserId?.trim() || undefined;
|
|
38
|
-
const read = await readInboundTrust({
|
|
39
|
-
channelType: channel.sourceChannel,
|
|
40
|
-
actorExternalId,
|
|
41
|
-
});
|
|
42
|
-
if (!read.ok) {
|
|
43
|
-
throw new PluginTurnNotAdmittedError("trust_resolution_failed");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (read.verdict.resolutionFailed) {
|
|
47
|
-
throw new PluginTurnNotAdmittedError("trust_resolution_failed");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const memberStatus = read.verdict.status;
|
|
51
|
-
if (memberStatus === "blocked" || memberStatus === "revoked") {
|
|
52
|
-
throw new PluginTurnNotAdmittedError(`member_${memberStatus}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const trustClass = read.verdict.trustClass;
|
|
56
|
-
if (
|
|
57
|
-
read.admissionPolicy != null &&
|
|
58
|
-
!meetsAdmissionFloor(read.admissionPolicy, trustClass)
|
|
59
|
-
) {
|
|
60
|
-
throw new PluginTurnNotAdmittedError(
|
|
61
|
-
`admission_policy_${read.admissionPolicy}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return trustContextFromVerdict(read.verdict, {
|
|
66
|
-
sourceChannel: channel.sourceChannel,
|
|
67
|
-
conversationExternalId: channel.externalChatId,
|
|
68
|
-
actorDisplayName: channel.displayName ?? undefined,
|
|
69
|
-
actorUsername: channel.username ?? undefined,
|
|
70
|
-
});
|
|
71
|
-
}
|