Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-cli-principal-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'electric-ax': patch
'@electric-ax/agents-server-ui': patch
---

Stop CLI and web UI sends from posting legacy `from` attribution and derive message senders from the authenticated Electric principal instead. The CLI now sends a default `Electric-Principal` header of `system:cli-<os-username>` when no explicit principal is configured.
48 changes: 1 addition & 47 deletions packages/agents-server-ui/src/lib/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@ import {
COMPOSER_INPUT_MESSAGE_TYPE,
createPendingTimelineOrder,
} from '@electric-ax/agents-runtime/client'
import {
getActivePrincipal,
getConfiguredActivePrincipal,
getConfiguredServerHeaders,
serverFetch,
} from './auth-fetch'
import { getActivePrincipal, serverFetch } from './auth-fetch'
import { entityApiUrl } from './entity-api'
import { loadCloudAuthState } from './server-connection'
import type { EntityStreamDBWithActions } from '@electric-ax/agents-runtime/client'
import type { ComposerInputPayload } from '@electric-ax/agents-runtime/client'

Expand Down Expand Up @@ -309,7 +303,6 @@ export async function sendEntityMessage({
mode = `queued`,
position,
attachments,
from,
}: {
baseUrl: string
entityUrl: string
Expand All @@ -320,13 +313,8 @@ export async function sendEntityMessage({
mode?: `immediate` | `queued` | `paused` | `steer`
position?: string
attachments?: Array<AttachmentInput>
from?: string
}): Promise<{ txid: string; attachmentTxids: Array<string> }> {
const url = entityApiUrl(baseUrl, entityUrl, `/send`)
const sender = await resolveSenderPrincipalUrl(
url,
from ?? getConfiguredActivePrincipal() ?? ``
)
const uploadedAttachments = await uploadMessageAttachments({
baseUrl,
entityUrl,
Expand All @@ -339,7 +327,6 @@ export async function sendEntityMessage({
method: `POST`,
headers: { 'content-type': `application/json` },
body: JSON.stringify({
from: sender,
key,
payload: effectivePayload,
mode,
Expand Down Expand Up @@ -381,33 +368,6 @@ export function readTextPayload(payload: unknown): string {
return ``
}

function principalUrl(principalKey: string): string {
return `/principal/${encodeURIComponent(principalKey)}`
}

function principalUrlFromConfiguredHeaders(url: string): string | null {
const headers = new Headers(getConfiguredServerHeaders(url))
const principal = headers.get(`electric-principal`)?.trim()
return principal ? principalUrl(principal) : null
}

async function resolveSenderPrincipalUrl(
url: string,
from: string
): Promise<string> {
if (from.startsWith(`/principal/`)) return from

const headerPrincipal = principalUrlFromConfiguredHeaders(url)
if (headerPrincipal) return headerPrincipal

const cloudAuth = await loadCloudAuthState().catch(() => null)
if (cloudAuth?.status === `signed-in` && cloudAuth.userId) {
return principalUrl(`user:${cloudAuth.userId}`)
}

return principalUrl(`system:dev-local`)
}

export function createSendMessageAction({
db,
baseUrl,
Expand Down Expand Up @@ -455,7 +415,6 @@ export function createSendMessageAction({
mode,
position,
attachments,
from,
})
await Promise.all([
...attachmentTxids.map((id) => db.utils.awaitTxId(id, 10_000)),
Expand All @@ -464,15 +423,10 @@ export function createSendMessageAction({
return
}
const url = entityApiUrl(baseUrl, entityUrl, `/send`)
const sender = await resolveSenderPrincipalUrl(
url,
from ?? getConfiguredActivePrincipal() ?? ``
)
const res = await serverFetch(url, {
method: `POST`,
headers: { 'content-type': `application/json` },
body: JSON.stringify({
from: sender,
key,
payload,
mode,
Expand Down
10 changes: 7 additions & 3 deletions packages/electric-ax/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ function getDefaultElectricAgentsIdentity(): string {
return `${userInfo().username}@${hostname()}`
}

function getDefaultElectricAgentsPrincipal(): string {
return `system:cli-${userInfo().username}`
}

function parseElectricAgentsHeaders(
raw: string | undefined
): Record<string, string> | undefined {
Expand Down Expand Up @@ -167,14 +171,15 @@ export function getElectricCliEnv(
env: NodeJS.ProcessEnv = process.env
): ElectricCliEnv {
const explicitIdentity = env.ELECTRIC_AGENTS_IDENTITY?.trim()
const explicitPrincipal = env.ELECTRIC_AGENTS_PRINCIPAL?.trim()
const headers = parseElectricAgentsHeaders(env.ELECTRIC_AGENTS_SERVER_HEADERS)
return {
electricAgentsUrl: env.ELECTRIC_AGENTS_URL || DEFAULT_ELECTRIC_AGENTS_URL,
electricAgentsIdentity:
explicitIdentity || getDefaultElectricAgentsIdentity(),
electricAgentsHeaders: mergeElectricPrincipalHeader(
headers,
env.ELECTRIC_AGENTS_PRINCIPAL
explicitPrincipal || getDefaultElectricAgentsPrincipal()
),
}
}
Expand Down Expand Up @@ -571,7 +576,6 @@ async function sendMessage(
const payload = parsePayload(message, options.json ?? false)

const body: Record<string, unknown> = {
from: env.electricAgentsIdentity,
payload,
}
if (options.type) {
Expand Down Expand Up @@ -825,7 +829,7 @@ function getHelpText(commandName: string): string {
Environment:
ELECTRIC_AGENTS_URL Base URL of the server (default: ${DEFAULT_ELECTRIC_AGENTS_URL})
ELECTRIC_AGENTS_IDENTITY Sender identity for messages (default: ${getDefaultElectricAgentsIdentity()})
ELECTRIC_AGENTS_PRINCIPAL Optional principal key sent as the Electric-Principal header
ELECTRIC_AGENTS_PRINCIPAL Principal key sent as the Electric-Principal header (default: ${getDefaultElectricAgentsPrincipal()})
ELECTRIC_AGENTS_SERVER_HEADERS Optional JSON object of additional server headers
ANTHROPIC_API_KEY Required for '${agentsCommand} start-builtin' and '${agentsCommand} quickstart'

Expand Down
42 changes: 40 additions & 2 deletions packages/electric-ax/test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { tmpdir, userInfo } from 'node:os'
import { join } from 'node:path'
import {
createElectricCliHandlers,
Expand Down Expand Up @@ -132,7 +132,18 @@ async function parse(argv: Array<string>, handlers = createHandlers()) {
}

describe(`createElectricProgram`, () => {
it(`adds an optional principal header from the environment`, () => {
it(`uses system cli username as the default principal header`, () => {
const env = getElectricCliEnv({
ELECTRIC_AGENTS_URL: `https://agents.example.test`,
ELECTRIC_AGENTS_IDENTITY: `tester@example.com`,
})

expect(env.electricAgentsHeaders).toEqual({
'electric-principal': `system:cli-${userInfo().username}`,
})
})

it(`allows the principal header to be overridden from the environment`, () => {
const env = getElectricCliEnv({
ELECTRIC_AGENTS_URL: `https://agents.example.test`,
ELECTRIC_AGENTS_IDENTITY: `tester@example.com`,
Expand Down Expand Up @@ -351,6 +362,33 @@ describe(`createElectricProgram`, () => {
)
})

it(`sends messages without legacy from attribution`, async () => {
const fetchMock = vi.spyOn(globalThis, `fetch`).mockResolvedValue(
new Response(JSON.stringify({ txid: `123` }), {
status: 200,
headers: { 'content-type': `application/json` },
})
)

try {
await createElectricCliHandlers(TEST_ENV).send(`/chat/test`, `hello`, {
type: `chat_message`,
})
expect(fetchMock).toHaveBeenCalledWith(
`http://localhost:4437/_electric/entities/chat/test/send`,
expect.objectContaining({
method: `POST`,
body: JSON.stringify({
payload: { text: `hello` },
type: `chat_message`,
}),
})
)
} finally {
fetchMock.mockRestore()
}
})

it(`sends signal requests to the entity signal endpoint`, async () => {
const fetchMock = vi.spyOn(globalThis, `fetch`).mockResolvedValue(
new Response(JSON.stringify({ txid: 123 }), {
Expand Down
Loading