以下情境为教学假设,人物与数字不代表经核实的真实案例。
开源许可证经常被当成道德选择:要么支持开源,要么反对开源。对做生意的人,它更像一道成本选择题:这段代码能不能闭源分发、修改后要不要公开、对外提供服务时要不要把源码交给用户。三个问题的答案不同,商业边界就完全不同。
Tidecast 和 LedgerLite 的处境正好相反。Tidecast 分发的是游戏客户端,GPL 义务跟着「分发」触发;LedgerLite 提供的是订阅服务,GPL 不触发,但 AGPL 会。同样是「用了开源代码」,一个在发货时触发,一个在提供服务时触发;触发点不同,合规成本和替代方案也不同。
这一节只做商业口径的判断,不提供法律意见。许可证条款的最终解释由适用法律和具体版本决定;文中「通常理解为」指的是行业常见的商业读法,不是唯一法律结论。所有金额都是教学假设,用来比较路径,不是报价。
三档边界:宽松、弱著佐权、强著佐权
| 档位 | 代表许可证 | 闭源分发 | 修改后公开 | 网络服务触发 | 商业含义 |
|---|---|---|---|---|---|
| 宽松 | MIT、Apache-2.0 | 可以 | 不要求 | 不触发 | 保留声明即可,最适合闭源产品 |
| 弱著佐权 | LGPL | 有条件可以 | 修改库本身要公开 | 不触发 | 应用可闭源,库要保持可替换 |
| 强著佐权 | GPL | 不可以 | 分发时要公开 | 不触发 | 分发组合作品就要交出源码 |
| 强著佐权 | AGPL | 不可以 | 分发时要公开 | 触发 | 对外提供服务也要交出源码 |
宽松许可证的商业边界最清楚:可以闭源分发,义务主要是保留版权声明和许可文本。Apache-2.0 额外包含专利授权和 NOTICE 要求,对担心专利风险的团队更友好;MIT 更短,义务更少。两者都不会因为你把代码放进闭源产品而要求公开源码。
弱著佐权的边界在「库」和「应用」之间。LGPL 通常理解为:应用可以闭源,但 LGPL 库本身要保持可替换;如果修改了库,修改部分要公开。动态链接、库未修改时,闭源分发通常可行;静态链接或修改库,就要提供库的源码和可重链接的目标文件。这里的「通常」很重要,链接方式、语言运行时和分发形式都会改变判断。
强著佐权的边界在「分发」。GPL 通常理解为:分发包含 GPL 代码的组合作品时,要向接收者提供对应源码;内部使用和纯 SaaS 不触发,因为没有「分发」这个动作。AGPL 补上了这个缺口:即使不分发,只要修改后的程序被用来向网络用户提供服务,也要向这些用户提供源码。GPL 和 AGPL 的差别,不是「开源程度」,而是「触发点」。
判定表:用了什么许可证,能不能闭源
| 许可证 | 内部使用 | 对外提供 SaaS | 闭源分发 | 修改后分发 |
|---|---|---|---|---|
| MIT / Apache-2.0 | 可闭源 | 可闭源 | 可以 | 可以,保留声明 |
| LGPL | 可闭源 | 可闭源 | 可以,动态链接且库未修改 | 应用可闭源,库修改须公开 |
| GPL | 可闭源 | 可闭源 | 不可以 | 不可以,须提供源码 |
| AGPL | 可闭源 | 不可以,须向用户提供源码 | 不可以 | 不可以,须提供源码 |
这张表回答三个问题。第一,能不能闭源分发:MIT/Apache 可以,LGPL 有条件可以,GPL/AGPL 不可以。第二,要不要公开修改:宽松许可证不要求,弱著佐权只要求公开库的修改,强著佐权要求公开整个组合作品。第三,网络服务是否触发:只有 AGPL 把「提供服务」当成分发。
三个问题里,最容易被低估的是「修改后分发」。很多团队以为只要不改许可证文件就没事,但 GPL/AGPL 的义务跟着组合作品走;你把 GPL 库链接进闭源程序再发货,义务不会因为库是「原样使用」而消失。反过来,LGPL 允许应用闭源,但前提是库保持可替换;如果静态链接把库焊死,可替换性就没了。
还有一个常见误区:把「内部使用」当成永久安全。内部使用不触发分发义务,但一旦把内部工具交给客户、放进客户环境、或者随产品一起交付,分发就发生了。判断触发点时,看的是代码有没有离开你的控制范围,而不是看它有没有被修改。
AGPL 的合规成本:公开源码值多少钱
AGPL 的成本不是许可证费,而是「公开源码之后少赚的钱」加「持续公开的工程成本」。用 LedgerLite 做算例:年收入 $240,240,假设核心服务里有一段 AGPL 组件,向用户提供服务就要提供对应源码。教学假设下,公开源码导致 10% 的收入流失,持续 3 年;工程侧投入 160 小时,按 $85/小时计。
| 项目 | 金额 | 口径 |
|---|---|---|
| 一次性工程成本 | $13,600 | 160 小时 × $85 |
| 年度竞争损失 | $24,024 | 10% × $240,240 |
| 三年竞争损失 | $72,072 | $24,024 × 3 |
| 合规总成本 | $85,672 | $13,600 + $72,072 |
| 占一年 ARR | 35.7% | $85,672 ÷ $240,240 |
$85,672 是什么量级?LedgerLite 的 SDE 是 $130,240,合规成本相当于 65.8% 的 SDE;它不是一笔法务预算,而是一年多的可支配现金。这个数字还没有算持续维护:源码公开后,每次版本更新都要同步、审查、处理外部贡献,工程成本会从一次性变成经常性。
替代方案也要摆在同一张桌上。重写一个功能等价的组件,教学假设需要 240 小时,按 $85/小时是 $20,400;购买商业许可的教学假设报价是 $30,000。三条路径的差距很大:
| 路径 | 成本 | 说明 |
|---|---|---|
| 继续用 AGPL,合规 | $85,672 | 工程 $13,600 + 三年竞争损失 $72,072 |
| 重写替代组件 | $20,400 | 240 小时 × $85,一次性为主 |
| 购买商业许可 | $30,000 | 教学假设报价,可能含年度支持 |
| 合规与重写的差额 | $65,272 | $85,672 − $20,400,相当于 27.2% 的 ARR |
商业判断因此很直接:如果合规成本高于重写或商业许可,就换路径;如果替换会破坏核心功能、或者重写周期长到影响收入,合规可能是更便宜的选择。关键不是「开源好不好」,而是把三条路径放在同一时间口径上比较。上表的竞争损失按三年算,重写成本按一次性算;如果重写后还要持续维护,两者差距会缩小。
这里必须加一句边界:AGPL 是否触发、触发后要提供哪些源码,取决于具体版本、修改方式和提供服务的形式。本节用的是商业口径的保守估算,不是法律结论;真正签约或上线前,应让专业人士按实际架构判断。
Tidecast 的分发场景:客户端与服务端分开看
Tidecast 分发的是游戏客户端,触发点是「发货」。如果客户端里链接了 GPL 代码,分发游戏就要提供组合作品的源码;如果用的是 LGPL 库,动态链接、库未修改时可以保持游戏闭源,但要允许用户替换库。对买断制游戏,GPL 的源码义务会直接冲击闭源发行,LGPL 通常是更可接受的折中。
AGPL 在客户端上的触发点仍然是「分发」:把 AGPL 代码打进游戏客户端并发货,就要向玩家提供源码;玩家连接服务器这件事本身不额外触发网络条款。网络条款真正单独命中的是服务端——如果 Tidecast 的云存档或排行榜服务运行了修改后的 AGPL 代码,并向玩家提供该服务,就要向这些用户提供源码。云存档和排行榜在 Tidecast 的收入结构里是成本中心,竞争损失有限,但工程与持续公开的成本仍要计入。
LedgerLite 则是直接撞上 AGPL 的案例:订阅服务本身就是「向网络用户提供服务」,没有「不分发就没事」的空间。上面算出的 $85,672 合规成本,对应的就是这种结构;如果核心组件不能重写、商业许可也拿不到,这笔成本只能接受,或者重新设计架构把它隔离出去。
代码:许可证边界判定
下面的 oss.ts 把三档边界固化成判定函数。decideBoundary 接收许可证、使用方式和 LGPL 链接方式,返回能否闭源、是否必须公开修改、是否必须向服务用户提供源码;agplComplianceCost 把工程成本与竞争损失加总,并折算成一年 ARR 的占比。
// file: src/contract/oss.ts
// tier: L1
// verify: node --test test/contract/oss.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type LicenseId =
| "mit"
| "apache_2"
| "lgpl"
| "gpl"
| "agpl"
| "proprietary";
export type LicenseTier =
| "permissive"
| "weak_copyleft"
| "strong_copyleft"
| "proprietary";
export type UseMode =
| "internal"
| "saas"
| "distribute"
| "distribute_modified";
export type LgplVariant = "separate" | "modified_or_static";
export type LicenseTerm = {
id: LicenseId;
label: string;
tier: LicenseTier;
};
export const LICENSE_TERMS: Record<LicenseId, LicenseTerm> = {
mit: { id: "mit", label: "MIT", tier: "permissive" },
apache_2: { id: "apache_2", label: "Apache-2.0", tier: "permissive" },
lgpl: { id: "lgpl", label: "LGPL", tier: "weak_copyleft" },
gpl: { id: "gpl", label: "GPL", tier: "strong_copyleft" },
agpl: { id: "agpl", label: "AGPL", tier: "strong_copyleft" },
proprietary: {
id: "proprietary",
label: "专有许可",
tier: "proprietary",
},
};
export const LICENSE_IDS = Object.keys(LICENSE_TERMS) as LicenseId[];
export type BoundaryDecision = {
license: LicenseId;
label: string;
tier: LicenseTier;
mode: UseMode;
canCloseSource: boolean;
mustPublishChanges: boolean;
mustOfferServiceSource: boolean;
noticeRequired: boolean;
note: string;
};
function isUseMode(value: string): value is UseMode {
return (
value === "internal" ||
value === "saas" ||
value === "distribute" ||
value === "distribute_modified"
);
}
/**
* Commercial reading, not a legal opinion. "Distribute" means the licensed
* code becomes part of a combined work shipped to users. Using a component as
* a separate process can move the boundary; that separation has to be real.
*/
export function decideBoundary(
license: LicenseId,
mode: UseMode,
lgplVariant: LgplVariant = "separate",
): Result<BoundaryDecision> {
if (!Object.hasOwn(LICENSE_TERMS, license)) {
return fail(`unknown license: ${license}`);
}
if (!isUseMode(mode)) {
return fail(`unknown use mode: ${mode}`);
}
const term = LICENSE_TERMS[license];
let canCloseSource = true;
let mustPublishChanges = false;
let mustOfferServiceSource = false;
let note = "";
if (license === "mit" || license === "apache_2") {
note =
license === "apache_2"
? "保留版权与 NOTICE,附带的专利授权随分发生效"
: "保留版权与许可声明,可闭源分发";
} else if (license === "lgpl") {
if (mode === "distribute" || mode === "distribute_modified") {
if (lgplVariant === "modified_or_static") {
mustPublishChanges = true;
note =
"应用层可闭源;LGPL 库及其修改须提供源码与可重链接目标文件";
} else {
note = "动态链接且库未修改时可闭源,须允许用户替换该库";
}
} else {
note = "内部使用或网络服务不触发 LGPL 的分发义务";
}
} else if (license === "gpl") {
if (mode === "distribute" || mode === "distribute_modified") {
canCloseSource = false;
mustPublishChanges = true;
note = "分发组合作品须向接收者提供对应源码";
} else {
note = "内部使用或网络服务不触发 GPL 的分发义务";
}
} else if (license === "agpl") {
if (
mode === "saas" ||
mode === "distribute" ||
mode === "distribute_modified"
) {
canCloseSource = false;
mustPublishChanges = true;
mustOfferServiceSource = mode === "saas";
note =
mode === "saas"
? "向网络用户提供服务即须提供对应源码"
: "分发与网络服务均触发源码义务";
} else {
note = "仅内部使用不触发 AGPL 的源码义务";
}
} else {
note = "自有代码可按专有条款分发;第三方专有组件以授权合同为准";
}
return ok({
license,
label: term.label,
tier: term.tier,
mode,
canCloseSource,
mustPublishChanges,
mustOfferServiceSource,
noticeRequired: license !== "proprietary",
note,
});
}
export type AgplCostInput = {
annualRevenueCents: Cents;
years: number;
engineeringHours: number;
hourlyRateCents: Cents;
revenueLossBps: Bps;
};
export type AgplCost = {
engineeringCents: Cents;
lostRevenueCents: Cents;
totalCents: Cents;
revenueShareBps: Bps;
};
/**
* The AGPL network trigger has two costs: one-time engineering to publish and
* keep publishing source, and the revenue exposed when competitors can copy the
* service layer. Both are teaching assumptions.
*/
export function agplComplianceCost(input: AgplCostInput): Result<AgplCost> {
if (
!Number.isInteger(input.annualRevenueCents) ||
input.annualRevenueCents <= 0
) {
return fail("annual revenue must be a positive integer amount in cents");
}
if (!Number.isInteger(input.years) || input.years <= 0 || input.years > 10) {
return fail("years must be an integer between 1 and 10");
}
if (
!Number.isInteger(input.engineeringHours) ||
input.engineeringHours < 0
) {
return fail("engineering hours must be a non-negative integer");
}
if (
!Number.isInteger(input.hourlyRateCents) ||
input.hourlyRateCents < 0
) {
return fail("hourly rate must be a non-negative integer amount in cents");
}
if (
!Number.isInteger(input.revenueLossBps) ||
input.revenueLossBps < 0 ||
input.revenueLossBps > 10000
) {
return fail("revenue loss must be between 0 and 10000 bps");
}
const engineeringCents = input.engineeringHours * input.hourlyRateCents;
const lostRevenueCents =
applyRate(input.annualRevenueCents, input.revenueLossBps) * input.years;
const totalCents = engineeringCents + lostRevenueCents;
return ok({
engineeringCents,
lostRevenueCents,
totalCents,
revenueShareBps: divide(totalCents, input.annualRevenueCents),
});
}
代码里的判定是商业口径,不是法律意见。distribute 指许可证代码成为交付给用户的组合作品的一部分;如果只是作为独立进程或内部工具使用,边界可能不同,但这种隔离必须是真实的,不能只画在架构图上。LGPL 的 modified_or_static 分支把「应用可闭源、库修改须公开」拆成两个布尔值,是为了避免把「库的义务」误当成「整个应用的义务」。
LedgerLite 的参数跑出来是:工程成本 $13,600,三年竞争损失 $72,072,合规总成本 $85,672,占一年 ARR 35.7%。这组数字不包含持续维护,也不包含替换方案的迁移风险;它只回答「如果继续用 AGPL 并合规,第一刀切掉多少」。真实决策还要把重写周期、客户流失和架构隔离成本放进来。
小结
- 开源许可证的商业边界由三个问题决定:能不能闭源分发、修改后要不要公开、网络服务是否触发。
- 宽松许可证(MIT/Apache-2.0)可以闭源分发,主要义务是保留声明;Apache 额外有专利授权与 NOTICE 要求。
- 弱著佐权(LGPL)允许应用闭源,但库要保持可替换;修改库或静态链接会带来源码与可重链接义务。
- 强著佐权(GPL)在分发组合作品时要求提供源码;内部使用和纯 SaaS 通常不触发。
- AGPL 把触发点从「分发」扩展到「向网络用户提供服务」;这是它和 GPL 的核心商业差别。
- LedgerLite 的 AGPL 合规成本(教学假设)是 $85,672,相当于一年 ARR 的 35.7%、SDE 的 65.8%;重写替代组件是 $20,400,差额 $65,272。
- 决策口径是把合规、重写、商业许可放在同一时间尺度上比较;许可证条款的最终解释不是本节能给出的结论。
阅读导航:上一节:7.1 商标、著作权、专利与商业秘密 · 下一节:7.3 IP 归属、转让与员工发明 。
常见错误与边界
- 把开源当成「免费」。 许可证费是零,合规成本不是零;AGPL 的竞争损失和持续公开成本可能远高于购买商业许可。
- 以为 GPL 也管 SaaS。 GPL 通常按分发触发,纯 SaaS 不触发;这正是 AGPL 出现的原因。
- 把 AGPL 当成「更严格的 GPL」。 两者的核心差别是触发点:AGPL 把网络提供服务纳入义务,不只是分发。
- 忽略 LGPL 的链接方式。 动态链接、库未修改时可以闭源;静态链接或修改库会带来库的源码与可重链接义务。
- 只看许可证文件,不看依赖树。 一个间接依赖的 GPL 组件也可能改变整个交付物的边界;依赖清单要包含许可证字段。
- 把「内部使用」当成永久安全。 代码一旦交付给客户、放进客户环境或随产品分发,分发就可能已经发生。
- 用法律结论的口吻做商业判断。 许可证解释取决于版本、修改方式和适用法律;本节用的是「通常理解为」的商业口径。
- 只算一次性工程成本。 公开源码后每次版本更新都要同步与审查;合规成本是经常性的,不是一次性。
- 忽略替换方案的迁移风险。 重写成本低不等于总成本低;重写周期、功能回退和客户流失都要计入。
- 把教学假设当成报价。 $85/小时、10% 流失、240 小时重写都是用于比较的假设,真实数字必须按项目和市场重新估算。
开源许可证不改变代码的质量,却会改变代码能不能变成闭源收入。判断顺序应该是:先看触发点,再看义务,最后算合规与替代的成本差。下一节讨论 IP 归属:即使许可证没问题,如果资产从一开始就不属于公司,后面的授权和出售仍然会卡住。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。