Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
18e72ea
feat: experiment: model-free `tool` CLI and MCP server on one adapter
hkaltai Oct 28, 2026
fc33c2e
feat: experiment: workspace tools on by default for linked projects
hkaltai Oct 28, 2026
06223f4
feat: experiment: mirror workspace skills into Claude Code project sk…
hkaltai Oct 28, 2026
2f45bc4
Merge remote-tracking branch 'origin/main' into tool-cli-daemon
hkaltai Oct 29, 2026
7d5d12c
fix: harden tool daemon startup, lock release, MCP write gate and ski…
hkaltai Oct 29, 2026
ef54882
feat: altimate integrations command for GitHub, Jira and Linear
hkaltai Oct 29, 2026
8822f29
test: restore shared dispatcher handlers and MCP SDK modules after mo…
hkaltai Oct 30, 2026
bd1f98e
fix: share the Claude Code plugin's skill-copy safeguards in the work…
hkaltai Oct 30, 2026
7f1230a
Merge remote-tracking branch 'origin/main' into tool-cli-daemon
hkaltai Oct 30, 2026
4e523b0
fix: reload daemon connections on edit, release locks in mcp serve, g…
hkaltai Oct 30, 2026
615f88b
fix: keep the workspace skill mirror in step with the plugin's sync u…
hkaltai Oct 30, 2026
9108070
feat: connect integrations with the workspace's own credential fields
hkaltai Oct 30, 2026
39525e4
Merge remote-tracking branch 'origin/main' into tool-cli-daemon
hkaltai Oct 30, 2026
c8a46b9
fix: publish skill copies only for an unchanged, still-owned snapshot…
hkaltai Oct 30, 2026
8ed972d
refactor: sync integrations through the existing connections API
hkaltai Nov 2, 2026
f87d128
fix: keep workspace skill copies when a SKILL.md disappears between i…
hkaltai Nov 2, 2026
6f03803
feat: attach synced integration connections to the linked workspace
hkaltai Nov 2, 2026
b139685
test: a SKILL.md that disappears between its check and its read keeps…
hkaltai Nov 2, 2026
1040986
feat: report whether the project is linked in integrations status
hkaltai Nov 2, 2026
024e58f
Merge remote-tracking branch 'origin/tool-cli-daemon' into feat-local…
hkaltai Nov 2, 2026
853bfa0
fix: address review findings in the tool CLI, MCP server and workspac…
hkaltai Nov 2, 2026
7109869
fix: never send a doubled quote to BigQuery or Databricks in batch re…
hkaltai Nov 2, 2026
4652bed
fix: keep data_diff and the tool daemon within read-only and release …
hkaltai Nov 2, 2026
fcc3c44
Merge remote-tracking branch 'origin/main' into tool-cli-daemon
hkaltai Nov 2, 2026
b7ce612
fix: harden integrations state, locking and credential lifecycle
hkaltai Nov 2, 2026
b52d686
Merge remote-tracking branch 'origin/tool-cli-daemon' into feat-local…
hkaltai Nov 2, 2026
c6d1aa3
fix: validate before sync, keep local copies safe, record expiry
hkaltai Nov 2, 2026
fdeddbf
fix: dialect-aware hard blocks, failed data_diff reported as an error…
hkaltai Nov 2, 2026
d500243
fix: hard blocks classify with each warehouse's dialect; daemon liste…
hkaltai Nov 2, 2026
6b54087
fix: data_diff checks an omitted source in the default warehouse's di…
hkaltai Nov 2, 2026
20fb04e
fix: data_diff treats an empty warehouse name as the default, as the …
hkaltai Nov 2, 2026
88f03f4
fix: read sql_execute SQL in every dialect an omitted warehouse can r…
hkaltai Nov 2, 2026
1311bf7
fix: uncertain SQL is hard-blocked on the words alone; dbt adapter ty…
hkaltai Nov 2, 2026
c9a9cf9
fix: Oracle, Trino and Fabric keep their reads; TRUNCATE /* c */ ( is…
hkaltai Nov 2, 2026
ee2190b
fix: offer a credential again when its workspace copy was deleted
hkaltai Nov 3, 2026
e0f8735
fix: project MCP config never supplies credentials; remove the token …
hkaltai Nov 3, 2026
ce84016
Merge remote-tracking branch 'origin/tool-cli-daemon' into feat-local…
hkaltai Nov 3, 2026
63024e6
fix: tell MCP sources apart by scope and report a kept workspace conn…
hkaltai Nov 3, 2026
4a373dc
fix: keep shared workspace connections and hold the lock for attach-only
hkaltai Nov 3, 2026
f7ecf8a
fix: delete secrets before the record, and read attach-only state und…
hkaltai Nov 3, 2026
d9fbfcd
fix: clean text output, normalise typed Jira sites, read the catalog …
hkaltai Nov 3, 2026
68728c9
fix: clean the masked secret tail and the hostname in text output
hkaltai Nov 3, 2026
e44e2f5
Merge remote-tracking branch 'origin/main' into feat-local-integratio…
hkaltai Nov 4, 2026
4cc4549
fix: report workspace and local state truthfully after partial failures
hkaltai Nov 4, 2026
beafa4b
fix: never write back a workspace list built from a bad answer; refus…
hkaltai Nov 4, 2026
2bdd65f
fix: keychain calls no longer block the lock heartbeat; workspace int…
hkaltai Nov 4, 2026
319d210
fix: a workspace that is not private gets a connection only with --share
hkaltai Nov 5, 2026
71d670c
fix: failed results report where the credential is, sync keeps an exp…
hkaltai Nov 5, 2026
8f64c00
fix: a draft that cannot be cleared no longer stops Keep local, and a…
hkaltai Nov 5, 2026
8172391
Merge remote-tracking branch 'origin/main' into feat-local-integratio…
hkaltai Nov 5, 2026
f0061d0
fix: a shared workspace is checked again right before writing, a conn…
hkaltai Nov 5, 2026
ecf9ec7
fix: a connection is reported shared only when the workspace entry us…
hkaltai Nov 5, 2026
9d64d5d
fix: an attach that would share nothing is not refused, and the CLI w…
hkaltai Nov 5, 2026
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
195 changes: 195 additions & 0 deletions packages/opencode/src/altimate/integrations/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// altimate_change - new file
//
// Client for the workspace connections API of the Altimate backend: list, create, update and delete
// the caller's own connections, plus the integrations catalog. Credentials come from the existing
// Altimate login; the integrations-only environment variables point this client at another backend.
import { AltimateApi } from "../api/client"

export interface BackendConfig {
apiUrl: string
tenant: string
token: string
}

export class BackendError extends Error {
constructor(
message: string,
readonly status: number,
readonly code: string | null,
) {
super(message)
this.name = "BackendError"
}
}

/** A connection as the connections API returns it. */
export interface Connection {
id: number
name: string
integration_id: string
config?: Record<string, unknown>
is_valid: boolean
last_validated_at?: string | null
created_at?: string | null
updated_at?: string | null
is_owner?: boolean
}

/** The login used for backend calls, or null when there is none. */
export async function backendConfig(): Promise<BackendConfig | null> {
const env = { url: process.env.ALTIMATE_INTEGRATIONS_API_URL, tenant: process.env.ALTIMATE_INTEGRATIONS_TENANT, token: process.env.ALTIMATE_INTEGRATIONS_TOKEN }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The environment-selected backend is not used by workspace binding resolution. With only ALTIMATE_INTEGRATIONS_* configured, connections can be saved successfully but cannot be attached to the linked workspace; pass the selected backend credentials through binding lookup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/integrations/backend.ts, line 40:

<comment>The environment-selected backend is not used by workspace binding resolution. With only `ALTIMATE_INTEGRATIONS_*` configured, connections can be saved successfully but cannot be attached to the linked workspace; pass the selected backend credentials through binding lookup.</comment>

<file context>
@@ -0,0 +1,155 @@
+
+/** The login used for backend calls, or null when there is none. */
+export async function backendConfig(): Promise<BackendConfig | null> {
+  const env = { url: process.env.ALTIMATE_INTEGRATIONS_API_URL, tenant: process.env.ALTIMATE_INTEGRATIONS_TENANT, token: process.env.ALTIMATE_INTEGRATIONS_TOKEN }
+  if (env.url && env.tenant && env.token) return { apiUrl: env.url.replace(/\/+$/, ""), tenant: env.tenant, token: env.token }
+  const creds = await AltimateApi.getCredentials().catch(() => null)
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the ALTIMATE_INTEGRATIONS_* variables are a development and test override of the backend, not a documented configuration. Binding lookup is a property of the signed-in Altimate account, and with only those variables set there is no account to look a link up with, so the connection is saved and reported as not linked.

if (env.url && env.tenant && env.token) return { apiUrl: env.url.replace(/\/+$/, ""), tenant: env.tenant, token: env.token }
const creds = await AltimateApi.getCredentials().catch((e) => {
if (e instanceof Error && /credentials not found/i.test(e.message)) return null
throw new Error(`Could not read your Altimate login (${e instanceof Error ? e.message : e}). Fix or remove the credentials file, then sign in again.`)
})
const apiUrl = (creds?.altimateUrl || "").replace(/\/+$/, "")
const token = creds?.altimateApiKey || ""
const tenant = creds?.altimateInstanceName || ""
if (!apiUrl || !token || !tenant) return null
return { apiUrl, tenant, token }
}

/** The entries of a catalog response: a bare list, or one wrapped in `items`, `integrations` or `data`. */
export function catalogEntries(data: unknown): any[] {
const list = Array.isArray(data) ? data : isObject(data) ? (data.items ?? data.integrations ?? data.data ?? []) : []
return Array.isArray(list) ? list : []
}

const FAILURE_TEXT: Record<number, string> = {
400: "The request was rejected",
404: "The requested resource was not found",
409: "The saved credential changed, reload and try again",
422: "The connection details were not accepted",
}

const isObject = (v: unknown): v is Record<string, any> => typeof v === "object" && v !== null && !Array.isArray(v)

/** The backend's error code: `detail.code`, or the type of the first validation error. */
export function failureCode(data: any): string | null {
const detail = data?.detail ?? data
if (Array.isArray(detail)) {
const first = detail.find(isObject)
return typeof first?.type === "string" ? first.type : null
}
return typeof detail?.code === "string" ? detail.code : null
}

/** The backend's own message and code when it sends them, so the user sees why a call failed. */
export function describeFailure(status: number, data: any): string {
const detail = data?.detail ?? data
const code = failureCode(data)
const listed = Array.isArray(detail) ? detail.filter(isObject).map((e) => (typeof e.msg === "string" ? e.msg : "")).filter(Boolean) : []
const text = listed.length
? listed.join("; ")
: typeof detail?.message === "string" && detail.message
? detail.message

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: describeFailure forwards backend error text verbatim, so an API validation or conflict response that echoes a token, key, or secret can print it in CLI errors. Redact credential-bearing values before returning this message, especially text associated with credential field names.

(Based on your team's feedback about masking credential-bearing error text.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/integrations/backend.ts, line 77:

<comment>`describeFailure` forwards backend error text verbatim, so an API validation or conflict response that echoes a token, key, or secret can print it in CLI errors. Redact credential-bearing values before returning this message, especially text associated with credential field names.

(Based on your team's feedback about masking credential-bearing error text.) </comment>

<file context>
@@ -0,0 +1,155 @@
+  const text = listed.length
+    ? listed.join("; ")
+    : typeof detail?.message === "string" && detail.message
+      ? detail.message
+      : typeof detail === "string" && detail
+        ? detail
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: describeFailure only prints the backend's msg, message or string detail and the error code; it never prints the request body or the validation entry's input field, and the tokens we send are not echoed by those fields. No evidence that the backend echoes credentials, so redaction would be speculative.

: typeof detail === "string" && detail
? detail
: (FAILURE_TEXT[status] ?? `status ${status}`)
return code ? `${text} (${code})` : text
}

async function call<T>(cfg: BackendConfig, method: string, path: string, body?: unknown): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 20_000)
try {
const res = await fetch(`${cfg.apiUrl}${path}`, {
method,
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cfg.token}`,
"x-tenant": cfg.tenant,
...AltimateApi.clientHeaders(),
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
})
const text = await res.text()
let data: any = null
let unreadable = false
try {
data = text ? JSON.parse(text) : null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: call accepts a successful empty body as null, so list calls look empty and workspace attachment can replace existing integrations with an incomplete list. Reject empty 2xx responses for JSON endpoints, while explicitly allowing 204 deletes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/altimate/integrations/backend.ts, line 112:

<comment>`call` accepts a successful empty body as `null`, so list calls look empty and workspace attachment can replace existing integrations with an incomplete list. Reject empty 2xx responses for JSON endpoints, while explicitly allowing 204 deletes.</comment>

<file context>
@@ -0,0 +1,175 @@
+    let data: any = null
+    let unreadable = false
+    try {
+      data = text ? JSON.parse(text) : null
+    } catch {
+      unreadable = true
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in beafa4b: an answer without a connections list, including an empty 200 body, is now an error for both list calls, and an empty or non-record workspace answer is an error before any integrations list is built. 204 and empty DELETE answers are untouched. Tests added.

} catch {
unreadable = true
}
// A success that cannot be read is not an empty answer: an empty list would read as "no connections".
if (res.ok && unreadable) throw new BackendError(`The Altimate API answered ${res.status} with a body that is not JSON`, res.status, null)
if (!res.ok) {
throw new BackendError(describeFailure(res.status, data), res.status, failureCode(data))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
return data as T
} catch (e) {
if (e instanceof BackendError) throw e
throw new BackendError(`Could not reach the Altimate API: ${e instanceof Error ? e.message : String(e)}`, 0, null)
} finally {
clearTimeout(timer)
}
}

/** Every connection the caller can see, their own and those shared with them. */
export async function listVisibleConnections(cfg: BackendConfig): Promise<Connection[]> {
return connectionList(await call<{ connections?: Connection[] }>(cfg, "GET", "/connections/?include_shared=true"))
}
/** The list in a connections answer. An answer without one is an error: an empty list would read as "no connections". */
function connectionList(res: { connections?: Connection[] } | null): Connection[] {
if (!Array.isArray(res?.connections)) throw new BackendError("The Altimate API answered without a list of connections", 200, null)
return res.connections
}
/** The caller's own connections only: nothing shared with them. */
export const ownConnections = (all: Connection[]): Connection[] => all.filter((c) => c.is_owner !== false)
export async function listOwnConnections(cfg: BackendConfig): Promise<Connection[]> {
return ownConnections(connectionList(await call<{ connections?: Connection[] }>(cfg, "GET", "/connections/?include_shared=false")))
}
export const createConnection = (cfg: BackendConfig, body: { name: string; integration_id: string; config: Record<string, unknown> }) =>
call<Connection>(cfg, "POST", "/connections/", body)
export const updateConnection = (cfg: BackendConfig, id: number, body: { config: Record<string, unknown> }) =>
call<Connection>(cfg, "PATCH", `/connections/${id}`, body)
export const deleteConnection = (cfg: BackendConfig, id: number) => call<unknown>(cfg, "DELETE", `/connections/${id}`)
/** The workspace integrations catalog: what each provider's web form asks for. */
export const integrationsCatalog = (cfg: BackendConfig) => call<unknown>(cfg, "GET", "/datamate_integrations/")

/** One integration entry of a workspace, as the datamates API stores it. */
export interface DatamateIntegrationEntry {
id: string
tools: Array<{ key: string }>
connection_id?: string | number | null
connection_ids?: string[] | null
[extra: string]: unknown
}
export interface DatamateRecord {
id: string
name: string
integrations: DatamateIntegrationEntry[]
/** False when the caller is not the workspace's owner: only the owner can change its integrations. */
is_owner?: boolean
/** "private" keeps an attached connection to its owner; any other value lets the workspace's members use it. */
privacy?: string
}

/** A workspace with its integrations list. */
export async function getDatamate(cfg: BackendConfig, id: string): Promise<DatamateRecord> {
let d: any
try {
const res = await call<any>(cfg, "GET", `/datamates/${encodeURIComponent(id)}/summary`)
d = res?.datamate ?? res
} catch (e) {
if (!(e instanceof BackendError) || e.status !== 404) throw e
// A backend without the summary route still lists its workspaces. The integrations list is replaced whole, so a listed workspace without one is not trusted.
const res = await call<any>(cfg, "GET", "/datamates/")
const all = Array.isArray(res) ? res : (res?.datamates ?? res?.data ?? [])
d = Array.isArray(all) ? all.find((x: any) => String(x?.id) === id) : undefined
if (!d) throw e
if (!Array.isArray(d.integrations)) throw new BackendError("The Altimate API listed the workspace without its integrations", 200, null)
}
// An answer that is not a workspace record is an error: building the next list from it could drop the workspace's integrations.
if (!isObject(d) || (d.id == null && d.name == null && !("integrations" in d))) throw new BackendError("The Altimate API answered without the workspace record", 200, null)
// The list is required in the workspace record and written back whole, so a missing or null one is as malformed as a non-list.
if (!Array.isArray(d.integrations)) throw new BackendError("The Altimate API answered with workspace integrations that are not a list", 200, null)
return { ...d, id: String(d.id ?? id), name: d.name ?? "", integrations: d.integrations }
}
/** Replaces the workspace's whole integrations list. */
export const updateDatamateIntegrations = (cfg: BackendConfig, id: string, integrations: DatamateIntegrationEntry[]) =>
call<unknown>(cfg, "PATCH", `/datamates/${encodeURIComponent(id)}`, { integrations })

export * as IntegrationsBackend from "./backend"
Loading
Loading