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
11 changes: 7 additions & 4 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1166,16 +1166,16 @@ Validate input data and run one connector action.
- Options: `-a, --action <action>` selects the action name and is required.
- Options: `-d, --data <data>` accepts inline JSON or `@path` to a JSON file.
`--input <data>` is an alias for `--data <data>`.
- Options: `--approval` declares that the user has authorized this operation.
The caller assesses whether existing authorization covers the action and its
parameters in context. Ask the user only when authorization is insufficient.
- Options: `--dry-run` validates the payload without executing the action.
- Options: `--app-id <app-id>` selects the exact connection from search.
It is mutually exclusive with `--connection-name`; empty IDs are rejected.
The selected team must own that connection; an app ID does not switch teams.
- Options: `--connection-name <connection-name>` runs the action with the
connector app connection name. Use `oo connector apps <serviceName>` to list
available connection names.
- Options: `--approval` declares that the user has authorized this operation.
The caller assesses whether existing authorization covers the action and its
parameters in context. Ask the user only when authorization is insufficient.
- Options: `--dry-run` validates the payload without executing the action.
- Options: `--wait` polls the selected action until it reaches a terminal state.
This option is only valid when the selected action schema declares an async
result lifecycle.
Expand Down Expand Up @@ -1281,6 +1281,9 @@ Proxy a provider API request through a connected connector app.
options.
- Options: `--body` is parsed as JSON. To send a text body, pass a JSON string
such as `"hello"`.
- Options: `--connection-name <name>` selects a named connection within the selected
identity. Leading and trailing whitespace is removed; empty names are rejected.
It works with both `--data` and split request options.
- Options: `--team <name>` runs the proxy request under the given team
identity. When omitted, the request runs under the team selected by
`OO_TEAM_ID` / `OO_TEAM_NAME` if set, then the active account's default team,
Expand Down
8 changes: 5 additions & 3 deletions docs/commands.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -973,13 +973,13 @@ CLI 默认记录受隐私约束的命令使用 telemetry。事件不包含 free-
- 选项:`-a, --action <action>` 用于指定 action 名称,且为必填。
- 选项:`-d, --data <data>` 支持直接传入 JSON,或使用 `@路径` 读取 JSON 文件。
`--input <data>` 是 `--data <data>` 的 alias。
- 选项:`--approval` 声明本次操作已有用户授权。调用方结合具体操作、参数、上下文
和用户已有指示,判断授权是否充分;不足时再询问用户。
- 选项:`--dry-run` 只做 payload 校验,不执行 action。
- 选项:`--app-id <app-id>` 精确选择搜索返回的连接,与 `--connection-name` 互斥,
不接受空值。当前选择的团队必须拥有该连接,app ID 本身不会切换团队。
- 选项:`--connection-name <connection-name>` 使用指定 connector app 连接名称
运行该 action。可用 `oo connector apps <serviceName>` 查看可用连接名称。
- 选项:`--approval` 声明本次操作已有用户授权。调用方结合具体操作、参数、上下文
和用户已有指示,判断授权是否充分;不足时再询问用户。
- 选项:`--dry-run` 只做 payload 校验,不执行 action。
- 选项:`--wait` 会轮询选中的 action,直到进入终态。只有选中 action 的
schema 声明了异步结果 lifecycle 时,这个选项才有效。
- 选项:`--wait-result` 会提交异步 submit action,然后轮询它配置的结果
Expand Down Expand Up @@ -1064,6 +1064,8 @@ CLI 默认记录受隐私约束的命令使用 telemetry。事件不包含 free-
credential。
- 选项:`--body` 会按 JSON 解析。如需发送文本 body,请传 JSON string,例如
`"hello"`。
- 选项:`--connection-name <name>` 选择当前身份下的指定连接。名称会去除首尾
空白,空名称会被拒绝。可与 `--data` 或拆分请求选项一起使用。
- 选项:`--team <name>` 以指定团队身份运行该 proxy 请求。省略时,若设置了
`OO_TEAM_ID` / `OO_TEAM_NAME` 则使用 env 选定的团队,其次使用
当前账号保存的默认团队,否则使用服务端默认团队。
Expand Down
22 changes: 0 additions & 22 deletions src/application/commands/connector/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,28 +88,6 @@ describe("formatConnectorAppsAsText", () => {
displayWidthBeforeStatus(lines[2]!),
);
});

