Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
116afd9
feat(chargebee): billing REST integration and its operations (#788)
CodeGhost21 Aug 13, 2026
1fa34d4
feat(chargebee): wire the billing tools into the agent toolbelt (#788)
CodeGhost21 Aug 13, 2026
8334e36
feat(chargebee): inbound webhook that tells the operator about paymen…
CodeGhost21 Aug 13, 2026
349464d
feat(chargebee): billing config endpoints for the Settings UI (#788, …
CodeGhost21 Aug 13, 2026
ab9dbea
feat(console): Settings → Billing page for the Chargebee connection (…
CodeGhost21 Aug 13, 2026
e1b01fa
feat(paypal): wallet and transaction REST layer (#789)
CodeGhost21 Aug 13, 2026
ebe8fb6
feat(paypal): agent tools, config endpoints and Settings UI (#789)
CodeGhost21 Aug 13, 2026
2c7c403
fix(billing): re-resolve credentials per turn and declare the tools (…
CodeGhost21 Aug 13, 2026
451fbb3
fix(chargebee): raise the invoice when a site refuses payment terms (…
CodeGhost21 Aug 13, 2026
d198d8a
chore: resolve Cargo.lock after merging upstream/main
CodeGhost21 Aug 13, 2026
9de03ab
fix(paypal): explain an unavailable transaction window instead of rel…
CodeGhost21 Aug 13, 2026
a55c113
fix(billing): gate the harness bridges on their own features (#788, #…
CodeGhost21 Aug 14, 2026
769a684
fix(billing): review findings — credential leaks, lax parsing, PII in…
CodeGhost21 Aug 14, 2026
694fb36
fix(billing): name the company and agent in the fail-closed warnings
CodeGhost21 Aug 14, 2026
c11fdf3
ci(staging): build the tenant image with chargebee and paypal (#788, …
CodeGhost21 Aug 14, 2026
1aa7d12
fix(billing): add the gated deps fields to the composio turn test
CodeGhost21 Aug 14, 2026
5f395dc
fix(billing): withhold unparseable response bodies, and always send a…
CodeGhost21 Aug 14, 2026
0902202
fix(billing): redact the client key, tighten the PayPal error match, …
CodeGhost21 Aug 14, 2026
79deeb9
test(billing): pin both fingerprints, and run the PayPal toolbelt tes…
CodeGhost21 Aug 14, 2026
e661c7b
fix(billing): finish the no-raw-bodies rule, and drop stale state on …
CodeGhost21 Aug 14, 2026
bf02465
fix(billing): authenticate the webhook before its body is read, and c…
CodeGhost21 Aug 14, 2026
3b2f29a
fix(billing): wire the gated billing fields at main's new HarnessDeps…
CodeGhost21 Aug 14, 2026
c86f9d8
fix(billing): remount BillingView per company so credentials cannot c…
CodeGhost21 Aug 14, 2026
6379e34
fix(billing): derive idempotency keys from a stable hash, and make th…
CodeGhost21 Aug 14, 2026
f46d045
fix(console): import CompanyFeed from its real module in the billing …
CodeGhost21 Aug 14, 2026
0d2efa9
fix(paypal): reject malformed transaction responses (#789)
oxoxDev Aug 15, 2026
5c5dc04
fix(deps): restore the tinymemory version main already locked
oxoxDev Aug 15, 2026
5d61f3f
Merge branch 'main' into feat/788-chargebee-tools
oxoxDev Aug 16, 2026
4f1405f
fix(paypal): never trust a token past its own expiry, and stop derivi…
oxoxDev Aug 16, 2026
7874c54
merge: main into feat/788-chargebee-tools
oxoxDev Aug 16, 2026
d93df1a
fix(harness): set the billing deps on main's workflow-build fixture
oxoxDev Aug 16, 2026
9a74267
fix(paypal): refuse a request path that could move the host (#856)
oxoxDev Aug 17, 2026
0ab81fd
fix(paypal): refuse a missing amount rather than reporting it as 0.00…
oxoxDev Aug 17, 2026
90097d4
fix(billing): apply credential writes as a batch that rolls back on f…
oxoxDev Aug 17, 2026
3b9186b
test(chargebee): assert a verified webhook actually raises the compan…
oxoxDev Aug 17, 2026
d7cd799
Merge remote-tracking branch 'upstream/main' into feat/788-chargebee-…
CodeGhost21 Aug 17, 2026
b3032e1
fix(billing-view): load Chargebee and PayPal independently (#856)
oxoxDev Aug 17, 2026
988c9af
test(billing-view): cover the load-failure and status branches (#856)
oxoxDev Aug 17, 2026
f7dbbd6
test(harness): cover per-company billing credential resolution (#856)
oxoxDev Aug 17, 2026
4650393
Merge remote-tracking branch 'codeghost/feat/788-chargebee-tools' int…
oxoxDev Aug 17, 2026
e4c0cb4
fix(billing): add the new CompanyRecord field to the test fixture (#856)
oxoxDev Aug 17, 2026
939e64a
fix(workflows): repair the gated-lane fixtures main left behind (#856)
oxoxDev Aug 17, 2026
9b2f8a4
fix(harness): gate the billing test fixture on its features (#856)
CodeGhost21 Aug 17, 2026
45bb956
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/856
senamakel Aug 17, 2026
494d720
fix(api): handle null values in chargebee API response
senamakel Aug 17, 2026
4bb091f
fix(api): handle null values in chargebee API response
senamakel Aug 17, 2026
86af3ac
fix(api): handle null values in chargebee API response
senamakel Aug 17, 2026
08bdd79
fix(test): remove unnecessary braces in async block
senamakel Aug 17, 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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,27 @@ jobs:
cargo test --locked --features acp --lib server::acp
cargo test --locked --features acp --lib harness::acp_run_turn

# Issue #788: Chargebee billing — the REST layer (`chargebee::`) and the
# toolbelt bridge (`harness::chargebee`) in one filter, which selects both.
# It runs HERE rather than on the fast default job because the bridge lives
# under `src/harness/` and so compiles only with `openhuman`; splitting it
# across two lanes would need two rows in feature-lanes.txt for one
# feature, which that table rejects. All of it is offline: the wire tests
# drive a stub on an ephemeral port and the rest never build a request.
- name: Test the Chargebee billing integration
run: scripts/ci/run-scoped-suite.sh "chargebee" openhuman,tinycortex,chargebee chargebee

# Issue #789: PayPal. Here rather than on the fast default job for the same
# reason as chargebee above — the toolbelt bridge is under `src/harness/`
# and so needs `openhuman`. It USED to ride the default job on `--features
# paypal` alone, which reached `company::paypal` and `paypal::` and could
# not compile `harness::paypal` at all: those tests existed and no lane ran
# them, which is exactly the #770 pathology this table exists to stop. One
# lane with the gated feature set selects all three. Offline: the token
# cache and error tests drive a stub on an ephemeral port.
- name: Test the PayPal integration
run: scripts/ci/run-scoped-suite.sh "paypal" openhuman,tinycortex,paypal paypal

- name: Assert every integration target actually runs
run: scripts/ci/assert-integration-targets-run.sh openhuman,tinycortex

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ env:
# medulla (hosted brain transport), tinycortex (local memory backend), and
# sidecar. `sqlite` is intentionally omitted — its bundled rusqlite needs a C
# compiler the slim build image lacks, and it is redundant on a mongodb tenant.
TENANT_FEATURES: mongodb,openhuman,openhuman-rpc,tinyplace,github,smtp,dns,tinyhumans,webhooks,platform-jwt,export,mcp,media,composio,imap,telegram,medulla,tinycortex,sidecar
TENANT_FEATURES: mongodb,openhuman,openhuman-rpc,tinyplace,github,smtp,dns,tinyhumans,webhooks,platform-jwt,export,mcp,media,composio,imap,telegram,medulla,tinycortex,sidecar,chargebee,paypal

jobs:
deploy:
Expand Down
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,16 @@ telegram = ["dep:reqwest"]
# read-only connections catalog compiles without this; only the token-exchanging
# write routes are gated here (they need the shared `reqwest` client).
oauth = ["dep:reqwest"]
# Issue #788: Chargebee billing integrated as backend service code, surfaced to
# agents as callable tools. NOT a separate MCP server and NOT a second binary —
# the integration lives in the tenant workload so the credential can be
# per-company, held in that company's own SecretStore. The only gated dependency
# is the shared `reqwest` client the REST calls use; `src/chargebee/` compiles
# only under this feature, so the default build is unaffected.
chargebee = ["dep:reqwest"]
# Issue #789: PayPal wallet + transaction reads, same shape as `chargebee`
# above — backend service code, per-company credentials, only `reqwest` gated.
paypal = ["dep:reqwest"]

[patch.crates-io]
# opencompany's own vendored tinyagents (2.1.0). Satisfies openhuman's `^2.1`.
Expand Down
2 changes: 1 addition & 1 deletion companies/openhuman_demo/agents/ceo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ description = "Sets direction, answers about the company, and delegates the work
tier = "orchestrator"
# `workspace.read` is read-only by design: writes need a bare `workspace` or an
# explicit `workspace.write`, so this cannot silently overwrite operator notes.
tools = ["mcp:*", "workspace.read"]
tools = ["mcp:*", "workspace.read", "chargebee", "paypal"]
2 changes: 1 addition & 1 deletion companies/openhuman_demo/company.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mode = "full"
# Both forms are needed: this list is matched with exact/glob semantics, so bare
# `workspace` covers an agent asking for `workspace` but NOT one asking for
# `workspace.read`, and `workspace.*` covers the sub-grants but not the bare one.
allow = ["mcp:*", "workspace", "workspace.*"]
allow = ["mcp:*", "workspace", "workspace.*", "chargebee", "paypal"]
Comment thread
CodeGhost21 marked this conversation as resolved.
Comment thread
CodeGhost21 marked this conversation as resolved.

# Desks (group chats) the orchestrator can hand a turn to. Each desk's first
# member is its lead; `delegate_to_desk` runs that member's turn.
Expand Down
4 changes: 4 additions & 0 deletions examples/live_company_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ async fn main() -> anyhow::Result<()> {
plan: None,
media: None,
composio: None,
#[cfg(feature = "chargebee")]
Comment thread
oxoxDev marked this conversation as resolved.
chargebee: None,
#[cfg(feature = "paypal")]
paypal: None,
steer: opencompany::company::steer::InflightRegistry::default(),
run_supervisor: opencompany::runtime::RunSupervisor::default(),
delivery: None,
Expand Down
121 changes: 121 additions & 0 deletions frontend/src/api/billing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// The Chargebee billing configuration API (issue #788, UI tracked in #527).
//
// Credentials are write-only: the API key and the webhook credential are sent
// on save and stored in the host's secret store; neither is ever returned. The
// read shape carries booleans and the (non-secret) site identifier only, so
// there is no field on this type that could leak a key into a rendered page.
//
// Standalone functions over the shared client, mirroring `api/mcp.ts` and
// `api/skills.ts`, so `OpenCompanyClient` needs no new methods.

import type { OpenCompanyClient } from "./client";

/**
* The non-secret view of a company's Chargebee configuration.
*
* Four separate flags rather than one `connected`, because they fail
* differently and a single boolean sends an operator to the wrong place for
* three of them — see `BillingView` for how each is worded.
*/
export interface BillingStatus {
/** Whether an API key is stored. Never the key. */
apiKeyConfigured: boolean;
/** The Chargebee site slug, e.g. `acme-test`. Not secret. */
site: string | null;
/** Whether a webhook credential is stored. */
webhookConfigured: boolean;
/** The URL to paste into Chargebee, or null on a host with no public URL. */
webhookUrl: string | null;
/** Whether the company's manifest explicitly grants `chargebee`. */
granted: boolean;
/** Whether the `chargebee` feature is compiled into the running host. */
inBuild: boolean;
}

/** The write-only save body. Omitted fields keep their stored value. */
export interface BillingConfig {
/** Write-only. Omit to leave the stored key unchanged. */
apiKey?: string;
/** The site identifier; accepts a bare slug, a host, or a full URL. */
site?: string;
/** Write-only `username:password` pair. Omit to leave it unchanged. */
webhookSecret?: string;
}

/** Reads the company's Chargebee configuration status. */
export async function getBilling(
client: OpenCompanyClient,
company: string | null,
): Promise<BillingStatus> {
return client.get<BillingStatus>(`${client.scopeFor(company)}/billing/chargebee`);
}

/**
* Saves whatever is supplied, and returns the resulting status.
*
* A patch, not a replace: the host applies only the fields present and
* non-empty, so correcting the site never means re-typing the API key — which
* an operator cannot do anyway, since it is never shown back to them.
*/
export async function saveBilling(
client: OpenCompanyClient,
company: string | null,
config: BillingConfig,
): Promise<BillingStatus> {
return client.put<BillingStatus>(`${client.scopeFor(company)}/billing/chargebee`, config);
}

/** Clears every stored Chargebee credential. */
export async function clearBilling(
client: OpenCompanyClient,
company: string | null,
): Promise<BillingStatus> {
return client.del<BillingStatus>(`${client.scopeFor(company)}/billing/chargebee/key`);
Comment thread
CodeGhost21 marked this conversation as resolved.
}
Comment thread
oxoxDev marked this conversation as resolved.

/** The non-secret view of a company's PayPal connection (issue #789). */
export interface PaypalStatus {
/** Whether a client id is stored. Never the id. */
clientIdConfigured: boolean;
/** Whether a client secret is stored. */
clientSecretConfigured: boolean;
/** `sandbox` or `live` — which PayPal world the credentials belong to. */
environment: string;
/** Whether the company's manifest explicitly grants `paypal`. */
granted: boolean;
/** Whether the `paypal` feature is compiled into the running host. */
inBuild: boolean;
}

/** The write-only PayPal save body. Omitted fields keep their stored value. */
export interface PaypalConfig {
clientId?: string;
clientSecret?: string;
/** `sandbox` or `live`; anything else is stored as `sandbox`. */
environment?: string;
}

/** Reads the company's PayPal configuration status. */
export async function getPaypal(
client: OpenCompanyClient,
company: string | null,
): Promise<PaypalStatus> {
return client.get<PaypalStatus>(`${client.scopeFor(company)}/billing/paypal`);
}

/** Saves whatever is supplied, and returns the resulting status. */
export async function savePaypal(
client: OpenCompanyClient,
company: string | null,
config: PaypalConfig,
): Promise<PaypalStatus> {
return client.put<PaypalStatus>(`${client.scopeFor(company)}/billing/paypal`, config);
}

/** Clears the stored PayPal credentials and resets the environment. */
export async function clearPaypal(
client: OpenCompanyClient,
company: string | null,
): Promise<PaypalStatus> {
return client.del<PaypalStatus>(`${client.scopeFor(company)}/billing/paypal/key`);
}
4 changes: 4 additions & 0 deletions frontend/src/lib/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ const TOOL_LABELS: Readonly<Record<string, string>> = {
// does not exist. Two permissions both reading "Use one of its tools" would be
// indistinguishable, so the tools that can actually hold one (the catch-all
// `Other` group) need real words rather than the generic fallback.
// Billing (issues #788, #789). Only the two that park need words here; the
// read tools never reach an approval card.
chargebee_send_invoice: "Send an invoice to a customer",
chargebee_create_customer: "Add a customer to Chargebee",
workspace_write: "Edit a note in its workspace",
workspace_read: "Read a note in its workspace",
workspace_list: "List its workspace notes",
Expand Down
Loading
Loading