test("returns the no-connections message for an empty all-scope listing", () => {
const output = formatConnectorAppsAsText(
[],
"all",
createTranslatorStub(),
createTerminalColors(false),
);

expect(output).toBe("connector.apps.text.noConnections");
});

test("returns the per-service no-results message for an empty service-scope listing", () => {
const output = formatConnectorAppsAsText(
[],
"service",
createTranslatorStub(),
createTerminalColors(false),
);

expect(output).toBe("connector.apps.text.noResults");
});
});

function sampleApp(overrides: Partial<ConnectorAppRow> = {}): ConnectorAppRow {
Expand Down
4 changes: 2 additions & 2 deletions src/application/commands/connector/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const connectorAppStatusColors = {
type ConnectorAppsListScope = "all" | "service";

interface ConnectorAppsInput {
team?: string;
serviceName?: string;
team?: string;
}

interface ConnectorAppListItem {
Expand Down Expand Up @@ -65,8 +65,8 @@ export const connectorAppsCommand: CliCommandDefinition<ConnectorAppsInput> = {
],
output: "standard",
inputSchema: z.object({
...teamIdentityInputShape,
serviceName: z.string().optional(),
...teamIdentityInputShape,
}),
handler: async (input, context) => {
const serviceName = input.serviceName?.trim();
Expand Down
9 changes: 9 additions & 0 deletions src/application/commands/connector/index.cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,8 @@ describe("connectorCommand CLI", () => {
"{\"accept\":\"application/json\"}",
"--body",
"{\"query\":\"hello\"}",
"--connection-name",
" private-work ",
"--team",
"acme",
"--json",
Expand Down Expand Up @@ -741,6 +743,7 @@ describe("connectorCommand CLI", () => {
expect(requests[0]?.method).toBe("POST");
expect(requests[0]?.url).toBe("https://connector.oomol.com/v1/proxy/tavily");
expect(requests[0]?.headers.get("x-oo-team-name")).toBe("acme");
expect(requests[0]?.headers.get("x-oo-connector-alias")).toBe("private-work");
await expect(requests[0]?.json()).resolves.toEqual({
body: {
query: "hello",
Expand All @@ -757,12 +760,14 @@ describe("connectorCommand CLI", () => {
expect(telemetryPayload).toMatchObject({
properties: {
command_full: "connector.proxy",
connection_selector: "connectionName",
data_size_bucket: "<1KB",
has_body: true,
identity_source: "flag",
method: "POST",
},
});
expect(JSON.stringify(telemetryPayload)).not.toContain("private-work");
expect(telemetryPayload?.properties).not.toHaveProperty("body");
expect(telemetryPayload?.properties).not.toHaveProperty("endpoint");
expect(telemetryPayload?.properties).not.toHaveProperty("headers");
Expand Down Expand Up @@ -1142,6 +1147,10 @@ describe("connectorCommand CLI", () => {

try {
const cases = [
{
argv: ["connector", "proxy", "tavily", "--data", "{\"endpoint\":\"/search\",\"method\":\"GET\"}", "--connection-name", " "],
message: "The --connection-name value cannot be empty.",
},
{
argv: ["connector", "proxy", "tavily"],
message: "The --endpoint option is required when --data is omitted.",
Expand Down
4 changes: 2 additions & 2 deletions src/application/commands/connector/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const connectorHealthResponseSchema = z.object({
}).passthrough();

interface ConnectorLoginInput {
token?: string;
url: string;
token?: string;
}

export const connectorLoginCommand: CliCommandDefinition<ConnectorLoginInput> = {
Expand All @@ -48,8 +48,8 @@ export const connectorLoginCommand: CliCommandDefinition<ConnectorLoginInput> =
},
],
inputSchema: z.object({
token: z.string().optional(),
url: z.string(),
token: z.string().optional(),
}),
handler: async (input, context) => {
const baseUrl = normalizeSelfHostedConnectorUrl(input.url);
Expand Down
30 changes: 21 additions & 9 deletions src/application/commands/connector/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "../team/identity.ts";
import { formatConnectorExecutionResultAsText } from "./result-text.ts";
import { resolveConnectorSession } from "./session.ts";
import { runConnectorProxy } from "./shared.ts";
import { parseConnectorConnectionSelector, runConnectorProxy } from "./shared.ts";
import { recordConnectorFailureTelemetry } from "./telemetry.ts";

const connectorProxyDataErrorKeys = {
Expand Down Expand Up @@ -49,14 +49,15 @@ const proxyRequestSchema = z.object({
type ConnectorProxyRequest = z.output<typeof proxyRequestSchema>;

interface ConnectorProxyInput {
body?: string;
serviceName: string;
data?: string;
endpoint?: string;
headers?: string;
method?: string;
team?: string;
query?: string;
serviceName: string;
headers?: string;
body?: string;
connectionName?: string;
team?: string;
}

export const connectorProxyCommand: CliCommandDefinition<ConnectorProxyInput> = {
Expand Down Expand Up @@ -110,23 +111,32 @@ export const connectorProxyCommand: CliCommandDefinition<ConnectorProxyInput> =
valueName: "body",
descriptionKey: "options.connectorProxyBody",
},
{
name: "connectionName",
longFlag: "--connection-name",
valueName: "connection-name",
descriptionKey: "options.connectorProxyConnectionName",
},
teamOption("options.connectorProxyTeam"),
],
output: "standard",
inputSchema: z.object({
body: z.string().optional(),
serviceName: z.string(),
data: z.string().optional(),
endpoint: z.string().optional(),
headers: z.string().optional(),
method: z.string().optional(),
...teamIdentityInputShape,
query: z.string().optional(),
serviceName: z.string(),
headers: z.string().optional(),
body: z.string().optional(),
connectionName: z.string().optional(),
...teamIdentityInputShape,
}),
handler: async (input, context) => {
// Payload parsing must stay ahead of the session: proxy usage errors
// are reported before any login requirement.
const proxyRequest = await buildConnectorProxyRequest(input, context);
const connectionSelector = parseConnectorConnectionSelector(input);

const { identity, target } = await resolveConnectorSession(
{
team: input.team,
Expand All @@ -135,6 +145,7 @@ export const connectorProxyCommand: CliCommandDefinition<ConnectorProxyInput> =
);

context.telemetry?.recordProperties({
connection_selector: connectionSelector === undefined ? "none" : "connectionName",
data_size_bucket: bucketTelemetryBytes(
Buffer.byteLength(JSON.stringify(proxyRequest)),
),
Expand All @@ -146,6 +157,7 @@ export const connectorProxyCommand: CliCommandDefinition<ConnectorProxyInput> =
try {
response = await runConnectorProxy(
{
connectionSelector,
identity,
proxyRequest,
serviceName: input.serviceName,
Expand Down
54 changes: 24 additions & 30 deletions src/application/commands/connector/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "./schema-cache.ts";
import { resolveConnectorSession } from "./session.ts";
import {
parseConnectorConnectionSelector,
requireConnectorActionName,
runConnectorAction,
} from "./shared.ts";
Expand Down Expand Up @@ -53,16 +54,16 @@ type ConnectorActionAsyncResultLifecycle = Extract<
const connectorAsyncLifecycleDefaultTimeoutMs = 6 * 3_600_000;

interface ConnectorRunInput {
appId?: string;
serviceName: string;
action?: string;
approval?: boolean;
connectionName?: string;
data?: string;
appId?: string;
connectionName?: string;
approval?: boolean;
dryRun?: boolean;
team?: string;
serviceName: string;
wait?: boolean;
waitResult?: boolean;
team?: string;
}

export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
Expand All @@ -78,12 +79,6 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
},
],
options: [
{
name: "appId",
longFlag: "--app-id",
valueName: "app-id",
descriptionKey: "options.connectorRunAppId",
},
{
name: "action",
longFlag: "--action",
Expand All @@ -99,6 +94,12 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
valueName: "data",
descriptionKey: "options.data",
},
{
name: "appId",
longFlag: "--app-id",
valueName: "app-id",
descriptionKey: "options.connectorRunAppId",
},
{
name: "connectionName",
longFlag: "--connection-name",
Expand Down Expand Up @@ -129,16 +130,16 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
],
output: "standard",
inputSchema: z.object({
appId: z.string().optional(),
serviceName: z.string(),
action: z.string().optional(),
approval: z.boolean().optional(),
connectionName: z.string().optional(),
data: z.string().optional(),
appId: z.string().optional(),
connectionName: z.string().optional(),
approval: z.boolean().optional(),
dryRun: z.boolean().optional(),
...teamIdentityInputShape,
serviceName: z.string(),
wait: z.boolean().optional(),
waitResult: z.boolean().optional(),
...teamIdentityInputShape,
}),
handler: async (input, context) => {
const actionName = requireConnectorActionName(input.action);
Expand All @@ -147,21 +148,14 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {
throw new CliUserError("errors.connectorRun.waitModeConflict", 2);
}

const connectionName = input.connectionName?.trim();
if (input.connectionName !== undefined && connectionName === "") {
throw new CliUserError("errors.connectorRun.connectionNameEmpty", 2);
}

const appId = input.appId?.trim();
if (input.appId !== undefined && appId === "") {
throw new CliUserError("errors.connectorRun.appIdEmpty", 2);
const connectionSelector = parseConnectorConnectionSelector(input);
let selectorMode: "none" | "appId" | "connectionName" = "none";
if (connectionSelector?.appId !== undefined) {
selectorMode = "appId";
}
if (appId !== undefined && connectionName !== undefined) {
throw new CliUserError("errors.connectorRun.connectionSelectorConflict", 2);
else if (connectionSelector?.connectionName !== undefined) {
selectorMode = "connectionName";
}
const connectionSelector = appId !== undefined
? { appId }
: connectionName === undefined ? undefined : { connectionName };

const { identity, target } = await resolveConnectorSession(
{
Expand All @@ -182,7 +176,7 @@ export const connectorRunCommand: CliCommandDefinition<ConnectorRunInput> = {

context.telemetry?.recordProperties({
action: actionName,
connection_selector: appId !== undefined ? "appId" : connectionName !== undefined ? "connectionName" : "none",
connection_selector: selectorMode,
data_size_bucket: bucketTelemetryBytes(
Buffer.byteLength(JSON.stringify(inputData)),
),
Expand Down
4 changes: 2 additions & 2 deletions src/application/commands/connector/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { requireConnectorActionName } from "./shared.ts";
import { resolveConnectorTarget } from "./target.ts";

interface ConnectorSchemaInput {
action?: string;
actionId?: string[];
action?: string;
refresh?: boolean;
}

Expand Down Expand Up @@ -52,8 +52,8 @@ export const connectorSchemaCommand: CliCommandDefinition<ConnectorSchemaInput>
],
output: "json-only",
inputSchema: z.object({
action: z.string().optional(),
actionId: z.array(z.string()).optional(),
action: z.string().optional(),
refresh: z.boolean().optional(),
}),
handler: async (input, context) => {
Expand Down
Loading
Loading